react源码学习-各阶段做的事情简要总结

375 阅读7分钟

准备工作与初始化阶段

当调用ReactDOM.render或ReactDOM.createRoot/root.render之后,会做如下事情:

  • 创建FiberRoot、RootFiber,并在RootFiber上初始化一系列信息,例如return child sibling等表示链表结构的属性,还有updateQueue更新队列相关的属性,还有first/last/nextEffect等effect相关的属性
  • 调用unbatchedUpdates -> updateContainer开启更新
  • 调用scheduleUpdateOnFiber进入调度系统
  • 在调度中最终执行到performSyncWorkOnRoot / performConcurrentWorkOnRoot,并且是以某个优先级执行的
  • performWorkOnRoot里面就会开启workLoop,执行performUnitOfWork

FiberRoot上有一些全局相关的属性,例如

  • container是整个应用的容器
  • context就是我们平时用的context
  • current用来跟踪页面上当前渲染的tree
  • times相关的,例如eventTimes expirationTimes
  • lanes赛道相关的

初次渲染时,大概步骤如下:

1、创建FiberRoot,并初始化其current属性为rootFiber对象

此处需要注意

  • 无论是rootFiber还是其他类型的Fiber,都有一个updateQueue字段,在创建Fiber的时候被赋值上

2、调用FiberRoot的render方法,render里会调一个很重要的方法updateContainer,一句话概括它的作用就是创建Update更新对象并开启调度

此处需要注意

  • 开启调度的方法是scheduleUpdateOnFiber
  • Update更新对象有个payload属性(update.payload),用来存放虚拟DOM

3、scheduleUpdateOnFiber方法内,做了很多判断和处理,和初次渲染主流程相关的是调用了ensureRootIsScheduled -> scheduleCallback,即以一个优先级NormalPriority将performConcurrentWorkOnRoot加入调度系统

此处需要注意

  • ensureRootIsScheduled里会根据本次更新的特征申请一个赛道Lane,申请赛道的方法是getNextLanes
  • 申请到赛道nextLanes后,会将其转换为一个优先级,初次更新时会转为NormalPriority,这是react系统里的优先级定义方式
  • 在调用scheduleCallback之前,上面的react优先级会进行转换,转换为Scheduler调度包里的优先级定义方式,并赋值给schedulerPriorityLevel

4、scheduleCallback里面,只要没有传入第3个参数options.delay,都会将任务加入到taskQueue中去,我们此处调度的performConcurrentWorkOnRoot也会加入这个队列

此处需要注意

  • 调度系统会根据优先级的不同给任务设定一个过期时间,对于NormalPriority的任务来说,过期时间是5000ms以后
  • 入队后,会调用requestHostCallback(flushWork)开启工作循环

5、requestHostCallback里调用了schedulePerformWorkUntilDeadline,通过schedulePerformWorkUntilDeadline -> port.postMessage -> performWorkUntilDeadline -> scheduledHostCallback(scheduledHostCallback已经被赋值为flushWork)

此处需要注意

  • 如果在一个时间片内任务没有完成,performWorkUntilDeadline里会继续执行schedulePerformWorkUntilDeadline,performWorkUntilDeadline里同时也有终止执行的条件

6、接下来进入scheduledHostCallback即flushWork中,flushWork中调用了workLoop开启了任务执行,它会先从taskQueue中取出一个任务,初次更新时,取出的任务就是performConcurrentWorkOnRoot方法

此处需要注意

  • 进入flushWork时,会检测是不是有延时任务开启的定时器,如果有的话将其取消,因为flushWork在将taskQueue里所有的任务遍历执行完后,会判断timeQueue里是否有任务,如果有的话,会开启一个延时定时器,为避免重复开启,要将之前那个取消

7、performConcurrentWorkOnRoot里面调用了一个很重要的方法renderRootConcurrent -> workLoopConcurrent -> performUnitOfWork

疑问?

  • 该方法里面首先开始的flushPassiveEffects是干什么的?
  • 之后会根据一个shouldTimeSlice变量判断执行renderRootSync还是renderRootConcurrent,初次渲染时,执行的是renderRootSync,由此就进入了render阶段

8、renderRootSync里继续执行workLoopSync,开始调用performUnitOfWork,继而进入beginWork和completeWork递归构建workingProgressFiber树

9、renderRootSync执行后会返回一个exitStatus,exitStatus如果值为已完成状态RootCompleted,则调用finishConcurrentRender -> commitRoot进入commit阶段,最终完成DOM的渲染

Fiber节点上的childLanes作用:在setState时从根fiber开始更新的时候,能更快地找到有更新的节点

render/schedule阶段

很多源码分析把render和schedule分开,在源码中确实也是分开的,但其实二者关系甚为紧密,调度的开启、中断、重新执行其实都和渲染时的workLoop、performUnitOfWork紧密相关

每个unitOfWork其实就是在每一帧的空闲时间内执行的一个宏任务

每个unitOfWork一定包含一个beginWork,可能会有completeWork,因为fiber树的构建是深度优先遍历,performUnitOfWork内部是判断beginWork返回的节点有没有next,从而决定completeWork是不是执行,如果没有next,那么证明当前遍历到的节点既没有child,也没有sibling,就可以执行completeUnitOfWork了,之后将workInProgress重置为父Fiber

beginWork

在beginWork阶段,主要的工作其实就是创建或更新Fiber,如果是第一次调用的情况为创建Fiber,之后则为更新Fiber

  • 更新Fiber,需要拿着新的props或state去更新,因此就涉及到了componentWillReceiveProps componentWillUpdate等Willxxx系列的钩子的执行

  • 无论创建还是更新,beginWork还有一个重要工作就是打effectTag,effectTag会标识这个Fiber在commit阶段要执行增删改查什么操作

  • 需要值得注意的是,在mount的阶段,由于要创建大量的dom,所以为了避免给大量的Fiber标记Placement标识,我们只会给rootFiber打一个Placement标识,然后在completeWork中创建一个离屏DOM,将其作为一个整体,在commit阶段插入

  • 另一个重要工作就是dom-diff,调用reconcileChildren方法的时候就会根据diff算法更新Fiber列表,react的diff算法相较于Vue来说,思路基本一致,但优化场景略少

completeWork

在第一次调用,即创建Fiber的时候,还会创建DOM节点

收集effect,并最终将有effect的Fiber节点全都归并到rootFiber上去

commit阶段

概述:commit阶段分为before mutation、mutation、layout三个子过程,这三个子过程都会开启while循环,遍历effectlist做对应的操作

before mutation阶段

  • 调用getSnapshotBeforeUpdate生命周期钩子。

    • Reactv16开始,componentWillXXX钩子前增加了UNSAFE_前缀。究其原因,是因为Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。这种行为和Reactv15不一致,所以标记为UNSAFE_
    • getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题。
  • 调度useEffect

    scheduleCallback(NormalSchedulerPriority, () => {
      // 触发useEffect
      flushPassiveEffects();
      return null;
    });
    
    • flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList
    • 当一个FunctionComponent含有useEffectuseLayoutEffect,他对应的Fiber节点也会被赋值effectTag
    • flushPassiveEffects方法内部会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。
    • 如果在此时直接执行,rootWithPendingPassiveEffects === null
    • 那么rootWithPendingPassiveEffects会在何时赋值呢?
    • 答案是在layout之后的代码片段中会根据rootDoesHavePassiveEffects === true?决定是否赋值rootWithPendingPassiveEffects
  • 所以整个useEffect异步调用分为三步:

    • before mutation阶段scheduleCallback中调度flushPassiveEffects
    • layout阶段之后将effectList赋值给rootWithPendingPassiveEffects
    • scheduleCallback触发flushPassiveEffectsflushPassiveEffects内部遍历rootWithPendingPassiveEffects

事实上,在这三个子过程前面和后面还有其他一些操作,例如在三个子过程前面,有重要的一步就是调用flushPassiveEffects来执行useEffect的回调

mutation阶段

  • 遍历effectlist并执行,根据每个effect的effectTag(Placement、PlacementAndUpdate、Update、Deletion),做对应的增删改查操作
  • 更新ref
  • 对于FunctionComponent的Update操作,会执行所有useLayoutEffect hook的销毁函数
  • 对于HostComponent或ClassComponent的Deletion操作,会从当前节点开始一直递归到叶子结点为止,执行这一链条上遇到的ClassComponent的componentWillUnmount钩子
  • 对于带有ref的组件,Deletion操作还会解绑ref
  • 对于FunctionComponent的Deletion操作,会调度useEffect的销毁函数

layout阶段

  • 对于ClassComponent,他会通过current === null?区分是mount还是update,调用componentDidMount componentDidUpdate钩子
  • 对于ClassComponent,触发setState的回调
  • 对于FunctionComponent,触发useLayoutEffect的回调,调度(注意是调度而非执行)useEffect销毁回调函数
  • 对于HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。
  • useLayoutEffect hook从上一次更新的销毁函数调用到本次更新的回调函数调用是同步执行的。而useEffect则需要先调度,在Layout阶段完成后再异步执行。
  • 获取DOM实例,更新ref
  • 由于componentWillUnmount是在mutation阶段执行的,在此之后才会将workingProgressFiber切换为currentFiber,因此在componentWillUnmount钩子里才会拿到销毁前的DOM

常见问题:

  • 如何理解fiber
    • 思考角度:fiber既是一种数据结构,又是一个执行单元