研究 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
,因此进入了死循环。