研究 setState 这个问题来源于一个疑惑:使用 redux 的时候 dispatch 一个 action,为什么可以导致视图的更新?
首先的猜想是 store 改变后,redux 在某处调用了 setState,通知了 react。
看了下代码发现确实如此,调用 dispatch action 会触发一个 onStateChange 的函数 (这个函数在 connect 的时候就被注册到 store 了, store 被 reducer 修改后触发),onStateChange 函数判断如果需要 shouldComponentUpdate 的话则执行 this.setState({}) 来触发 react 更新。
那么问题来了:
- 为什么
setState可以让视图更新,它是如何一步步到virtualDOM然后渲染的呢 setState为什么有时表现是异步的有时又是同步的?- 为什么在生命周期函数中,
willReceiveProps里可以setState而willUpdate不行?
捋了一下流程得出下图,图中每个流程块冒号前即为被执行的函数:
简要的说一下流程:
setState后将传入的state放入队列queue,enqueueUpdate方法会根据isBatchingUpdate标志位判断,若当前已经在更新组件则将直接当前组件放入dirtyComponents数组,否则将isBatchingUpdate置为 true 并开启一个 "批量更新 (batchedUpdates)" 的事务(transaction)。
简单地说,一个所谓的
Transaction就是将需要执行的method使用wrapper封装起来,再通过Transaction提供的perform方法执行。而在perform之前,先执行所有wrapper中的initialize方法;perform完成之后(即method执行后)再执行所有的close方法。一组initialize及close方法称为一个wrapper,Transaction支持多个wrapper叠加。
事务开启后会依次执行 initialize、perform、close 方法。可以看到,batchedUpdates 在 perform 阶段会再次执行 enqueueUpdate 方法,由于这时的 isBatchingUpdate 已经是 true 了所以会将当前组件放入 dirtyComponents。关键就在 close 阶段了,如果 dirtyComponents 为空则表示不需要更新,否则就开始更新,开启 flushBatchedUpdates 事务。
flushBatchedUpdates在perform阶段会将dirtyComponents中的组件按父 > 子组件的顺序调用更新方法,组件在更新的时候会依次执行:
willReceiveProps -> 将 queue 中缓存的 state 与缓存的 state 合并 -> shouldComponentUpdate。
如果判断需要更新,则执行组件的 render 方法得到新的 reactElement,将其与之前的 reactElement 做 diff 即可,将 diff 结果(删除,移动等)通过 setInnerHTML 等封装方法更新视图即可,细节可见图。
flushBatchedUpdates在close阶段会再次检查dirtyComponents长度有没有变化,如果变化了说明存在有新的dirtyComponent,需要再来一次flushBatchedUpdates。
补上 updateComponent 代码:
// 更新组件
updateComponent: function(transaction, prevParentElement, nextParentElement) {
var prevContext = this.context;
var prevProps = this.props;
var nextContext = prevContext;
var nextProps = prevProps;
if (prevParentElement !== nextParentElement) {
nextContext = this._processContext(nextParentElement._context);
nextProps = this._processProps(nextParentElement.props);
// 当前状态为 RECEIVING_PROPS
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
// 如果存在 componentWillReceiveProps,则执行
if (this.componentWillReceiveProps) {
this.componentWillReceiveProps(nextProps, nextContext);
}
}
// 设置状态为 null,更新 state
this._compositeLifeCycleState = null;
var nextState = this._pendingState || this.state;
this._pendingState = null;
var shouldUpdate =
this._pendingForceUpdate ||
!this.shouldComponentUpdate ||
this.shouldComponentUpdate(nextProps, nextState, nextContext);
if (!shouldUpdate) {
// 如果确定组件不更新,仍然要设置 props 和 state
this._currentElement = nextParentElement;
this.props = nextProps;
this.state = nextState;
this.context = nextContext;
this._owner = nextParentElement._owner;
return;
}
this._pendingForceUpdate = false;
......
// 如果存在 componentWillUpdate,则触发
if (this.componentWillUpdate) {
this.componentWillUpdate(nextProps, nextState, nextContext);
}
// render 递归渲染
var nextMarkup = this._renderedComponent.mountComponent(
thisID,
transaction,
this._mountDepth + 1
);
// 如果存在 componentDidUpdate,则触发
if (this.componentDidUpdate) {
transaction.getReactMountReady().enqueue(
this.componentDidUpdate.bind(this, prevProps, prevState, prevContext),
this
);
}
},
捋完整个流程可以回答之前一些疑惑:
- 为什么
setState后紧接着打 log,有时state没有立刻变,有时候又变了?
生命周期中的 setState 处于一个大的 transaction 中,此时的 isBatchingUpdate 为 true,执行 setState 只会让 dirtyComponents 数组 push 当前组件而不会进一步处理,此时 log 来看的话 state 还是没有变的。而如果在 transaction 之外,例如 setTimeout 里 setState,此时 isBatchingUpdate 为 false,会一路直接执行下来更改 state,所以此时 log 出来 state 是被立刻改变了的。因此 setState 不保证是同步,而不是说它一定是异步。
2. 都在同一个 tranaction 中,为什么在 willReceiveProps 时还可以 setState,而在 shouldComponentUpdate 和 willUpdate 的时候 setState 会导致浏览器死循环?
组件内部有一标志位 _compositeLifeCycleState 表示当前生命周期状态,在 willReceiveProps 前被设置为 RECEIVING_PROPS,在 willReceiveProps 执行后被设置为 null,而 performUpdateIfNecessary 函数在当前状态为 MOUNTING 或 RECEIVING_PROPS 时不会继续调用 updateComponent 函数。
performUpdateIfNecessary: function(transaction) {
var compositeLifeCycleState = this._compositeLifeCycleState;
// ■■■■■■■■重点■■■■■■■■■■■■
// 当状态为 MOUNTING 或 RECEIVING_PROPS 时,则不更新
if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING ||
compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
return;
}
var prevElement = this._currentElement;
var nextElement = prevElement;
if (this._pendingElement != null) {
nextElement = this._pendingElement;
this._pendingElement = null;
}
// 调用 updateComponent
this.updateComponent(
transaction,
prevElement,
nextElement
);
}
因此在 willReceiveProps 时 setState 由于 _compositeLifeCycleState 已经是 RECEIVING_PROPS 了,不回触发新的 updateComponent,而在 willUpdate 的时候 _compositeLifeCycleState 已经被置回 null 了,因此会引发下一次的 updateComponent,然后就再次触发组件的各生命周期,当然也会免不了执行 willUpdate,因此进入了死循环。