从源码出发,梳理React构建更新流程

架构

  1. 接口层

    react

  2. 内核层

    1. 调度器scheduler,负责执行回调
    2. 构造器react-reconciler,调度配合react, react-dom,scheduler
    3. 渲染器react-dom,生成DOM节点或SSR字符串

工作循环

  1. 任务调度循环,在scheduler

    采用堆排序算法清空任务队列,执行宏观的任务(task)

  2. fiber构造循环,在react-concilerReactFiberWorkLoop中,是任务调度循环的一部分

    采用深度遍历算法,是任务(task)的一部分,比如task包含:fiber树的构造,DOM渲染,调度检查

主干逻辑

  1. 输入:将每次更新视为一次更新需求
  2. 注册:react-reconciler收到更新需求,不会立即更新Fiber树,而是在scheduler注册一个任务task,将更新需求转换成task
  3. 执行调度任务(输出):scheduler通过任务调度循环执行task,task执行会重新返回到react-reconciler
    1. fiber构造循环:构建出最新的Fiber
    2. commitRoot:将最新的fiber树渲染到页面上

任务调度循环fiber构造循环可实现批量更新,可中断渲染

高频对象

  1. react
    1. ReactElement: React元素,可能是HTML原生,可能是组件
    2. ReactComponent:class类型的React元素,会在reconciler阶段生成fiber对象,触发对应的生命周期,调用render方法获取jsx
    3. 函数式组件 :reconciler阶段执行获取子节点
  2. react-reconciler
    1. fiber对象:保存了节点类型,副作用,父子兄fiber信息
    2. UpdateQueue对象:链式队列,有WIP和Current两个队列。更新会按顺序,渲染会跳过低优先级
    3. Hook对象:hooks背后的对象,是一个链式队列
  3. Scheduler
    1. Task对象:小顶堆数组,每个元素是一个Task

启动

  1. 创建ReactDomRoot对象

    暴露render,unmount方法,引导React启动

  2. 创建fiberRoot对象,包含fiber树的全局上下文,fiber构造循环的各种状态,控制执行逻辑

  3. 创建HostRootFiber对象,第一个Fiber对象,fiber树的根节点

最终调用updateContainer,进入react-reconciler,调用schedulerUpdateOnFiber

reconciler运作原理

  1. reconciler的主要作用是:
    1. 暴露api,如给react-dom暴露schedulerUpdateOnFiber
    2. 注册调度任务:在react-scheduler上注册任务
    3. 执行任务回调:构建fiber树,与react-dom交互构造与fiber树对应的DOM节点
    4. 输出:与react-dom交互,渲染DOM节点
  2. 注册调度任务:逻辑在ReactFiberWorkLoop
    1. 会根据任务的优先级performSyncWorkOnRoot直接构造fiber树,还是ensureRootIsScheduled注册调度任务

    2. ensureRootIsScheduled首先会判断是否需要注册新的调度

      然后注册任务,在任务中添加回调,会根据优先级类型使用同步的performSyncWorkOnRoot还是异步的performConcurrentWorkOnRoot回调构造fiber

  3. 执行任务回调:
    1. 同步performSyncWorkOnRoot

      构造fiber

      判断异常

      提交输出commitRoot

    2. 异步performConcuurrentWorkOnRoot

      检查是否正在render ,是否需要恢复上一次渲染

      如果渲染被中断,返回新的performConcuurrentWorkOnRoot等待下一次调用,这就是可中断

  4. 输出:主要执行不同生命周期的副作用队列
    1. commitBeforeMutationEffects:DOM突变前的副作用队列
    2. commitMutationEffects:DOM变更的副作用队列
    3. commitlayoutEffects:DOM变更后的副作用队列

优先级管理

  1. fiber树的的lanePriority,采用位运算,位数越低优先级越高
  2. scheduler的schedulerPriority
  3. 用于上面两套系统的转换ReactPriorityLevel

调度管理

  1. 核心函数

    1. requestHostCallback:请求及时调度 MessageChannel
    2. cancelHostCallback:取消及时调度 scheduledHostCallabck = nul
    3. requestHostTimeout:请求延时调度 setTimeout
    4. cancelHostTimeout:取消延时调度 cancelTimeout
    5. shouldYieldToHost:是否让让出主线程 current ≥ deadline && (needPaint || inputPending)
    6. requestPaint:请求请求绘制 needPaint = true
    7. getCurrentTime:获取现在时间 performance.now()
    8. forceFrameRate:强制设置让出主线程间隔 yieldInterval = 1000 / fps
  2. 调度中心

    调度中心通过宏任务MessageChannel异步地请求和消费调度,将任务回调保存在scheduledHostCallabck中,在MessageChannel.port的onmessage中判断callback是否被取消

    requestHostCallback请求调度

    performWorkUntilDeadline 消费调度

    hasMoreWork 有更多任务,再度请求调度

  1. 任务

    任务创建会根据优先级设置一个过期时间expirationTime,优先级越高,过期时间越短,任务插入任务队列的sortIndex就是expirationTime

  2. 消费任务

    1. 调用requestHostCallback消费任务,flushWork是回调函数,内部调用workLoop循环消费任务队列。
    2. workLoop会不断从taskQueue取出任务,调用currentTask.callback(callback可中断)消费,callback的执行在react-reconciler中构建fiber树,callback可能产生新的回调,重新保存在currentTask.callback
    3. 每次取出前会判断是否超时currnetTask.expriationTime > currentTime,应该交还给主线程shouldYieldToHost,还有剩余时间hasTimeRemaining

    时间切片:消费每个任务前都会进行超时检查

    可中断渲染:每个task.callback 可以自己检测超时,fiber构造过程中,每构造完一个单元performUnitOfWork都会进行超时检查shouldYield,超时就退出fiber树构造循环

  3. 注册任务时的节流防抖

    节流:exsitingCallbackPriority === newCallbackPriority 优先级相同,沿用上一个task,无需注册新的task

    防抖:existingCallbackNode !== null 优先级已经改变,取消当前task,重新注册

整个调度流程:

Fiber树构造的基本概念

  1. ReactElementFiberDOM的关系

    ReactElement是通过书写jsx语法,编译器使用React.createElement转换

    Fiber是通过ReactElement进行创建的,Fiber树是构造DOM树的数据模型,Fiber树的改动最终体现在DOM树上

  2. 双缓冲技术

    内存中存在两个Fiber

    当前界面的fiber树挂载在fiberRoot.current

    正在构造的fiber树在fiberRoot.alternate

    正在构造的节点称为workInProgress

  3. 优先级

    有3种优先级,renderLane,fiberLaneupdateLane,只有命位运算& 有交集才处理,否则跳过

    1. renderLane:当前任务要处理的优先级任务,判断下一次要处理的lane的顺序是pendingLanes,expiredLanes,suspendLanes,pingedLanes
    2. renderLane:当前任务要处理的优先级任务,判断下一次要处理的lane的顺序是pendingLanes,expiredLanes,suspendLanes,pingedLanes
    3. fiberLane:包含当前fiber所有updatelane的,fiber复用的条件是fiber.lanesrenderLanes没有交集
    4. updateLanesetState可以创建更新,fiber中的updateQueue是当前组件需要更新的更新,updateLane是当前update的优先级
  4. 栈帧

    使用一组全局变量表示当前的活动记录,当fiber树构造被打断的时候可以依赖他们恢复上一次构造状态。prepareFreshStack会刷新栈帧到初始状态,并创建HostRootFiber.alternate = workInProgress

Fiber树的初次构造

  1. 启动阶段:采用同步构造fiber树的方法performSyncWorkOnRoot
    1. 获取当前构造的优先级getNextLanes,从所有已有的优先级获取最高的lane,初次为最高NoLanes
    2. 清空栈帧perpareFreshStack,在构造fiber树开始都需要初始化,清空栈帧的时候会创建HostRootFiber.alternate并将workInPregress指向它
    3. 开始按照深度遍历的顺序构造
  2. 构造阶段
    1. 循环构造,legacy和concurrent的区别是每次调用performUnitOfWork前都会检查是否需要让渡主线程给浏览器shouldYield
    2. 探寻阶段beginWork,从HostRootFiber.alternate开始,根据不同的workInProgress.tag类型执行不同的updateXXX(根fiber→updateHostRoot,普通DOM节点→updateHostComponent),最后返回子节点fiber,即为nextnext不为空继续循环构造
      1. 根据新的propsfiber.pendingProps和setState得到的updatefiber.updateQueue计算出新的状态fiber.memorizedProps
      2. 执行获取下一级的ReactElementClass组件实例挂载到fiber.stateNode调用render函数组件是直接执行。
      3. 根据实际情况设置fiber.flags
      4. 调用reconcilerChildren生成次级 子节点的fiber
    3. 探寻阶段是如何遍历updateQueue的:
      1. baseQueue保存着尚未处理的更新
      2. shared.pendingsetState异步添加的更新
      3. shared.pending追加baseQueue 末尾,根据renderLanes遍历执行优先级够的update,不够的保存到baseQueue中
      4. update.callback收集副作用,在commit阶段执行。如果有副作用,标记workInProgress.flag |= Callback
    4. 回溯阶段:completeUnitOfWork
      1. 调用completeWork,为HostComponent,HostText创建DOM实例,设置属性,添加事件,append子节点的DOM;设置fiber.stateNode局部状态(如tagHostComponent,HostText则是DOM实例)
      2. 将当前fiber的副作用队列(元素是fiber)添加到父fiber的副作用队列。判断fiber.flags,如果当前节点有副作用,也将当前fiber添加到父fiber的副作用队列
      3. 如果当前fiber节点有兄弟节点。将workInProgress设为sibling,继续beginWork。否则向上回溯

Fiber树的对比更新

  1. 更新入口

    1. Class组件setState
    2. Function组件dispatchAction
    3. render重复触发
  2. 创建更新并入队

    创建update对象,并入队到当前的fiber上,执行scheduleUpdateOnFiber执行输入

  3. 调度执行ensureRootIsScheduled,首先markUpdateLaneFromFiberToRoot将当前fiber的更新优先级向当前fiber.lanes当前fiber.alternate.laneschildLanes合并,然后向父fiber节点的lanes,alternate.lanes,childLanes合并直到HostRootFiber

  4. 接下来和初次渲染一样,刷新栈帧,从HostRoot开始深度遍历

  5. 探寻阶段beginWork需要判断是不是老节点current ≠= null

    1. 如果是老节点判断有没有更新(lanes和当前renderLanes有重合部分),如果不需要更新直接进入completeUnitOfWork阶段
    2. 如果includeSomeLanes说明子节点有更新,cloneChildFibers克隆子节点并返回,进入updateXXX更新fiber。复用fiber结构并重置flags,effects属性
    3. reconcilerChildren阶段会对比新旧fiber决定是否复用
      1. 根据实际情况设置fiber.flags 如新增Placement,删除Delection
    4. 需要删除的fiber.flags = Delection提前添加到父节点的effects链表中,不会进入completeWork阶段
  6. 回溯阶段completeUnitOfWork和初次构建一致,遇到current≠=null表明有老节点,标记fiber.flags |= Update,在commit阶段处理

Fiber树的渲染

  1. 调用commitRoot渲染
  2. 构建完成,即将渲染的fiber树在fiberRoot.finishedWork上,fiber树的特点:
    1. 副作用列队挂在根节点上finishedWork.firstEffect
    2. 最新页面的DOM对象挂载在fiber树首个HostComponentstateNode
  3. 分为三个阶段:渲染前渲染渲染后
  4. 渲染前
    1. 设置全局状态(更新fiber属性)
    2. 重置全局状态(workInProgress等)
    3. 处理根节点副作用,如果根节点有副作用,将它添加到副作用链表尾部
  5. 渲染:分为3个阶段,DOM突变前,中,后
  6. DOM突变前,调用commitBeforeMutationEffects处理副作用,处理带有SnapShot,Passivefiber节点
    1. SnapShot标记可以有2种方式创建
      1. 调用Class组件的getSnapShotBeforeUpdate生命周期函数
      2. 调用clearContainer,清空容器(div#app
    2. Passive标记是使用hook才会创建的,如useEffect。通过scheduleCallback异步执行副作用
  7. DOM突变,调用commitMutationEffects处理副作用,这里修改DOM,界面会更新。处理带有ContentReset,Ref,Placement,update,Delection,Hydrating的fiber节点。处理完成后会切换fiber.current = finishedWork
    1. Ref类型会清空,在CommitLayoutEffect会重新赋值
    2. Placement,update,Delection,Hydrating类型是新增,更新,删除节点,会通过与react-dom包交互完成DOM更新
  8. DOM突变后会处理带有Ref,Update,Callbackfiber节点
    1. Ref类型会重新进行ref赋值
    2. Update,Callabck类型可以在ClassComponentHostComponet出现
      1. 调用ClassComponent的生命周期componentDidMountcomponetDidUpdateupdate的回调update.callback(如this.setState({}, callabck)
      2. HostComponet带有Update标记需要设置原生状态如focus
  9. 渲染后:
    1. 遍历副作用链表,清空链表让gc回收
    2. 调用ensureRootIsScheduled,检查是否有异步的任务,发起异步调度(如componentDidMount会再次setState
    3. 检查同步的任务flushSyncCallabckQueue,如果有再次进入fiber构造循环

状态与副作用

fiber对象中的属性有两类属性分别与自身状态副作用相关

  1. 状态和副作用影响fiber构建与渲染不同阶段
    1. 状态相关:在fiber树构造阶段为节点提供确定的输入,直接影响子节点的生成,是静态的功能
    2. 副作用相关:在commitRoot阶段,如果fiber有副作用,会同步/异步执行。是动态的功能
  2. 影响fiber状态和副作用的属性
    1. 状态:
      1. pendingProps 最新传入的props,和memorizedProps对比可以得出属性是否改变
      2. memorizedProps上一次子节点生成时用到的props
      3. updateQueue update更新链表,发起更新创建的update对象会保存在链表中
      4. memorizedState上一次子节点生成保存的state
    2. 副作用
      1. flags标志位,表明副作用类型
      2. firstEffect副作用链表,指向第一个fiber对象
      3. lastEffect副作用链表,指向最后一个fiber对象
      4. nextEffect副作用链表,指向下一个fiber对象
  3. 生命周期函数hook用于直接或间接控制上面两类属性
    1. Class组件

      this.setState 发起更新请求,进入reconciler阶段

      1. 状态相关:constructor可以设置state
      2. 状态相关:getDerivedStateFromProps 让组件在修改props的时候更新state
      3. 状态相关:shouldComponentUpdate 每次state或props修改前调用
      4. 副作用相关:getSnapshotBeforeUpdate
      5. 副作用相关:componentDidMount
      6. 副作用相关:componentDidUpdate
    2. 函数式组件

      useStatesetXxx发起更新请求,进入reconciler阶段

      1. 状态相关:useState可以修改Hook.memorizedState
      2. 副作用相关:useEffect flags是Passive | Update,在beforeMutation异步执行
      3. 副作用相关:useLayoutStateflgas是Update,在afterMutation同步执行

Hook原理(概览)

  1. 状态hook与副作用hook区分
    1. 实现了状态持久化且没有没有副作用的hook:useState,useReducer,useCallback,useMemo,useContext,useRef
    2. 会修改flber.flags的hook,useEffect,useLayoutEffect
    3. 组合:同时有状态和副作用2种属性:useDeferredValue,useTransition,useSyncExternalStore
  2. Function类型的fiber节点通过updateFuncitonComponent创建和更新,内部使用renderWithHooks调用函数
  3. 通过Hook API创建hook对象,Hook对象包含以下属性
    1. hook.memorizedState保存在内存中的局部状态
    2. hook.baseStatehook.baseQueue合并后的状态
    3. hook.baseQueue链表存储高于当前渲染优先级的update对象
    4. hook.queue链表存储所有优先级的update对象
    5. hook.next指向链表汇中下一个hook对象
  4. 当前fiberhook对象以链表的形式存储在fiber.memorizedState
  5. fiber首次构造时使用moutWorkInProgressHook构造fiber.memorizedState链表
  6. fiber对比更新时使用updateWorkInProgressHook构造fiber.memorizedState链表,维护全局的currentHookworkInProgressHook,将current上的hook对象复制到alternate

Hook原理(状态)

  1. 状态初始化:useState使用mountStateuseReducer使mountReducer创建hook对象,进行
    1. 初始化一个hook对象,包含上面5个属性
    2. baseStatememorizedState设置为调用useStateinitailState
    3. 初始化hook.queue对象,pending将来存储发起更新请求的后的update对象
    4. 创建一个dispatch方法,调用这个方法发起更新请求dispatchAction
    5. 最后返回[memorizedState, dispatch]
  2. 状态更新:调用dispatch函数发起一个调度更新请求
    1. 创建update对象
    2. update对象添加到hook.queue.pending链表中
    3. 发起调度更新scheduleUpdateOnFiber,多次同步的dispatch会因为调度更新判断渲染优先级相同 不会创建新的task
    4. fiber对比更新中调用updateReducer
    5. hook.queue.pendingupdate链表拼接到hook.baseQueue
    6. 遍历baseQueue,渲染优先级高update的执行,渲染优先级低update的重新添加到baseQueue等待下次执行
    7. 调用reducer计算得到memorizedState
  3. 性能优化:
    1. dispatchAction的时候,如果当前创建的updatequeue.penging中第一个update(即本次调度中第一个setXxx
    2. 直接计算update后的state,记为eagerState
    3. 对比eagerStatememorizedState,如果相同直接返回,不会请求调度

Hook原理(副作用)

  1. 初始化:useEffectuseLayoutEffect可以创建hook对象

    1. 创建hook对象
    2. 将当前fiber(workInProgress)flags标记为fiberFlags
      1. useEffectUpdateEffect | PassiveEffect
      2. useLayoutEffectUpdateEffect
    3. 创建effect对象,放在循环链表上,挂载在hook.memorized上;并添加到fiber的链表尾部fiber.lastEffect
  2. effect对象

    1. tag:标记effect类型
      1. useEffectHookHasEffect | HookPassive
      2. useLayoutEffectHookHasEffect | HookLayout
    2. create:传入的函数
    3. deps:依赖项
    4. destory:传入create函数reutrn的回调
    5. next:指向下一个effect
  3. 处理回调,在commitRoot阶段处理

    1. useEffect
      1. commitBeforeMutationEffects阶段执行scheduleCallback调度执行flushPassiveEffects
      2. flushPassiveEffects会遍历高于当前渲染优先级的effects链表,将effect对象的destorycreate分别存储在两个数组中。
      3. 先遍历执行destory数组,后遍历执行create数组
    2. useLayoutEffect
      1. commitMutationEffects先遍历effcts链表,同步执行高于当前优先级的destory
      2. commitLayoutEffects遍历effcts链表,同步执行高于当前优先级的create
  4. fiber对比更新阶段

  5. desotry函数会直接复用

  6. 对比deps是否有变化,如果没有,新创建的effect.tag没有HookHasEffect标志,不会在commitRoot阶段执行

  7. 组件销毁,会在commitUnmout阶段遍历执行destory函数

context原理

  1. 创建context:
    1. pendingProps.value保存到_currentValue
    2. 创建ProviderConsumer两个ReactElement对象
  2. 消费context
    1. prepareToReadContext进行重置操作
    2. readContext构造一个contextItem,将它push到当前fiberworkInProgress.dependencies中,最后返回context._currentValue
  3. 更新context
    1. 对比pendingPropsmemorizedProps的新旧value
    2. 如果没有变化进入bailout阶段对比子节点
    3. 如果变化,进入propagateContextChange
    4. 向下查找fiber,找到所有fiber.dependencies依赖该contextfiberconsumer),创建update入队,并标记节点的tagforceUpdate。调度更新
    5. consumer节点,向上查找fiber修改fiber.childLanes表明子节点有改动

合成事件

  1. 不能冒泡的事件

    scroll,resize,focue,blur,mouseleave,mouseenter

  2. 采用事件委托在根节点注册事件,不能代理的特殊处理

  3. 事件绑定:listenToAllSupportedEvents绑定支持的事件类型,完成原生事件代理

    1. addEventBubbleListener监听冒泡事件
    2. addEventCaptureListener监听捕获事件
  4. 调度函数dispatchEvent,React将不同的事件化为3个等级,采用不同的调度函数

    1. disPatchDiscreateEvent:优先级最高,如click,input
    2. dispatchUserBlockingEvent:优先级次之,如scroll,drag
    3. dispatchContinuoutEvent:优先级最低,如load,animate
  5. 事件触发,使用dispatchEvent触发事件

    1. attempToDispatchEvent关联fiber
      1. 定位到原生DOM,调用getEventTarget
      2. 获取DOM对应的fibergetClosestInstanceFromNode
      3. 通过插件系统派发事件dispatchEventForPluginSystem
    2. targetFiber上遍历,获取所有对应事件(如onClick)的事件回调,回调保存在fiber.memorizedProps中(如fiber.memorizedProps.onClick
    3. 创建合成事件SyntheticEvent,添加到派发队列dispatchQueue中。合成事件主要抹平了不同平台的差异
    4. 执行派发,因为dispatchQueue是从子节点向父节点收集,capture倒序触发,bubble顺序触发
  6. 综上:主要步骤是

    1. 监听原生事件,找到target元素对应的fiber
    2. 向上遍历fiber树,收集所有监听本事件的listener
    3. 构建合成事件,遍历listener数组派发(触发)事件
相关推荐
AI程序员罗尼38 分钟前
React SSR 水合(Hydration)详解
react.js
AI程序员罗尼1 小时前
React 服务端渲染 (SSR) 详解
react.js
AI程序员罗尼1 小时前
React useEffect 在服务端渲染中的执行行为
react.js
Rachel_wang2 小时前
如何安装并启动 electron-prokit(react+ts) 项目
react.js·electron
就是我2 小时前
如何用lazy+ Suspense实现组件延迟加载
javascript·react native·react.js
新时代农民工Top4 小时前
React + JavaScript 实现可拖拽进度条
前端·javascript·react.js
曼陀罗5 小时前
自动找空闲端口再启动 mock-server」的脚本
前端框架
小钰能吃三碗饭5 小时前
第八篇:【React 性能调优】从优化实践到自动化性能监控
前端·javascript·react.js
Jackson_Mseven5 小时前
如何从0到1搭建基于antd的monorepo库——使用dumi进行文档展示(五)
前端·react.js·ant design
Kairo_016 小时前
使用 Node.js、Express 和 React 构建强大的 API
react.js·node.js·express