React 中组件的更新大致有两种,第一种是由于单向数据流传到当前组件中 props
的变化导致,另一种是由于组件中 setState
引起的 local state 的变化导致。由于 props
的单项数据流始于最外层的组件中的 local state(如果不使用 Redux 等状态管理工具的话),我们不妨从 setState
入手,分析 React 是如何更新 DOM 的。
在继续下去之前,推荐阅读:
在 React 中,setState
是异步的,这是因为 React 对 setState
进行了 batch 操作,即将短时间内的几个 setState
合并为一个。为什么要这么做呢?因为计算由 setState
而引发的 DOM diff 是很费时的,batch 使整个流程从读取、修改、读取、修改、读取、修改……变成了读取、读取、读取、修改,减少了大量的计算操作。
为了简化,我们暂不考虑 batch 的实现。并且不考虑 setState
接受一个回调函数作为参数的情况。
首先回忆一下,setState
只能在 Class Component 中使用,这意味着这个方法位于 Component
这个文件中:
class Component {
// ...
setState(partialState) {
this._pendingState = Object.assign({}, this.state, partialState)
this.updateComponent(this._currentElement, this._currentElement)
}
updateComponent(prevElement, nextElement) {}
}
为什么这里要调用 updateComponent
的两个参数都是 currentElement
呢?我们知道 React 用一个 element 来表示一个组件的 DOM 结构。并且在上文提到,组件的更新无非有两种,一种是组件的 props
发生变化,这会改变 Element 的数据,而 state 的改变却并不会改变 Element。所以这里 element 在 setState
的操作中是没有变化的。
知道了这一点后,我们也知道要在 updateComponent
中区分这两种情况了。
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(prevRenderedElement, nextRenderedElement)) {
Reconciler.receiveComponent(this._renderedComponent, nextElement)
} else {
// remount everything under this node
Reconciler.unmountComponent(this._renderedComponent)
const nextRenderedComponent = instantiateComponent(nextElement)
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 中,我们了解到,现有的 Tree Diff Algorithm 的复杂度是 O(n^3),而 React 基于两个假设得出了一个 O(n) 的 Diff 算法,也就是我们所说的 Virtual DOM Diff Algorithm。
这两个假设是:
- 不同类型的 Element 会生成不同的子树。例如
div
变化成ul
,或者复合组建由<Counter />
变为<Header />
- 通过
key
这个属性,React 可以得知在重绘中需要具体更新哪几个节点。
我们暂且不考虑 key
的实现,只考虑第一点。这样一来也很简单地实现了 shouldUpdateComponent
这个函数。(注意区分 shouldUpdateComponent 和 shouldComponentUpdate 两个方法,前者用来判断组件 element 的 type 有没有变化,后者是 React 组件内部的生命周期函数)
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
(但是这个命名并不是很好)。它的实现如下:
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)。
举个例子:
class Counter extends React.Component {
constructor() {
super()
this.state = { count: 0 }
setInterval(() => {
this.setState({ count: this.state.count + 1 })
}, 1000)
}
render() {
return (
<div>
<span>{ this.state.count }</span>
</div>
)
}
}
那么整个更新的流程图应该是这样的:
由此可以看出,和 mounting 一样,真正的 updating 也是发生在 DOMComponent
里。
那么我们进一步去看 DOMComponent
内部是怎么进行 update 的。
// 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 算法的内部细节。