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