1.流程
同步模式
异步并发模式(concurrent):我们所有的更新都有优先级,异步调度的方式执行
函数组件创建的update,新的state值放到action值,这个update放到hook对象的updateQueue中
const update: Update<S, A> = {
lane,
action,//新的state值
eagerReducer: null,
eagerState: null,
next: (null: any),
};
然后进入schduleUpdateOnfiber 进入调度 然后再进入render 再进去commit
流程:
我们知道更新的是app这个functioncmp 但是我们要执行performconcurrentWorkonroot,调度的是根节点,所以我们需要遍历找到app的根节点,这一路上所有孩子节点的优先级改为和app一样的优先级,然后根节点去调度,获取root下优先级最高的lane,转换成schedule的优先级,传入schedulecallback进行调度,调度的就是performconcurrentWorkonroot这个方法,这个方法会和schdule中所有的回调函数一起调度,找到优先级最高的回调函数来执行,当时间到了这里performconcurrentWorkonroot就会被调用,就进入render阶段,去diff,然后commit.
ps:进入调度器的函数是performconcurrentWorkonroot即render阶段的开始函数,不是一个单单的update,之前以为每个update都是个函数可以被调度,其实所有dom创建的update都是触发performconcurrentWorkonroot函数这一个函数进入调度,里面的update还是按照深度优先遍历来diff
主要函数为ensureRootisScheduled
if (newCallbackPriority === SyncLanePriority) {
// 任务已经过期,需要同步执行render阶段
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root)
);
} else {
// 根据任务优先级异步执行render阶段
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
判断任务优先级是同步还是异步 同步调用performSyncWorkOnRoot,异步调用performConcurrentWorkOnRoot,并传入这个函数的优先级去调度看什么时候执行
2.优先级和update
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
// TODO: Use symbols?
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
6种优先级
ImmediatePriority就是同步优先级(sync),是最高的优先级
UserBlockingPriority 是用户触发的优先级 比如点击事件的回调 this.state()产生的update就将有UserBlockingPriority这个优先级
NormalPriority:一般的优先级(最常用),比如请求服务端数据后更新状态,这个update就是NormalPriority
LowPriority:suspense采用的
update是在fiber中的,
一个组件的状态可由以下计算出,useState也会创建一个小fiber,也有内部的状态,即state得值,组件fiber的状态就是this.state的值
update1是NormalPriority,update2是UserBlockingPriority,
如果本次更新是UserBlockingPriority,那么basestate只会执行update2的更新,因为update1的优先级低于本次更新,所以不执行
如果本次更新是NormalPriority,那么两个update都会执行
重要:优先级是个全局概念,update只是存在某个fiber中的
3.优先级和update举例
此时f组件在mount生命周期里触发了一次setState,那么创建update的优先级为normal,这个优先级会一直往上传直到root节点,root节点以normal优先级进入调度,执行的函数为performConcurrentWorkOnRoot,到render阶段,假如diff过程很长,这个时候f点击事件创建啦一个update

这个update为UserBlockingPriority,又会一直上传到root节点,此时又去以UserBlockingPriority的优先级调度performConcurrentWorkOnRoot,但是这个优先级更高,所以正在render阶段的第一个performConcurrentWorkOnRoot函数就中断,第二个UserBlockingPriority的performConcurrentWorkOnRoot执行,执行到f的时候,只有UserBlockingPriority的update能够执行改变state.
4.update的数据结构和计算
理念参照啦git版本控制的理念
1.数据结构
classcmp的update数据结构
const update: Update<*> = {
eventTime,//!update发生的时间,采用performance.now()获取
lane,//update的优先级
suspenseConfig,
tag: UpdateState,//updatestate/forceupdate/replaceudpate三种
payload: null,//state的新值 或者elemnt元素
callback: null,//setstate的第二个参数
next: null,//指针形成链表
};
classcmp的updateQueue的数据结构
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,//保存update.callback!=null的update,因为要执行callback属于副作用
};
之所以在更新产生前该Fiber节点内就存在Update,是由于某些Update优先级较低所以在上次render阶段由Update计算state时被跳过。这个update会记录在firstbaseupdate到lastbaseupdate的链表上,下次更新的时候会在最前面加上这些update
2.update执行顺序
如何保证Update不丢失
实际上shared.pending会被同时连接在workInProgress updateQueue.lastBaseUpdate与current updateQueue.lastBaseUpdate后面。
当render阶段被中断后重新开始时,会基于current updateQueue克隆出workInProgress updateQueue。由于current updateQueue.lastBaseUpdate已经保存了上一次的Update,所以不会丢失。上次的update会加到这次update前面
a2->b1(其中a2是上次的update被打断了 b1新update进来了 会先去current里面看上次的update并加到最前面),总之上次被打断的或者没更新的都会放到最前面。
当commit阶段完成渲染,由于workInProgress updateQueue.lastBaseUpdate中保存了上一次的Update,所以 workInProgress Fiber树变成current Fiber树后也不会造成Update丢失。
如何保证状态依赖的连续性
当某个Update由于优先级低而被跳过时,保存在baseUpdate中的不仅是该Update,还包括链表中该Update之后的所有Update。
考虑如下例子:
baseState: ''
shared.pending: A1 --> B2 --> C1 --> D2
其中字母代表该Update要在页面插入的字母,数字代表优先级,值越低优先级越高。
第一次render,优先级为1。
baseState: ''
baseUpdate: null
render阶段使用的Update: [A1, C1]
memoizedState: 'AC'
其中B2由于优先级为2,低于当前优先级,所以他及其后面的所有Update会被保存在baseUpdate中作为下次更新的Update(即B2 C1 D2)。
这么做是为了保持状态的前后依赖顺序。
第二次render,优先级为2。
baseState: 'A'
baseUpdate: B2 --> C1 --> D2
render阶段使用的Update: [B2, C1, D2]
memoizedState: 'ABCD'
注意这里baseState并不是上一次更新的memoizedState。这是由于B2被跳过了。
即当有Update被跳过时,下次更新的baseState !== 上次更新的memoizedState。
跳过
B2的逻辑见这里(opens new window)
通过以上例子我们可以发现,React保证最终的状态一定和用户触发的交互一致,但是中间过程状态可能由于设备不同而不同。
总结:被打断或者被跳过的update都作为下次低优先级的render阶段的开始的update,只有执行完一轮processQueue后update才会没了否则创建啦update就是在链上,下一轮才没有,被跳过的下次baseState不是meriziedState 而是被跳过的update之前的state,而且这次跳过的update之后的链都会保存到下一次Updatequeue前面,保证最终结果正确。
import React,{useState,useEffect,useRef} from 'react'
export default function App(){
const [number, setNumber] = useState(0)
const button = useRef(null)
useEffect(()=>{
setTimeout(() => {
setNumber(1)//!主要是这个hook对象内部的state已经发生改变啦
}, 1000);
setTimeout(() => {
setNumber(2)
button.current.click()
}, 1004);
},[])
return (
<div>
<button onClick={()=>{setNumber(number=>number+2)}} ref={button}>加2</button>
{
new Array(4500).fill(0).map((_,index)=><span>{number}</span>)
}
</div>
)
}
/* 1->2->number->number+2 */
首先创建一个1的update 正在render 然后又有一个2的update加入到queue中,然后又有number=>number+2加入queue中 此时是useblocking下,只有第三个update执行,number为2,commit后会有2
然后进入normal的环节,跳过的是第一个update,所以这一轮也是1->2->number=>number+2这个queue,baseState是第一个update前的状态是1,所以最后执行完是4,
所以有一瞬间为2马上到4.
setNumber(number+2)
setNumber(number+2)//这种传递依赖的是baseState
setNumber(number=>number+2)/这种传递依赖的是上次update后的state
3.ReactDom.render执行流程
这个update对象的payload就是第一个element子元素,然后触发performSynconRoot
updatehostcmp会把state赋值给nextchildren 开始recocilechildren
来自卡颂react电子书