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冒泡

相关推荐
cjsnyxz几秒前
【Ant Design】解决树形组件面板收起问题
前端
Cache技术分享3 分钟前
92. Java 数字和字符串 - 字符串
前端·后端
YYsuni4 分钟前
记 Array vs Object 性能表现
前端·javascript
一位搞嵌入式的 genius9 分钟前
最悉心的指导教程——阿里云创建ECS实例教程+Vue+Django前后端的服务器部署(通过宝塔面板)
前端·后端·python·阿里云·宝塔页面
sunddy_x11 分钟前
宝塔部署 Vue + NestJS 全栈项目
前端·javascript·vue.js·node.js
几道之旅15 分钟前
前端antd,后端fastapi,解决文件上传
前端·fastapi
~央千澈~16 分钟前
评论功能开发全解析:从数据库设计到多语言实现-优雅草卓伊凡
java·前端·数据库
uhakadotcom22 分钟前
Husky:自动检查代码,提升团队协作效率
前端·面试·github
二十雨辰43 分钟前
[CSS3]响应式布局
前端·css·html·css3