react源码解析之stack reconciler

关于源码解读的系列文章,可以关注我的github的这个仓库, 现在才刚刚写,后续有空就写点。争取把react源码剖析透学习透。有不正确的地方希望大家帮忙指正。大家互相学习,共同进步。

本篇文章是官方文档的翻译,英文原文请访问官网

这个章节是stack reconciler的一些实现说明.

它的技术性很强并假定你能完全理解React的公开API,以及它是如何划分为核心、渲染器和协调器的。如果你对React代码不是很熟悉,请先阅读代码概览。

它还假定你能够理解React组件、实例和元素的区别。

Stack reconciler 被用在React 15 以及更早的版本中, 它在源代码中的位置是src/renderers/shared/stack/reconciler.

视频:从零开始构建React

Paul O'Shannessy给出了一个关于从零开始构建React的讨论,在很大程度上对本文档给予了启发。

本文档与上边的视频都是对实际代码库的简化,因此你可以通过熟悉两者来更好地理解。

概述

协调器本身没有公共 API. 但是诸如React DOM 和React Native的渲染器使用它依据用户所编写的React组件来有效地更新用户界面.

以递归过程的形式装载

让我们考虑首次装载组件的情形:

1ReactDOM.render(<App />, rootEl); 2

React DOM会将 <App />传递给协调器。请记住, <App />是一个React元素,也就是说是对哪些要渲染的东西的说明。你可以把它看成一个普通的对象:

1console.log(<App />); 2// { type: App, props: {} } 3

协调器(reconciler)会检查 App是类还是函数。如果 App 是函数,协调器会调用App(props)来获取所渲染的元素。如果App是类,协调器则会使用new App(props)创建一个App实例,调用 componentWillMount() 生命周期方法,进而调用 render() 方法来获取所渲染的元素。无论如何,协调器都会学习App元素的“渲染行为”。

此过程是递归的。App 可能渲染为<Greeting />,而<Greeting />可能渲染为 <Button />,如此类推。因为协调器会依次学习他们各自将如何渲染,所以协调器会递归地“向下钻取”所有用户定义组件。

你可以通过如下伪代码来理解该过程:

1function isClass(type) { 2 // React.Component的子类都会含有这一标志 3 return ( 4 Boolean(type.prototype) && 5 Boolean(type.prototype.isReactComponent) 6 ); 7} 8 9// This function takes a React element (e.g. <App />) 10// and returns a DOM or Native node representing the mounted tree. 11// 此函数读取一个React元素(例如<App />) 12// 并返回一个表达所装载树的DOM或内部节点。 13function mount(element) { 14 var type = element.type; 15 var props = element.props; 16 17 // 我们以此判断所渲染元素: 18 // 是以函数型运行该类型 19 // 还是创建新实例并调用render()。 20 var renderedElement; 21 if (isClass(type)) { 22 // Component class 23 var publicInstance = new type(props); 24 // Set the props 25 publicInstance.props = props; 26 // Call the lifecycle if necessary 27 if (publicInstance.componentWillMount) { 28 publicInstance.componentWillMount(); 29 } 30 // 调用render()以获取所渲染元素 31 renderedElement = publicInstance.render(); 32 } else { 33 // 组件函数 34 renderedElement = type(props); 35 } 36 37 // 该过程是递归实现,原因在于组件可能返回一个其它组件类型的元素。 38 return mount(renderedElement); 39 40 // 注意:该实现不完整,且将无穷递归! 它只处理<App />或<Button />等元素。尚不处理<div />或<p />等元素。 41} 42 43var rootEl = document.getElementById('root'); 44var node = mount(<App />); 45rootEl.appendChild(node); 46

注意:

这的确是一段伪代码。它与真实的实现不同。它会导致栈溢出,因为我们还没有讨论何时停止递归。

让我们回顾一下上面示例中的几个关键概念:

  • React元素是表示组件类型(例如 App)与属性的普通对象。
  • 用户定义组件(例如 App)可以为类或者函数,但它们都会被“渲染为”元素。
  • “装载”(Mounting)是一个递归过程,当给定顶级React元素(例如<App />)时创建DOM或内部节点树。

装载主机元素(Mounting Host Elements)

该过程将没有任何意义,如果最终没有渲染内容到屏幕上。

除了用户定义的(“复合”)组件外, React元素还可能表示特定于平台的(“主机”)组件。例如,Button可能会从其渲染方法中返回 <div />

如果元素的type属性是一个字符串,即表示我们正在处理一个主机元素(host element):

1console.log(<div />); 2// { type: 'div', props: {} } 3

主机元素(host elements)不存在关联的用户定义代码。

当协调器遇到主机元素(host element)时,它会让渲染器(renderer)装载它(mounting)。例如,React DOM将会创建一个DOM节点。

如果主机元素(host element)有子级,协调器(reconciler)则会用上述相同算法递归地将它们装载。而不管子级是主机元素(如<div><hr /></div>)还是混合元素(如<div><Button /></div>)或是两者兼有。

由子级组件生成的DOM节点将被追加到DOM父节点,同时整的DOM结构会被递归装配。

注意:

协调器本身(reconciler)并不与DOM捆绑。装载(mounting)的具体结果(有时在源代码中称为“装载映像”)取决于渲染器(renderer),可能为 DOM节点(React DOM)、字符串(React DOM服务器)或表示本机视图的数值(React Native)。

我们来扩展一下代码,以处理主机元素(host elements):

1function isClass(type) { 2 // React.Component 子类含有这一标志 3 return ( 4 Boolean(type.prototype) && 5 Boolean(type.prototype.isReactComponent) 6 ); 7} 8 9// 该函数仅处理含复合类型的元素。 例如,它处理<App />和<Button />,但不处理<div />。 10function mountComposite(element) { 11 var type = element.type; 12 var props = element.props; 13 14 var renderedElement; 15 if (isClass(type)) { 16 // 组件类 17 var publicInstance = new type(props); 18 // 设置属性 19 publicInstance.props = props; 20 // 若必要,则调用生命周期函数 21 if (publicInstance.componentWillMount) { 22 publicInstance.componentWillMount(); 23 } 24 renderedElement = publicInstance.render(); 25 } else if (typeof type === 'function') { 26 // 组件函数 27 renderedElement = type(props); 28 } 29 30 // 该过程是递归,一旦该元素为主机(如<div />}而非复合(如<App />)时,则逐渐结束 31 return mount(renderedElement); 32} 33 34// 该函数仅处理含主机类型的元素(handles elements with a host type)。 例如,它处理<div />和<p />但不处理<App />。 35function mountHost(element) { 36 var type = element.type; 37 var props = element.props; 38 var children = props.children || []; 39 if (!Array.isArray(children)) { 40 children = [children]; 41 } 42 children = children.filter(Boolean); 43 44 // 该代码块不可出现在协调器(reconciler)中。 45 // 不同渲染器(renderers)可能会以不同方式初始化节点。 46 // 例如,React Native会生成iOS或Android视图。 47 var node = document.createElement(type); 48 Object.keys(props).forEach(propName => { 49 if (propName !== 'children') { 50 node.setAttribute(propName, props[propName]); 51 } 52 }); 53 54 // 装载子节点 55 children.forEach(childElement => { 56 // 子节点有可能是主机元素(如<div />)或复合元素(如<Button />). 57 // 所以我们应该递归的装载 58 var childNode = mount(childElement); 59 60 // 此行代码仍是特定于渲染器的。不同的渲染器则会使用不同的方法 61 node.appendChild(childNode); 62 }); 63 64 // 返回DOM节点作为装载结果 65 // 此处即为递归结束. 66 return node; 67} 68 69function mount(element) { 70 var type = element.type; 71 if (typeof type === 'function') { 72 // 用户定义的组件 73 return mountComposite(element); 74 } else if (typeof type === 'string') { 75 // 平台相关的组件,比如说浏览器中的div,ios和安卓中的视图 76 return mountHost(element); 77 } 78} 79 80var rootEl = document.getElementById('root'); 81var node = mount(<App />); 82rootEl.appendChild(node); 83

该代码能够工作但仍与协调器(reconciler)的真正实现相差甚远。其所缺少的关键部分是对更新的支持。

介绍内部实例

React 的关键特征是您可以重新渲染所有内容, 它不会重新创建 DOM 或重置状态:

1ReactDOM.render(<App />, rootEl); 2// 应该重新使用现存的 DOM: 3ReactDOM.render(<App />, rootEl); 4

但是, 上面的实现只知道如何装载初始树。它无法对其执行更新, 因为它没有存储所有必需的信息, 例如所有 publicInstance ,
或者哪个 DOM 节点 对应于哪些组件。

堆栈协调(stack reconciler)的基本代码是通过使 mount () 函数成为一个方法并将其放在类上来解决这一问题。
这种方式有一些缺陷,但是目前代码中仍然使用的是这种方式。不过目前我们也正在重写协调器(reconciler)

我们将创建两个类: DOMComponent 和 CompositeComponent , 而不是单独的 mountHost 和 mountComposite 函数。

两个类都有一个接受 element 的构造函数, 以及一个能返回已装入节点的 mount () 方法。我们将用一个能实例化正确类的工厂函数替换掉之前
例子里的mount函数:

1function instantiateComponent(element) { 2 var type = element.type; 3 if (typeof type === 'function') { 4 // 用户自定义组件 5 return new CompositeComponent(element); 6 } else if (typeof type === 'string') { 7 // 特定于平台的组件 8 return new DOMComponent(element); 9 } 10} 11

首先, 让我们考虑如何实现 CompositeComponent:

1class CompositeComponent { 2 constructor(element) { 3 this.currentElement = element; 4 this.renderedComponent = null; 5 this.publicInstance = null; 6 } 7 8 getPublicInstance() { 9 // 针对复合组合, 返回类的实例. 10 return this.publicInstance; 11 } 12 13 mount() { 14 var element = this.currentElement; 15 var type = element.type; 16 var props = element.props; 17 18 var publicInstance; 19 var renderedElement; 20 if (isClass(type)) { 21 // 组件类 22 publicInstance = new type(props); 23 // 设置属性 24 publicInstance.props = props; 25 // 如果有必要,调用生命周期 26 if (publicInstance.componentWillMount) { 27 publicInstance.componentWillMount(); 28 } 29 renderedElement = publicInstance.render(); 30 } else if (typeof type === 'function') { 31 // Component function 32 publicInstance = null; 33 renderedElement = type(props); 34 } 35 36 // Save the public instance 37 this.publicInstance = publicInstance; 38 39 // 通过element实例化内部的child实例,这个实例有可能是DOMComponent,比如<div /> or <p /> 40 // 也可能是CompositeComponent 比如说<App /> or <Button /> 41 var renderedComponent = instantiateComponent(renderedElement); 42 this.renderedComponent = renderedComponent; 43 44 // 增加渲染输出 45 return renderedComponent.mount(); 46 } 47} 48

这与我们以前的 mountComposite() 实现没有太大的不同, 但现在我们可以保存一些信息,
比如this.currentElement、this.renderedComponent 和 this.publicInstance ,这些保存的信息会在更新期间被使用。

请注意, CompositeComponent的实例与用户提供的 element.type 的实例不是一回事。
CompositeComponent是我们的协调器(reconciler)的一个实现细节, 从不向用户公开。
用户自定义类是我们从 element.type 读取的,并且通过 CompositeComponent 创建它的一个实例。

为避免混乱,我们将CompositeComponent和DOMComponent的实例称为“内部实例”。
由于它们的存在, 我们可以将一些长寿数据(ong-lived)与它们关联起来。只有渲染器(renderer)和协调器(reconciler)知道它们的存在。

另一方面, 我们将用户定义的类的实例称为 "公共实例"(public instance)。公共实例是您在 render() 和自定义组件的其他方法中看到的 this

mountHost() 函数被重构为 DOMComponent 类上的 mount()方法, 也看起来很熟悉:

1class DOMComponent { 2 constructor(element) { 3 this.currentElement = element; 4 this.renderedChildren = []; 5 this.node = null; 6 } 7 8 getPublicInstance() { 9 // For DOM components, only expose the DOM node. 10 return this.node; 11 } 12 13 mount() { 14 var element = this.currentElement; 15 var type = element.type; 16 var props = element.props; 17 var children = props.children || []; 18 if (!Array.isArray(children)) { 19 children = [children]; 20 } 21 22 // Create and save the node 23 var node = document.createElement(type); 24 this.node = node; 25 26 // Set the attributes 27 Object.keys(props).forEach(propName => { 28 if (propName !== 'children') { 29 node.setAttribute(propName, props[propName]); 30 } 31 }); 32 33 // Create and save the contained children. 34 // Each of them can be a DOMComponent or a CompositeComponent, 35 // depending on whether the element type is a string or a function. 36 var renderedChildren = children.map(instantiateComponent); 37 this.renderedChildren = renderedChildren; 38 39 // Collect DOM nodes they return on mount 40 var childNodes = renderedChildren.map(child => child.mount()); 41 childNodes.forEach(childNode => node.appendChild(childNode)); 42 43 // Return the DOM node as mount result 44 return node; 45 } 46} 47

从 mountHost () 重构后的主要区别在于, 我们现在将 this.node 和 this.renderedChildren 与内部 DOM 组件实例相关联。
我们还将使用它们在将来应用非破坏性更新。

因此, 每个内部实例 (复合实例或主机实例)(composite or host) 现在都指向内部的子实例。为帮助可视化, 如果功能 <App> 组件呈现 <Button> 类组件, 并且 <Button> 类呈现<div>, 则内部实例树将如下所显示:

1[object CompositeComponent] { 2 currentElement: <App />, 3 publicInstance: null, 4 renderedComponent: [object CompositeComponent] { 5 currentElement: <Button />, 6 publicInstance: [object Button], 7 renderedComponent: [object DOMComponent] { 8 currentElement: <div />, 9 node: [object HTMLDivElement], 10 renderedChildren: [] 11 } 12 } 13} 14

在 DOM 中, 您只会看到<div> 。但是, 内部实例树同时包含复合和主机内部实例(composite and host internal instances)。

内部的复合实例需要存储下面的信息:

  • 当前元素(The current element).
  • 如果元素类型是类, 则将类实例化并存为公共实例(The public instance if element type is a class).
  • 一个通过运行render()之后并传入工厂函数而得到的内部实例(renderedComponent)。它可以是一个DOMComponent或一个CompositeComponent。

内部的主机实例需要存储下面的信息:

  • 当前元素(The current element).
  • DOM 节点(The DOM node).
  • 所有的内部子实例,他们可以是 DOMComponent or a CompositeComponent。(All the child internal instances. Each of them can be either a DOMComponent or a CompositeComponent).

如果你很难想象一个内部的实例树是如何在更复杂的应用中构建的, React DevTools)可以给出一个非常接近的近似,因为它突出显示了带有灰色的主机实例,以及用紫色表示的组合实例:

<img src="../images/implementation-notes-tree.png" width="500" style="max-width: 100%" alt="React DevTools tree" />

为了完成这个重构,我们将引入一个函数,它将一个完整的树挂载到一个容器节点,就像ReactDOM.render()。它返回一个公共实例,也类似于 ReactDOM.render():

1function mountTree(element, containerNode) { 2 // 创建顶级内部实例 3 var rootComponent = instantiateComponent(element); 4 5 // 将顶级组件装载到容器中 6 var node = rootComponent.mount(); 7 containerNode.appendChild(node); 8 9 // 返回它所提供的公共实例 10 var publicInstance = rootComponent.getPublicInstance(); 11 return publicInstance; 12} 13 14var rootEl = document.getElementById('root'); 15mountTree(<App />, rootEl); 16

卸载(Unmounting)

现在,我们有了保存有它们的子节点和DOM节点的内部实例,我们可以实现卸载。对于一个复合组件(composite component),卸载将调用一个生命周期钩子然后递归进行。

1class CompositeComponent { 2 3 // ... 4 5 unmount() { 6 // Call the lifecycle hook if necessary 7 var publicInstance = this.publicInstance; 8 if (publicInstance) { 9 if (publicInstance.componentWillUnmount) { 10 publicInstance.componentWillUnmount(); 11 } 12 } 13 14 // Unmount the single rendered component 15 var renderedComponent = this.renderedComponent; 16 renderedComponent.unmount(); 17 } 18} 19

对于DOMComponent,卸载操作让每个孩子进行卸载:

1class DOMComponent { 2 3 // ... 4 5 unmount() { 6 // Unmount all the children 7 var renderedChildren = this.renderedChildren; 8 renderedChildren.forEach(child => child.unmount()); 9 } 10} 11

在实践中,卸载DOM组件也会删除事件侦听器并清除一些缓存,为了便于理解,我们暂时跳过这些细节。

现在我们可以添加一个顶级函数,叫作unmountTree(containerNode),它与ReactDOM.unmountComponentAtNode()类似:

1function unmountTree(containerNode) { 2 // Read the internal instance from a DOM node: 3 // (This doesn't work yet, we will need to change mountTree() to store it.) 4 var node = containerNode.firstChild; 5 var rootComponent = node._internalInstance; 6 7 // Unmount the tree and clear the container 8 rootComponent.unmount(); 9 containerNode.innerHTML = ''; 10} 11

为了使其工作,我们需要从一个DOM节点读取一个内部根实例。我们将修改 mountTree() 以将 _internalInstance 属性添加到DOM 根节点。
我们也将教mountTree()去销毁任何现存树,以便将来它可以被多次调用:

1function mountTree(element, containerNode) { 2 // Destroy any existing tree 3 if (containerNode.firstChild) { 4 unmountTree(containerNode); 5 } 6 7 // Create the top-level internal instance 8 var rootComponent = instantiateComponent(element); 9 10 // Mount the top-level component into the container 11 var node = rootComponent.mount(); 12 containerNode.appendChild(node); 13 14 // Save a reference to the internal instance 15 node._internalInstance = rootComponent; 16 17 // Return the public instance it provides 18 var publicInstance = rootComponent.getPublicInstance(); 19 return publicInstance; 20} 21

现在,可以反复运行unmountTree()或者 mountTree(),清除旧树并且在组件上运行 componentWillUnmount() 生命周期钩子。

更新(Updating)

在上一节中,我们实现了卸载。然而,如果每个组件的prop的变动都要卸载并挂载整个树,这是不可接受的。幸好我们设计了协调器。
协调器(reconciler)的目标是重用已存在的实例,以便保留DOM和状态:

1var rootEl = document.getElementById('root'); 2 3mountTree(<App />, rootEl); 4// 应该重用现有的DOM: 5mountTree(<App />, rootEl); 6

我们将用一种方法扩展我们的内部实例。
除了 mount()和 unmount()。DOMComponent和 CompositeComponent将实现一个新的方法,它叫作 receive(nextElement):

1class CompositeComponent { 2 // ... 3 4 receive(nextElement) { 5 // ... 6 } 7} 8 9class DOMComponent { 10 // ... 11 12 receive(nextElement) { 13 // ... 14 } 15} 16

它的工作是做任何必要的工作,以使组件(及其任何子节点) 能够根据 nextElement 提供的信息保持信息为最新状态。

这是经常被描述为"virtual DOM diffing"的部分,尽管真正发生的是我们递归地遍历内部树,并让每个内部实例接收到更新指令。

更新复合组件(Updating Composite Components)

当一个复合组件接收到一个新元素(element)时,我们运行componentWillUpdate()生命周期钩子。

然后,我们使用新的props重新render组件,并获得下一个render的元素(rendered element):

1class CompositeComponent { 2 3 // ... 4 5 receive(nextElement) { 6 var prevProps = this.currentElement.props; 7 var publicInstance = this.publicInstance; 8 var prevRenderedComponent = this.renderedComponent; 9 var prevRenderedElement = prevRenderedComponent.currentElement; 10 11 // Update *own* element 12 this.currentElement = nextElement; 13 var type = nextElement.type; 14 var nextProps = nextElement.props; 15 16 // Figure out what the next render() output is 17 var nextRenderedElement; 18 if (isClass(type)) { 19 // Component class 20 // Call the lifecycle if necessary 21 if (publicInstance.componentWillUpdate) { 22 publicInstance.componentWillUpdate(nextProps); 23 } 24 // Update the props 25 publicInstance.props = nextProps; 26 // Re-render 27 nextRenderedElement = publicInstance.render(); 28 } else if (typeof type === 'function') { 29 // Component function 30 nextRenderedElement = type(nextProps); 31 } 32 33 // ... 34

下一步,我们可以看一下渲染元素的type。如果自从上次渲染,type 没有被改变,组件接下来可以被适当更新。

例如,如果它第一次返回 <Button color="red" />,并且第二次返回 <Button color="blue" />,我们可以告诉内部实例去 receive() 下一个元素:

1 // ... 2 3 // 如果被渲染元素类型没有被改变, 4 // 重用现有的组件实例. 5 if (prevRenderedElement.type === nextRenderedElement.type) { 6 prevRenderedComponent.receive(nextRenderedElement); 7 return; 8 } 9 10 // ... 11 12

但是,如果下一个被渲染元素和前一个相比有一个不同的type ,我们不能更新内部实例。因为一个 <button> 不“能变”为一个 <input>.

相反,我们必须卸载现有的内部实例并挂载对应于渲染的元素类型的新实例。
例如,这就是当一个之前被渲染的元素<button />之后又被渲染成一个 <input /> 的过程:

1 // ... 2 3 // If we reached this point, we need to unmount the previously 4 // mounted component, mount the new one, and swap their nodes. 5 6 // Find the old node because it will need to be replaced 7 var prevNode = prevRenderedComponent.getHostNode(); 8 9 // Unmount the old child and mount a new child 10 prevRenderedComponent.unmount(); 11 var nextRenderedComponent = instantiateComponent(nextRenderedElement); 12 var nextNode = nextRenderedComponent.mount(); 13 14 // Replace the reference to the child 15 this.renderedComponent = nextRenderedComponent; 16 17 // Replace the old node with the new one 18 // Note: this is renderer-specific code and 19 // ideally should live outside of CompositeComponent: 20 prevNode.parentNode.replaceChild(nextNode, prevNode); 21 } 22} 23

总而言之,当一个复合组件(composite component)接收到一个新元素时,它可能会将更新委托给其渲染的内部实例((rendered internal instance),
或者卸载它,并在其位置上挂一个新元素。

另一种情况下,组件将重新挂载而不是接收一个元素,并且这发生在元素的key变化时。本文档中,我们不讨论key 处理,因为它将使原本复杂的教程更加复杂。

注意,我们需要添加一个叫作getHostNode()的新方法到内部实例(internal instance),以便可以定位特定于平台的节点并在更新期间替换它。
它的实现对两个类都很简单:

1class CompositeComponent { 2 // ... 3 4 getHostNode() { 5 // 请求渲染的组件提供它(Ask the rendered component to provide it). 6 // 这将递归地向下钻取任何组合(This will recursively drill down any composites). 7 return this.renderedComponent.getHostNode(); 8 } 9} 10 11class DOMComponent { 12 // ... 13 14 getHostNode() { 15 return this.node; 16 } 17} 18

更新主机组件(Updating Host Components)

主机组件实现(例如DOMComponent), 是以不同方式更新.当它们接收到一个元素时,它们需要更新底层特定于平台的视图。在 React DOM 中,这意味着更新 DOM 属性:

1class DOMComponent { 2 // ... 3 4 receive(nextElement) { 5 var node = this.node; 6 var prevElement = this.currentElement; 7 var prevProps = prevElement.props; 8 var nextProps = nextElement.props; 9 this.currentElement = nextElement; 10 11 // Remove old attributes. 12 Object.keys(prevProps).forEach(propName => { 13 if (propName !== 'children' && !nextProps.hasOwnProperty(propName)) { 14 node.removeAttribute(propName); 15 } 16 }); 17 // Set next attributes. 18 Object.keys(nextProps).forEach(propName => { 19 if (propName !== 'children') { 20 node.setAttribute(propName, nextProps[propName]); 21 } 22 }); 23 24 // ... 25

接下来,主机组件需要更新它们的子元素。与复合组件不同的是,它们可能包含多个子元素。

在这个简化的例子中,我们使用一个内部实例的数组并对其进行迭代,是更新或替换内部实例,这取决于接收到的type是否与之前的type匹配。
真正的调解器(reconciler)同时在帐户中获取元素的key并且追踪变动,除了插入与删除,但是我们现在先忽略这一逻辑。

我们在列表中收集DOM操作,这样我们就可以批量地执行它们。

1 // ... 2 3 // // 这些是React元素(element)数组: 4 var prevChildren = prevProps.children || []; 5 if (!Array.isArray(prevChildren)) { 6 prevChildren = [prevChildren]; 7 } 8 var nextChildren = nextProps.children || []; 9 if (!Array.isArray(nextChildren)) { 10 nextChildren = [nextChildren]; 11 } 12 // 这些是内部实例(internal instances)数组: 13 var prevRenderedChildren = this.renderedChildren; 14 var nextRenderedChildren = []; 15 16 // 当我们遍历children时,我们将向数组中添加操作。 17 var operationQueue = []; 18 19 // 注意:以下章节大大减化! 20 // 它不处理reorders,空children,或者keys。 21 // 它只是用来解释整个流程,而不是具体的细节。 22 23 for (var i = 0; i < nextChildren.length; i++) { 24 // 尝试为这个子级获取现存内部实例。 25 var prevChild = prevRenderedChildren[i]; 26 27 // 如果在这个索引下没有内部实例,那说明是一个child被添加了末尾。 28 // 这时应该去创建一个内部实例,挂载它,并使用它的节点。 29 if (!prevChild) { 30 var nextChild = instantiateComponent(nextChildren[i]); 31 var node = nextChild.mount(); 32 33 // 记录一下我们将来需要append一个节点(node) 34 operationQueue.push({type: 'ADD', node}); 35 nextRenderedChildren.push(nextChild); 36 continue; 37 } 38 39 // 如果它的元素类型匹配,我们只需要更新该实例即可 40 // 例如, <Button size="small" /> 可以更新为 41 // <Button size="large" /> 但是不能被更新为 <App />. 42 var canUpdate = prevChildren[i].type === nextChildren[i].type; 43 44 // 如果我们不能更新现有的实例,我们就必须卸载它。然后装一个新的替代它。 45 if (!canUpdate) { 46 var prevNode = prevChild.getHostNode(); 47 prevChild.unmount(); 48 49 var nextChild = instantiateComponent(nextChildren[i]); 50 var nextNode = nextChild.mount(); 51 52 // 记录一下我们将来需要替换这些nodes 53 operationQueue.push({type: 'REPLACE', prevNode, nextNode}); 54 nextRenderedChildren.push(nextChild); 55 continue; 56 } 57 58 // 如果我们可以更新现存的内部实例(internal instance), 59 // 我们仅仅把下一个元素传入其receive即可,让其receive函数处理它的更新即可 60 prevChild.receive(nextChildren[i]); 61 nextRenderedChildren.push(prevChild); 62 } 63 64 // 最后,卸载(unmount)哪些不存在的children 65 for (var j = nextChildren.length; j < prevChildren.length; j++) { 66 var prevChild = prevRenderedChildren[j]; 67     var node = prevChild.getHostNode(); 68 prevChild.unmount(); 69 70 // 记录一下我们将来需要remove这些node 71 operationQueue.push({type: 'REMOVE', node}); 72 } 73 74 // Point the list of rendered children to the updated version. 75 this.renderedChildren = nextRenderedChildren; 76 77 // ... 78

作为最后一步,我们执行DOM操作。还是那句话,真正的协调器(reconciler)代码更复杂,因为它还能处理移动:

1 // ... 2 3 // 处理队列里的operation。 4 while (operationQueue.length > 0) { 5 var operation = operationQueue.shift(); 6 switch (operation.type) { 7 case 'ADD': 8 this.node.appendChild(operation.node); 9 break; 10 case 'REPLACE': 11 this.node.replaceChild(operation.nextNode, operation.prevNode); 12 break; 13 case 'REMOVE': 14 this.node.removeChild(operation.node); 15 break; 16 } 17 } 18 } 19} 20

这是用来更新主机组件(host components)的。

顶级更新(Top-Level Updates)

现在 CompositeComponent 与 DOMComponent 都实现了 receive(nextElement) 方法,
我们现在可以改变顶级 mountTree() 函数了,当元素(element)的type相同时,我们可以使用receive了。

1function mountTree(element, containerNode) { 2 // Check for an existing tree 3 if (containerNode.firstChild) { 4 var prevNode = containerNode.firstChild; 5 var prevRootComponent = prevNode._internalInstance; 6 var prevElement = prevRootComponent.currentElement; 7 8 // 如果可以,使用现存根组件 9 if (prevElement.type === element.type) { 10 prevRootComponent.receive(element); 11 return; 12 } 13 14 // 否则,卸载现存树 15 unmountTree(containerNode); 16 } 17 18 // ... 19 20} 21

现在调用 mountTree()两次,同样的类型不会先卸载再装载了:

1var rootEl = document.getElementById('root'); 2 3mountTree(<App />, rootEl); 4// 复用现存 DOM: 5mountTree(<App />, rootEl); 6

These are the basics of how React works internally.

我们遗漏的还有什么?

与真正的代码库相比,这个文档被简化了。有一些重要的方面我们没有提到:

  • 组件可以渲染null,而且,协调器(reconciler)可以处理数组中的“空槽(empty slots)”并显示输出。
  • 协调器(reconciler)可以从元素中读取 key ,并且用它来建立在一个数组中内部实例与元素的对应关系。实际的 React 实现的大部分复杂性与此相关。
  • 除了复合和主机内部实例类之外,还存在用于“文本”和“空”组件的类。它们表示文本节点和通过渲染 null得到的“空槽”。
  • 渲染器(Renderers)使用injection

将主机内部类传递给协调器(reconciler)。例如,React DOM 告诉协调器使用 ReactDOMComponent 作为主机内部实现实例。

  • 更新子列表的逻辑被提取到一个名为 ReactMultiChild 的mixin中,它被主机内部实例类实现在 React DOM和 React Native时都使用。
  • 协调器也实现了在复合组件(composite components)中支持setState()。事件处理程序内部的多个更新将被打包成一个单一的更新。
  • 协调器(reconciler)还负责复合组件和主机节点的refs。
  • 在DOM准备好之后调用的生命周期钩子,例如 componentDidMount() 和 componentDidUpdate(),收集到“回调队列”,并在单个批处理中执行。
  • React 将当前更新的信息放入一个名为“事务”的内部对象中。事务对于跟踪挂起的生命周期钩子的队列、

为了warning而嵌套的当前DOM(the current DOM nesting for the warnings)以及任何“全局”到特定的更新都是有用的。
事务还可以确保在更新后“清除所有内容”。例如,由 React DOM提供的事务类在任何更新之后恢复input的选中与否。

直接查看代码(Jumping into the Code)

  • 在 ReactMount 中可以查看此教程中类似 mountTree() 和 unmountTree() 的代码.

它负责装载(mounting)和卸载(unmounting)顶级组件。
ReactNativeMount is its React Native analog.

  • ReactDOMComponent

在教程中与DOMComponent等同. 它实现了 React DOM渲染器(renderer)的主机组件类(host component class。
ReactNativeBaseComponent is its React Native analog.

  • ReactCompositeComponent

在教程中与 CompositeComponent 等同. 它处理调用用户定义的组件并维护它们的状态。

  • instantiateReactComponent

包含选择正确的内部实例类并运行element的构造函数。在本教程中,它与instantiateComponent()等同。

  • ReactReconciler

是一个具有 mountComponent(), receiveComponent(), 和 unmountComponent() 方法的封装.
它调用内部实例的底层实现,但也包含了所有内部实例实现共享的代码。

  • ReactChildReconciler

根据元素的 key ,实现了mounting、updating和unmounting的逻辑.

  • ReactMultiChild

独立于渲染器的操作队列,实现了处理child的插入、删除和移动

  • 由于遗留的原因 mount(), receive(), and unmount() 被称作 mountComponent(),

receiveComponent(), and unmountComponent() 但是他们却接收elements

  • 内部实例的属性以一个下划线开始, 例如, _currentElement. 在整个代码库中,它们被认为是只读的公共字段。

未来方向(Future Directions)

堆栈协调器具有固有的局限性, 如同步和无法中断工作或分割成区块。
我们正在实现一个新的协调器Fiber reconciler,
你可以在这里看它的具体思路
将来我们会用fiber协调器代替stack协调器(译者注:其实现在react16已经发布,在react16中fiber算法已经取代了stack算法)

下一步(Next Steps)

阅读next section以了解有关协调器的当前实现的详细信息。

代码交流 2021