diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 8cf179d6ab..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: circleci/node:12 - steps: - - add_ssh_keys: - fingerprints: - - "b8:65:3c:86:e2:5c:64:82:d6:49:1f:4d:da:da:00:87" - - checkout - - restore_cache: - keys: - - dependencies-{{ checksum "yarn.lock" }} - - run: - name: Install - command: yarn install --pure-lockfile - - save_cache: - paths: - - node_modules - key: dependencies-{{ checksum "yarn.lock" }} - - run: - name: Run deploy scripts - command: bash ./.circleci/deploy.sh -workflows: - version: 2 - build_and_deploy: - jobs: - - build: - filters: - branches: - only: master diff --git a/.circleci/deploy.sh b/.circleci/deploy.sh deleted file mode 100644 index 169c3631b0..0000000000 --- a/.circleci/deploy.sh +++ /dev/null @@ -1,7 +0,0 @@ -git config --global user.name "QC-L" -git config --global user.email "github@liqichang.com" -git remote set-url origin git@github.com:reactjs/zh-hans.reactjs.org.git - -chmod -R 777 node_modules/gh-pages/ -yarn build -yarn deploy diff --git a/content/blog/2020/02/26/react-v16.13.0.md b/content/blog/2020/02/26/react-v16.13.0.md deleted file mode 100644 index b8d29b4876..0000000000 --- a/content/blog/2020/02/26/react-v16.13.0.md +++ /dev/null @@ -1,209 +0,0 @@ ---- -title: "React v16.13.0" -author: [threepointone] -redirect_from: - - "blog/2020/03/02/react-v16.13.0.html" ---- - -今天我们发布了 React 16.13.0。此版本修复了部分 bug 并添加了新的弃用警告,以助力接下来的主要版本。 - -## 新的警告 {#new-warnings} - -### Render 期间更新的警告 {#warnings-for-some-updates-during-render} - -React 组件不应在 render 期间对其他组件产生副作用。 - -在 render 期间调用 `setState` 是被支持的,但是 [仅仅适用于*同一个组件*](/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops)。 如果你在另一个组件 render 期间调用 `setState`,现在你将会看到一条警告。 - -``` -Warning: Cannot update a component from inside the function body of a different component. -``` - -**这些警告将会帮助你找到应用中由意外的状态改变引起的 bug。** 在极少数情况下,由于渲染,你有意要更改另一个组件的状态,你可以将 `setState` 调用包装为 `useEffect`。 - -### 冲突的样式规则警告 {#warnings-for-conflicting-style-rules} - -当动态地应用包含了全写和简写的 `style` 版本的 CSS 属性时,特定的更新组合可能会导致样式不一致。例如: - -```js -
- ... -
-``` - -你可能期待这个 `
` 总是拥有红色背景,不论 `toggle` 的值是什么。然而,在 `true` 和 `false`之间交替使用`toggle`时,背景色开始是 `red`,然后在 `transparent` 和 `blue`之间交替, [正如你能在这个 demo 中看到的](https://codesandbox.io/s/serene-dijkstra-dr0vev)。 - -**React 现在检测到冲突的样式规则并打印出警告。**要解决此问题,请不要在 `style` 属性中混合使用同一CSS属性的简写和全写版本。 - -### 某些废弃字符串 ref 的警告 {#warnings-for-some-deprecated-string-refs} - -[字符串 ref 是过时的 API](/docs/refs-and-the-dom.html#legacy-api-string-refs) 这是不可取的,将来将被弃用: - -```js - - {isPending ? " Loading..." : null} - - -); -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/jovial-lalande-26yep)** - -现在,这感觉好多了!当我们点击 Next 按钮的时候,它变得不可用,因为点击它很多次并没有意义。而且新增的“Loading...”提示让用户知道程序并没有卡住。 - -### 回顾更改 {#reviewing-the-changes} - -我们来回顾基于 [原始示例](https://codesandbox.io/s/infallible-feather-xjtbu) 做出的所有更改: - -```js{3-5,9,11,14,19} -function App() { - const [resource, setResource] = useState(initialResource); - const [startTransition, isPending] = useTransition({ - timeoutMs: 3000 - }); - return ( - <> - - {isPending ? " Loading..." : null} - - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/jovial-lalande-26yep)** - -我们只用了 7 行代码来实现这个切换: - -* 我们引入了 `useTransition` Hook 并在更新 state 的组件中使用了它。 -* 我们传入了 `{timeoutMs: 3000}` 使得前一个页面在屏幕上最多保持3秒钟。 -* 我们把 state 更新包裹在 `startTransition` 中,以通知 React 可以延迟这个更新。 -* 我们使用 `isPending` 来告诉用户界面切换的进展并禁用按钮。 - -最后的结果是,点击 “Next” 按钮不会立刻切换界面并展示加载中,而是停留在前一个界面并同步加载进度。 - -### 是在哪里更新的? {#where-does-the-update-happen} - -这并不是很难实现。但是,如果你已经开始思考这是如何工作的,它可能会让你感到费解。既然我们更新了 state,为什么我们不能立刻看到结果呢?而下一个 `` 又是在*哪里*渲染的呢? - -很显然,两个“版本”的 `` 同时存在了。我们知道旧的存在是因为它在界面上,而且它还显示了一个进度提示。我们也知道新的存在于*某个地方*,因为它就是我们正在等待的那个界面! - -**但是同一个组件的两个版本的是如何同时存在的呢?** - -这原因就在于 Concurrent 模式本身。我们 [之前提到](/docs/concurrent-mode-intro.html#intentional-loading-sequences) 它有点像在 “branch” 上运行的的一个 state 更新。或者我们可以想象成,当我们把 state 更新包裹在 `startTransition` 的时候会在*“另一个宇宙中”*开始渲染,就像科幻电影一样。我们并不能直接看到那个宇宙 -- 但是我们能够从那个宇宙探知一些事情正在发生的事情(`isPending`)。当更新完成的时候,我们的“多个宇宙”合并成一个,我们在屏幕上看到最终的结果! - -在 [示例](https://codesandbox.io/s/jovial-lalande-26yep) 中多练习一下,然后试着想象它正在发生。 - -当然,两个版本的树*同时*渲染只是个假象,正如所有程序同时在你电脑上运行的想法也同样是假象。操作系统会在不同的应用之间快速的切换。类似的,React 可以在不同版本的树上进行切换,一个是你屏幕上看到的那个版本,另一个是它“准备”接下来给你显示的版本。 - -一个 `useTransition` 这样的 API 可以让你专注于期望的用户体验,而不需要思考这些机制是如何实现的。仍然,想象 `startTransition` 包裹的更新是“在一个分支上”或者“在另一个世界”发生的,是个很有帮助的比喻。 - -### 很多场景可以使用 transition {#transitions-are-everywhere} - -正如我们从 [Suspense 简介](/docs/concurrent-mode-suspense.html) 中所了解,所有所需数据没有准备好的组件都可以 “suspend” 一段时间。我们可以从策略上用 `` 把树的不同部分圈起来处理,但这并不总是足够的。 - -我们回到 [第一个 Suspense 示例](https://codesandbox.io/s/frosty-hermann-bztrp) 那时还是只有一个界面的。现在我们增加一个“Refresh”按钮,用来检查服务端的数据更新。 - -我们的第一次尝试大概看起来是这样的: - -```js{6-8,13-15} -const initialResource = fetchUserAndPosts(); - -function ProfilePage() { - const [resource, setResource] = useState(initialResource); - - function handleRefreshClick() { - setResource(fetchUserAndPosts()); - } - - return ( - Loading profile...}> - - - Loading posts...}> - - - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/boring-shadow-100tf)** - -在这个例子中,我们会在加载*和*每次点击 “Refresh” 按钮的时候开始数据获取。我们把 `fetchUserAndPosts()` 的结果放到 state 中,这样下级的组件可以从我们刚刚发起的请求中读取新的数据。 - -我们可以看到在[示例](https://codesandbox.io/s/boring-shadow-100tf)中点击 “Refresh” 是可以正常工作的。`` 和 `` 组件接收代表新数据的 `resource` prop,它会因为我们尚未得到服务端响应而 “suspend”,所以我们看到了降级方案界面。当服务端响应加载完成,我们看到更新后的文章(我们的伪造接口每 3 秒增加一些文章)。 - -然而,这种交互体验极差。用户正在浏览页面,但是在与页面进行交互的时候,内容却被加载状态覆盖。这会让人感觉匪夷所思。**正如前面那样,要避免显示加载中,我们把 state 更新放到 transition 中:** - -```js{2-5,9-11,21} -function ProfilePage() { - const [startTransition, isPending] = useTransition({ - // Wait 10 seconds before fallback - timeoutMs: 10000 - }); - const [resource, setResource] = useState(initialResource); - - function handleRefreshClick() { - startTransition(() => { - setResource(fetchProfileData()); - }); - } - - return ( - Loading profile...}> - - - Loading posts...}> - - - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/sleepy-field-mohzb)** - -这下感觉好多了!点击 “Refresh” 按钮不会再阻断页面浏览了。我们会看到有内容正在“内联”加载,并且当数据准备好,它就显示出来了。 - -### 把 Transition 融合到你应用的设计系统 {#baking-transitions-into-the-design-system} - -`useTransition` 是*非常*常见的需求。几乎所有可能导致组件挂起的点击或交互操作都需要使用 `useTransition`,以避免意外隐藏用户正在交互的内容。 - -这可能会导致组件存在大量重复代码。这正是**我们通常建议把 `useTransition` 融合到你应用的*设计系统*组件中**的原因。例如,我们可以把 transition 逻辑抽取到我们自己的 ` - {isPending ? spinner : null} - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/modest-ritchie-iufrh)** - -需要注意按钮并不关心我们会更新*什么*。它把发生在它 `onClick` 处理器过程中的*任意* state 更新包装到一个 transition 中。这样我们的 ` - Loading posts...}> - - - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/modest-ritchie-iufrh)** - -当一个按钮点击的时候,它开启一个 transition 并在 transition 内部调用 `props.onClick()` —— 这会触发 `` 组件中的 `handleRefreshClick`。我们开始获取最新数据,但这并不会触发一个降级界面,因为我们正运行在 transition 中,并且 `useTransition` 调用时指定的 10 秒钟尚未达到。当一个 transition 等待的时候,这个按钮会内联显示加载中的提示。 - -我们现在可以看出 Concurrent 模式能够帮助我们在不牺牲组件的独立性和模块性的同时达成更好的用户体验。由 React 来协调 transition。 - -## 3 个阶段 {#the-three-steps} - -至此我们已经讨论了更新时可能经历的所有不同的显示状态。在这一节中,我们会给它们命名并讨论它们之间的关联。 - -
- -Three steps - -在最后,我们达到 **Complete(完成)** 状态。那是我们最终想要达到的状态。它代表着下一个界面完全渲染并且不再加载新数据的时刻。 - -但是在屏幕完全展示之前,我们可能需要加载一些数据或代码。当我们已经在下一个界面,但它的某些部分还在加载中,我们称此状态为 **Skeleton(骨架)**。 - -最后,有两种主要方式可以使我们进入骨架状态。我们会通过具体的示例详细描述它们之间的区别。 - -### 默认方式:Receded → Skeleton → Complete {#default-receded-skeleton-complete} - -打开[此示例](https://codesandbox.io/s/prod-grass-g1lh5)并点击 “Open Profile”。你会陆续看到几种显示状态: - -* **Receded(后退)**:第一秒,你会看到 `

Loading the app...

` 降级界面。 -* **Skeleton:** 你会看到 `` 组件中显示着 `

Loading posts...

` . -* **Complete:** 你会看到 `` 组件不再显示降级界面。所有内容获取完毕。 - -我们如何区分 Receded 和 Skeleton 状态呢?它们之间的区别在于 **Receded** 感觉像是面向用户“向后退一步”,而 **Skeleton** 模式感觉像是在我们的进程中“向前走一步”来展示更多的内容。 - -在此示例中,我们从 `` 开始我们的旅程: - -```js - - {/* previous screen */} - - -``` - -点击之后,React 开始渲染下一个界面: - -```js - - {/* next screen */} - - - - - - - -``` - -`` 和 `` 都需要数据来渲染,所以它们将被 suspend: - -```js{4,6} - - {/* next screen */} - - {/* suspends! */} - Loading posts...}> - {/* suspends! */} - - - -``` - -当组件被 suspend 时,React 需要显示最近的那个降级界面。但是对 `` 来说最近的降级界面就已经是最顶层了: - -```js{2,3,7} - 导致 -

Loading the app...

-}> - {/* next screen */} - - {/* suspends! */} - - - - -
-``` - -这就是当我们点击按钮之后,它感觉像是我们“后退了一步”。`` 范围本来显示的有用内容(``)必须“后退”并显示降级界面(`

Loading the app...

`)。我们称之为**Receded(后退)**状态。 - -当我们加载了更多内容的时候,React 会重新尝试渲染,这时 `` 能够成功渲染。最终,我们进入 **Skeleton** 状态。我们看到了尚未完全渲染的新的页面。 - -```js{6,7,9} - - {/* 下一屏 */} - - - 导致 -

Loading posts...

- }> - {/* suspends! */} -
-
-
-``` - -最终,它们也加载好了,然后我们达到 **Complete** 状态 - -这个剧情(Receded → Skeleton → Complete)是默认情况。但是 Receded 状态是非常不友好的,因为它“隐藏”了已经存在的信息。这正是 React 让我们通过 `useTransition` 进入另一个序列(**Pending(等待)** → Skeleton → Complete)的原因。 - -### 期望方式: Pending → Skeleton → Complete {#preferred-pending-skeleton-complete} - -当我们使用 `useTransition` 的时候,React 会让我们“停留”在前一个页面 -- 并在那显示一个进度提示。我们称它为一个**Pending(暂停)**状态。这种的体验会比 Receded 状态好很多,因为我们已经显示的信息不会消失,而且页面仍可以交互。 - -你可以对比这两个例子来体验其中的差异: - -* 默认方式: [Receded → Skeleton → Complete](https://codesandbox.io/s/prod-grass-g1lh5) -* **期望方式: [Pending → Skeleton → Complete](https://codesandbox.io/s/focused-snow-xbkvl)** - -这两个例子唯一的不同就在于第一个使用的是普通 ` - {isPending ? spinner : null} - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/floral-thunder-iy826)** - -这让用户知道有什么事情正在发生。但是,如果这个 transition 过程相对较短的时候(小于 500ms),它有可能过于分散注意力,并且使得 transition 本身感觉上*更慢*。 - -一个可能的方法就是*延迟等待提示*的显示: - -```css -.DelayedSpinner { - animation: 0s linear 0.5s forwards makeVisible; - visibility: hidden; -} - -@keyframes makeVisible { - to { - visibility: visible; - } -} -``` - -```js{2-4,10} -const spinner = ( - - {/* ... */} - -); - -return ( - <> - - {isPending ? spinner : null} - -); -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/gallant-spence-l6wbk)** - -通过这个更改,即使我们进入了 Pending 状态,在 500ms 过去之前我们都不会给用户显示任何提示。这对于一些 API 响应较慢的情况算不上是很大的改进。但是在 API 响应快的情况下对比感受下 [使用前](https://codesandbox.io/s/thirsty-liskov-1ygph) 和 [使用后](https://codesandbox.io/s/hardcore-http-s18xr)。即使其他的代码并没有更改,通过不在延迟上吸引用户注意,隐藏掉“过快”的加载状态以达到提升感官体验。 - -### 回顾 {#recap} - -到此我们所学的虽重要的东西如下: - -* 默认情况下,我们的加载顺序是 Receded → Skeleton → Complete. -* Receded 状态的体验并不友好,因为它会隐藏内容。 -* 通过 `useTransition`,我们可以切换到首先进入 Pending 状态而非 Receded。这让我们可以在下一个界面准备好之前停留在前一个页面上。 -* 如果我们不想因为一部分组件延迟整个 transition,我们可以把他们各自用 `` 区域包裹起来。 -* 与其在每个不同组件中使用 `useTransition`,我们不如把它组织到系统设计中。 - -## 其他模式 {#other-patterns} - -Transition 大概是你能遇到的最常见的 Concurrent 模式了,但是还有一些其他的模式你有可能用得着。 - -### 根据优先级分割 state {#splitting-high-and-low-priority-state} - -当你设计 React 组件的时候,找到 state 的“极小表示法”通常是最好的方式。例如,与其在 state 中保存 `firstName`、`lastName` 和 `fullName`,不如只保存 `firstName` 和 `lastName` 这样通常会更好,然后在渲染时通过计算得到 `fullName`。这可以避免我们只更新了某个 state 却忘记了更新关联 state 所导致的错误。 - -然而,在 Concurrent 模式中有些情况下你会*想要*把一些数据*冗余*到不同的 state 变量中。请考虑这个小 translation 应用: - -```js -const initialQuery = "Hello, world"; -const initialResource = fetchTranslation(initialQuery); - -function App() { - const [query, setQuery] = useState(initialQuery); - const [resource, setResource] = useState(initialResource); - - function handleChange(e) { - const value = e.target.value; - setQuery(value); - setResource(fetchTranslation(value)); - } - - return ( - <> - - Loading...

}> - -
- - ); -} - -function Translation({ resource }) { - return ( -

- {resource.read()} -

- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/brave-villani-ypxvf)** - -请注意当我们在输入框打字的时候,`` 组件式如何 suspend 的,并且我们会在得到新的结果之前看到 `

Loading...

` 这个降级界面。这并不理想。当我们在获取下一个翻译的同时如果我们可以多看一会*上一个*翻译效果应该会更好。 - -事实上,如果我们打开控制台,我们会看到一则警告: - -``` -Warning: App triggered a user-blocking update that suspended. -# Warning: App 触发了一个会 suspend 的 user-blocking 更新。 - -The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes. -# 修复方法是把更新分离到不同的部分:一个用于提供直接反馈的 user-blocking 更新,和一个用于触发主体变化的更新。 - -Refer to the documentation for useTransition to learn how to implement this pattern. -# 参考 useTransition 的文档来了解如何实现这个模式。 -``` - -正如我们之前提到的,如果一个 state 更新会导致一个组件 suspend,那么这个 state 更新就应该用 transition 包裹起来。我们来把 `useTransition` 添加到我们的组件中: - -```js{4-6,10,13} -function App() { - const [query, setQuery] = useState(initialQuery); - const [resource, setResource] = useState(initialResource); - const [startTransition, isPending] = useTransition({ - timeoutMs: 5000 - }); - - function handleChange(e) { - const value = e.target.value; - startTransition(() => { - setQuery(value); - setResource(fetchTranslation(value)); - }); - } - - // ... - -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/zen-keldysh-rifos)** - -现在尝试往输入框里敲字吧。有点不对劲!输入框的更新非常慢。 - -我们解决了第一个问题(没有使用 transition 的 suspend)。但是因为这个 transition,我们的 state 无法立刻更新,而且无法“驱动”一个受控的输入框! - -这个问题的解决方法是**把 state 分离到两个不同的部分:**一个“高优先级”的直接更新的部分,一个“低优先级的”等待 transition 的部分。 - -在我们的例子中,我们已经有两个 state 变量了。输入框文本在 `query` 变量中,和我们从中读取翻译的 `resource` 变量。我们希望 `query` state 的变化可以立刻发生,但是对 `resource` 的变化(例如获取一个新的翻译)应当触发一个 transition。 - -所以正确的解决办法是把(不会 suspend 的)`setQuery` 放到 transition *外面*,但保持(会 suspend 的)`setResource` 仍在 transition *里面*。 - -```js{4,5} -function handleChange(e) { - const value = e.target.value; - - // Outside the transition (urgent) - setQuery(value); - - startTransition(() => { - // Inside the transition (may be delayed) - setResource(fetchTranslation(value)); - }); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/lively-smoke-fdf93)** - -通过这个更改,它可以正常工作了。我们可以直接在输入框敲字,翻译会在稍后“跟上”我们所输入的内容。 - -### 延迟一个值 {#deferring-a-value} - -默认情况下,React 总是渲染一个一致的 UI。思考下面这段代码: - -```js -<> - - - -``` - -React 保证不论什么时候你去看屏幕上的这两个组件,他们所反映的数据来自同一个 `user`。如果一个不同的 `user` 因为 state 更新传递下来,你会发现他们同时变化。你无法通过录屏找到某一帧显示着不同的 `user`。(你要是真的发现了,请提交一个BUG!) - -这在绝大多数情况下是合理的。不一致的 UI 会让人困惑并误导用户。(例如,如果一个通讯软件的发送按钮和对话内容界面当前选择的会话“不一致”的话是非常糟糕的。) - -然而,有些时候故意引入不一致可能是有帮助的。我们可以像上面那样把 state “分离”开,但 React 也提供了一个内置的 Hook 来做这件事: - -```js -import { useDeferredValue } from 'react'; - -const deferredValue = useDeferredValue(value, { - timeoutMs: 5000 -}); -``` -要演示这个特性,我们要用到 [详情页切换示例](https://codesandbox.io/s/musing-ramanujan-bgw2o)。点击“Next”按钮并注意它是如何使用1秒钟完成 transition 的。 - -假设我们获取用户详情是非常快的,只需要 300 毫秒。现在,因为我们要等待用户信息和文章列表并保持详情页显示的一致性,我们等待了整整1秒。但是如果我们只是希望详情能更快的显示出来呢? - -如果你希望牺牲一致性,我们可以通过**给拖延我们 transition 的组件传递可能过时的数据**来实现。那正是 `useDeferredValue()` 可以帮我们做的事情: - -```js{2-4,10,11,21} -function ProfilePage({ resource }) { - const deferredResource = useDeferredValue(resource, { - timeoutMs: 1000 - }); - return ( - Loading profile...}> - - Loading posts...}> - - - - ); -} - -function ProfileTimeline({ isStale, resource }) { - const posts = resource.posts.read(); - return ( -
    - {posts.map(post => ( -
  • {post.text}
  • - ))} -
- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/vigorous-keller-3ed2b)** - -我们所做的权衡是,`` 会和其他组件不一致并很可能会显示一个过时的内容。多点几次“Next”按钮,你就会发现这个问题。但是也正因如此,我们才能把 transition 的时间从 1000ms 降到 300ms。 - -这到底是是不是一个合理的权衡取决于具体情况。但这是个很方便的工具,尤其是在界面切换的时候内容变化并不明显的情况下,或者用户可能根本不会注意到他看的是一秒前版本的旧数据的情况。 - -值得注意的是 `useDeferredValue` 并不*仅仅*在获取数据的时候有用。它在更新组件树的工作量过大导致交互(例如:在输入框输入内容)卡顿的情况也是有用的。正如我们可以“延迟”一个花费长时间请求的值(并且显示之前的值而不影响其他组件的更新),我们也可以把它用在组件树需要花费较长时间更新的情况。 - -举个例子,请考虑像这样的一个可筛选列表: - -```js -function App() { - const [text, setText] = useState("hello"); - - function handleChange(e) { - setText(e.target.value); - } - - return ( -
- - ... - -
- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/pensive-shirley-wkp46)** - -在这个例子中,**`` 中的每个项都有一个人为添加的延迟 -- 每个项会延迟渲染进程几毫秒**。我们永远也不会在真实的应用中这样做,但是这是帮助我们模拟在一个已经没有优化余地的深层嵌套的组件树中会发生的事情。 - -我们可以看到往输入框敲内容是如何导致卡顿的。现在我们把 `useDeferredValue` 加进去: - -```js{3-5,18} -function App() { - const [text, setText] = useState("hello"); - const deferredText = useDeferredValue(text, { - timeoutMs: 5000 - }); - - function handleChange(e) { - setText(e.target.value); - } - - return ( -
- - ... - -
- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/infallible-dewdney-9fkv9)** - -现在输入已经很少卡顿了 -- 尽管我们是通过延迟显示结果来实现这一点的。 - -这和 debouncing 有什么区别?我们的例子有一个固定的人为延迟(80个项,每项延迟 3ms),所以它总是会延迟,不论我们的电脑有多快。然而,`useDeferredValue` 的值只会在渲染耗费时间的情况下“滞后”。React 并不会加入一点多余的延迟。在一个更实际的工作负荷,你可以预期这个滞后会根据用户的设备而不同。在较快的机器上,滞后会更少或者根本不存在,在较慢的机器上,它会变得更明显。不论哪种情况,应用都会保持可响应。这就是此机制优于 debouncing 或 throttling 的地方,它们总是会引入最小延迟而且不可避免的会在渲染的时候阻塞进程。 - -尽管在响应性上有所提升,这个例子还并不是很有说服力,因为对于这个用例 Concurrent 模式缺少了一些关键优化。但是,看到像 `useDeferredValue`(或 `useTransition`)这样的特性不论是在我们等待网络还是等待计算工作完成的情况都有用还是很有趣的。 - -### SuspenseList {#suspenselist} - -`` 是有关组织加载状态的最后一个模式了。 - -请考虑这个例子: - -```js{5-10} -function ProfilePage({ resource }) { - return ( - <> - - Loading posts...}> - - - Loading fun facts...}> - - - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/proud-tree-exg5t)** - -在这个例子中 API 调用的时长是随机的。如果你持续的刷新,你会发现有的时候文章列表会先到达,有的时候“趣闻”会先到达。 - -这带来了一个问题。如果趣闻先到达了,我们会发现趣闻展示在文章列表的降级界面 `

Loading posts...

`。我们可能会先开始阅读这些,但是稍后*文章列表*的响应到达,把所有的趣闻推到下面。这感觉很不好。 - -其中一种解决办法是我们通过把他们放在同一个 Suspense 边界中: - -```js -Loading posts and fun facts...}> - - - -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/currying-violet-5jsiy)** - -这个办法的问题在于现在我们*总是*要等待这两个数据都获取到之后。但是,如果是*文章列表*先到达,我们就不需要延迟显示它们。当趣闻后到达的时候,因为他们本身就在文章列表下方所以他们并不会导致布局抖动。 - -另一种解决办法是,比如通过一种特殊的方式组织 Promise,当我们需要从树中多个不同组件中加载 state 的时候会变得越来越难以实现。 - -要解决这个问题,我们要用到 `SuspenseList`: - -```js -import { SuspenseList } from 'react'; -``` - -`` 协调它下面的最接近的 `` 节点的“展开顺序”: - -```js{3,11} -function ProfilePage({ resource }) { - return ( - - - Loading posts...}> - - - Loading fun facts...}> - - - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/black-wind-byilt)** - -这个 `revealOrder="forwards"` 配置表示这个列表中最接近的 `` **只会根据在树中的显示顺序来“展开”它们的内容 -- 即使它们的数据在不同的顺序到达**。`` 还有其他有趣的模式:尝试把 `"forwards"` 换成 `"backwards"` 或 `"together"` 并观察效果。 - -你可以利用 `tail` prop 来控制同时显示多少个加载状态。如果我们制定 `tail="collapsed"`,我们只能看到*最多一个*降级界面。你可以在 [这里](https://codesandbox.io/s/adoring-almeida-1zzjh) 体验一下。 - -请记住和 React 中的其他东西一样 `` 也是可以组合的。例如,你可以做一个 `` table 中放着 `` row 的表格。 - -## 下一步 {#next-steps} - -Concurrent 模式提供了一个强大的 UI 编程模型和一系列的新的可组合的指令集来帮助你构建愉快的用户体验。 - -这是通过多年的调查和开发的结果,但它尚未完结。在 [采用 Concurrent 模式](/docs/concurrent-mode-adoption.html) 中,我们会讲如何使用它以及它的效果。 diff --git a/content/docs/concurrent-mode-reference.md b/content/docs/concurrent-mode-reference.md deleted file mode 100644 index 4f6f180c24..0000000000 --- a/content/docs/concurrent-mode-reference.md +++ /dev/null @@ -1,203 +0,0 @@ ---- -id: concurrent-mode-reference -title: Concurrent 模式 API 参考(实验版) -permalink: docs/concurrent-mode-reference.html -prev: concurrent-mode-adoption.html ---- - - - -
- ->注意: -> ->本章节所描述的功能还处于实验阶段,在稳定版本中尚不可用。它面向的人群是早期使用者以及好奇心较强的人。 -> ->本页面中许多信息现已过时,仅仅是为了存档而存在。欲了解最新信息,**请参阅 [React 18 Alpha 版公告](/blog/2021/06/08/the-plan-for-react-18.html)**。 -> ->在 React 18 发布前,我们将用稳定的文档替代此章节。 - -
- -本章节为 [Concurrent 模式](/docs/concurrent-mode-intro.html)的 React API 参考。如果你想找使用指南,请查阅 [Concurrent UI 模式](/docs/concurrent-mode-patterns.html)。 - -**注意:这是社区的预览版,并不是最终的稳定版本。这些 API 将来可能会发生变化。请自行承担风险!** - -- [启用 Concurrent 模式](#concurrent-mode) - - [`createRoot`](#createroot) -- [Suspense](#suspense) - - [`Suspense`](#suspensecomponent) - - [`SuspenseList`](#suspenselist) - - [`useTransition`](#usetransition) - - [`useDeferredValue`](#usedeferredvalue) - -## 启用 Concurrent 模式 {#concurrent-mode} - -### `createRoot` {#createroot} - -```js -ReactDOM.createRoot(rootNode).render(); -``` - -使用上述代码替换 `ReactDOM.render(, rootNode)` 并启用 Concurrent 模式。 - -欲了解有关 Concurrent 模式的更多信息,请查阅 [Concurrent 模式文档](/docs/concurrent-mode-intro.html) - -## Suspense API {#suspense} - -### `Suspense` {#suspensecomponent} - -```js -加载中...}> - - - -``` - -`Suspense` 让你的组件在渲染之前进行“等待”,并在等待时显示 fallback 的内容。 - -在这个示例中,`ProfileDetails` 正在等待异步 API 调用来获取某些数据。在等待 `ProfileDetails` 和 `ProfilePhoto` 时,我们将显示`加载中...`的 fallback。请注意,在 `` 中的所有子组件都加载之前,我们将继续显示这个 fallback。 - -`Suspense` 接受两个 props: -* **fallback** 接受一个加载指示器。这个 fallback 在 `Suspense` 所有子组件完成渲染之前将会一直显示。 -* **unstable_avoidThisFallback** 接受一个布尔值。它告诉 React 是否在初始加载时“跳过”显示这个边界,这个 API 可能会在后续版本中删除。 - -### `` {#suspenselist} - -```js - - - - - - - - - - - ... - -``` - -`SuspenseList` 通过编排向用户显示这些组件的顺序,来帮助协调许多可以挂起的组件。 - -当多个组件需要获取数据时,这些数据可能会以不可预知的顺序到达。不过,如果你将这些项目包装在 `SuspenseList` 中,React 将不会在列表中显示这个项目,直到它之前的项目已经显示(此行为可调整)。 - -`SuspenseList` 接受两个 props: -* **revealOrder (forwards, backwards, together)** 定义了 `SuspenseList` 子组件应该显示的顺序。 - * `together` 在*所有*的子组件都准备好了的时候显示它们,而不是一个接着一个显示。 -* **tail (collapsed, hidden)** 指定如何显示 `SuspenseList` 中未加载的项目。 - * 默认情况下,`SuspenseList` 将显示列表中的所有 fallback。 - * `collapsed` 仅显示列表中下一个 fallback。 - * `hidden` 未加载的项目不显示任何信息。 - -请注意,`SuspenseList` 只对其下方最近的 `Suspense` 和 `SuspenseList` 组件进行操作。它不会搜索深度超过一级的边界。不过,可以将多个 `SuspenseList` 组件相互嵌套来构建栅格。 - -### `useTransition` {#usetransition} - -```js -const SUSPENSE_CONFIG = { timeoutMs: 2000 }; - -const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG); -``` - -`useTransition` 允许组件在**切换到下一个界面**之前等待内容加载,从而避免不必要的加载状态。它还允许组件将速度较慢的数据获取更新推迟到随后渲染,以便能够立即渲染更重要的更新。 - -`useTransition` hook 返回两个值的数组。 -* `startTransition` 是一个接受回调的函数。我们用它来告诉 React 需要推迟的 state。 -* `isPending` 是一个布尔值。这是 React 通知我们是否正在等待过渡的完成的方式。 - -**如果某个 state 更新导致组件挂起,则该 state 更新应包装在 transition 中** - -```js -const SUSPENSE_CONFIG = { timeoutMs: 2000 }; - -function App() { - const [resource, setResource] = useState(initialResource); - const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG); - return ( - <> - - {isPending ? " 加载中..." : null} - }> - - - - ); -} -``` - -在这段代码中,我们使用 `startTransition` 包装了我们的数据获取。这使我们可以立即开始获取用户资料的数据,同时推迟下一个用户资料页面以及其关联的 `Spinner` 的渲染 2 秒钟(`timeoutMs` 中显示的时间)。 - -`isPending` 布尔值让 React 知道我们的组件正在切换,因此我们可以通过在之前的用户资料页面上显示一些加载文本来让用户知道这一点。 - -**深入了解 transition,可以阅读 [Concurrent UI 模式](/docs/concurrent-mode-patterns.html#transitions).** - -#### useTransition 配置 {#usetransition-config} - -```js -const SUSPENSE_CONFIG = { timeoutMs: 2000 }; -``` - -`useTransition` 接受带有 `timeoutMs` 的**可选的 Suspense 配置**。 此超时(毫秒)告诉 React 在显示下一个状态(上例中为新的用户资料页面)之前等待多长时间。 - -**注意:我们建议你在不同的模块之间共享 Suspense 配置。** - - -### `useDeferredValue` {#usedeferredvalue} - -```js -const deferredValue = useDeferredValue(value, { timeoutMs: 2000 }); -``` - -返回一个延迟响应的值,该值可能“延后”的最长时间为 `timeoutMs`。 - -这通常用于在具有基于用户输入立即渲染的内容,以及需要等待数据获取的内容时,保持接口的可响应性。 - -文本输入框是个不错的示例。 - -```js -function App() { - const [text, setText] = useState("hello"); - const deferredText = useDeferredValue(text, { timeoutMs: 2000 }); - - return ( -
- {/* 保持将当前文本传递给 input */} - - ... - {/* 但在必要时可以将列表“延后” */} - -
- ); - } -``` - -这让我们可以立即显示 `input` 的新文本,从而感觉到网页的响应。同时,`MySlowList` “延后” 2 秒,根据 `timeoutMs` ,更新之前,允许它在后台渲染当前文本。 - -**深入了解延迟值,可以阅读 [Concurrent UI 模式](/docs/concurrent-mode-patterns.html#deferring-a-value)。** - -#### useDeferredValue 配置 {#usedeferredvalue-config} - -```js -const SUSPENSE_CONFIG = { timeoutMs: 2000 }; -``` - -`useDeferredValue` 所接受的**配置参数 Suspense 可选**,该参数包含 `timeoutMs` 字段。此超时(以毫秒为单位)表示延迟的值允许延后多长时间。 - -当网络和设备允许时,React 始终会尝试使用较短的延迟。 diff --git a/content/docs/concurrent-mode-suspense.md b/content/docs/concurrent-mode-suspense.md deleted file mode 100644 index a47f33c257..0000000000 --- a/content/docs/concurrent-mode-suspense.md +++ /dev/null @@ -1,738 +0,0 @@ ---- -id: concurrent-mode-suspense -title: 用于数据获取的 Suspense(试验阶段) -permalink: docs/concurrent-mode-suspense.html -prev: concurrent-mode-intro.html -next: concurrent-mode-patterns.html ---- - - - -
- ->**注意**: -> ->本章节所描述的功能还处于实验阶段,在稳定版本中尚不可用。它面向的人群是早期使用者以及好奇心较强的人。 -> ->本页面中许多信息现已过时,仅仅是为了存档而存在。欲了解最新信息,**请参阅 [React 18 Alpha 版公告](/blog/2021/06/08/the-plan-for-react-18.html)**。 -> ->在 React 18 发布前,我们将用稳定的文档替代此章节。 - -
- -React 16.6 新增了 `` 组件,让你可以“等待”目标代码加载,并且可以直接指定一个加载的界面(像是个 spinner),让它在用户等待的时候显示: - -```jsx -const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懒加载 - -// 在 ProfilePage 组件处于加载阶段时显示一个 spinner -}> - - -``` - -用于数据获取的 Suspense 是一个新特性,你可以使用 `` **以声明的方式来“等待”任何内容,包括数据。**本文重点介绍它在数据获取的用例,它也可以用于等待图像、脚本或其他异步的操作。 - -- [何为 Suspense?](#what-is-suspense-exactly) - - [什么不是 Suspense](#what-suspense-is-not) - - [Suspense 可以做什么](#what-suspense-lets-you-do) -- [在实践中使用 Suspense](#using-suspense-in-practice) - - [如果我不使用 Relay,怎么办?](#what-if-i-dont-use-relay) - - [致库作者](#for-library-authors) -- [传统实现方法 vs Suspense](#traditional-approaches-vs-suspense) - - [方法 1:Fetch-on-render(渲染之后获取数据,不使用 Suspense)](#approach-1-fetch-on-render-not-using-suspense) - - [方法 2:Fetch-then-render(接收到全部数据之后渲染,不使用 Suspense)](#approach-2-fetch-then-render-not-using-suspense) - - [方法 3:Render-as-you-fetch(获取数据之后渲染,使用 Suspense)](#approach-3-render-as-you-fetch-using-suspense) -- [尽早开始获取数据](#start-fetching-early) - - [我们仍在寻求方法中](#were-still-figuring-this-out) -- [Suspense 和 Race Conditions](#suspense-and-race-conditions) - - [涉及 useEffect 的 Race Conditions](#race-conditions-with-useeffect) - - [涉及 componentDidUpdate 的 Race Conditions](#race-conditions-with-componentdidupdate) - - [Race Conditions 问题](#the-problem) - - [借助 Suspense 消除 Race Condition](#solving-race-conditions-with-suspense) -- [错误处理](#handling-errors) -- [下一步](#next-steps) - -## 何为 Suspense?{#what-is-suspense-exactly} - -Suspense 让组件“等待”某个异步操作,直到该异步操作结束即可渲染。在下面[例子](https://codesandbox.io/s/frosty-hermann-bztrp)中,两个组件都会等待异步 API 的返回值: - -```js -const resource = fetchProfileData(); - -function ProfilePage() { - return ( - Loading profile...}> - - Loading posts...}> - - - - ); -} - -function ProfileDetails() { - // 尝试读取用户信息,尽管该数据可能尚未加载 - const user = resource.user.read(); - return

{user.name}

; -} - -function ProfileTimeline() { - // 尝试读取博文信息,尽管该部分数据可能尚未加载 - const posts = resource.posts.read(); - return ( -
    - {posts.map(post => ( -
  • {post.text}
  • - ))} -
- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/frosty-hermann-bztrp)** - -上面的 demo 只是个示意。别担心看不懂代码。我们后面会详细说明这部分代码的运作方式。需要记住的是,Suspense 其实更像是一种*机制*,而 demo 中那些具体的 API,如 `fetchProfileData()` 或者 `resource.posts.read()`,这些 API 本身并不重要。不过,如果你还是对它们很好奇,可以在这个 [demo sandbox](https://codesandbox.io/s/frosty-hermann-bztrp) 中找到它们的定义。 - -Suspense 不是一个数据请求的库,而是一个机制。这个**机制是用来给数据请求库**向 React 通信说明*某个组件正在读取的数据当前仍不可用*。通信之后,React 可以继续等待数据的返回并更新 UI。在 Facebook,我们用了 Relay 和它的[集成 Suspense 新功能](https://relay.dev/docs/getting-started/step-by-step-guide/) 。我们期望其他的库,如 Apollo,也能支持类似的集成。 - -从长远来看,我们想让 Suspense 成为组件读取异步数据的主要方式——无论数据来自何方。 - -### 什么不是 Suspense {#what-suspense-is-not} - -Suspense 和当下其他解决异步问题的方法存在明显差异,因而,第一次接触 Suspense 容易让人产生误解。下面我们阐述下常见的误解: - -* **它不是数据获取的一种实现。**它并不假定你使用 GraphQL,REST,或者任何其他特定的数据格式、库、数据传输方式、协议。 - -* **它不是一个可以直接用于数据获取的客户端。**你不能用 Suspense 来“替代” `fetch` 或者 Relay。不过你可以使用集成 Suspense 的库(比如说,[新的 Relay API](https://relay.dev/docs/api-reference/relay-environment-provider/))。 - -* **它不使数据获取与视图层代码耦合。**它协助编排加载状态在 UI 中的显示,但它并不将你的网络逻辑捆绑到 React 组件。 - -### Suspense 可以做什么 {#what-suspense-lets-you-do} - -说了那么多,Suspense 到底有什么用呢?对于这个问题,我们可以从不同的角度来回答: - -* **它能让数据获取库与 React 紧密整合。**如果一个数据请求库实现了对 Suspense 的支持,那么,在 React 中使用 Suspense 将会是自然不过的事。 - -* **它能让你有针对性地安排加载状态的展示。**虽然它不干涉数据_怎样_获取,但它可以让你对应用的视图加载顺序有更大的控制权。 - -* **它能够消除 race conditions。**即便是用上 `await`,异步代码还是很容易出错。相比之下,Suspense 更给人*同步*读取数据的感觉 —— 假定数据已经加载完毕。 - -## 在实践中使用 Suspense {#using-suspense-in-practice} - -在 Facebook 中,我们目前只在生产环境使用集成了 Suspense 的 Relay。**如果你正在找一份实用指南来上手 Suspense,[可以看这份 Relay 指南](https://relay.dev/docs/getting-started/step-by-step-guide/)**!指南中写明了当前运行在我们在生产环境中的可用模式。 - -**本文所有演示代码均使用“伪”API 实现,而不是 Relay。**我们这样做的目的是想让代码本身更易懂些,让不熟悉 GraphQL 的读者也能看懂代码。也正因为示例代码使用“伪 API”,示例代码本身并不是在应用中使用 Suspense 的“正确方式”。可以说,本文是从概念上出发,目的是帮你了解*为什么* Suspense 是以特定方式运行,以及 Suspense 解决了哪些问题这两件事情。 - -### 如果我不使用 Relay,怎么办?{#what-if-i-dont-use-relay} - -如果你当下并不使用 Relay,那么你暂时无法在应用中试用 Suspense。因为迄今为止,在实现了 Suspense 的库中,Relay 是我们唯一在生产环境测试过,且对它的运作有把握的一个库。 - -在接下来的几个月里,许多库将会实现它们各自支持 Suspense 的 API。**如果你倾向于等到技术更加稳定之后才开始学习,那大概率你会先不看这部分文档,等到 Suspense 的生态更成熟之后再回来学习。** - -如果你有兴趣的话,也可以自己开发,然后将你对 Suspense 的实现集成到某个数据请求库中。 - -### 致库作者 {#for-library-authors} - -我们很期待看到社区中其他库对 Suspense 进行试验。对于数据请求库的作者,有一件重要的事情需要你们引起注意。 - -尽管实现对 Suspense 的支持从技术上是可行的,Suspense 当前**并不**作为在组件渲染的时候开始获取数据的方式。反而,它让组件表达出它们在正在“等待”*已经发出获取行为的*数据。**[使用 Concurrent 模式和 Suspense 来构建优秀的用户体验](/blog/2019/11/06/building-great-user-experiences-with-concurrent-mode-and-suspense.html)一文说明了这一点的重要性,以及如何在实践中实现这个模式。** - -除非你有现成的解决方法来避免瀑布(waterfall)问题,我们建议采用支持在渲染之前就能先获取数据的 API。关于实现这类 API 的具体例子,你可以查看 [Relay Suspense API](https://relay.dev/docs/api-reference/use-preloaded-query/) 实现预加载的方式。对于这方面的信息,我们当前给出的和过去给出的并不完全一致。因为 Suspense 用于数据获取还处于试验阶段,我们的建议会随着我们对 Suspense 在生产环境中的使用习得和对瀑布问题的理解,而发生变化。 - -## 传统实现方式 vs Suspense {#traditional-approaches-vs-suspense} - -我们可以完全不提及当前主流的数据获取方式,只介绍 Suspense。但这样做的话,以下 3 件事情:Suspense 解决了什么问题、为什么这些问题值得处理、以及 Suspense 和其他现存方法的不同,会更难理解。 - -因此,我们把 Suspense 看作是一系列解决方法的下一步逻辑演化。从这个角度对其展开介绍: - -* **Fetch-on-render(渲染之后获取数据,如:在 `useEffect` 中 `fetch`):**先开始渲染组件,每个完成渲染的组件都可能在它们的 effects 或者生命周期函数中获取数据。这种方式经常导致“瀑布”问题。 -* **Fetch-then-render(接收到全部数据之后渲染,如:不使用 Suspense 的 Relay):**先尽早获取下一屏需要的所有数据,数据准备好后,渲染新的屏幕。但在数据拿到之前,我们什么事也做不了。 -* **Render-as-you-fetch(获取数据之后渲染,如:使用了 Suspense 的 Relay):**先尽早获取下一屏需要的所有数据,然后*立刻*渲染新的屏幕——*在网络响应可用之前就开始*。在接收到数据的过程中,React迭代地渲染需要数据的组件,直到渲染完所有内容为止。 - ->**注意** -> ->这部分流程经过了简化处理,在实际应用中,真正被采用的方法通常是由不同方法混合而成。但是,我们接下来还是会逐一审视这些方法,因为这样可以让我们更好理解它们各自的优劣。 - -为了对比这 3 个方法,我们分别用它们实现一个 profile 页面。 - -### 方法 1:Fetch-on-render(渲染之后获取数据,不使用 Suspense){#approach-1-fetch-on-render-not-using-suspense} - -目前 React 应用中常用的数据获取方式是使用 effect: - -```js -// 在函数组件中: -useEffect(() => { - fetchSomething(); -}, []); - -// 或者,在 class 组件里: -componentDidMount() { - fetchSomething(); -} -``` - -我们称这种方法为“fetch-on-render”(渲染之后获取数据),因为数据的获取是发生在组件被渲染到屏幕*之后*。这种方法会导致“瀑布”的问题。 - -仔细看下面的 `` 和 `` 组件: - -```js{4-6,22-24} -function ProfilePage() { - const [user, setUser] = useState(null); - - useEffect(() => { - fetchUser().then(u => setUser(u)); - }, []); - - if (user === null) { - return

Loading profile...

; - } - return ( - <> -

{user.name}

- - - ); -} - -function ProfileTimeline() { - const [posts, setPosts] = useState(null); - - useEffect(() => { - fetchPosts().then(p => setPosts(p)); - }, []); - - if (posts === null) { - return

Loading posts...

; - } - return ( -
    - {posts.map(post => ( -
  • {post.text}
  • - ))} -
- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/fragrant-glade-8huj6)** - -如果你运行上面的代码,你会发现 console 打印如下信息: - -1. We start fetching user details(我们开始获取用户信息) -2. We wait...(我们处于等待中) -3. We finish fetching user details(我们接收完所有的用户信息) -4. We start fetching posts(我们开始获取博文数据) -5. We wait...(我们处于等待中) -6. We finish fetching posts(我们接收完所有的博文数据) - -假设获取用户信息需要 3 秒,那么在这个方法中,我们只能在 3 秒之后,才*开始*获取博文数据。这就是上面提到的“瀑布”问题:本该并行发出的请求无意中被*串行*发送出去。 - -在渲染之后再获取数据是引发“瀑布”问题的常见原因。虽然这种情况下的“瀑布”问题可以被解决,但随着项目代码的增多,开发者更倾向于选用其他不会引发这个问题的数据获取方法。 - -### 方法 2:Fetch-then-render(接收到全部数据之后渲染,不使用 Suspense){#approach-2-fetch-then-render-not-using-suspense} - -通过提供更集中化的方式来实现数据获取,库可以避免“瀑布”问题。比如说,Relay 是通过把组件所需的数据转移到可静态分析的*fragments*上,*fragments*随后会被整合进一个单一的请求。 - -在本文中,我们不假定读者了解 Relay,因而我们不会在方法 2 的示例代码中使用它。我们做的,是手动把获取数据的方法合并到一起,来模拟 Relay 的行为: - -```js -function fetchProfileData() { - return Promise.all([ - fetchUser(), - fetchPosts() - ]).then(([user, posts]) => { - return {user, posts}; - }) -} -``` - -在下面例子中,`` 等待两个并行发出的请求: - -```js{1,2,8-13} -// 尽早开始获取数据 -const promise = fetchProfileData(); - -function ProfilePage() { - const [user, setUser] = useState(null); - const [posts, setPosts] = useState(null); - - useEffect(() => { - promise.then(data => { - setUser(data.user); - setPosts(data.posts); - }); - }, []); - - if (user === null) { - return

Loading profile...

; - } - return ( - <> -

{user.name}

- - - ); -} - -// 子组件不再触发数据请求 -function ProfileTimeline({ posts }) { - if (posts === null) { - return

Loading posts...

; - } - return ( -
    - {posts.map(post => ( -
  • {post.text}
  • - ))} -
- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/wandering-morning-ev6r0)** - -在方法 2 中,console 打印的信息变成这样: - -1. We start fetching user details(我们开始获取用户信息) -2. We start fetching posts(我们开始获取博文数据) -3. We wait...(我们处于等待中) -4. We finish fetching user details(我们接收完所有的用户信息) -5. We finish fetching posts(我们接收完所有的博文数据) - -这里,我们解决了之前出现的网络“瀑布”问题,却意外引出另外一个问题。我们在 `fetchProfileData` 方法里利用 `Promise.all()` 来等待*所有*数据,这就导致了,即便我们先接收完用户信息的数据,我们也不能先渲染 `ProfileDetails` 这个组件,还得等到博文信息也接收完才行。在这个方法中,我们必须等到两份数据都接收完毕。 - -当然,这个例子的问题是可以解决的。我们可以去掉 `Promise.all()` ,改用分别等待两个 Promise 的方式来解决。但随着数据和组件树复杂度的增加,这个方法的缺点会逐渐显现出来。如果数据树中出现部分数据的缺失或者过时,则很难写出健壮可靠的组件。因此,在拿到新屏幕所需的全部数据之后,*再*去渲染页面通常是一个比较现实的选择。 - -### 方法 3:Render-as-you-fetch(获取数据之后渲染,使用 Suspense){#approach-3-render-as-you-fetch-using-suspense} - -在上面方法 2 中,我们是在调用 `setState` 之前就开始获取数据: - -1. 开始获取数据 -2. 结束获取数据 -3. 开始渲染 - -有了 Suspense,我们依然可以先获取数据,而且可以给上面流程的 2、3 步骤调换顺序: - -1. 开始获取数据 -2. **开始渲染** -3. **结束获取数据** - -**有了 Suspense,我们不必等到数据全部返回才开始渲染**。实际上,我们是一发送网络请求,*就马上*开始渲染: - -```js{2,17,23} -// 这不是一个 Promise。这是一个支持 Suspense 的特殊对象。 -const resource = fetchProfileData(); - -function ProfilePage() { - return ( - Loading profile...}> - - Loading posts...}> - - - - ); -} - -function ProfileDetails() { - // 尝试读取用户信息,尽管信息可能未加载完毕 - const user = resource.user.read(); - return

{user.name}

; -} - -function ProfileTimeline() { - // 尝试读取博文数据,尽管数据可能未加载完毕 - const posts = resource.posts.read(); - return ( -
    - {posts.map(post => ( -
  • {post.text}
  • - ))} -
- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/frosty-hermann-bztrp)** - -以下是方法 3 中当我们渲染 `` 时会发生的事情: - -1. 我们一开始就通过 `fetchProfileData()` 发出请求。这个方法返回给我们一个特殊的对象“resource”,而不是一个 Promise。在现实的案例中,这个对象是由 Relay 通过集成了 Suspense 来提供的。 -2. React 尝试渲染 ``。该组件返回两个子组件:`` 和 ``。 -3. React 尝试渲染 ``。该组件调用了 `resource.user.read()`,但因为读取的数据还没被获取完毕,所以组件会处于一个“挂起”的状态。React 会跳过这个组件,继续渲染组件树中的其他组件。 -4. React 尝试渲染 ``。该组件调用了 `resource.posts.read()`,和上面一样,数据还没获取完毕,所以这个组件也是处在“挂起”的状态。React 同样跳过这个组件,去渲染组件树中的其他组件。 -5. 组件树中已经没有其他组件需要渲染了。因为 `` 组件处于“挂起”状态,React 则是显示出距其上游最近的 `` fallback:`

Loading profile...

` 。渲染到这里就结束了。 - -这里的 `resource` 对象表示尚未存在但最终将被加载的数据。当我们调用 `read()` 的时候,我们要么获取数据,要么组件处于“挂起”状态。 - -**随着更多数据的到来,React 将尝试重新渲染,并且每次都可能渲染出更加完整的组件树。**当 `resource.user` 的数据获取完毕之后,`` 组件就能被顺利渲染出来,这时,我们就不再需要展示 `

Loading profile...

` 这个 fallback 了。当我们拿到全部数据之后,所有的 fallbacks 就都可以不展示了。 - -这意味着一个有趣的事实,即使我们使用 GraphQL 客户端来收集单个请求中需要的所有数据,*流式响应也可以使我们尽早显示更多的内容*。*在数据获取时(render-as-we-fetch)*(而不是全部数据获取*后*)渲染,因此,如果 `user` 在响应中比 `posts` 出现得更早,我们则可以在响应结束之前“解锁”外层的 `` 边界。我们之前并没有意识到这一点,即便是 fetch-then-render(接收到全部数据之后渲染)这个解决方案,在数据获取和渲染之间也有“瀑布”问题。Suspense 没有这个“瀑布”问题,像 Relay 这样的库就利用了这个优势。 - -请注意,我们是如何在组件中去掉 `if (…)`“is loading” 这个检查的。这不仅删除了样板代码,还简化了代码设计快速转变的流程。举个例子,如果我们想同时“弹出” `` 组件和 `` 组件,只需删除两者之间的 ``。或者,我们可以通过*给它们各自的*`` 来让两者彼此独立。通过Suspense,我们可以更改加载状态的粒度并控制顺序,而无需调整代码。 - -## 尽早开始获取数据 {#start-fetching-early} - -如果你正在开发数据请求库,对于“获取数据之后渲染”这个方法,你会想知道一件至关重要的事情:**我们是在渲染*之前*就进行数据获取**。可以仔细观察下面代码: - -```js -// 一早就开始数据获取,在渲染之前! -const resource = fetchProfileData(); - -// ... - -function ProfileDetails() { - // 尝试读取用户信息 - const user = resource.user.read(); - return

{user.name}

; -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/frosty-hermann-bztrp)** - -请注意,此示例中的 `read()` 调用尚未*开始*。 它只是试图读取**已经获取的数据**。 在创建具有 Suspense 的敏捷应用程序时,这种差异非常重要。 我们并不想把数据获取推迟到组件渲染之后。因此,作为数据请求库的作者,你可以将 `resource` 对象设计成在数据开始请求之前无法被获取,来实现数据请求先发生于组件渲染。 本文中所有使用“伪 API” 的演示都实现了对请求和渲染的顺序控制。 - -你可能认为演示代码中“在最顶层”获取数据的操作不切实际。如果我们想跳转到另一个人的 profile 页面怎么办?我们可能希望根据 props 获取数据。则解决问题的方法是:**在事件处理函数中开始获取数据**。下面是在不同 profile 页面间导航的简单示例: - -```js{1,2,10,11} -// 开始获取数据,越快越好 -const initialResource = fetchProfileData(0); - -function App() { - const [resource, setResource] = useState(initialResource); - return ( - <> - - - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/infallible-feather-xjtbu)** - -通过这种方法,我们可以**并行获取代码和数据**。在页面之间导航时,我们不必等待页面上的代码加载就可以开始加载页面数据。我们可以同时开始获取代码和数据(单击链接时),从而提供更好的用户体验。 - -接下来需解决的问题是:在渲染下个页面之前,我们怎么知道要获取*什么*数据”。对此解决方法有多种(例如,将数据请求集成到路由解决方案附近)。如果你正在开发数据请求库,那么[使用 Concurrent 模式和 Suspense 来构建优秀的用户体验](/blog/2019/11/06/building-great-user-experiences-with-concurrent-mode-and-suspense.html)将深入探讨了如何解决此问题及其重要性。 - -### 我们仍在寻求方法中 {#were-still-figuring-this-out} - -Suspense 本身作为一个机制而言,它灵活可变并且没有太多的限制。而产品的代码需要足够多的限制来保障代码中不会有“瀑布”问题。关于如何提供保障这一点,目前是有不同的实现方式。当下,我们仍在探索以下问题: - -* 提前请求数据可能很困难。我们可以轻松避免瀑布问题吗? -* 当我们获取页面数据时,API是否应该包含数据以便*从*该页面立即转换? -* 响应的有效期是多长?缓存应该是全局的还是本地的?谁管理缓存? -* 可以不通过插入 `read()`,让代理协助表示延迟加载的 APIs 吗? -* 对于任意给定的 Suspense 数据,GraphQL 查询的替代物是什么? - -对于这些问题,Relay 有自己的答案。当然,解决这些问题的方法不止一种,我们很期待看到即将出现在 React 社区的新想法 - -## Suspense 和 Race Conditions {#suspense-and-race-conditions} - -Race Conditions 是由于对代码运行顺序的错误假设而导致的 bug。使用生命周期方法(如:`useEffect` Hook 和类的 `componentDidUpdate` 方法)获取数据经常会导致这种情况。Suspense 在这里很有用,让我们看看如何实现。 - -为了说明问题,我们增加一个顶层组件 `` 来渲染 `` ,并放置一个可以**在不同的 profile 页面之间切换**的按钮: - -```js{9-11} -function getNextId(id) { - // ... -} - -function App() { - const [id, setId] = useState(0); - return ( - <> - - - - ); -} -``` - -让我们比较一下不同的数据获取方法如何实现这个需求。 - -### 涉及 `useEffect` 的 Race Conditions {#race-conditions-with-useeffect} - -首先,我们将尝试使用原始的“在 effect 中获取数据”示例,重写在 `` props 中传递的 `id` 参数,以传给 `fetchUser(id)` 和 `fetchPosts(id)`: - -```js{1,5,6,14,19,23,24} -function ProfilePage({ id }) { - const [user, setUser] = useState(null); - - useEffect(() => { - fetchUser(id).then(u => setUser(u)); - }, [id]); - - if (user === null) { - return

Loading profile...

; - } - return ( - <> -

{user.name}

- - - ); -} - -function ProfileTimeline({ id }) { - const [posts, setPosts] = useState(null); - - useEffect(() => { - fetchPosts(id).then(p => setPosts(p)); - }, [id]); - - if (posts === null) { - return

Loading posts...

; - } - return ( -
    - {posts.map(post => ( -
  • {post.text}
  • - ))} -
- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/nervous-glade-b5sel)** - -请注意,我们将 effect 的依赖从 `[]` 更改为 `[id]`——因为我们希望每次 `id` 更改时都重新运行 effect。 如果不这样做,我们将无法再次获取到新的数据。 - -如果我们尝试运行此代码,一开始看起来运行得很好。但是,如果我们将“伪 API”实现的延迟时间随机化,快速点击“Next”按钮,就会发现控制台日志中有些问题。**有时,在把 profile 页面切换成别的 ID 后,旧的 profile 的请求会“返回”——这种情况下,其他 ID 用的旧响应会覆盖新的 state。** - -这个问题是可以解决的(通过在 effect 里头配置 cleanup 函数来过滤、或者取消过期请求),但它依然是个反直觉的问题,且难以检测。 - -### 涉及 componentDidUpdate 的 Race Conditions {#race-conditions-with-componentdidupdate} - -有的人可能认为 race conditions 这个问题只跟 `useEffect` 有关系,或者只和 Hooks 有关。如果我们把上面代码改用 class 组件来实现,或者用 `async / await` 来写,可能就能避开这个问题? - -先一起试试看: - -```js -class ProfilePage extends React.Component { - state = { - user: null, - }; - componentDidMount() { - this.fetchData(this.props.id); - } - componentDidUpdate(prevProps) { - if (prevProps.id !== this.props.id) { - this.fetchData(this.props.id); - } - } - async fetchData(id) { - const user = await fetchUser(id); - this.setState({ user }); - } - render() { - const { id } = this.props; - const { user } = this.state; - if (user === null) { - return

Loading profile...

; - } - return ( - <> -

{user.name}

- - - ); - } -} - -class ProfileTimeline extends React.Component { - state = { - posts: null, - }; - componentDidMount() { - this.fetchData(this.props.id); - } - componentDidUpdate(prevProps) { - if (prevProps.id !== this.props.id) { - this.fetchData(this.props.id); - } - } - async fetchData(id) { - const posts = await fetchPosts(id); - this.setState({ posts }); - } - render() { - const { posts } = this.state; - if (posts === null) { - return

Loading posts...

; - } - return ( -
    - {posts.map(post => ( -
  • {post.text}
  • - ))} -
- ); - } -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/trusting-clarke-8twuq)** - -上面代码看似简单易读,实则暗含同样的问题。 - -所以很不幸,无论是改用 class 组件,还是改用 `async / await` 都没能解决 race conditions。上面代码和用 Hooks 一样都有问题,问题的源头也一样。 - -### Race Conditions 问题 {#the-problem} - -React 组件有它们自己的“生命周期”。组件可能在任意时间点接收到 props 或者更新 state。然而,每一个异步请求*同样也*有自己的“生命周期”。异步请求的生命周期开始于我们发出请求,结束于我们收到响应报文。这里我们面临的问题是,如何在这两类生命周期之间及时进行“同步”。这个问题很是棘手。 - -### 借助 Suspense 消除 Race Condition {#solving-race-conditions-with-suspense} - -我们再来重写上面代码,但这次我们只用 Suspense 来写: - -```js -const initialResource = fetchProfileData(0); - -function App() { - const [resource, setResource] = useState(initialResource); - return ( - <> - - - - ); -} - -function ProfilePage({ resource }) { - return ( - Loading profile...}> - - Loading posts...}> - - - - ); -} - -function ProfileDetails({ resource }) { - const user = resource.user.read(); - return

{user.name}

; -} - -function ProfileTimeline({ resource }) { - const posts = resource.posts.read(); - return ( -
    - {posts.map(post => ( -
  • {post.text}
  • - ))} -
- ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/infallible-feather-xjtbu)** - -在这个用上 Suspense 的示例中,我们只需要获取一个数据 `resource`,所以我们把它提到最外层,作为顶层变量。考虑到我们有多个 resources,我们把这个变量放入 `` 组件的 state。 - -```js{4} -const initialResource = fetchProfileData(0); - -function App() { - const [resource, setResource] = useState(initialResource); -``` - -当我们点击“Next”按钮,`` 组件便开始发出获取下一个 profile 的请求,并把*请求返回的*对象下传给 `` 组件。 - -```js{4,8} - <> - - - -``` - -再次申明,要注意**这里我们并不是干等到响应报文被接收之后,才去更新 state,而是反过来:我们发出请求之后,马上就开始更新 state(外加开始渲染)**。一旦收到响应,React 就把可用的数据用到 `` 组件里头。 - -和之前的两版一样,这版代码的可读性也很强,但它和之前的有所不同,Suspense 的这版不会有 race conditions 问题。你可能好奇为什么不会有,这是因为,相比之前的两版,在 Suspense 的这版里,我们不需要太怎么考虑*时机*这个事情。在第一版有 race conditions 问题的代码中,state 需要*在准确的时机*设置,否则就会出错。然而在 Suspense 版本里,我们是都是在获取数据之后,立马就设置 state——因此大大降低出错的概率。 - -## 错误处理 {#handling-errors} - -每当使用 Promises,大概率我们会用 `catch()` 来做错误处理。但当我们用 Suspense 时,我们不*等待* Promises 就直接开始渲染,这时 `catch()` 就不适用了。这种情况下,错误处理该怎么进行呢? - -在 Suspense 中,获取数据时抛出的错误和组件渲染时的报错处理方式一样——你可以在需要的层级渲染一个[错误边界](/docs/error-boundaries.html)组件来“捕捉”层级下面的所有的报错信息。 - -首先,给我们的项目定义一个错误边界组件: - -```js -// 目前,错误边界组件只支持通过 class 组件定义。 -class ErrorBoundary extends React.Component { - state = { hasError: false, error: null }; - static getDerivedStateFromError(error) { - return { - hasError: true, - error - }; - } - render() { - if (this.state.hasError) { - return this.props.fallback; - } - return this.props.children; - } -} -``` - -其次,我们将它放到组件树中任意我们想要的地方来捕捉错误: - -```js{5,9} -function ProfilePage() { - return ( - Loading profile...}> - - Could not fetch posts.}> - Loading posts...}> - - - - - ); -} -``` - -**[在 CodeSandbox 中尝试](https://codesandbox.io/s/adoring-goodall-8wbn7)** - -上面代码中的错误边界组件既能捕捉渲染过程的报错,*也*能捕捉 Suspense 里头数据获取的报错。理论上,我们在组件树中插入多少个错误边界组件都是可以的,但这并不是推荐的做法,错误边界组件的位置最好是深思熟虑之后再确定。 - -## 下一步 {#next-steps} - -到这里,我们已经介绍完当 Suspense 用于数据获取时的基本内容。更重要的是,我们现在对 Suspense 有自己运作方式的*原因*,以及它是如何在数据获取这个领域中发挥自己的作用这两点有更好的理解。 - -Suspense 本身解答了一些问题,但同时它也引出一些新的问题: - -* 如果部分组件处于“挂起”状态,整个应用会卡死吗?该怎么避免这个问题? -* 如果我们不想在目标组件的上层,而想在其他地方展示 spinner,可以实现吗? -* 如果我们*想*有计划地在一个短的时间内展示不同的 UI,能够实现吗? -* 除了展示个 spinner,我们能添加额外的视觉效果吗?像是给现有界面加上蒙层之类的? -* 在[最后一个 Suspense 的代码示例中](https://codesandbox.io/s/infallible-feather-xjtbu),为什么在点击了“Next”按钮之后,会报出警告? - -对于上述问题的解答,我们将交由下一章节 [Concurrent UI 模式](/docs/concurrent-mode-patterns.html)处理。 diff --git a/content/docs/conditional-rendering.md b/content/docs/conditional-rendering.md deleted file mode 100644 index a9b0716786..0000000000 --- a/content/docs/conditional-rendering.md +++ /dev/null @@ -1,257 +0,0 @@ ---- -id: conditional-rendering -title: 条件渲染 -permalink: docs/conditional-rendering.html -prev: handling-events.html -next: lists-and-keys.html -redirect_from: - - "tips/false-in-jsx.html" ---- - -> Try the new React documentation. -> -> These new documentation pages teach modern React and include live examples: -> -> - [Conditional Rendering](https://beta.reactjs.org/learn/conditional-rendering) -> -> The new docs will soon replace this site, which will be archived. [Provide feedback.](https://github.com/reactjs/reactjs.org/issues/3308) - - -在 React 中,你可以创建不同的组件来封装各种你需要的行为。然后,依据应用的不同状态,你可以只渲染对应状态下的部分内容。 - -React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 [`if`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if...else) 或者[条件运算符](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Conditional_Operator)去创建元素来表现当前的状态,然后让 React 根据它们来更新 UI。 - -观察这两个组件: - -```js -function UserGreeting(props) { - return

Welcome back!

; -} - -function GuestGreeting(props) { - return

Please sign up.

; -} -``` - -再创建一个 `Greeting` 组件,它会根据用户是否登录来决定显示上面的哪一个组件。 - -```javascript{3-7,11,12} -function Greeting(props) { - const isLoggedIn = props.isLoggedIn; - if (isLoggedIn) { - return ; - } - return ; -} - -const root = ReactDOM.createRoot(document.getElementById('root')); -// Try changing to isLoggedIn={true}: -root.render(); -``` - -[**在 CodePen 上尝试**](https://codepen.io/gaearon/pen/ZpVxNq?editors=0011) - -这个示例根据 `isLoggedIn` 的值来渲染不同的问候语。 - -### 元素变量 {#element-variables} - -你可以使用变量来储存元素。 它可以帮助你有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变。 - -观察这两个组件,它们分别代表了注销和登录按钮: - -```js -function LoginButton(props) { - return ( - - ); -} - -function LogoutButton(props) { - return ( - - ); -} -``` - -在下面的示例中,我们将创建一个名叫 `LoginControl` 的[有状态的组件](/docs/state-and-lifecycle.html#adding-local-state-to-a-class)。 - -它将根据当前的状态来渲染 `` 或者 ``。同时它还会渲染上一个示例中的 ``。 - -```javascript{20-25,29,30} -class LoginControl extends React.Component { - constructor(props) { - super(props); - this.handleLoginClick = this.handleLoginClick.bind(this); - this.handleLogoutClick = this.handleLogoutClick.bind(this); - this.state = {isLoggedIn: false}; - } - - handleLoginClick() { - this.setState({isLoggedIn: true}); - } - - handleLogoutClick() { - this.setState({isLoggedIn: false}); - } - - render() { - const isLoggedIn = this.state.isLoggedIn; - let button; - - if (isLoggedIn) { - button = ; - } else { - button = ; - } - - return ( -
- - {button} -
- ); - } -} - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render(); -``` - -[**在 CodePen 上尝试**](https://codepen.io/gaearon/pen/QKzAgB?editors=0010) - -声明一个变量并使用 `if` 语句进行条件渲染是不错的方式,但有时你可能会想使用更为简洁的语法。接下来,我们将介绍几种在 JSX 中内联条件渲染的方法。 - -### 与运算符 && {#inline-if-with-logical--operator} - -通过花括号包裹代码,你可以[在 JSX 中嵌入表达式](/docs/introducing-jsx.html#embedding-expressions-in-jsx)。这也包括 JavaScript 中的逻辑与 (&&) 运算符。它可以很方便地进行元素的条件渲染: - -```js{6-10} -function Mailbox(props) { - const unreadMessages = props.unreadMessages; - return ( -
-

Hello!

- {unreadMessages.length > 0 && -

- You have {unreadMessages.length} unread messages. -

- } -
- ); -} - -const messages = ['React', 'Re: React', 'Re:Re: React']; - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render(); -``` - -[**在 CodePen 上尝试**](https://codepen.io/gaearon/pen/ozJddz?editors=0010) - -之所以能这样做,是因为在 JavaScript 中,`true && expression` 总是会返回 `expression`, 而 `false && expression` 总是会返回 `false`。 - -因此,如果条件是 `true`,`&&` 右侧的元素就会被渲染,如果是 `false`,React 会忽略并跳过它。 - -请注意,[falsy 表达式](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) 会使 `&&` 后面的元素被跳过,但会返回 falsy 表达式的值。在下面示例中,render 方法的返回值是 `
0
`。 - -```javascript{2,5} -render() { - const count = 0; - return ( -
- {count &&

Messages: {count}

} -
- ); -} -``` - -### 三目运算符 {#inline-if-else-with-conditional-operator} - -另一种内联条件渲染的方法是使用 JavaScript 中的三目运算符 [`condition ? true : false`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Conditional_Operator)。 - -在下面这个示例中,我们用它来条件渲染一小段文本: - -```javascript{5} -render() { - const isLoggedIn = this.state.isLoggedIn; - return ( -
- The user is {isLoggedIn ? 'currently' : 'not'} logged in. -
- ); -} -``` - -同样的,它也可以用于较为复杂的表达式中,虽然看起来不是很直观: - -```js{5,7,9} -render() { - const isLoggedIn = this.state.isLoggedIn; - return ( -
- {isLoggedIn - ? - : - } -
- ); -} -``` - -就像在 JavaScript 中一样,你可以根据团队的习惯来选择可读性更高的代码风格。需要注意的是,如果条件变得过于复杂,那你应该考虑如何[提取组件](/docs/components-and-props.html#extracting-components)。 - -### 阻止组件渲染 {#preventing-component-from-rendering} - -在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让 `render` 方法直接返回 `null`,而不进行任何渲染。 - -下面的示例中,`` 会根据 prop 中 `warn` 的值来进行条件渲染。如果 `warn` 的值是 `false`,那么组件则不会渲染: - -```javascript{2-4,29} -function WarningBanner(props) { - if (!props.warn) { - return null; - } - - return ( -
- Warning! -
- ); -} - -class Page extends React.Component { - constructor(props) { - super(props); - this.state = {showWarning: true}; - this.handleToggleClick = this.handleToggleClick.bind(this); - } - - handleToggleClick() { - this.setState(state => ({ - showWarning: !state.showWarning - })); - } - - render() { - return ( -
- - -
- ); - } -} - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render(); -``` - -[**在 CodePen 上尝试**](https://codepen.io/gaearon/pen/Xjoqwm?editors=0010) - -在组件的 `render` 方法中返回 `null` 并不会影响组件的生命周期。例如,上面这个示例中,`componentDidUpdate` 依然会被调用。 diff --git a/content/docs/context.md b/content/docs/context.md deleted file mode 100644 index 519874d4f0..0000000000 --- a/content/docs/context.md +++ /dev/null @@ -1,273 +0,0 @@ ---- -id: context -title: Context -permalink: docs/context.html ---- - -> Try the new React documentation. -> -> These new documentation pages teach modern React and include live examples: -> -> - [Passing Data Deeply with Context](https://beta.reactjs.org/learn/passing-data-deeply-with-context) -> - [`useContext`](https://beta.reactjs.org/reference/react/useContext) -> -> The new docs will soon replace this site, which will be archived. [Provide feedback.](https://github.com/reactjs/reactjs.org/issues/3308) - -Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。 - -在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但此种用法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。 - -- [何时使用 Context {#when-to-use-context}](#何时使用-context-when-to-use-context) -- [使用 Context 之前的考虑 {#before-you-use-context}](#使用-context-之前的考虑-before-you-use-context) -- [API {#api}](#api-api) - - [`React.createContext` {#reactcreatecontext}](#reactcreatecontext-reactcreatecontext) - - [`Context.Provider` {#contextprovider}](#contextprovider-contextprovider) - - [`Class.contextType` {#classcontexttype}](#classcontexttype-classcontexttype) - - [`Context.Consumer` {#contextconsumer}](#contextconsumer-contextconsumer) - - [`Context.displayName` {#contextdisplayname}](#contextdisplayname-contextdisplayname) -- [示例 {#examples}](#示例-examples) - - [动态 Context {#dynamic-context}](#动态-context-dynamic-context) - - [在嵌套组件中更新 Context {#updating-context-from-a-nested-component}](#在嵌套组件中更新-context-updating-context-from-a-nested-component) - - [消费多个 Context {#consuming-multiple-contexts}](#消费多个-context-consuming-multiple-contexts) -- [注意事项 {#caveats}](#注意事项-caveats) -- [过时的 API {#legacy-api}](#过时的-api-legacy-api) - -## 何时使用 Context {#when-to-use-context} - -Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式: - -`embed:context/motivation-problem.js` - -使用 context, 我们可以避免通过中间元素传递 props: - -`embed:context/motivation-solution.js` - -## 使用 Context 之前的考虑 {#before-you-use-context} - -Context 主要应用场景在于*很多*不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。 - -**如果你只是想避免层层传递一些属性,[组件组合(component composition)](/docs/composition-vs-inheritance.html)有时候是一个比 context 更好的解决方案。** - -比如,考虑这样一个 `Page` 组件,它层层向下传递 `user` 和 `avatarSize` 属性,从而让深度嵌套的 `Link` 和 `Avatar` 组件可以读取到这些属性: - -```js - -// ... 渲染出 ... - -// ... 渲染出 ... - -// ... 渲染出 ... - - - -``` - -如果在最后只有 `Avatar` 组件真的需要 `user` 和 `avatarSize`,那么层层传递这两个 props 就显得非常冗余。而且一旦 `Avatar` 组件需要更多从来自顶层组件的 props,你还得在中间层级一个一个加上去,这将会变得非常麻烦。 - -一种 **无需 context** 的解决方案是[将 `Avatar` 组件自身传递下去](/docs/composition-vs-inheritance.html#containment),因为中间组件无需知道 `user` 或者 `avatarSize` 等 props: - -```js -function Page(props) { - const user = props.user; - const userLink = ( - - - - ); - return ; -} - -// 现在,我们有这样的组件: - -// ... 渲染出 ... - -// ... 渲染出 ... - -// ... 渲染出 ... -{props.userLink} -``` - -这种变化下,只有最顶部的 Page 组件需要知道 `Link` 和 `Avatar` 组件是如何使用 `user` 和 `avatarSize` 的。 - -这种对组件的*控制反转*减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。 - -而且你的组件并不限制于接收单个子组件。你可能会传递多个子组件,甚至会为这些子组件(children)封装多个单独的“接口(slots)”,[正如这里的文档所列举的](/docs/composition-vs-inheritance.html#containment) - -```js -function Page(props) { - const user = props.user; - const content = ; - const topBar = ( - - - - - - ); - return ( - - ); -} -``` - -这种模式足够覆盖很多场景了,在这些场景下你需要将子组件和直接关联的父组件解耦。如果子组件需要在渲染前和父组件进行一些交流,你可以进一步使用 [render props](/docs/render-props.html)。 - -但是,有的时候在组件树中很多不同层级的组件需要访问同样的一批数据。Context 能让你将这些数据向组件树下所有的组件进行“广播”,所有的组件都能访问到这些数据,也能访问到后续的数据更新。使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据,这比替代方案要简单的多。 - -## API {#api} - -### `React.createContext` {#reactcreatecontext} - -```js -const MyContext = React.createContext(defaultValue); -``` - -创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 `Provider` 中读取到当前的 context 值。 - -**只有**当组件所处的树中没有匹配到 Provider 时,其 `defaultValue` 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 `undefined` 传递给 Provider 的 value 时,消费组件的 `defaultValue` 不会生效。 - -### `Context.Provider` {#contextprovider} - -```js - -``` - -每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。 - -Provider 接收一个 `value` 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。 - -当 Provider 的 `value` 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 [.contextType](#classcontexttype) 和 [useContext](/docs/hooks-reference.html#usecontext))的传播不受制于 `shouldComponentUpdate` 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。 - -通过新旧值检测来确定变化,使用了与 [`Object.is`](//developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#Description) 相同的算法。 - -> 注意 -> -> 当传递对象给 `value` 时,检测变化的方式会导致一些问题:详见[注意事项](#caveats)。 - -### `Class.contextType` {#classcontexttype} - -```js -class MyClass extends React.Component { - componentDidMount() { - let value = this.context; - /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */ - } - componentDidUpdate() { - let value = this.context; - /* ... */ - } - componentWillUnmount() { - let value = this.context; - /* ... */ - } - render() { - let value = this.context; - /* 基于 MyContext 组件的值进行渲染 */ - } -} -MyClass.contextType = MyContext; -``` - -挂载在 class 上的 `contextType` 属性可以赋值为由 [`React.createContext()`](#reactcreatecontext) 创建的 Context 对象。此属性可以让你使用 `this.context` 来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中。 - -> 注意: -> -> 你只通过该 API 订阅单一 context。如果你想订阅多个,阅读[使用多个 Context](#consuming-multiple-contexts) 章节 -> -> 如果你正在使用实验性的 [public class fields 语法](https://babeljs.io/docs/plugins/transform-class-properties/),你可以使用 `static` 这个类属性来初始化你的 `contextType`。 - - -```js -class MyClass extends React.Component { - static contextType = MyContext; - render() { - let value = this.context; - /* 基于这个值进行渲染工作 */ - } -} -``` - -### `Context.Consumer` {#contextconsumer} - -```js - - {value => /* 基于 context 值进行渲染*/} - -``` - -一个 React 组件可以订阅 context 的变更,此组件可以让你在[函数式组件](/docs/components-and-props.html#function-and-class-components)中可以订阅 context。 - -这种方法需要一个[函数作为子元素(function as a child)](/docs/render-props.html#using-props-other-than-render)。这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 `value` 值等价于组件树上方离这个 context 最近的 Provider 提供的 `value` 值。如果没有对应的 Provider,`value` 参数等同于传递给 `createContext()` 的 `defaultValue`。 - -> 注意 -> -> 想要了解更多关于 “函数作为子元素(function as a child)” 模式,详见 [render props](/docs/render-props.html)。 - -### `Context.displayName` {#contextdisplayname} - -context 对象接受一个名为 `displayName` 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。 - -示例,下述组件在 DevTools 中将显示为 MyDisplayName: - -```js{2} -const MyContext = React.createContext(/* some value */); -MyContext.displayName = 'MyDisplayName'; - - // "MyDisplayName.Provider" 在 DevTools 中 - // "MyDisplayName.Consumer" 在 DevTools 中 -``` - -## 示例 {#examples} - -### 动态 Context {#dynamic-context} - -一个更加复杂的方案是对上面的 theme 例子使用动态值(dynamic values): - -**theme-context.js** -`embed:context/theme-detailed-theme-context.js` - -**themed-button.js** -`embed:context/theme-detailed-themed-button.js` - -**app.js** -`embed:context/theme-detailed-app.js` - -### 在嵌套组件中更新 Context {#updating-context-from-a-nested-component} - -从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。在这种场景下,你可以通过 context 传递一个函数,使得 consumers 组件更新 context: - -**theme-context.js** -`embed:context/updating-nested-context-context.js` - -**theme-toggler-button.js** -`embed:context/updating-nested-context-theme-toggler-button.js` - -**app.js** -`embed:context/updating-nested-context-app.js` - -### 消费多个 Context {#consuming-multiple-contexts} - -为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。 - -`embed:context/multiple-contexts.js` - -如果两个或者更多的 context 值经常被一起使用,那你可能要考虑一下另外创建你自己的渲染组件,以提供这些值。 - -## 注意事项 {#caveats} - -因为 context 会根据引用标识来决定何时进行渲染(本质上是 `value` 属性值的浅比较),所以这里可能存在一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,由于 `value` 属性总是被赋值为新的对象,以下的代码会重新渲染下面所有的 consumers 组件: - -`embed:context/reference-caveats-problem.js` - -为了防止这种情况,将 value 状态提升到父节点的 state 里: - -`embed:context/reference-caveats-solution.js` - -## 过时的 API {#legacy-api} - -> 注意 -> -> 先前 React 使用实验性的 context API 运行,旧的 API 将会在所有 16.x 版本中得到支持,但用到它的应用应该迁移到新版本。过时的 API 将在未来的 React 版本中被移除。阅读[过时的 context 文档](/docs/legacy-context.html)了解更多。 diff --git a/content/docs/create-a-new-react-app.md b/content/docs/create-a-new-react-app.md deleted file mode 100644 index 3350007a06..0000000000 --- a/content/docs/create-a-new-react-app.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -id: create-a-new-react-app -title: 创建新的 React 应用 -permalink: docs/create-a-new-react-app.html -redirect_from: - - "docs/add-react-to-a-new-app.html" -prev: add-react-to-a-website.html -next: cdn-links.html ---- - -使用集成的工具链,以实现最佳的用户和开发人员体验。 - -本页将介绍一些流行的 React 工具链,它们有助于完成如下任务: - -* 扩展文件和组件的规模。 -* 使用来自 npm 的第三方库。 -* 尽早发现常见错误。 -* 在开发中实时编辑 CSS 和 JS。 -* 优化生产输出。 - -本页推荐的工具链**无需配置即可开始使用**。 - -## 你可能不需要工具链 {#you-might-not-need-a-toolchain} - -如果你没有碰到上述的问题,或者还不习惯使用 JavaScript 工具,可以考虑[把 React 作为普通的 ` -``` - -并且确保 CDN 以 `Access-Control-Allow-Origin: *` HTTP 请求头应答: - -![Access-Control-Allow-Origin: *](../images/docs/cdn-cors-header.png) - -### Webpack {#webpack} - -#### 源码映射 {#source-maps} - -一些 JavaScript 打包器可能在开发过程中以 `eval` 包装应用代码。(例如,Webpack 会用这个如果设置 [`devtool`](https://webpack.js.org/configuration/devtool/) 的值任意包含"eval")。这个可能会引起被视为跨源错误。 - -如果你使用 Webpack,我们推荐在开发中使用 `cheap-module-source-map` 设置来避免这个问题。 - -#### 代码拆分 {#code-splitting} - -如果你的应用被拆分成多个包,这些包可能会使用 JSONP被加载。这些包会被视为跨源资源,从而可能会引发错误。 - -为了处理这个错误, 在标签 ` - -``` - -注意只有以 `.production.min.js` 为结尾的 React 文件适用于生产。 - -### Brunch {#brunch} - -通过安装 [`terser-brunch`](https://github.com/brunch/terser-brunch) 插件,来获得最高效的 Brunch 生产构建: - -``` -# 如果你使用 npm -npm install --save-dev terser-brunch - -# 如果你使用 Yarn -yarn add --dev terser-brunch -``` - -接着,在 `build` 命令后添加 `-p` 参数,以创建生产构建: - -``` -brunch build -p -``` - -请注意,你只需要在生产构建时这么做。你不需要在开发环境中使用 `-p` 参数或者应用这个插件,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。 - -### Browserify {#browserify} - -为了最高效的生产构建,需要安装一些插件: - -``` -# 如果你使用 npm -npm install --save-dev envify terser uglifyify - -# 如果你使用 Yarn -yarn add --dev envify terser uglifyify -``` - -为了创建生产构建,确保你添加了以下转换器 **(顺序很重要)**: - -* [`envify`](https://github.com/hughsk/envify) 转换器用于设置正确的环境变量。设置为全局 (`-g`)。 -* [`uglifyify`](https://github.com/hughsk/uglifyify) 转换器移除开发相关的引用代码。同样设置为全局 (`-g`)。 -* 最后,将产物传给 [`terser`](https://github.com/terser-js/terser) 并进行压缩([为什么要这么做?](https://github.com/hughsk/uglifyify#motivationusage))。 - -举个例子: - -``` -browserify ./index.js \ - -g [ envify --NODE_ENV production ] \ - -g uglifyify \ - | terser --compress --mangle > ./bundle.js -``` - -请注意,你只需要在生产构建时用到它。你不需要在开发环境应用这些插件,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。 - -### Rollup {#rollup} - -为了最高效的 Rollup 生产构建,需要安装一些插件: - -``` -# 如果你使用 npm -npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser - -# 如果你使用 Yarn -yarn add --dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser -``` - -为了创建生产构建,确保你添加了以下插件 **(顺序很重要)**: - -* [`replace`](https://github.com/rollup/rollup-plugin-replace) 插件确保环境被正确设置。 -* [`commonjs`](https://github.com/rollup/rollup-plugin-commonjs) 插件用于支持 CommonJS。 -* [`terser`](https://github.com/TrySound/rollup-plugin-terser) 插件用于压缩并生成最终的产物。 - -```js -plugins: [ - // ... - require('rollup-plugin-replace')({ - 'process.env.NODE_ENV': JSON.stringify('production') - }), - require('rollup-plugin-commonjs')(), - require('rollup-plugin-terser')(), - // ... -] -``` - -[点击](https://gist.github.com/Rich-Harris/cb14f4bc0670c47d00d191565be36bf0)查看完整的安装示例。 - -请注意,你只需要在生产构建时用到它。你不需要在开发中使用 `terser` 插件或者 `replace` 插件替换 `'production'` 变量,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。 - -### webpack {#webpack} - ->**注意:** -> ->如果你使用了 Create React App,请跟随[上面的说明](#create-react-app)进行操作。
->只有当你直接配置了 webpack 才需要参考以下内容。 - -在生产模式下,Webpack v4+ 将默认对代码进行压缩: - -```js -const TerserPlugin = require('terser-webpack-plugin'); - -module.exports = { - mode: 'production', - optimization: { - minimizer: [new TerserPlugin({ /* additional options here */ })], - }, -}; -``` - -你可以在 [webpack 文档](https://webpack.js.org/guides/production/)中了解更多内容。 - -请注意,你只需要在生产构建时用到它。你不需要在开发中使用 `TerserPlugin` 插件,因为这会隐藏有用的 React 警告信息并使得构建速度变慢。 - -## 使用开发者工具中的分析器对组件进行分析 {#profiling-components-with-the-devtools-profiler} - -`react-dom` 16.5+ 和 `react-native` 0.57+ 加强了分析能力。在开发模式下,React 开发者工具会出现分析器标签。 -你可以在[《介绍 React 分析器》](/blog/2018/09/10/introducing-the-react-profiler.html)这篇博客中了解概述。 -你也可以[在 YouTube 上](https://www.youtube.com/watch?v=nySib7ipZdk)观看分析器的视频指导。 - -如果你还未安装 React 开发者工具,你可以在这里找到它们: - -- [Chrome 浏览器扩展](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) -- [Firefox 浏览器扩展](https://addons.mozilla.org/en-GB/firefox/addon/react-devtools/) -- [独立 Node 包](https://www.npmjs.com/package/react-devtools) - -> 注意 -> ->`react-dom` 的生产分析包也可以在 `react-dom/profiling` 中找到。 ->通过查阅 [fb.me/react-profiling](https://fb.me/react-profiling) 来了解更多关于使用这个包的内容。 - -> 注意 -> -> 在 React 17 之前,我们使用了标准的 [User Timing API](https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API),用 chrome 的 performance 性能选项卡来配置组件。 -> 更详细的攻略,请参阅 [Ben Schwarz 的文章](https://calibreapp.com/blog/react-performance-profiling-optimization)。 - -## 虚拟化长列表 {#virtualize-long-lists} - -如果你的应用渲染了长列表(上百甚至上千的数据),我们推荐使用“虚拟滚动”技术。这项技术会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量。 - -[react-window](https://react-window.now.sh/) 和 [react-virtualized](https://bvaughn.github.io/react-virtualized/) 是热门的虚拟滚动库。 -它们提供了多种可复用的组件,用于展示列表、网格和表格数据。 -如果你想要一些针对你的应用做定制优化,你也可以创建你自己的虚拟滚动组件,就像 [Twitter 所做的](https://medium.com/@paularmstrong/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale-d28a00e780a3)。 - -## 避免调停 {#avoid-reconciliation} - -React 构建并维护了一套内部的 UI 渲染描述。它包含了来自你的组件返回的 React 元素。该描述使得 React 避免创建 DOM 节点以及没有必要的节点访问,因为 DOM 操作相对于 JavaScript 对象操作更慢。虽然有时候它被称为“虚拟 DOM”,但是它在 React Native 中拥有相同的工作原理。 - -当一个组件的 props 或 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。当它们不相同时,React 会更新该 DOM。 - -即使 React 只更新改变了的 DOM 节点,重新渲染仍然花费了一些时间。在大部分情况下它并不是问题,不过如果它已经慢到让人注意了,你可以通过覆盖生命周期方法 `shouldComponentUpdate` 来进行提速。该方法会在重新渲染前被触发。其默认实现总是返回 `true`,让 React 执行更新: - -```javascript -shouldComponentUpdate(nextProps, nextState) { - return true; -} -``` - -如果你知道在什么情况下你的组件不需要更新,你可以在 `shouldComponentUpdate` 中返回 `false` 来跳过整个渲染过程。其包括该组件的 `render` 调用以及之后的操作。 - -在大部分情况下,你可以继承 [`React.PureComponent`](/docs/react-api.html#reactpurecomponent) 以代替手写 `shouldComponentUpdate()`。它用当前与之前 props 和 state 的浅比较覆写了 `shouldComponentUpdate()` 的实现。 - -## shouldComponentUpdate 的作用 {#shouldcomponentupdate-in-action} - -这是一个组件的子树。每个节点中,`SCU` 代表 `shouldComponentUpdate` 返回的值,而 `vDOMEq` 代表返回的 React 元素是否相同。最后,圆圈的颜色代表了该组件是否需要被调停。 - -
- -节点 C2 的 `shouldComponentUpdate` 返回了 `false`,React 因而不会去渲染 C2,也因此 C4 和 C5 的 `shouldComponentUpdate` 不会被调用到。 - -对于 C1 和 C3,`shouldComponentUpdate` 返回了 `true`,所以 React 需要继续向下查询子节点。这里 C6 的 `shouldComponentUpdate` 返回了 `true`,同时由于渲染的元素与之前的不同使得 React 更新了该 DOM。 - -最后一个有趣的例子是 C8。React 需要渲染这个组件,但是由于其返回的 React 元素和之前渲染的相同,所以不需要更新 DOM。 - -显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而对于 C2 的子节点和 C7,由于 `shouldComponentUpdate` 使得 `render` 并没有被调用。因此它们也不需要对比元素了。 - -## 示例 {#examples} - -如果你的组件只有当 `props.color` 或者 `state.count` 的值改变才需要更新时,你可以使用 `shouldComponentUpdate` 来进行检查: - -```javascript -class CounterButton extends React.Component { - constructor(props) { - super(props); - this.state = {count: 1}; - } - - shouldComponentUpdate(nextProps, nextState) { - if (this.props.color !== nextProps.color) { - return true; - } - if (this.state.count !== nextState.count) { - return true; - } - return false; - } - - render() { - return ( - - ); - } -} -``` - -在这段代码中,`shouldComponentUpdate` 仅检查了 `props.color` 或 `state.count` 是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可以使用类似“浅比较”的模式来检查 `props` 和 `state` 中所有的字段,以此来决定组件是否需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 `React.PureComponent` 就行了。所以这段代码可以改成以下这种更简洁的形式: - -```js -class CounterButton extends React.PureComponent { - constructor(props) { - super(props); - this.state = {count: 1}; - } - - render() { - return ( - - ); - } -} -``` - -大部分情况下,你可以使用 `React.PureComponent` 来代替手写 `shouldComponentUpdate`。但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。例如,你想要一个 `ListOfWords` 组件来渲染一组用逗号分开的单词。它有一个叫做 `WordAdder` 的父组件,该组件允许你点击一个按钮来添加一个单词到列表中。以下代码*并不*正确: - -```javascript -class ListOfWords extends React.PureComponent { - render() { - return
{this.props.words.join(',')}
; - } -} - -class WordAdder extends React.Component { - constructor(props) { - super(props); - this.state = { - words: ['marklar'] - }; - this.handleClick = this.handleClick.bind(this); - } - - handleClick() { - // 这部分代码很糟,而且还有 bug - const words = this.state.words; - words.push('marklar'); - this.setState({words: words}); - } - - render() { - return ( -
-
- ); - } -} -``` - -问题在于 `PureComponent` 仅仅会对新老 `this.props.words` 的值进行简单的对比。由于代码中 `WordAdder` 的 `handleClick` 方法改变了同一个 `words` 数组,使得新老 `this.props.words` 比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, `ListOfWords` 却并没有被更新。 - -## 不可变数据的力量 {#the-power-of-not-mutating-data} - -避免该问题最简单的方式是避免更改你正用于 props 或 state 的值。例如,上面 `handleClick` 方法可以用 `concat` 重写: - -```javascript -handleClick() { - this.setState(state => ({ - words: state.words.concat(['marklar']) - })); -} -``` - -ES6 数组支持[扩展运算符](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator),这让代码写起来更方便了。如果你在使用 Create React App,该语法已经默认支持了。 - -```js -handleClick() { - this.setState(state => ({ - words: [...state.words, 'marklar'], - })); -}; -``` - -你可以用类似的方式改写代码来避免可变对象的产生。例如,我们有一个叫做 `colormap` 的对象。我们希望写一个方法来将 `colormap.right` 设置为 `'blue'`。我们可以这么写: - -```js -function updateColorMap(colormap) { - colormap.right = 'blue'; -} -``` - -为了不改变原本的对象,我们可以使用 [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 方法: - -```js -function updateColorMap(colormap) { - return Object.assign({}, colormap, {right: 'blue'}); -} -``` - -现在 `updateColorMap` 返回了一个新的对象,而不是修改老对象。`Object.assign` 是 ES6 的方法,需要 polyfill。 - -这里有一个 JavaScript 的提案,旨在添加[对象扩展属性](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)以使得更新不可变对象变得更方便: - -```js -function updateColorMap(colormap) { - return {...colormap, right: 'blue'}; -} -``` - -此特性已被收录在 JavaScript 的 ES2018 中。 - -如果你在使用 Create React App,`Object.assign` 以及对象扩展运算符已经默认支持了。 - -当处理深层嵌套对象时,以 immutable (不可变)的方式更新它们令人费解。如遇到此类问题,请参阅 [Immer](https://github.com/mweststrate/immer) 或 [immutability-helper](https://github.com/kolodny/immutability-helper)。这些库会帮助你编写高可读性的代码,且不会失去 immutability (不可变性)带来的好处。 diff --git a/content/docs/portals.md b/content/docs/portals.md deleted file mode 100644 index e3f3cb57c3..0000000000 --- a/content/docs/portals.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -id: portals -title: Portals -permalink: docs/portals.html ---- - -> Try the new React documentation. -> -> These new documentation pages teach modern React and include live examples: -> -> - [`createPortal`](https://beta.reactjs.org/reference/react-dom/createPortal) -> -> The new docs will soon replace this site, which will be archived. [Provide feedback.](https://github.com/reactjs/reactjs.org/issues/3308) - -Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。 - -```js -ReactDOM.createPortal(child, container) -``` - -第一个参数(`child`)是任何[可渲染的 React 子元素](/docs/react-component.html#render),例如一个元素,字符串或 fragment。第二个参数(`container`)是一个 DOM 元素。 - -## 用法 {#usage} - -通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点: - -```js{4,6} -render() { - // React 挂载了一个新的 div,并且把子元素渲染其中 - return ( -
- {this.props.children} -
- ); -} -``` - -然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的: - -```js{6} -render() { - // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。 - // `domNode` 是一个可以在任何位置的有效 DOM 节点。 - return ReactDOM.createPortal( - this.props.children, - domNode - ); -} -``` - -一个 portal 的典型用例是当父组件有 `overflow: hidden` 或 `z-index` 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框: - -> 注意: -> -> 当在使用 portal 时, 记住[管理键盘焦点](/docs/accessibility.html#programmatically-managing-focus)就变得尤为重要。 -> -> 当用于门户网站时,请记得 [管理键盘焦点](/docs/accessibility.html#programmatically-managing-focus),这点非常重要。 -> -> 对于模态对话框,通过遵循 [WAI-ARIA 模态开发实践](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/),来确保每个人都能够运用它。 - -[**在 CodePen 上尝试**](https://codepen.io/gaearon/pen/yzMaBd) - -## 通过 Portal 进行事件冒泡 {#event-bubbling-through-portals} - -尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 *React 树*, 且与 *DOM 树* 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。 - -这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 *React 树*的祖先,即便这些元素并不是 *DOM 树* 中的祖先。假设存在如下 HTML 结构: - -```html - - -
- - - -``` - -在 `#app-root` 里的 `Parent` 组件能够捕获到未被捕获的从兄弟节点 `#modal-root` 冒泡上来的事件。 - -```js{28-31,42-49,53,61-63,70-71,74} -// 在 DOM 中有两个容器是兄弟级 (siblings) -const appRoot = document.getElementById('app-root'); -const modalRoot = document.getElementById('modal-root'); - -class Modal extends React.Component { - constructor(props) { - super(props); - this.el = document.createElement('div'); - } - - componentDidMount() { - // 在 Modal 的所有子元素被挂载后, - // 这个 portal 元素会被嵌入到 DOM 树中, - // 这意味着子元素将被挂载到一个分离的 DOM 节点中。 - // 如果要求子组件在挂载时可以立刻接入 DOM 树, - // 例如衡量一个 DOM 节点, - // 或者在后代节点中使用 ‘autoFocus’, - // 则需添加 state 到 Modal 中, - // 仅当 Modal 被插入 DOM 树中才能渲染子元素。 - modalRoot.appendChild(this.el); - } - - componentWillUnmount() { - modalRoot.removeChild(this.el); - } - - render() { - return ReactDOM.createPortal( - this.props.children, - this.el - ); - } -} - -class Parent extends React.Component { - constructor(props) { - super(props); - this.state = {clicks: 0}; - this.handleClick = this.handleClick.bind(this); - } - - handleClick() { - // 当子元素里的按钮被点击时, - // 这个将会被触发更新父元素的 state, - // 即使这个按钮在 DOM 中不是直接关联的后代 - this.setState(state => ({ - clicks: state.clicks + 1 - })); - } - - render() { - return ( -
-

Number of clicks: {this.state.clicks}

-

- Open up the browser DevTools - to observe that the button - is not a child of the div - with the onClick handler. -

- - - -
- ); - } -} - -function Child() { - // 这个按钮的点击事件会冒泡到父元素 - // 因为这里没有定义 'onClick' 属性 - return ( -
- -
- ); -} - -const root = ReactDOM.createRoot(appRoot); -root.render(); -``` - -[**在 CodePen 上尝试**](https://codepen.io/gaearon/pen/jGBWpE) - -在父组件里捕获一个来自 portal 冒泡上来的事件,使之能够在开发时具有不完全依赖于 portal 的更为灵活的抽象。例如,如果你在渲染一个 `` 组件,无论其是否采用 portal 实现,父组件都能够捕获其事件。 diff --git a/content/docs/react-without-es6.md b/content/docs/react-without-es6.md deleted file mode 100644 index f81a3dccb3..0000000000 --- a/content/docs/react-without-es6.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -id: react-without-es6 -title: 不使用 ES6 -permalink: docs/react-without-es6.html ---- - -通常我们会用 JavaScript 的 `class` 关键字来定义 React 组件: - -```javascript -class Greeting extends React.Component { - render() { - return

Hello, {this.props.name}

; - } -} -``` - -如果你还未使用过 ES6,你可以使用 `create-react-class` 模块: - - -```javascript -var createReactClass = require('create-react-class'); -var Greeting = createReactClass({ - render: function() { - return

Hello, {this.props.name}

; - } -}); -``` - -ES6 中的 class 与 `createReactClass()` 方法十分相似,但有以下几个区别值得注意。 - -## 声明默认属性 {#declaring-default-props} - -无论是函数组件还是 class 组件,都拥有 `defaultProps` 属性: - -```javascript -class Greeting extends React.Component { - // ... -} - -Greeting.defaultProps = { - name: 'Mary' -}; -``` - -如果使用 `createReactClass()` 方法创建组件,那就需要在组件中定义 `getDefaultProps()` 函数: - -```javascript -var Greeting = createReactClass({ - getDefaultProps: function() { - return { - name: 'Mary' - }; - }, - - // ... - -}); -``` - -## 初始化 State {#setting-the-initial-state} - -如果使用 ES6 的 class 关键字创建组件,你可以通过给 `this.state` 赋值的方式来定义组件的初始 state: - -```javascript -class Counter extends React.Component { - constructor(props) { - super(props); - this.state = {count: props.initialCount}; - } - // ... -} -``` - -如果使用 `createReactClass()` 方法创建组件,你需要提供一个单独的 `getInitialState` 方法,让其返回初始 state: - -```javascript -var Counter = createReactClass({ - getInitialState: function() { - return {count: this.props.initialCount}; - }, - // ... -}); -``` - -## 自动绑定 {#autobinding} - -对于使用 ES6 的 class 关键字创建的 React 组件,组件中的方法遵循与常规 ES6 class 相同的语法规则。这意味着这些方法不会自动绑定 `this` 到这个组件实例。 你需要在 constructor 中显式地调用 `.bind(this)`: - -```javascript -class SayHello extends React.Component { - constructor(props) { - super(props); - this.state = {message: 'Hello!'}; - // 这一行很重要! - this.handleClick = this.handleClick.bind(this); - } - - handleClick() { - alert(this.state.message); - } - - render() { - // 由于 `this.handleClick` 已经绑定至实例,因此我们才可以用它来处理点击事件 - return ( - - ); - } -} -``` - -如果使用 `createReactClass()` 方法创建组件,组件中的方法会自动绑定至实例,所以不需要像上面那样做: - -```javascript -var SayHello = createReactClass({ - getInitialState: function() { - return {message: 'Hello!'}; - }, - - handleClick: function() { - alert(this.state.message); - }, - - render: function() { - return ( - - ); - } -}); -``` - -这就意味着,如果使用 ES6 class 关键字创建组件,在处理事件回调时就要多写一部分代码。但对于大型项目来说,这样做可以提升运行效率。 - -如果模板代码对你来说不太友好,你可以尝试使用 [ES2022 Class Properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields#public_instance_fields) 语法: - - -```javascript -class SayHello extends React.Component { - constructor(props) { - super(props); - this.state = {message: 'Hello!'}; - } - - // 在这里使用箭头函数就可以把方法绑定给实例: - handleClick = () => { - alert(this.state.message); - }; - - render() { - return ( - - ); - } -} -``` - -为了安全起见,你可以采用以下几种方式: - -* 在 constructor 中绑定方法。 -* 使用箭头函数,比如:`onClick={(e) => this.handleClick(e)}`。 -* 继续使用 `createReactClass`。 - -## Mixins {#mixins} - ->**注意:** -> ->ES6 本身是不包含任何 mixin 支持。因此,当你在 React 中使用 ES6 class 时,将不支持 mixins 。 -> ->**我们也发现了很多使用 mixins 然后出现了问题的代码库。[并且不建议在新代码中使用它们](/blog/2016/07/13/mixins-considered-harmful.html)。** -> -> 以下内容仅作为参考。 - -如果完全不同的组件有相似的功能,这就会产生["横切关注点(cross-cutting concerns)"问题](https://en.wikipedia.org/wiki/Cross-cutting_concern)。针对这个问题,在使用 createReactClass 创建 React 组件的时候,引入 `mixins` 功能会是一个很好的解决方案。 - -比较常见的用法是,组件每隔一段时间更新一次。使用 `setInterval()` 可以很容易实现这个功能,但需要注意的是,当你不再需要它时,你应该清除它以节省内存。React 提供了[生命周期方法](/docs/react-component.html#the-component-lifecycle),这样你就可以知道一个组件何时被创建或被销毁了。让我们创建一个简单的 mixin,它使用这些方法提供一个简单的 `setInterval()` 函数,它会在组件被销毁时被自动清理。 - -```javascript -var SetIntervalMixin = { - componentWillMount: function() { - this.intervals = []; - }, - setInterval: function() { - this.intervals.push(setInterval.apply(null, arguments)); - }, - componentWillUnmount: function() { - this.intervals.forEach(clearInterval); - } -}; - -var createReactClass = require('create-react-class'); - -var TickTock = createReactClass({ - mixins: [SetIntervalMixin], // 使用 mixin - getInitialState: function() { - return {seconds: 0}; - }, - componentDidMount: function() { - this.setInterval(this.tick, 1000); // 调用 mixin 上的方法 - }, - tick: function() { - this.setState({seconds: this.state.seconds + 1}); - }, - render: function() { - return ( -

- React has been running for {this.state.seconds} seconds. -

- ); - } -}); - -const root = ReactDOM.createRoot(document.getElementById('example')); -root.render(); -``` - -如果组件拥有多个 mixins,且这些 mixins 中定义了相同的生命周期方法(例如,当组件被销毁时,几个 mixins 都想要进行一些清理工作),那么这些生命周期方法都会被调用的。使用 mixins 时,mixins 会先按照定义时的顺序执行,最后调用组件上对应的方法。 diff --git a/content/docs/react-without-jsx.md b/content/docs/react-without-jsx.md deleted file mode 100644 index 2f5ea9ffff..0000000000 --- a/content/docs/react-without-jsx.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -id: react-without-jsx -title: 不使用 JSX 的 React -permalink: docs/react-without-jsx.html ---- - -React 并不强制要求使用 JSX。当你不想在构建环境中配置有关 JSX 编译时,不在 React 中使用 JSX 会更加方便。 - -每个 JSX 元素只是调用 `React.createElement(component, props, ...children)` 的语法糖。因此,使用 JSX 可以完成的任何事情都可以通过纯 JavaScript 完成。 - -例如,用 JSX 编写的代码: - -```js -class Hello extends React.Component { - render() { - return
Hello {this.props.toWhat}
; - } -} - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render(); -``` - -可以编写为不使用 JSX 的代码: - -```js -class Hello extends React.Component { - render() { - return React.createElement('div', null, `Hello ${this.props.toWhat}`); - } -} - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render(React.createElement(Hello, {toWhat: 'World'}, null)); -``` - -如果你想了解更多 JSX 转换为 JavaScript 的示例,可以尝试使用 [在线 Babel 编译器](babel://jsx-simple-example)。 - -组件可以是字符串,也可以是 `React.Component` 的子类,它还能是一个普通的函数。 - -如果你不想每次都键入 `React.createElement`,通常的做法是创建快捷方式: - -```js -const e = React.createElement; - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render(e('div', null, 'Hello World')); -``` - -如果你使用了 `React.createElement` 的快捷方式,那么在没有 JSX 的情况下使用 React 几乎一样方便。 - -或者,你也可以参考社区项目,如:[`react-hyperscript`](https://github.com/mlmorg/react-hyperscript) 和 [`hyperscript-helpers`](https://github.com/ohanhi/hyperscript-helpers),它们提供了更简洁的语法。 - diff --git a/content/docs/reconciliation.md b/content/docs/reconciliation.md deleted file mode 100644 index fb524abdc1..0000000000 --- a/content/docs/reconciliation.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -id: reconciliation -title: 协调 -permalink: docs/reconciliation.html ---- - -> Try the new React documentation. -> -> These new documentation pages teach modern React and include live examples: -> -> - [Preserving and Resetting State](https://beta.reactjs.org/learn/preserving-and-resetting-state) -> -> The new docs will soon replace this site, which will be archived. [Provide feedback.](https://github.com/reactjs/reactjs.org/issues/3308) - -React 提供的声明式 API 让开发者可以在对 React 的底层实现并不了解的情况下编写应用。在开发者编写应用时,可以保持相对简单的心智,但开发者无法了解其内部的实现原理。本文描述了在实现 React 的 "diffing" 算法过程中所作出的设计决策,以保证组件更新可预测,且在繁杂业务场景下依然保持应用的高性能。 - -## 设计动机 {#motivation} - -在某一时间节点调用 React 的 `render()` 方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 `render()` 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何高效的更新 UI,以保证当前 UI 与最新的树保持同步。 - -此算法有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作次数。然而,即使使用[最优的算法](http://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf),该算法的复杂程度仍为 O(n 3 ),其中 n 是树中元素的数量。 - -如果在 React 中使用该算法,那么展示 1000 个元素则需要 10 亿次的比较。这个开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法: - -1. 两个不同类型的元素会产生出不同的树; -2. 开发者可以使用 `key` 属性标识哪些子元素在不同的渲染中可能是不变的。 - -在实践中,我们发现以上假设在几乎所有实用的场景下都成立。 - -## Diffing 算法 {#the-diffing-algorithm} - -当对比两棵树时,React 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。 - -### 对比不同类型的元素 {#elements-of-different-types} - -当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。举个例子,当一个元素从 `` 变成 ``,从 `
` 变成 ``,或从 `