image.png 核心还有个 调度的
用户交互->this.setState手动修改状态 -> reconcile计算出状态的变更(调用组件的render方法) -> 将变更commit到render
render阶段 -> commit阶段,会经过各自生命周期钩子

生命周期

image.png
标红的:在react17中已被废弃,转而标绿的替代了他们

1、首屏渲染执行过程Mount:
render阶段:DFS创建fiber树
image.png
commit阶段:渲染fiber树对应的dom树
渲染完成后,会从子节点开始执行对应的生命周期函数
image.png

2、交互产生的更新update:
C2组件产生交互更新,需要将C2由蓝色变为绿色,this.setState
每次调用setState树会产生新的fiber树

image.png
render阶段:
reconciler算法标记两个fiber树的差异,知道了是C2在更新
C2触发生命周期函数:getDerivedStateFromProps、render函数

image.png
commit阶段:
将C2组件对应的dom更新,触发C2上的生命周期函数:getSnapshotBeforeUpdate、componentDidUpdate
此时完成更新,新fiber树替换旧fiber树

setState

image.png
legacy模式:可以看出,setstate是异步的,
react内部源码里,batchupdates批处理
频繁多次更新,也只会执行一次render
image.png
如何实现的? 怎么判断 当前setState是一个批处理,而settimeout里的不是一个批处理?
===> 上下文

  1. export function batchedUpdates<A, R>(fn: A => R, a: A): R {
  2. const prevExecutionContext = executionContext;
  3. executionContext |= BatchedContext; #BatchedContext 如果存在表示当前是在一个执行上下文
  4. try {
  5. return fn(a);
  6. } finally {
  7. executionContext = prevExecutionContext;
  8. // If there were legacy sync updates, flush them at the end of the outer
  9. // most batchedUpdates-like method.
  10. if (executionContext === NoContext) {
  11. resetRenderTimer();
  12. flushSyncCallbacksOnlyInLegacyMode();
  13. }
  14. }
  15. }

即在legacy模式下,在一个上下文下的多次setState是批量处理,即异步;在settimeout里的是同步处理

currentmode:批处理全自动化,即使包裹在settimeout里也是批处理,即异步更新
image.png
下面可以展开讲讲react的批处理机制

批处理

半自动批处理

v18之前,只有事件回调、生命周期回调中的更新会批处理,比如上例中的onClick
而在promisesetTimeout等异步回调中不会批处理,即异步代码里的回调里的setState不会再经过react的一层异步队列更新,回调触发则回调里的setState就触发

  1. export function batchedUpdates<A, R>(fn: A => R, a: A): R {
  2. const prevExecutionContext = executionContext;
  3. # BatchedContext: 拥有这个状态位代表当前执行上下文需要批处理
  4. executionContext |= BatchedContext;
  5. try {
  6. return fn(a);
  7. } finally { # 执行完之后 恢复执行上下文
  8. executionContext = prevExecutionContext;
  9. // If there were legacy sync updates, flush them at the end of the outer
  10. // most batchedUpdates-like method.
  11. if (executionContext === NoContext) {
  12. resetRenderTimer();
  13. flushSyncCallbacksOnlyInLegacyMode();
  14. }
  15. }
  16. }

传入一个回调函数fn,此时会通过「位运算」为代表当前执行上下文状态的变量executionContext增加BatchedContext状态.

React源码内部(18之前),执行onClick时的逻辑类似如下:

  1. batchedUpdates(onClick, e);
  2. # onClick内部的this.setState 会默认批处理;

onClick内部的this.setState中,获取到的executionContext包含BatchedContext,不会立刻进入更新流程,等退出该上下文后再统一执行一次更新流程,这就是「半自动批处理」

由于batchedUpdates方法是同步调用的,所以他没法拦截助那些异步函数, 给异步函数里的setState加上批处理枷锁😂

  1. onClick() {
  2. setTimeout(() => {
  3. this.setState({a: 3}); # 真正执行this.setStatebatchedUpdates早已执行完
  4. this.setState({a: 4});
  5. })
  6. }

所以这种「只对同步流程中的this.setState进行批处理」,只能说是「半自动」

手动批处理

为了弥补「半自动批处理」的不灵活,ReactDOM中导出了unstable_batchedUpdates方法供开发者手动调用。

  1. onClick() {
  2. setTimeout(() => {
  3. ReactDOM.unstable_batchedUpdates(() => {
  4. this.setState({a: 3});
  5. this.setState({a: 4});
  6. })
  7. })
  8. }

自动批处理

v18是怎么实现在各种上下文环境都能批处理呢?

v18实现「自动批处理」的关键在于两点:

  • 增加调度的流程
  • 不以全局变量executionContext为批处理依据,而是以更新的「优先级」为依据

调用this.setState后源码内部会依次执行:

  1. 根据当前环境选择一个「优先级」
  2. 创造一个代表本次更新的update对象,赋予他步骤1的优先级
  3. update挂载在当前组件对应fiber(虚拟DOM)上
  4. 进入调度流程

每次调用this.setState会产生update对象,根据调用的场景他会拥有不同的lane(优先级)

  1. onClick() {
  2. # 事件回调中的this.setState会产生同步优先级的更新,这是最高的优先级(lane1
  3. this.setState({a: 3});
  4. this.setState({a: 4});
  5. # lane16,代表Normal(即一般优先级)
  6. setTimeout(() => {
  7. this.setState({a: 3});
  8. this.setState({a: 4});
  9. })
  10. }

在组件对应fiber挂载update后,就会进入「调度流程」

一个大型应用,在某一时刻,应用的不同组件都触发了更新。
那么在不同组件对应的fiber中会存在不同优先级的update
「调度流程」的作用就是:选出这些update中优先级最高的那个,以该优先级进入更新流程。

render

看一个demo,父组件里的自己的数据更新时候,会联带child组件更新吗?
image.pngimage.png
===> 不会, 是的 react 优化的就是这么智能!

render需要满足的条件

React创建Fiber树时,每个组件对应的fiber都是通过如下两个逻辑之一创建的:

  • render。即调用render函数,根据返回的JSX创建新的fiber。
  • bailout。即满足一定条件时,React判断该组件在更新前后没有发生变化,则复用该组件在上一次更新的fiber作为本次更新的fiber。

可以看到,当命中bailout逻辑时,是不会调用render函数的。
所以,Son组件不会打印child render!是因为命中了bailout逻辑。

react是如何判断出 son组件是bailout的呢?

bailout需要满足的条件:
1、oldProps === newProps ?
2、context没有变化
3、workInProgress.type === current.type ?
4、!includesSomeLane(renderLanes, updateLanes) ?

Demo的详细执行逻辑:
Demo中Son进入bailout逻辑,一定是同时满足以上4个条件。我们一个个来看。
条件2,Demo中没有用到context,满足。
条件3,更新前后type都为Son对应的函数组件,满足。
条件4,Son本身无法触发更新,满足。
条件1:oldProps === newProps ?

  1. FiberRootNode
  2. |
  3. RootFiber
  4. |
  5. App fiber # RootFiberbailout逻辑返回
  6. |
  7. Parent fiber # 当前组件触发更新, render的逻辑

接下来是关键

  1. 如果render返回的Son是如下形式:
  2. <Parent>
  3. <Son/> # React.createElement(Son, null), 由于props的引用改变,oldProps !== newProps。会走render逻辑。
  4. <Parent/>

但是在Demo中Son是如下形式:{props.children}
其中,props.children是Son对应的JSX,而这里的props是App fiber走bailout逻辑后返回的。
所以Son对应的JSX与上次更新时一致,JSX中保存的props也就一致

参考资料