核心还有个 调度的
用户交互->this.setState手动修改状态 -> reconcile计算出状态的变更(调用组件的render方法) -> 将变更commit到render
render阶段 -> commit阶段,会经过各自生命周期钩子
生命周期
标红的:在react17中已被废弃,转而标绿的替代了他们
1、首屏渲染执行过程Mount:
render阶段:DFS创建fiber树
commit阶段:渲染fiber树对应的dom树
渲染完成后,会从子节点开始执行对应的生命周期函数
2、交互产生的更新update:
C2组件产生交互更新,需要将C2由蓝色变为绿色,this.setState
每次调用setState树会产生新的fiber树
render阶段:
reconciler算法标记两个fiber树的差异,知道了是C2在更新
C2触发生命周期函数:getDerivedStateFromProps、render函数
commit阶段:
将C2组件对应的dom更新,触发C2上的生命周期函数:getSnapshotBeforeUpdate、componentDidUpdate
此时完成更新,新fiber树替换旧fiber树
setState
legacy模式:可以看出,setstate是异步的,
react内部源码里,batchupdates批处理
频繁多次更新,也只会执行一次render
如何实现的? 怎么判断 当前setState是一个批处理,而settimeout里的不是一个批处理?
===> 上下文
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext; #BatchedContext 如果存在表示当前是在一个执行上下文
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// If there were legacy sync updates, flush them at the end of the outer
// most batchedUpdates-like method.
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
}
即在legacy模式下,在一个上下文下的多次setState是批量处理,即异步;在settimeout里的是同步处理
currentmode:批处理全自动化,即使包裹在settimeout里也是批处理,即异步更新
下面可以展开讲讲react的批处理机制
批处理
半自动批处理
在v18之前,只有事件回调、生命周期回调中的更新会批处理,比如上例中的onClick。
而在promise、setTimeout等异步回调中不会批处理,即异步代码里的回调里的setState不会再经过react的一层异步队列更新,回调触发则回调里的setState就触发
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
# BatchedContext: 拥有这个状态位代表当前执行上下文需要批处理
executionContext |= BatchedContext;
try {
return fn(a);
} finally { # 执行完之后 恢复执行上下文
executionContext = prevExecutionContext;
// If there were legacy sync updates, flush them at the end of the outer
// most batchedUpdates-like method.
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
}
传入一个回调函数fn,此时会通过「位运算」为代表当前执行上下文状态的变量executionContext增加BatchedContext状态.
React源码内部(18之前),执行onClick时的逻辑类似如下:
batchedUpdates(onClick, e);
# onClick内部的this.setState 会默认批处理;
在onClick内部的this.setState中,获取到的executionContext包含BatchedContext,不会立刻进入更新流程,等退出该上下文后再统一执行一次更新流程,这就是「半自动批处理」
由于batchedUpdates方法是同步调用的,所以他没法拦截助那些异步函数, 给异步函数里的setState加上批处理枷锁😂
onClick() {
setTimeout(() => {
this.setState({a: 3}); # 真正执行this.setState时batchedUpdates早已执行完
this.setState({a: 4});
})
}
所以这种「只对同步流程中的this.setState进行批处理」,只能说是「半自动」。
手动批处理
为了弥补「半自动批处理」的不灵活,ReactDOM中导出了unstable_batchedUpdates方法供开发者手动调用。
onClick() {
setTimeout(() => {
ReactDOM.unstable_batchedUpdates(() => {
this.setState({a: 3});
this.setState({a: 4});
})
})
}
自动批处理
v18是怎么实现在各种上下文环境都能批处理呢?
v18实现「自动批处理」的关键在于两点:
- 增加调度的流程
- 不以全局变量executionContext为批处理依据,而是以更新的「优先级」为依据
调用this.setState后源码内部会依次执行:
- 根据当前环境选择一个「优先级」
- 创造一个代表本次更新的update对象,赋予他步骤1的优先级
- 将update挂载在当前组件对应fiber(虚拟DOM)上
- 进入调度流程
每次调用this.setState会产生update对象,根据调用的场景他会拥有不同的lane(优先级)
onClick() {
# 事件回调中的this.setState会产生同步优先级的更新,这是最高的优先级(lane为1)
this.setState({a: 3});
this.setState({a: 4});
# lane为16,代表Normal(即一般优先级)
setTimeout(() => {
this.setState({a: 3});
this.setState({a: 4});
})
}
在组件对应fiber挂载update后,就会进入「调度流程」
一个大型应用,在某一时刻,应用的不同组件都触发了更新。
那么在不同组件对应的fiber中会存在不同优先级的update
「调度流程」的作用就是:选出这些update中优先级最高的那个,以该优先级进入更新流程。
render
看一个demo,父组件里的自己的数据更新时候,会联带child组件更新吗?
===> 不会, 是的 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 ?
FiberRootNode
|
RootFiber
|
App fiber # 是RootFiber走bailout逻辑返回
|
Parent fiber # 当前组件触发更新, 走render的逻辑
接下来是关键
如果render返回的Son是如下形式:
<Parent>
<Son/> # React.createElement(Son, null), 由于props的引用改变,oldProps !== newProps。会走render逻辑。
<Parent/>
但是在Demo中Son是如下形式:{props.children}
其中,props.children是Son对应的JSX,而这里的props是App fiber走bailout逻辑后返回的。
所以Son对应的JSX与上次更新时一致,JSX中保存的props也就一致