将React当做前端UI运行时环境来看待

大多数教程把 React 称作是一个 UI 库。这是有道理的,因为 React 就是一个 UI 库。正如官网上的标语所说的那样。

我曾经写过关于构建[用户界面][1]会遇到的难题一文。但是本篇文章将以一种不同的方式来讲述 React — 因为它更像是一种编程运行时

本篇文章不会教你任何有关如何创建用户界面的技巧。 但是它可能会帮助你更深入地理解 React 编程模型。


注意:如果你还在学习 React ,请移步到官方文档进行学习

⚠️

本篇文章将会非常深入 — 所以并不适合初学者阅读。 在本篇文章中,我会从最佳原则的角度尽可能地阐述 React 编程模型。我不会解释如何使用它 — 而是讲解它的原理。

文章面向有经验的程序员和那些使用过其他 UI 库但在项目中权衡利弊后最终选择了 React 的人,我希望它会对你有所帮助!

许多人成功使用了 React 多年却从未考虑过下面我将要讲述的主题。 这肯定是从程序员的角度来看待 React ,而不是以设计者的角度。但我并不认为站在两个不同的角度来重新认识 React 会有什么坏处。

话不多说,让我们开始深入理解 React 吧!


宿主树

一些程序输出数字。另一些程序输出诗词。不同的语言和它们的运行时通常会对特定的一组用例进行优化,而 React 也不例外。

React 程序通常会输出一棵会随时间变化的树。 它有可能是一棵 DOM 树 ,iOS 视图层 ,PDF 原语 ,又或是 JSON 对象 。然而,通常我们希望用它来展示 UI 。我们称它为“宿主树”,因为它往往是 React 之外宿主环境中的一部分 — 就像 DOM 或 iOS 。宿主树通常有自己的命令式 API 。而 React 就是它上面的那一层。

所以到底 React 有什么用呢?非常抽象地,它可以帮助你编写可预测的,并且能够操控复杂的宿主树进而响应像用户交互、网络响应、定时器等外部事件的应用程序。

当专业的工具可以施加特定的约束且能从中获益时,它比一般的工具要好。React 就是这样的典范,并且它坚持两个原则:

  • 稳定性。 宿主树是相对稳定的,大多数情况的更新并不会从根本上改变其整体结构。如果应用程序每秒都会将其所有可交互的元素重新排列为完全不同的组合,那将会变得难以使用。那个按钮去哪了?为什么我的屏幕在跳舞?
  • 通用性。 宿主树可以被拆分为外观和行为一致的 UI 模式(例如按钮、列表和头像)而不是随机的形状。

这些原则恰好适用于大多数 UI 。 然而,当输出没有稳定的“模式”时 React 并不适用。例如,React 也许可以帮助你编写一个 Twitter 客户端,但对于一个 3D 管道屏幕保护程序 并不会起太大作用。

宿主实例

宿主树由节点组成,我们称之为“宿主实例”。

在 DOM 环境中,宿主实例就是我们通常所说的 DOM 节点 — 就像当你调用 document.createElement(‘div’) 时获得的对象。在 iOS 中,宿主实例可以是从 JavaScript 到原生视图唯一标识的值。

宿主实例有它们自己的属性(例如 domNode.className 或者 view.tintColor )。它们也有可能将其他的宿主实例作为子项。

(这和 React 没有任何联系 — 因为我在讲述宿主环境。)

通常会有原生的 API 用于操控这些宿主实例。例如,在 DOM 环境中会提供像 appendChild、removeChild、setAttribute 等一系列的 API 。在 React 应用中,通常你不会调用这些 API ,因为那是 React 的工作。

渲染器

渲染器教会 React 如何与特定的宿主环境通信以及如何管理它的宿主实例。React DOM、React Native 甚至 Ink 都可以称作 React 渲染器。你也可以创建自己的 React 渲染器 。

React 渲染器能以下面两种模式之一进行工作。

绝大多数渲染器都被用作“突变”模式。这种模式正是 DOM 的工作方式:我们可以创建一个节点,设置它的属性,在之后往里面增加或者删除子节点。宿主实例是完全可变的。

但 React 也能以”不变“模式工作。这种模式适用于那些并不提供像 appendChild 的 API 而是克隆双亲树并始终替换掉顶级子树的宿主环境。在宿主树级别上的不可变性使得多线程变得更加容易。React Fabric 就利用了这一模式。

作为 React 的使用者,你永远不需要考虑这些模式。我只想强调 React 不仅仅只是从一种模式转换到另一种模式的适配器。它的用处在于以一种更好的方式操控宿主实例而不用在意那些低级视图 API 范例。

React 元素

在宿主环境中,一个宿主实例(例如 DOM 节点)是最小的构建单元。而在 React 中,最小的构建单元是 React 元素。

React 元素是一个普通的 JavaScript 对象。它用来描述一个宿主实例。

``` // JSX 是用来描述这些对象的语法糖。 //

React 元素是轻量级的因为没有宿主实例与它绑定在一起。同样的,它只是对你想要在屏幕上看到的内容的描述。

就像宿主实例一样,React 元素也能形成一棵树:

``` // JSX 是用来描述这些对象的语法糖。 // // { type: 'dialog', props: { children: [{ type: 'button', props: { className: 'blue' } }, { type: 'button', props: { className: 'red' } }] } } ```

(注意:我省略了一些对此解释不重要的[属性][2])

但是,请记住 React 元素并不是永远存在的 。它们总是在重建和删除之间不断循环着。

React 元素具有不可变性。例如,你不能改变 React 元素中的子元素或者属性。如果你想要在稍后渲染一些不同的东西,你需要从头创建新的 React 元素树来描述它。

我喜欢将 React 元素比作电影中放映的每一帧。它们捕捉 UI 在特定的时间点应该是什么样子。它们永远不会再改变。

入口

每一个 React 渲染器都有一个“入口”。正是那个特定的 API 让我们告诉 React ,将特定的 React 元素树渲染到真正的宿主实例中去。

例如,React DOM 的入口就是 ReactDOM.render :

``` ReactDOM.render( // { type: 'button', props: { className: 'blue' } }

我们调用 ReactDOM.render(reactElement, domContainer) 时,我们的意思是:“亲爱的 React ,将我的 reactElement 映射到 domContaienr 的宿主树上去吧。“

React 会查看 reactElement.type (在我们的例子中是 button )然后告诉 React DOM 渲染器创建对应的宿主实例并设置正确的属性:

``` // 在 ReactDOM 渲染器内部(简化版) function createHostInstance(reactElement) { let domNode = document.createElement(reactElement.type); domNode.className = reactElement.props.className; return domNode; } ```

我们的例子中,React 会这样做:

``` let domNode = document.createElement('button');domNode.className = 'blue'; domContainer.appendChild(domNode); ```

如果 React 元素在 reactElement.props.children 中含有子元素,React 会在第一次渲染中递归地为它们创建宿主实例。

协调

如果我们用同一个 container 调用 ReactDOM.render() 两次会发生什么呢?

``` ReactDOM.render(

同样的启发式方法也适用于子树。例如,当我们在 

 中新增两个 
); }

</div>

它返回一对值:当前的状态和更新该状态的函数。

数组的<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Array_destructuring" target="_blank" rel="nofollow noopener noreferrer">解构语法</a>让[我们](https://www.w3cdoc.com)可以给状态变量自定义名称。例如,我在这里称它们为 count 和 setCount ,但是它们也可以被称作 banana 和 setBanana 。在这些文字之下,[我们](https://www.w3cdoc.com)会用 setState 来替代第二个值无论它在具体的例子中被称作什么。

_(你能在 <a href="https://reactjs.org/docs/hooks-intro.html" target="_blank" rel="nofollow noopener noreferrer">React 文档</a> 中学习到更多关于 useState 和 其他 Hooks 的知识。)_

## 一致性 {#一致性}

即使[我们](https://www.w3cdoc.com)想将协调过程本身分割成<a href="https://www.youtube.com/watch?v=mDdgfyRB5kg" target="_blank" rel="nofollow noopener noreferrer">非阻塞</a>的工作块,[我们](https://www.w3cdoc.com)仍然需要在同步的循环中对真实的宿主实例进行操作。这样[我们](https://www.w3cdoc.com)才能保证用户不会看见半更新状态的 UI ,[浏览器](https://www.w3cdoc.com)也不会对用户不应看到的中间状态进行不必要的布局和样式的重新计算。

这也是为什么 React 将所有的工作分成了”渲染阶段“和”提交阶段“的原因。_渲染阶段_ 是当 React 调用你的组件然后进行协调的时段。在此阶段进行干涉是安全的且在<a href="https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html" target="_blank" rel="nofollow noopener noreferrer">未来</a>这个阶段将会变成异步的。_提交阶段_ 就是 React 操作宿主树的时候。而这个阶段永远是同步的。

## 缓存 {#缓存}

当父组件通过 setState 准备更新时,React 默认会协调整个子树。因为 React 并不知道在父组件中的更新是否会影响到其子代,所以 React 默认保持一致性。这听起来会有很大的性能消耗但事实上对于小型和中型的子树来说,这并不是问题。

当树的深度和广度达到一定程度时,你可以让 React 去<a href="https://en.wikipedia.org/wiki/Memoization" target="_blank" rel="nofollow noopener noreferrer">缓存</a>子树并且重用先前的渲染结果当 prop 在浅比较之后是相同时:

<div class="gatsby-highlight" data-language="jsx">

function Row({ item }) { // … }

export default React.memo(Row);

</div>

现在,在父组件 <Table> 中调用 setState 时如果 <Row> 中的 item 与先前渲染的结果是相同的,React 就会直接跳过协调的过程。

你可以通过 <a href="https://reactjs.org/docs/hooks-reference.html#usememo" target="_blank" rel="nofollow noopener noreferrer">useMemo() Hook</a> 获得单个表达式级别的细粒度缓存。该缓存于其相关的组件紧密联系在一起,并且将与局部状态一起被销毁。它只会保留最后一次计算的结果。

默认情况下,React 不会故意缓存组件。许多组件在更新的过程中总是会接收到不同的 props ,所以对它们进行缓存只会造成净亏损。

## 原始模型 {#原始模型}

令人讽刺地是,React 并没有使用“反应式”的系统来支持细粒度的更新。换句话说,任何在顶层的更新只会触发协调而不是局部更新那些受影响的组件。

这样的设计是有意而为之的。对于 web 应用来说<a href="https://calibreapp.com/blog/time-to-interactive/" target="_blank" rel="nofollow noopener noreferrer">交互时间</a>是一个关键指标,而通过遍历整个模型去设置细粒度的监听器只会浪费宝贵的时间。此外,在很多应用中交互往往会导致或小(按钮悬停)或大(页面转换)的更新,因此细粒度的订阅只会浪费内存资源。

React 的设计原则之一就是它可以处理原始数据。如果你拥有从网络请求中获得的一组 JavaScript 对象,你可以将其直接交给组件而无需进行预处理。没有关于可以访问哪些属性的问题,或者当结构有所变化时造成的意外的性能缺损。React 渲染是 O(_视图大小_) 而不是 O(_模型大小_) ,并且你可以通过 <a href="https://react-window.now.sh/#/examples/list/fixed-size" target="_blank" rel="nofollow noopener noreferrer">windowing</a> 显著地减少视图大小。

有那么一些应用细粒度订阅对它们来说是有用的 — 例如股票代码。这是一个极少见的例子,因为“所有的东西都需要在同一时间内持续更新”。虽然命令式的方法能够优化此类代码,但 React 并不适用于这种情况。同样的,如果你想要解决该问题,你就得在 React 之上自己实现细粒度的订阅。

**注意,即使细粒度订阅和“反应式”系统也无法解决一些常见的性能问题。** 例如,渲染一棵很深的树(在每次页面转换的时候发生)而不阻塞[浏览器](https://www.w3cdoc.com)。改变跟踪并不会让它变得更快 — 这样只会让其变得更慢因为[我们](https://www.w3cdoc.com)执行了额外的订阅工作。另一个问题是[我们](https://www.w3cdoc.com)需要等待返回的数据在渲染视图之前。在 React 中,[我们](https://www.w3cdoc.com)用<a href="https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html" target="_blank" rel="nofollow noopener noreferrer">并发渲染</a>来解决这些问题。

## 批量更新 {#批量更新}

一些组件也许想要更新状态来响应同一事件。下面这个例子是假设的,但是却说明了一个常见的模式:

<div class="gatsby-highlight" data-language="jsx">

function Parent() { let [count, setCount] = useState(0); return ( <div onClick={() => setCount(count + 1)}> Parent clicked {count} times

); }

function Child() { let [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> Child clicked {count} times ); }

</div>

当事件被触发时,子组件的 onClick 首先被触发(同时触发了它的 setState )。然后父组件在它自己的 onClick 中调用 setState 。

如果 React 立即重渲染组件以响应 setState 调用,最终[我们](https://www.w3cdoc.com)会重渲染子组件两次:

<div class="gatsby-highlight" data-language="jsx">

*** 进入 React 浏览器 click 事件处理过程 *** Child (onClick)

</div>

第一次 Child 组件渲染是浪费的。并且[我们](https://www.w3cdoc.com)也不会让 React 跳过 Child 的第二次渲染因为 Parent 可能会传递不同的数据由于其自身的状态更新。

**这就是为什么 React 会在组件内所有事件触发完成后再进行批量更新的原因:**

<div class="gatsby-highlight" data-language="jsx">

*** 进入 React 浏览器 click 事件处理过程 *** Child (onClick)

</div>

组件内调用 setState 并不会立即执行重渲染。相反,React 会先触发所有的事件处理器,然后再触发一次重渲染以进行所谓的批量更新。

批量更新虽然有用但可能会让你感到惊讶如果你的代码这样写:

<div class="gatsby-highlight" data-language="jsx">

const [count, setCounter] = useState(0);

function increment() { setCounter(count + 1); }

function handleClick() { increment(); increment(); increment(); }

</div>

如果[我们](https://www.w3cdoc.com)将 count 初始值设为  ,上面的代码只会代表三次 setCount(1) 调用。为了解决这个问题,[我们](https://www.w3cdoc.com)给 setState 提供了一个 “updater” 函数作为参数:

<div class="gatsby-highlight" data-language="jsx">

const [count, setCounter] = useState(0);

function increment() { setCounter(c => c + 1); }

function handleClick() { increment(); increment(); increment(); }

</div>

React 会将 updater 函数放入队列中,并在之后按顺序执行它们,最终 count 会被设置成 3 并作为一次重渲染的结果。

当状态逻辑变得更加复杂而不仅仅只是少数的 setState 调用时,我建议你使用 <a href="https://reactjs.org/docs/hooks-reference.html#usereducer" target="_blank" rel="nofollow noopener noreferrer">useReducer Hook</a> 来描述你的局部状态。它就像 “updater” 的升级模式在这里你可以给每一次更新命名:

<div class="gatsby-highlight" data-language="jsx">

const [counter, dispatch] = useReducer((state, action) => { if (action === ‘increment’) { return state + 1; } else { return state; } }, 0);

function handleClick() { dispatch(‘increment’); dispatch(‘increment’); dispatch(‘increment’); }

</div>

action 字段可以是任意值,尽管对象是常用的选择。

## 调用树 {#调用树}

编程语言的运行时往往有<a href="https://medium.freecodecamp.org/understanding-the-javascript-call-stack-861e41ae61d4" target="_blank" rel="nofollow noopener noreferrer">调用栈</a> 。当函数 a() 调用 b() ,b() 又调用 c() 时,在 JavaScript 引擎中会有像 [a, b, c] 这样的数据结构来“跟踪”当前的位置以及接下来要执行的代码。一旦 c 函数执行完毕,它的调用栈帧就消失了!因为它不再被需要了。[我们](https://www.w3cdoc.com)返回到函数 b 中。当[我们](https://www.w3cdoc.com)结束函数 a 的执行时,调用栈就被清空。

当然,React 以 JavaScript 运行当然也遵循 JavaScript 的规则。但是[我们](https://www.w3cdoc.com)可以想象在 React 内部有自己的调用栈用来记忆[我们](https://www.w3cdoc.com)当前正在渲染的组件,例如 [App, Page, Layout, Article /* 此刻的位置 */] 。

React 与通常意义上的编程语言进行时不同因为它针对于渲染 UI 树,这些树需要保持“活性”,这样才能使[我们](https://www.w3cdoc.com)与其进行交互。在第一次 ReactDOM.render() 出现之前,DOM 操作并不会执行。

这也许是对隐喻的延伸,但我喜欢把 React 组件当作 “调用树” 而不是 “调用栈” 。当[我们](https://www.w3cdoc.com)调用完 Article 组件,它的 React “调用树” 帧并没有被摧毁。[我们](https://www.w3cdoc.com)需要将局部状态保存以便映射到宿主实例的<a href="https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7" target="_blank" rel="nofollow noopener noreferrer">某个地方</a>。

这些“调用树”帧会随它们的局部状态和宿主实例一起被摧毁,但是只会在[协调][4]规则认为这是必要的时候执行。如果你曾经读过 React 源码,你就会知道这些帧其实就是 <a href="https://en.wikipedia.org/wiki/Fiber_(computer_science)" target="_blank" rel="nofollow noopener noreferrer">Fibers</a> 。

Fibers 是局部状态真正存在的地方。当状态被更新后,React 将其下面的 Fibers 标记为需要进行协调,之后便会调用这些组件。

## 上下文 {#上下文}

在 React 中,[我们](https://www.w3cdoc.com)将数据作为 props 传递给其他组件。有些时候,大多数组件需要相同的东西 — 例如,当前选中的可视主题。将它一层层地传递会变得十分麻烦。

在 React 中,[我们](https://www.w3cdoc.com)通过 <a href="https://reactjs.org/docs/context.html" target="_blank" rel="nofollow noopener noreferrer">Context</a> 解决这个问题。它就像组件的<a href="http://wiki.c2.com/?DynamicScoping" target="_blank" rel="nofollow noopener noreferrer">动态范围</a> ,能让你从顶层传递数据,并让每个子组件在底部能够读取该值,当值变化时还能够进行重新渲染:

<div class="gatsby-highlight" data-language="jsx">

const ThemeContext = React.createContext( ’light’ // 默认值作为后备 );

function DarkApp() { return ( <ThemeContext.Provider value=“dark”> </ThemeContext.Provider> ); }

function SomeDeeplyNestedChild() { // 取决于其子组件在哪里被渲染 const theme = useContext(ThemeContext); // … }

</div>

当 SomeDeeplyNestedChild 渲染时, useContext(ThemeContext) 会寻找树中最近的 <ThemeContext.Provider> ,并且使用它的 value 。

(事实上,React 维护了一个上下文栈当其渲染时。)

如果没有 ThemeContext.Provider 存在,useContext(ThemeContext) 调用的结果就会被调用 createContext() 时传递的默认值所取代。在上面的例子中,这个值为 'light' 。

## 副作用 {#副作用}

[我们](https://www.w3cdoc.com)在之前提到过 React 组件在渲染过程中不应该有可观察到的副作用。但是有些时候副作用确实必要的。[我们](https://www.w3cdoc.com)也许需要进行管理 focus 状态、用 canvas 画图、订阅数据源等操作。

在 React 中,这些都可以通过声明 effect 来完成:

<div class="gatsby-highlight" data-language="jsx">

function Example() { const [count, setCount] = useState(0);

useEffect(() => { document.title = You clicked ${count} times; }); return (

You clicked {count} times

<button onClick={() => setCount(count + 1)}> Click me
); }

</div>

如果可能,React 会推迟执行 effect 直到[浏览器](https://www.w3cdoc.com)重新绘制屏幕。这是有好处的因为像订阅数据源这样的代码并不会影响<a href="https://calibreapp.com/blog/time-to-interactive/" target="_blank" rel="nofollow noopener noreferrer">交互时间</a>和<a href="https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint" target="_blank" rel="nofollow noopener noreferrer">首次绘制时间</a> 。

(有一个<a href="https://reactjs.org/docs/hooks-reference.html#uselayouteffect" target="_blank" rel="nofollow noopener noreferrer">极少使用</a>的 Hook 能够让你选择退出这种行为并进行一些同步的工作。请尽量避免使用它。)

effect 不只执行一次。当组件第一次展示给用户以及之后的每次更新时它都会被执行。在 effect 中能触及当前的 props 和 state,例如上文例子中的 count 。

effect 可能需要被清理,例如订阅数据源的例子。在订阅之后将其清理,effect 能够返回一个函数:

<div class="gatsby-highlight" data-language="jsx">

useEffect(() => { DataSource.addSubscription(handleChange); return () => DataSource.removeSubscription(handleChange); });

</div>

React 会在下次调用该 effect 之前执行这个返回的函数,当然是在组件被摧毁之前。

有些时候,在每次渲染中都重新调用 effect 是不符合实际需要的。 你可以告诉 React 如果相应的变量不会改变则跳过此次调用:

<div class="gatsby-highlight" data-language="jsx">

useEffect(() => { document.title = You clicked ${count} times; }, [count]);

</div>

但是,这往往会成为过早地优化并会造成一些问题如果你不熟悉 JavaScript 中的闭包是如何工作的话。

例如,下面的这段代码是有 bug 的:

<div class="gatsby-highlight" data-language="jsx">

useEffect(() => { DataSource.addSubscription(handleChange); return () => DataSource.removeSubscription(handleChange); }, []);

</div>

它含有 bug 因为 [] 代表着“不再重新执行这个 effect 。”但是这个 effect 中的 handleChange 是被定义在外面的。handleChange 也许会引用任何的 props 或 state :

<div class="gatsby-highlight" data-language="jsx">

function handleChange() { console.log(count); }

</div>

如果[我们](https://www.w3cdoc.com)不再让这个 effect 重新调用,handleChange 始终会是第一次渲染时的版本,而其中的 count 也永远只会是  。

为了解决这个问题,请保证你声明了特定的依赖数组,它包含**所有**可以改变的东西,即使是函数也不例外:

<div class="gatsby-highlight" data-language="jsx">

useEffect(() => { DataSource.addSubscription(handleChange); return () => DataSource.removeSubscription(handleChange); }, [handleChange]);

</div>

取决于你的代码,在每次渲染后 handleChange 都会不同因此你可能仍然会看到不必要的重订阅。 <a href="https://reactjs.org/docs/hooks-reference.html#usecallback" target="_blank" rel="nofollow noopener noreferrer">useCallback</a> 能够帮你解决这个问题。或者,你可以直接让它重订阅。例如[浏览器](https://www.w3cdoc.com)中的 addEventListener API 非常快,但为了在组件中避免使用它可能会带来更多的问题而不是其真正的价值。

_(你能在 <a href="https://reactjs.org/docs/hooks-effect.html" target="_blank" rel="nofollow noopener noreferrer">React 文档</a> 中学到更多关于 useEffect 和其他 Hooks 的知识。)_

## 自定义钩子 {#自定义钩子}

由于 useState 和 useEffect 是函数调用,因此[我们](https://www.w3cdoc.com)可以将其组合成自己的 Hooks :

<div class="gatsby-highlight" data-language="jsx">

function MyResponsiveComponent() { const width = useWindowWidth(); // 我们自己的 Hook return (

Window width is {width}

); }

function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener(‘resize’, handleResize); return () => { window.removeEventListener(‘resize’, handleResize); }; }); return width; }

</div>

自定义 Hooks 让不同的组件共享可重用的状态逻辑。注意状态本身是不共享的。每次调用 Hook 都只声明了其自身的独立状态。

_(你能在 <a href="https://reactjs.org/docs/hooks-custom.html" target="_blank" rel="nofollow noopener noreferrer">React 文档</a> 中学习更多关于构建自己的 Hooks 的内容。)_

## 静态使用顺序 {#静态使用顺序}

你可以把 useState 想象成一个可以定义“React 状态变量”的语法。它并不是真正的语法,当然,[我们](https://www.w3cdoc.com)仍在用 JavaScript 编写应用。但是[我们](https://www.w3cdoc.com)将 React 作为一个运行时环境来看待,因为 React 用 JavaScript 来描绘整个 UI 树,它的特性往往更接近于语言层面。

假设 use 是语法,将其使用在组件函数顶层也就说得通了:

<div class="gatsby-highlight" data-language="jsx">

// 😉 注意:并不是真的语法 component Example(props) { const [count, setCount] = use State(0); return (

You clicked {count} times

<button onClick={() => setCount(count + 1)}> Click me
); }

</div>

当它被放在条件语句中或者组件外时又代表什么呢?

<div class="gatsby-highlight" data-language="jsx">

// 😉 注意:并不是真的语法

// 它是谁的…局部状态? const [count, setCount] = use State(0);

component Example() { if (condition) { // 要是 condition 是 false 时会发生什么呢? const [count, setCount] = use State(0); }

function handleClick() { // 要是离开了组件函数会发生什么? // 这和一般的变量又有什么区别呢? const [count, setCount] = use State(0); }

</div>

React 状态和在树中与其相关的组件紧密联系在一起。如果 use 是真正的语法当它在组件函数的顶层调用时也能说的通:

<div class="gatsby-highlight" data-language="jsx">

// 😉 注意:并不是真的语法 component Example(props) { // 只在这里有效 const [count, setCount] = use State(0);

if (condition) { // 这会是一个语法错误 const [count, setCount] = use State(0); }

</div>

这和 import 声明只在模块顶层有用是一样的道理。

**当然,use 并不是真正的语法。** (它不会带来很多好处,并且会带来很多摩擦。)

然而,React 的确期望所有的 Hooks 调用只发生在组件的顶部并且不在条件语句中。这些 Hooks 的<a href="https://reactjs.org/docs/hooks-rules.html" target="_blank" rel="nofollow noopener noreferrer">规则</a>能够被 <a href="https://www.npmjs.com/package/eslint-plugin-react-hooks" target="_blank" rel="nofollow noopener noreferrer">linter plugin</a> 所规范。有很多关于这种设计选择的激烈争论,但在实践中我并没有看到它让人困惑。我还写了关于为什么通常提出的替代方案<a href="https://overreacted.io/why-do-hooks-rely-on-call-order/" target="_blank" rel="nofollow noopener noreferrer">不起作用</a>的文章。

Hooks 的内部实现其实是<a href="https://dev.to/aspittel/thank-u-next-an-introduction-to-linked-lists-4pph" target="_blank" rel="nofollow noopener noreferrer">链表</a> 。当你调用 useState 的时候,[我们](https://www.w3cdoc.com)将指针移到下一项。当[我们](https://www.w3cdoc.com)退出组件的[“调用树”帧][5]时,会缓存该结果的列表直到下次渲染开始。

<a href="https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e" target="_blank" rel="nofollow noopener noreferrer">这篇文章</a>简要介绍了 Hooks 内部是如何工作的。数组也许是比链表更好解释其原理的模型:

<div class="gatsby-highlight" data-language="jsx">

// 伪代码 let hooks, i; function useState() { i++; if (hooks[i]) { // 再次渲染时 return hooks[i]; } // 第一次渲染 hooks.push(…); }

// 准备渲染 i = -1; hooks = fiber.hooks || []; // 调用组件 YourComponent(); // 缓存 Hooks 的状态 fiber.hooks = hooks;

</div>

_(如果你对它感兴趣,真正的代码在<a href="https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.js" target="_blank" rel="nofollow noopener noreferrer">这里</a> 。)_

这大致就是每个 useState() 如何获得正确状态的方式。就像[我们](https://www.w3cdoc.com)[之前][4]所知道的,“匹配”对 React 来说并不是什么新的知识 — 这与协调依赖于在渲染前后元素是否匹配是同样的道理。

## 未提及的知识 {#未提及的知识}

[我们](https://www.w3cdoc.com)已经触及到 React 运行时环境中几乎所有重要的方面。如果你读完了本篇文章,你可能已经比 90% 的开发者更了解 React !这一点也没有错!

当然有一些地方我并没有提及到 — 主要是因为[我们](https://www.w3cdoc.com)对它们也不太清楚。React 目前对多道渲染并没有太好的支持,即当父组件的渲染需要子组件提供信息时。<a href="https://reactjs.org/docs/error-boundaries.html" target="_blank" rel="nofollow noopener noreferrer">错误处理 API</a> 目前也还没有 Hooks 的版本。这两个问题可能会被一起解决。并发模式在目前看来并不稳定,也有很多关于 Suspense 该如何适应当前版本的有趣问题。也许我会在它们要完成的时候再来讨论,并且 Suspense 已经准备好比 <a href="https://reactjs.org/blog/2018/10/23/react-v-16-6.html#reactlazy-code-splitting-with-suspense" target="_blank" rel="nofollow noopener noreferrer">lazy loading</a> 能够做的更多。

**我认为 React API 的成功之处在于,即使在没有考虑过上面这些大多数主题的情况下,你也能轻松使用它并且可以走的很远。** 在大多数情况下,像协调这样好的默认特性启发式地为[我们](https://www.w3cdoc.com)做了正确的事情。在你忘记添加 key 这样的属性时,React 能够好心提醒你。

如果你是痴迷于 UI 库的书呆子,我希望这篇文章对你来说会很有趣并且是深入阐明了 React 是如何工作的。又或许你会觉得 React 太过于复杂为此你不会再去深入理解它。



原文:[react-as-a-ui-runtime][6]

 [1]: https://overreacted.io/zh-hans/the-elements-of-ui-engineering/
 [2]: https://overreacted.io/zh-hans/why-do-react-elements-have-typeof-property/
 [3]: https://overreacted.io/zh-hans/react-as-a-ui-runtime/#%E7%BA%AF%E5%87%80
 [4]: https://overreacted.io/zh-hans/react-as-a-ui-runtime/#%E5%8D%8F%E8%B0%83
 [5]: https://overreacted.io/zh-hans/react-as-a-ui-runtime/#%E8%B0%83%E7%94%A8%E6%A0%91
 [6]: https://overreacted.io/zh-hans/react-as-a-ui-runtime/
← 基于发布订阅模式写一个eventEmitter
如何编写vscode extension插件 →


请遵守《互联网环境法规》文明发言,欢迎讨论问题
扫码反馈

扫一扫,反馈当前页面

咨询反馈
扫码关注
返回顶部