React 渲染流程
一句话表明React ui = fn(state)
fn 基本执行过程
const state = reconciler(update)
const ui = commit(state)
update 是初始化或者对比新老节点差异的时候产生的 state 通过reconciler(协调器)的计算,找出节点差异,并生成effectList ui 通过commit进行渲染,也就是通常所说的render阶段,将vdom转换成真实dom的过程
fn 大致执行阶段
-
Scheduler(调度器): 对任务优先级进行排序,让优先级高的任务先进入reconciler(协调)阶段
-
Reconciler(协调器): 找出节点变化,并生成effectList,Render(渲染器)进入render阶段
-
Render(渲染器): 将effctList, 渲染到真实的dom节点上
👇 小二上图
几个概念
jsx
jsx是js语言的一种拓展, 也是 React.createElement的语法糖,可以通过babel转成js,最终生成virtual-dom。
fiber结构
通过链表形成的一种数据结构,对vdom进行了再次包装,也是react能够实现任务优先级调整,可以暂停任务,让出执行权的关键。
Fiber对象上面保存了包括这个节点的属性、类型、dom等,Fiber通过child、sibling、return(指向父节点)来形成Fiber树,还保存了更新状态时用于计算state的updateQueue,updateQueue是一种链表结构,上面可能存在多个未计算的update,update也是一种数据结构,上面包含了更新的数据、优先级等,除了这些之外,上面还有和副作用有关的信息。
关注我 >>> 后续学会了详解😝
fiber 双缓存
指的是在react更新和挂载的过程中,存在两颗fiber tree, 原始的(current Fiber), 正在构建的(workInProgress Fiber),两颗树的节点通过alternate相连。通过对比两个树可以知道哪些节点变了,哪些节点没变可以进行复用。
在mount时(首次渲染),会根据jsx对象(Class Component或的render函数者Function Component的返回值),构建Fiber对象,形成Fiber树,然后这颗Fiber树会作为current Fiber应用到真实dom上,在update(状态更新时如setState)的时候,会根据状态变更后的jsx对象和current Fiber做对比形成新的workInProgress Fiber,然后workInProgress Fiber切换成current Fiber应用到真实dom就达到了更新的目的,而这一切都是在内存中发生的,从而减少了对dom好性能的操作。
🌰: current Fiber 与 workInProgress Fiber 对照过程
function App() {
const [count, setCount] = useState(0);
return (
<>
<div
onClick={() => {
setCount(() => count + 1);
}}>
<p title={count}>{count}</p>好人
</div>
</>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
scheduler
Scheduler的作用是调度任务,react15没有Scheduler这部分,所以所有任务没有优先级,也不能中断,只能同步执行。实现异步可中断的更新,需要浏览器指定一个时间,如果没有时间剩余了就需要暂停任务,requestIdleCallback可选,但是它存在兼容和触发不稳定的原因,react17中采用MessageChannel来实现。
// 工作循环
function workLoopConcurrent() {
// 当前fiber树没有遍历并且没有被中断
while (workInProgress !== null && !shouldYield()) {//shouldYield判断是否暂停任务
workInProgress = performUnitOfWork(workInProgress);
}
}
在Scheduler中的每的每个任务的优先级使用过期时间表示的,如果一个任务的过期时间离现在很近,说明它马上就要过期了,优先级很高,如果过期时间很长,那它的优先级就低,没有过期的任务存放在timerQueue中,过期的任务存放在taskQueue中,timerQueue和timerQueue都是小顶堆,所以peek取出来的都是离现在时间最近也就是优先级最高的那个任务,然后优先执行它。
Lane(赛道)模型
Lane用二进制位表示优先级,二进制中的1表示位置,同一个二进制数可以有多个相同优先级的位,这就可以表示‘批’的概念,而且二进制方便计算。
像是一场赛车比赛, 刚开始的时候,所有赛车都会尽可能的抢占内道(对应react中优先级较高的任务),比赛的尾声,最后一名赛车如果落后了很多,它也会跑到内圈的赛道,最后到达目的地(对应react中就是饥饿问题,低优先级的任务如果被高优先级的任务一直打断,到了它的过期时间,它也会变成高优先级)
Lane用二进制位表示,1的bits越多,优先级越低
reconciler (render phase)
Reconciler发生在render阶段,render阶段会分别为节点执行beginWork(捕获的过程)和completeWork(冒泡的过程),或者计算state,对比节点的差异,为节点赋值相应的effectFlags(对应dom节点的增删改)
协调器是在render阶段工作的,概括就是Reconciler会创建或者更新Fiber节点。在mount的时候会根据jsx生成Fiber对象,在update的时候会根据最新的state形成的jsx对象和current Fiber树对比构建workInProgress Fiber树,这个对比的过程就是diff算法。
diff算法发生在render阶段的reconcileChildFibers函数中,diff算法分为单节点的diff和多节点的diff(例如一个节点中包含多个子节点就属于多节点的diff),单节点会根据节点的key和type,props等来判断节点是复用还是直接新创建节点,多节点diff会涉及节点的增删和节点位置的变化。
reconcile时会在这些Fiber上打上Flags标签,在commit阶段把这些标签应用到真实dom上,这些标签代表节点的增删改,render阶段遍历Fiber树类似dfs的过程,‘捕获’阶段发生在beginWork函数中,该函数做的主要工作是创建Fiber节点,计算state和diff算法,‘冒泡’阶段发生在completeWork中,该函数主要是做一些收尾工作,例如处理节点的props、和形成一条effectList的链表,该链表是被标记了更新的节点形成的链表。
🌰: 标记过程
function App() {
const [count, setCount] = useState(0);
return (
<>
<div
onClick={() => {
setCount(() => count + 1);
}}
>
<p title={count}>{count}</p> 好人
</div>
</>
)
}
renderer(commit phase)
Renderer发生在commit阶段,commit阶段遍历effectList执行对应的dom操作或部分生命周期。
Renderer是在commit阶段工作的,commit阶段会遍历render阶段形成的effectList,并执行真实dom节点的操作和一些生命周期,不同平台对应的Renderer不同,例如浏览器对应的就是react-dom。
commit阶段发生在commitRoot函数中,该函数主要遍历effectList,分别用三个函数来处理effectList上的节点,这三个函数是commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects处理
concurrent
它是一类功能的合集(如fiber、schduler、lane、suspense),其目的是为了提高应用的响应速度,使应用cpu密集型的更新不在那么卡顿,其核心是实现了一套异步可中断、带优先级的更新。
我们知道一般浏览器的fps是60Hz,也就是每16.6ms会刷新一次,而js执行线程和GUI也就是浏览器的绘制是互斥的,因为js可以操作dom,影响最后呈现的结果,所以如果js执行的时间过长,会导致浏览器没时间绘制dom,造成卡顿。react17会在每一帧分配一个时间(时间片)给js执行,如果在这个时间内js还没执行完,那就要暂停它的执行,等下一帧继续执行,把执行权交回给浏览器去绘制。