React-setState源码实现理解

2次阅读

共计 4468 个字符,预计需要花费 12 分钟才能阅读完成。

<!– TOC –>

  • Q1–setState 改变状态之后,不会立即更新 state 值
  • A1

    • [1. react 生命周期]()
    • 2. react 更新 state 具体做了什么
    • 3. transaction 事务
    • 4. 回顾上述问题
    • 5. 总结
  • Q2– 在 render 函数里,无法 setState
  • A2

<!– /TOC –>

Q1

setState 改变状态之后,不会立即更新 state 值。所以,如果改变 state 值,react 是什么时候进行组件的更新呢?setState()到底做了一些什么呢?

A1

1. react 生命周期

2. react 更新 state 具体做了什么

引入一段源码

react 中定义的 setState 方法,定义了两个参数(partialState,callback)。

partialState: 新的 state 值;
callback: 回调函数。

getInternalInstanceReadyForUpdate 方法的目的是获取当前组件对象,将其赋值给 internalInstance 变量。接下来判断当前组件对象的 state 更新队列是否存在,如果存在则将 partialState 也就是新的 state 值加入队列;如果不存在,则创建该对象的更新队列。然后进入 enqueueUpdate 方法。

enqueueCallback 也是先获取当前组件对象,如果已经存在其他回调,就加入等待回调队列,如果当前没有回调,就创建等待回调队列。然后进入 enqueueUpdate 方法。

可以发现,enqueueSetState&enqueueCallback 最终都是进入 enqueueUpdate 方法。下面我们来看看 enqueueUpdate 方法。

官方注解是:给组件做个标记:需要重新渲染,并且将可选的回调函数添加到函数列表中,这些函数将在重新渲染的时候执行。

我们看一下函数具体做了哪些事。发现这个函数只是做了一个判断: 如果 batchingStrategy.isBatchingUpdates 为 false, 就执行 batchingStrategy.batchedUpdates(enqueueUpdate,component),否则就加入 dirtyComponents。

这里提到 batchingStrategy,批量更新策略。

批量更新策略是什么呢?看代码发现 batchingStrategy 批量更新策略只是一个简单的对象,定义了一个 isBatchingUpdates 的布尔值和一个 batchedUpdates 方法。默认 isBatchingUpdates(下面称为更新标志)为 false, 然后会进入 batchedUpdates 方法,先把更新标志 isBatchingUpdates 设为 true,然后执行 transaction.perform(callback),即 transaction.perform(enqueueUpdate)。

React 内部采用了 ” 状态机 ” 的概念,组件处于不同的状态时,所执行的逻辑也并不相同。以组件更新流程为例,React 以事务 + 状态的形式对组件进行更新。

通过上面的一部分代码,我们发现 setState()方法主要是 enqueueUpdate()进行状态更新,怎样进行状态更新呢?定义了一个批量更新策略:判断更新标志 isBatchingUpdates 的值,如果为 false,调用 batchedUpdates()–>(先把更新标志 isBatchingUpdates 改为 true,然后调用 transaction.perform(enqueueUpdate))。如果为 true,就把组件加入 dirtyComponents 数组中。

React 内部采用了 ” 状态机 ” 的概念,组件处于不同的状态时,所执行的逻辑也并不相同。以组件更新流程为例,React 以事务 + 状态的形式对组件进行更新,因此接下来我们看看事务的机制。

3. transaction 事务

                    wrappers (injected at creation time)
                                   +        +
                                   |        |
                 +-----------------|--------|--------------+
                 |                 v        |              |
                 |      +---------------+   |              |
                 |   +--|    wrapper1   |---|----+         |
                 |   |  +---------------+   v    |         |
                 |   |          +-------------+  |         |
                 |   |     +----|   wrapper2  |--------+   |
                 |   |     |    +-------------+  |     |   |
                 |   |     |                     |     |   |
                 |   v     v                     v     v   | wrapper
                 | +---+ +---+   +---------+   +---+ +---+ | invariants
perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
+----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
                 | |   | |   |   |         |   |   | |   | |
                 | |   | |   |   |         |   |   | |   | |
                 | |   | |   |   |         |   |   | |   | |
                 | +---+ +---+   +---------+   +---+ +---+ |
                 |  initialize                    close    |
                 +-----------------------------------------+

这是官方代码的解析图。

可以看出调用函数是 perform(anyMethod), 然后方法 anyMethod 被 wrapper 包裹了,wrapper 依次执行了 initialize->anyMethod->close

function anyMethod(){console.log('xx')
};
transaction.perform(anyMethod);

代码的执行顺序是

initialize()
输出 xx
close()

所以这里 wrapper 是怎样定义的呢?

第二个 wrapper 比较简单,先来看一下第二个 wrapper。

第二个 wrapper(RESET_BATCHED_UPDATES)的作用是将更新标志 isBatchingUpdates 重置为 false; 我的理解这里是收集完所有要更新的 state 值,都加入_pendingStateQueue 待更新状态队列了,然后组件更新完了之后,将更新标志重置为 false,等待下次更新。然后下面来看一下第一个 wrapper。


5&f=png&s=54081)

第一个 wrapper 主要的作用是更新组件,执行了 ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)。

可以看到 flushBatchedUpdates 方法循环遍历所有的 dirtyComponents,又通过事务的形式调用 runBatchedUpdates 方法。

一共做了两件事:

  • 一是通过执行 updateComponent 方法来更新组件
  • 二是若 setState 方法传入了回调函数则将回调函数存入 callbackQueue 队列。

然后看一下 updateComponent 方法, 官方注释是:更新组件,会调用 shouldComponentUpdate, 然后调用剩余的生命周期函数,更新 DOM 结构

这里终于更新了组件。看代码会发现在 shouldComponentUpdate 之前,执行了 _processPendingState 方法, 该方法主要对 state 进行处理:

  • 1. 如果更新队列为 null,那么返回原来的 state;
  • 2. 如果更新队列有一个更新,那么返回更新值;
  • 3. 如果更新队列有多个更新,那么通过 for 循环将它们合并;

综上说明了,在一个生命周期内,在 componentShouldUpdate 执行之前,所有的 state 变化都会被合并,最后统一处理。

4. 回顾上述问题

综上,

  • setState()为啥没有立即更新 this.state 值呢
  • 如果在 componentDidMount()中连续多次 setState, 无法进行 state 累加呢
  • 批量更新策略 isBatchingStrategy 干了什么,怎么做到更新的呢

那按照上述说的批量更新,第一次 setState–> 进入 enqueueUpdate()–> 此时 isBatchingUpdates 默认为 false–>batchedUpdates(enqueueUpdate,…)–> 设置 isBatchingUpdates 为 true;transaction.perform(enqueueUpdates);–>(第一个 wrapper:FLUSH_BATCHED_UPDATES)组件更新 –>(第二个 wrapper:RESET_BATCHED_UPDATES 的 close 方法)设置 isBatchingUpdates 为 false–>第二次 setState–>isBatchingUpdates 为 false–>..–> 组件更新 –>isBatchingUpdates 恢复为 false。

这样和结果不对呀?按上述逻辑的话,岂不是每次 setState 都会更新 this.state 的值?

调试代码会发现,原来整个将 React 组件渲染到 DOM 中的过程就处于一个大的 Transaction 中。

在进入生命周期之前,就会调用 batchedUpdates(),所以此时 isBatchingUpdates 已经修改为 true 了。后面第一次进入 setState()时,就会进入加入 dirtyComponent 中。所以这也就是为什么两次打印 this.state.foods 都是 ” 的原因,新的 state 还没有被应用到组件中。

5. 总结

  • setState(partialState, callback),不会立即更新 state 值,要合并所有的 state 变化后,然后重新渲染的时候,state 值才会更新。
  • setState(partialState, callback): callback 会在所有状态更新之后再调用(demo 中 state 的 foods&drinks 全部更新之后才会调用)
  • 事务这么有用,那我们可以调用事务吗?答案是不可以。
  • 另外在 componentWillMount 里面 setState()不会触发重新渲染

Q2

在 render 函数里,无法 setState

A2

在 render 函数中不能 setState()。

从 react 生命周期可以看出:state 更新会重新触发 render(),所以会导致 setState()–>re-render()–>setState()–re-render()–>…–>setState()–>re-render(),一直循环往复。

所以,同理在 state 更新的生命周期的函数中(componentWillUpdate/componentDidUpdate),都不能 setState()

参考资料

https://juejin.im/post/59cc4c…

https://zh-hans.reactjs.org/d…

https://www.imooc.com/article…

https://segmentfault.com/a/11…

正文完
 0