React源码系列(四):React的render阶段-Reconciler

前言

React v16及以后架构可分为三个部分

  1. Scheduler(调度器)------ 调度任务的优先级,高优先级任务优先进入Reconciler;
  2. Reconciler(协调器)------ VDOM的实现,负责根据状态改变计算出UI变化
  3. Renderer(渲染器)------ 负责将变化的UI渲染到页面上

而调度和协调的工作部分又被称为React的render阶段,本章只讨论Reconciler的工作内容

1、触发更新

触发更新的方式主要有以下几种:ReactDOM.createRoot().render、setState、useState的dispatch方法 等

2、在react的启动过程中都发生了什么?

javascript 复制代码
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)

接下来,进入源码

找到creatRoot这个方法,发现接受两个参数,第二个为可选参数,这里的container 就是我们传入的 div#rout

这里去调用了createContainer方法,创建了一个fiberRoot节点,最后返回了一个ReactDOMRoot的实例

代码中的root 就是这个返回的实例,根据并且通过_internalRoot指向了createFiberRoot创建了React应用的根结点 FiberRootNode

在createFiberRoot方法内部又调用了createHostRootFiber方法,

这个方法顾名思义就是创建了一个叫做HostRootFiber的一个节点,并且通过current指针,绑定在了root节点上,也就是FiberRoot.current = HostRootFiber

在进入render方法前,先看一下这张图

所以这里的FiberRootNode 和 HostRootFiber 就是这么来的。

接下来进入root.render()方法

root方法接受一个childrenList类型的参数children,那也就是代码中传递的

updateContainer这个函数接受四个参数,分别传递的是 children 也就是 和 root,root就是刚才创建的的FiberRootNode,还有parentComponent和cb,在启动的时候,传递的是null。

在函数内部,定义了current变量,这里也就是hostFiberRoot节点

这个时候在updateContainer方法内部会去执行scheduleUpdateOnFiber这个函数,

scheduleUpdateOnFiber就是开启render阶段的入口函数

总结:

在初始的时候,会创建FiberRootNode以及HostRootFiber,FiberRootNode就是整个react应用的根结点,通过current指向HostRootFiber,也就是div#root对应的fiber node,在render方法内部,会去调用scheduleUpdateOnFiber方法来开启任务的调度以及wip fiber树的构建

至此,走了这么长的调用链,终于走到了workLoopConcurrent方法,开启render的工作内容

workLoopConcurrent 做的事情就是通过 while 循环反复判断 workInProgress 是否为空,判断条件是否存在shouldYield的执行,如果浏览器没有足够的时间,那么会终止while循环,并在不为空的情况下针对它执行 performUnitOfWork 函数。

这里的workInProgress就是定义的一个变量,用来表示新创建的fiber node, 简称wip

performUniOfWork:这个方法会创建下一个fiberNode 并赋值给wip,并将wip与已经创建的fiber node连接起来构成fiber tree,这个过程采用FSD的方式向下遍历,可以分为"递"和"归"2个阶段

3、react 工作的2大循环

这张图也就对应了在16版本及以后react采用的fiber 架构,所对应的其中2个部分,一个是scheduler部分,也就是负责任务调度循环 ,另一个就是reconciler部分,负责fiber构造循环

fiber 构造循环,也就是刚才所讲的采用FSD的方式向下遍历,可以分为"递"和"归"2个阶段 ,而递和归又分别对应mount和update两种场景

在递的阶段,从根节点rootFiber开始,遍历到叶子节点,每次遍历到的节点都会执行beginWork,并且传入当前Fiber节点,然后创建或复用它的子Fiber节点,并赋值给workInProgress.child。

在归的阶段, 在归阶段遍历到子节点之后,会执行completeWork方法,执行完成之后会判断此节点的兄弟节点存不存在,如果存在就会为兄弟节点执行completeWork,当全部兄弟节点执行完之后,会向上冒泡到父节点执行completeWork,直到rootFiber。

3.1 区别

任务调度

1、任务调度循环是以二叉堆为数据结构, 循环执行堆的顶点, 直到堆被清空.

2、任务调度循环的逻辑偏向宏观, 它调度的是每一个任务(task), 而不关心这个任务具体是干什么的(甚至可以将Scheduler包脱离react使用), 具体任务其实就是执行回调函数performSyncWorkOnRoot或performConcurrentWorkOnRoot.

fiber构造

1、fiber构造循环是以树为数据结构, 从上至下执行深度优先遍历(详见react 算法之深度优先遍历).

2、fiber构造循环的逻辑偏向具体实现, 它只是任务(task)的一部分(如performSyncWorkOnRoot包括: fiber树的构造, DOM渲染, 调度检测), 只负责fiber树的构造.

3.2 联系

fiber构造循环是任务调度循环中的任务(task)的一部分. 它们是从属关系, 每个任务都会重新构造一个fiber树.

4、render阶段流程

render阶段说白了就是为了生成fiber树,在上一章已经阐述了fiber tree的构造流程,本节将会深入描述,fiber tree是怎么生成出来的,首先要明确的一点是,在这个过程中,recociler 消费的是jsx方法的执行,也就是ReactElement

还有就是在render阶段,会使用上一章讲到的双缓存树机制

眼前有2棵树,一颗你看见的(对应的dom树), 另一颗你看不见

你看见的那棵树叫current fiber tree

你看不见的那棵树叫wip fiber tree

renderRootConcurrent会循环遍历,每次执行performUnitOfWork都会根据传入的FiberNode生成下一级的fiberNode

performUnitOfWork的工作分为2个部分,就是上面提到的递和归

在递的阶段,会从HostRootFiber以DFS的方式遍历,遍历到的每个节点都会去执行beginWork方法,这个方法会根据传入的fiber node 创建下一级的 fiber node,当遍历生成的子节点为 null (相当于遍历到叶子结点)时进入 "归" ,对应completeUnitOfWork 中的 completeWork 方法,主要功能为 flags 进行冒泡。当某个 FIberNode 执行 completeWork 完成后,如果其存在兄弟节点,则会进入兄弟节点的 "递" 过程,如果不存在兄弟节点,则进入父节点的"归"过程。按照这个流程, "递" 和 "归" 会交错执行直到 HostRoot 执行 "归"为止。

javascript 复制代码
function Index() { 
    return <p>函数组件 <span>123</span> </p> 
}
function App() { 
    return ( 
    <div className="App">
        <h1>子节点</h1>
        <Index /> 
    </div> 
    ) 
}
  • HostRoot beginWork 生成 App FIberNode
  • App beginWork 生成 div FiberNode
  • div beginWork 生成 h1 FiberNode
  • h1 beginWork 返回 null(DFS 到叶子结点,当前'递'结束,开始'归')
  • h1 执行 completeWork(发现 h1 有兄弟,继续兄弟的'递' '归')
  • Index beginWork 生成 p FiberNode
  • p beginWork 生成 '函数组件' FiberNode
  • '函数组件' beginWork 返回 null(DFS 到叶子结点,当前'递'结束,开始'归')
  • '函数组件' 执行 completeWork(发现'函数组件'有兄弟,继续兄弟的'递''归')
  • span beginWork 返回 null(DFS 到叶子结点,当前'递'结束,开始'归')
  • span 执行 completeWork(没有兄弟节点,开始进行父节点的'归')
  • p 执行 completeWork(没有兄弟节点,开始进行父节点的'归')
  • Index 执行 completeWork(没有兄弟节点,开始进行父节点的'归')
  • div 执行 completeWork(没有兄弟节点,开始进行父节点的'归')
  • App 执行 completeWork(没有兄弟节点,开始进行父节点的'归')
  • HostRoot 执行 completeWork(没有兄弟节点,开始进行父节点的'归')

4.1 beginWork --- 递的阶段

首先 beginWork主要干了三件事

1、创建节点:根据ReactElement对象 创建 fiberNode,最终构造出来wip fiber tree (通过return child sibling指针)

2、打flags:也就是给节点打上增删改的flags标签,等待completeWork的时候处理

3、设置真是DOM的局部状态:真是DOM是通过fiber.stateNode来存储的(如Class类型节点: fiber.StateNode = new Class())

current: 代表当前页面正在使用的fiber node ,也就是workInProgress.alternate

workInProgress 表示当前正在遍历构建的fiber node

首先,beginWork会判断当前流程输入mount还是update流程,判断依据是current fiber 是否存在,如果不存在,则为mount阶段

HostComponent代表原生Element类型 比如div span

IndeterminateComponent 是 FC mount时进入的分支 update时jinruFunctionComponent分支

HostText代表文本元素类型

比如HostComponent 会去执行updateHostComponent, 这个方法内部最终会去执行reconcileChildren方法

发现在mount的时候会去执行mountChildFibers, 在update的时候会去执行reconcileChildFibers

发现这俩函数其实最终执行的都是createChildReconciler方法,只是传递的参数不同,createChildReconciler接收一个参数,shouldTrackSideEffects代表是否追踪副作用,翻译过来就是在创建下一级fiberNode的时候,要不要打flags

从上面又看到,createChildReconciler其实 最终又是执行的reconcileChildFibers方法

ChildDeletion 代表删除操作

Placement 代表插入或者移动操作

单个节点的diff过程

returnFiber代表父级的fiber 也就是workInProgress ,执行reconcileChildFibers会返回wip的子fiberNode

currentFirstchild代表当前fiber的第一个子fiber, 在mount的时候这个参数为null

element,也就是传递过来的newChild,代表下一级的子ReactElement集合可能有多个

lanes先忽略

由代码看到 如果currentFirstChild不为null,意思就是说currentFiber下面还有,则进入while循环,

首先判断child的key 和 传递进来的ReactElement的key, 如果不相同 直接进入deleteChild函数,这个函数就是给returnFiber也就是wip上面打上删除这个current.fiber的标签,也就是要把当前的这个child在commit阶段删掉

如果key相同,则需要继续判断两者的type是否相同,这里面会执行2个关键函数 useFiber和deleteRemainingChildren

useFiber就是根据新的ReactElement和old的fibernode 创建新的fiber

deleteRemainingChildren就是用来删除兄弟和子节点的,原因是这里是单个节点的diff

多个节点的diff

在多个节点diff的时候 会经历两次循环

首先React的diff 一定是同级diff,在对比ReactElement序列和old fiber序列的时候 会先通过index的顺序来对比key和type,调用updateSlot对比index相同的child的key是否相同,如果是,返回该对象,如果不是,返回null决定是否复用old fiber节点,也就是第一轮循环是在找相同,如果遇到不同,则直接跳出循环

如果newChildren先遍历完了,则删除old fiber剩余的节点

如果old fiber先遍历完了,则给剩余的new child 打上插入的标签

在第二轮循环,此时是将剩余的ReactElement和剩余的old fiber node进行对比

面试中问的key是用来干嘛的,最关键的就是用在这, 在执行第二轮遍历的时候,会用一个map对象把剩余的fiber node存储起来,map{key: fiberNode},以这种形式来做一个映射,

然后继续遍历newChildren 也就是ReactElement序列,如果map中存在ReactElement的key,则map就会把这个移除掉,表明可以复用,如果没有,则根据ReactElement 创建新的fibernode

遍历完了,map中还有剩余的话,就会被全部打上删除的标签

这个就是diff的过程

4.2 completeWork---归的阶段

在执行beginWork的时候,会创建当前节点的子节点,如果当前节点没有子节点,说明当前节点是一个叶子节点。则会去执行completeWork,也就是会进入归的阶段,这个阶段是自下而上的,更新过程中,会计算dom节点的属性,一旦属性更新,就会为dom节点在对应的wip节点上打上更新的标记

在归的阶段主要的工作内容

1、给fiber节点创建DOM实例,并且通过fiber.stateNode指向这个DOM实例

2、给DOM节点设置属性,绑定合成事件

3、给fiberNode打标记,设置flags

4、flags冒泡

框住的代码就对应了 mount的时候,由于不存在alternate,所以就会走到构建dom的流程

在update阶段,回去执行updateHostComponent这个方法

createInstance干的事情就是通过fibernode的type创建DOM节点,并且通过return拿到当前的父节点,将创建好的element添加到父节点的stateNode中,最后返回这个element,然后执行appendAllChildren方法,这个方法的任务就是将后代节点插入到当前节点

这里的插入 不是说将当前的DOM插入到它的父节点,而是将当前这个DOM的第一个子节点插入到wip下面

css 复制代码
function App(){
  <div>
    hello
    <span>wor<span/>
  <div/>
}

执行appendAllChildren方法的fiberNode依次顺序

1、 span fibernode 此时 node === null 退出

2、div fibernode 此时node 为hello fibernode

2.1、HTMLDivElement.appendChild("Hello", textNode)

2.2、node 为span fibernode

2.3 HTMLDivElement.appendChild(HTMLSpanElement)

最后会去执行 bubbleProperties 将flags冒泡

4.3 flags冒泡

当更新流程经过Reconciler后,会得到一个wip fiber tree,其中部分被标记了flags, 在下一阶段Renderer需要对被标记的fiberNode对应的DOm元素执行对应的操作,那么如果高效的找到这些散落的被标记的fiber node?

在归的阶段,从叶子元素开始,整体流程是自下而上的, 通过fiberNode.subtreeFlags记录了该fibernode的所有子孙fibernode上被标记的flags,早期版本并没有使用subTreeFlags,而是使用了一种Effect List的链表结构来保存被标记的flags,因为suspense的原因,suspense需要遍历子树,而subtreeFlags油笔effects list遍历更多的节点,所以在18版本中,使用了subtreeFlags替换掉了effect list

将当前fibernode的flags层层记录在父fibernode的subtreeFlags这个过程,就是flags冒泡

相关推荐
ccnocare1 分钟前
浅浅看一下设计模式
前端
Lee川4 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix31 分钟前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人34 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl38 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅41 分钟前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_1 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript