内容参考自 《深入 React 技术栈》

一、setState 实现机制:

setState 通过一个队列机制实现 state 更新。

当执行 setState 的时候,会将需要更新的 state 合并后放入状态队列,而不会立即更新 this.state ,队列机制可以高效的批量更新 state

如果不通过 setState 而直接修改 this.state 值,那么该 state 将不会被放入状态队列中,当下次调用 setState 并对状态队列进行合并时,将会忽略之前直接被修改的 state,而造成无法预知的错误。

二、 setState 循环调用风险:

当调用 setState 方法的时候实际上会执行 enqueueSetState 方法,并且对 partialState_pendingStateQueue 更新队列进行合并操作,最终通过 enqueueUpdate 执行 state 更新。

performUpdateIfNessary 方法会获取 _pendingElement_pendingStateQueue_pendingForceUpdate,并调用 receiveComponentupdateComponent 方法进行组件更新。

如果在 shouldComponentUpdatecomponentWillUpdate 中调用 setState ,此时 this._pendingStateQueue != null,则 performUpdateIfNessary 方法就会调用 updateComponent 方法进行组件更新,而 updateComponent 方法又会调用 shouldComponentUpdatecomponentWillUpdate 方法,因此造成循环调用,耗光浏览器内存之后崩溃掉。

如下图所示:

4.jpg

上图来自 《深入 React 技术栈》 图 3-14

三、setState 调用栈

下图为作者给出的 setState 简化调用栈:

5.jpg

上图来自 《深入 React 技术栈》 图 3-15

enqueueUpdate 的作用是判断 batchingStrategy.isBatchingUpdates 如果是 true,则对所有队列中的更新执行 batchUpdates 方法,否则只把当前组件(调用了 setState 的组件)放入 dirtyComponents 数组中。

enqueueUpdate 源码:

function enqueueUpdate(component){
  ensureInjected();
  // 如果不是批量更新模式
  if (!batchingStrategy.isBatchingUpdates){
    batchingStrategy.batchingUpdates(enqueueUpdate, component);
    return ;
  }
  // 如果处于批量更新模式 则将该组件保存在 dirtyComponents 中
  dirtyComponents.push(component)
}

batchingStrategy 只是定义了一个 Boolean 类型的变量 isBatchingUpdates 和 一个 batchedUpdates 方法:

var ReactDefaultBatchingStrategy = {
    isBatchingUpdates: false,
    batchedUpdates: function(callback, a, b, c, d, e) {
        var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
        ReactDefaultBatchingStrategy.isBatchingUpdates = true;
        if (alreadyBatchingUpdates) {
            callback(a, b, c, d, e);
        } else {
            // transaction.perform 涉及一个事务的概念
            transaction.perform(callback, null, a, b, c, d, e);
        }
    },
}

四、事务

这里事务的概念如下:

将需要执行的方法使用 wrapper 封装起来,再通过事务提供的 perform 方法执行。

perform 之前,先执行所有 wrapper 中的 initialize 方法,执行完 perform 方法之后(执行 method 方法后)再执行所有的 close 方法。

一组 initializeclose 方法称为一个 wrapper,多个 wrapper 可以叠加。

如下图所示:

6.jpg

上图来自 《深入 React 技术栈》 图 3-16

五、一个有趣的问题

在一个组件的 componentDidMount 中继续四次 setState(this.state.val 已经初始化为 0):

componentDidMount(){
    this.setState({val: this.state.val + 1});
    console.log(this.state.val); // 第 1 次输出
    this.setState({val: this.state.val + 1});
    console.log(this.state.val); // 第 2 次输出
    setTimeout(() => {
        this.setState({val: this.state.val + 1});
        console.log(this.state.val); // 第 3 次输出
        this.setState({val: this.state.val + 1});
        console.log(this.state.val); // 第 4 次输出
    }, 0);
}

正确答案是: 0 0 2 3

我第一次看到这个写的结果是: 0 0 2 2

究其原因还是 调用栈事务 的原因:

    • componentDidMount 中直接调用的两次 setState,调用栈比较复杂,在这之前 batchingStrategyisBatchingUpdates 已经被设为 true, 所以两次 setState 没有立即生效,而是放进了 dirtyComponents 中,因此两个 console 都是 0.
    • 在 setTimeout 中的两次 setState,因为没有前置的 batchUpdate 的调用,所以 batchingStrategyisbatchingUpdates 标志位是 false,也导致了 state 的马上生效。

    针对代码中的阐述如下:

        this.setState({val: this.state.val + 1}); 此时 this.state.val 是 0 
        console.log(this.state.val); // 第 1 次输出  此时 this.state.val 是 0 
        this.setState({val: this.state.val + 1}); 此时 this.state.val 是 0 
        console.log(this.state.val); // 第 2 次输出 此时 this.state.val 是 0 
        setTimeout(() => {
            this.setState({val: this.state.val + 1}); 此时 this.state.val 是 1(+1之前)
            console.log(this.state.val); // 第 3 次输出  此时 this.state.val 是 2(+1之后)
            this.setState({val: this.state.val + 1}); 此时 this.state.val 是 2 (+1之前)
            console.log(this.state.val); // 第 4 次输出 此时 this.state.val 是 3(+1之后)
        }, 0);