diff --git a/blog/assets/update-process.jpg b/blog/assets/update-process.jpg new file mode 100644 index 0000000..91dc02a Binary files /dev/null and b/blog/assets/update-process.jpg differ diff --git a/blog/update.md b/blog/update.md new file mode 100644 index 0000000..672025f --- /dev/null +++ b/blog/update.md @@ -0,0 +1,139 @@ +# `setState` and Update + +React 中组件的更新大致有两种,第一种是由于单向数据流传到当前组件中 `props` 的变化导致,另一种是由于组件中 `setState` 引起的 local state 的变化导致。由于 `props` 的单项数据流始于最外层的组件中的 local state(如果不使用 Redux 等状态管理工具的话),我们不妨从 `setState` 入手,分析 React 是如何更新 DOM 的。 + +在继续下去之前,推荐阅读: + +[Reconciliation](https://reactjs.org/docs/reconciliation.html) + +在 React 中,`setState` 是异步的,这是因为 React 对 `setState` 进行了 batch 操作,即将短时间内的几个 `setState` 合并为一个。为什么要这么做呢?因为计算由 `setState` 而引发的 DOM diff 是很费时的,batch 使整个流程从**读取、修改、读取、修改、读取、修改……**变成了**读取、读取、读取、修改**,减少了大量的计算操作。 + +为了简化,我们暂不考虑 batch 的实现。并且不考虑 `setState` 接受一个回调函数作为参数的情况。 + +首先回忆一下,`setState` 只能在 Class Component 中使用,这意味着这个方法位于 `Component` 这个文件中: + +```js +class Component { + // ... + setState(partialState) { + this._pendingState = Object.assign({}, this.props, partialState) + this.updateComponent(this._currentElement, this._currentElement) + } + + updateComponent(prevElement, nextElement) {} +} +``` + +为什么这里要调用 `updateComponent` 的两个参数都是 `currentElement` 呢?我们知道 React 用一个 element 来表示一个组件的 DOM 结构。并且在上文提到,组件的更新无非有两种,一种是组件的 `props` 发生变化,这会改变 Element 的数据,而 state 的改变却并不会改变 Element。所以这里 element 在 `setState` 的操作中是没有变化的。 + +知道了这一点后,我们也知道要在 `updateComponent` 中区分这两种情况了。 + +```js +updateComponent(prevElement, nextElement) { + if (prevElement !== nextElement) { + // should get re-render because of the changes of props passed down from parents + // react calls componentWillReceiveProps here + } + + // re-bookmarking + this._currentElement = nextElement + + this.props = nextElement.props + this.state = this._pendingState + this._pendingState = null + + const prevRenderedElement = this._renderedComponent._currentElement + const nextRenderedElement = this.render() + + if (shouldUpdateComponent(prevRenderedComponent, nextRenderedComponent)) { + Reconciler.receiveComponent(this._renderedComponent, nextElement) + } else { + // remount everything under this node + Reconciler.unmountComponent(this._renderedComponent) + + const nextRenderedComponent = instantiateComponent(nextRenderedComponent) + this._renderedNode = Reconciler.mountComponent(nextRenderedComponent) + + DOM.replaceNode(this._renderedComponent._domNode, this._renderedNode) + } +} +``` + +在这段代码中,如上所述,我们首先通过判断 `prevElement` 和 `nextElement` 是否相等,来得出是 `props` 变化还是 `state` 变化导致的 re-render。如果 `Element` 发生变化,说明 `props` 发生了改变,React 此时也会调用 `componentWillReceiveProps` 这个生命周期函数。 + +接着,我们重新设置当前 component instance 的 `props` 和 `state`。由于 React 组件就是 `(props, state) => element` 的一个函数映射,所以此时我们通过 `render` 得出了新的 element。 + +接下来我们需要正式进入通过对边 `prevElement` 和 `nextElement` 尽兴更新的环节。在 [Reconciliation](https://reactjs.org/docs/reconciliation.html) 中,我们了解到,现有的 [Tree Diff Algorithm](https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf) 的复杂度是 O(n^3),而 React 基于两个假设得出了一个 O(n) 的 Diff 算法,也就是我们所说的 Virtual DOM Diff Algorithm。 + +这两个假设是: + +1. 不同类型的 Element 会生成不同的子树。例如 `div` 变化成 `ul`,或者复合组建由 `` 变为 `
` +1. 通过 `key` 这个属性,React 可以得知在重绘中需要具体更新哪几个节点。 + +我们暂且不考虑 `key` 的实现,只考虑第一点。这样一来也很简单地实现了 `shouldUpdateComponent` 这个函数。(**注意区分 shouldUpdateComponent 和 shouldComponentUpdate 两个方法,前者用来判断组件 element 的 type 有没有变化,后者是 React 组件内部的生命周期函数**) + +```js +function shouldUpdateComponent(prevElement, nextElement) { + return prevElement.type === nextElement.type +} +``` + +当 `element` 类型发生改变时,React 选择重新绘制由此向下的所有节点。所以我们看到了 `else` 部分的代码:首先销毁当前 component instance,然后重新 instantiate,并 mount component。 + +当 `element` 类型没有改变时,我们需要**更新**相应的 DOM 节点,而不是重新 mount。我们记得之前在 mounting 中讲到,React 通过 Reconciler 实现了 `mountComponent` 接口的多态。这里我们再介绍一个新的方法,叫做 `receiveComponent`(但是这个命名并不是很好)。它的实现如下: + +```js +function receiveComponent(component, nextElement) { + if (component._currentElement === nextElement) return + component.updateComponent(component._currentElement, nextElement) +} +``` + +实际上就是调用了对应组件内部的 `updateComponent` 这个方法。 + +需要额外注意的是,从最开始的 mounting,亦或是从 `setState` 开始的 updating,class component 内部的 `this._renderedComponent` 和 `this._currentElement` 是 **`render` 函数最外层的组件类型**,调用的 `updateComponent` 从 Class Component defer到了 DOM Component)。 + +举个例子: + +```js +class Counter extends React.Component { + constructor() { + super() + this.state = { count: 0 } + setInterval(() => { + this.setState({ count: this.state.count + 1 }) + }, 1000) + } + + render() { + return ( +
+ { this.state.count } +
+ ) + } +} +``` + +那么整个更新的流程图应该是这样的: + +![update-process](assets/update-process.jpg) + +由此可以看出,和 mounting 一样,真正的 updating 也是发生在 `DOMComponent` 里。 + +那么我们进一步去看 `DOMComponent` 内部是怎么进行 update 的。 + +```js +// DOMComponent.js +updateComponent(prevElement, nextElement) { + this._currentElement = nextElement + this._updateNodeProperties(prevElement.props, nextElement.props) + this._updateDOMChildren(prevElement.props, nextElement.props) +} +``` + +非常简洁是不是?更新当前 DOM 节点的属性(上节已经讲过),然后递归更新子树。 + +但是到目前位置我们还并没有详细进入 `_updateDOMChildren` 这个函数的细节,而这正是 React Virtual DOM 的 Diff 算法的精华。 + +这一节我们着重分析 **React update 的整个流程**,下一节我们会分析这个函数带来的一系列操作,并开始分析 Diff 算法的内部细节。 diff --git a/dilithium/src/Component.js b/dilithium/src/Component.js index a9a1b5b..751b2b9 100644 --- a/dilithium/src/Component.js +++ b/dilithium/src/Component.js @@ -72,11 +72,6 @@ class Component { } } - receiveComponent(nextElement) { - // the new element that the current component should update itself to - this.updateComponent(this._currentElement, nextElement) - } - performUpdateIfNecessary() { // react uses a batch here, we are just gonna call it directly without delay this.updateComponent(this._currentElement, this._currentElement) diff --git a/dilithium/src/DOMComponent.js b/dilithium/src/DOMComponent.js index 1b5ce2d..5d684a1 100644 --- a/dilithium/src/DOMComponent.js +++ b/dilithium/src/DOMComponent.js @@ -24,10 +24,6 @@ class DOMComponent extends MultiChild { this.unmountChildren() } - receiveComponent(nextElement) { - this.updateComponent(this._currentElement, nextElement) - } - updateComponent(prevElement, nextElement) { this._currentElement = nextElement this._updateNodeProperties(prevElement.props, nextElement.props) diff --git a/dilithium/src/Reconciler.js b/dilithium/src/Reconciler.js index dc24c11..9a25b9d 100644 --- a/dilithium/src/Reconciler.js +++ b/dilithium/src/Reconciler.js @@ -14,7 +14,7 @@ function receiveComponent(component, nextElement) { const prevElement = component._currentElement if (prevElement === nextElement) return - component.receiveComponent(nextElement) + component.updateComponent(component._currentElement, nextElement) } module.exports = {