-
Notifications
You must be signed in to change notification settings - Fork 0
Description
React原理相关知识点
[TOC]
React-diff
参考:
一个
DOM节点在某一时刻最多会有4个节点和他相关。
current Fiber。如果该DOM节点已在页面中,current Fiber代表该DOM节点对应的Fiber节点。workInProgress Fiber。如果该DOM节点将在本次更新中渲染到页面中,workInProgress Fiber代表该DOM节点对应的Fiber节点。DOM节点本身。JSX对象。即ClassComponent的render方法的返回结果,或FunctionComponent的调用结果。JSX对象中包含描述DOM节点的信息。
Diff算法的本质是对比1和4,生成2。
-
为了降低算法复杂度,
React的diff会预设三个限制:-
只对同级元素进行
Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。 -
两个不同类型的元素会产生出不同的树。如果元素由
div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。 -
开发者可以通过
key prop来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:// 更新前 <div> <p key="ka">ka</p> <h3 key="song">song</h3> </div> // 更新后 <div> <h3 key="song">song</h3> <p key="ka">ka</p> </div>
如果没有
key,React会认为div的第一个子节点由p变为h3,第二个子节点由h3变为p。这符合限制2的设定,会销毁并新建。但是当我们用
key指明了节点前后对应关系后,React知道key === "ka"的p在更新后还存在,所以DOM节点可以复用,只是需要交换下顺序。
-
-
实现:
- 当
newChild类型为object、number、string,代表同级只有一个节点 - 当
newChild类型为Array,同级有多个节点
-
我们从
Diff的入口函数reconcileChildFibers出发,该函数会根据newChild(即JSX对象)类型调用不同的处理函数。// 根据newChild类型选择不同diff函数处理 function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, ): Fiber | null { const isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { // object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: // 调用 reconcileSingleElement 处理 // // ...省略其他case } } if (typeof newChild === 'string' || typeof newChild === 'number') { // 调用 reconcileSingleTextNode 处理 // ...省略 } if (isArray(newChild)) { // 调用 reconcileChildrenArray 处理 // ...省略 } // 一些其他情况调用处理函数 // ...省略 // 以上都没有命中,删除节点 return deleteRemainingChildren(returnFiber, currentFirstChild); }
-
单节点Diff:
-
不论当前节点个数,此次更新的节点为单个的情况
-
以
object类型为例,会进入reconcileSingleElement函数const isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { // 对象类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: // 调用 reconcileSingleElement 处理 // ...其他case } }
-
reconcileSingleElement:function reconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement ): Fiber { const key = element.key; let child = currentFirstChild; // 首先判断是否存在对应DOM节点 while (child !== null) { // 上一次更新存在DOM节点,接下来判断是否可复用 // 首先比较key是否相同 if (child.key === key) { // key相同,接下来比较type是否相同 switch (child.tag) { // ...省略case default: { if (child.elementType === element.type) { // type相同则表示可以复用 // 返回复用的fiber return existing; } // type不同则跳出switch break; } } // 代码执行到这里代表:key相同但是type不同 // 将该fiber及其兄弟fiber标记为删除 deleteRemainingChildren(returnFiber, child); break; } else { // key不同,将该fiber标记为删除 deleteChild(returnFiber, child); } child = child.sibling; } // 创建新Fiber,并返回 ...省略 }
-
总结:
- 先判断
key,key相同,判断type,相同则复用,不同则删除当前fiber和兄弟fiber- 为什么要删除兄弟
fiber?- 因为走到这个地方的前提是
key相同,type不同。既然唯一的可能性(key代表唯一标识)都不能复用,那么剩下的fiber都没有机会了,自然被删除。
- 因为走到这个地方的前提是
- 为什么要删除兄弟
key不同则删除当前fiber
- 先判断
-
-
多节点Diff:
-
一个
JSX对象,children属性为数组,就会走到reconcileChildrenArray函数{ $$typeof: Symbol(react.element), key: null, props: { children: [ {$$typeof: Symbol(react.element), type: "li", key: "0", ref: null, props: {…}, …} {$$typeof: Symbol(react.element), type: "li", key: "1", ref: null, props: {…}, …} {$$typeof: Symbol(react.element), type: "li", key: "2", ref: null, props: {…}, …} {$$typeof: Symbol(react.element), type: "li", key: "3", ref: null, props: {…}, …} ] }, ref: null, type: "ul" }
-
几种情况:
-
节点更新:
// 之前 <ul> <li key="0" className="before">0<li> <li key="1">1<li> </ul> // 之后 情况1 —— 节点属性变化 <ul> <li key="0" className="after">0<li> <li key="1">1<li> </ul> // 之后 情况2 —— 节点类型更新 <ul> <div key="0">0</div> <li key="1">1<li> </ul>
-
节点新增或减少
-
节点位置变化
-
同级多个节点的
Diff,一定属于以上三种情况中的一种或多种。
-
-
实现:
-
React团队发现,在日常开发中,更新组件相较于新增和删除,发生的频率更高。所以Diff会优先判断当前节点是否属于更新。 注意
在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。
虽然本次更新的
JSX对象newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。即
newChildren[0]与fiber比较,newChildren[1]与fiber.sibling比较。所以无法使用双指针优化。
-
基于以上原因,
Diff算法的整体逻辑会经历两轮遍历: -
第一轮遍历:处理
更新的节点。-
let i = 0,遍历newChildren,将newChildren[i]与oldFiber比较,判断DOM节点是否可复用。 -
如果可复用,
i++,继续比较newChildren[i]与oldFiber.sibling,可以复用则继续遍历。 -
如果不可复用,分两种情况:
-
key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。// 之前 <li key="0">0</li> <li key="1">1</li> <li key="2">2</li> // 之后 <li key="0">0</li> <li key="2">1</li> <li key="1">2</li>
- 第一个节点可复用,遍历到
key === 2的节点发现key改变,不可复用,跳出遍历,等待第二轮遍历处理。 - 此时
oldFiber剩下key === 1、key === 2未遍历,newChildren剩下key === 2、key === 1未遍历
- 第一个节点可复用,遍历到
-
key相同type不同导致不可复用,会将oldFiber标记为DELETION,并继续遍历// 之前 <li key="0" className="a">0</li> <li key="1" className="b">1</li> // 之后 情况1 —— newChildren与oldFiber都遍历完 <li key="0" className="aa">0</li> <li key="1" className="bb">1</li> // 之后 情况2 —— newChildren没遍历完,oldFiber遍历完 // newChildren剩下 key==="2" 未遍历 <li key="0" className="aa">0</li> <li key="1" className="bb">1</li> <li key="2" className="cc">2</li> // 之后 情况3 —— newChildren遍历完,oldFiber没遍历完 // oldFiber剩下 key==="1" 未遍历 <li key="0" className="aa">0</li>
-
-
如果
newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。
-
-
第二轮遍历:处理剩下的不属于
更新的节点。-
newChildren和oldFiber同时遍历完- 最理想的情况:只需在第一轮遍历进行组件
更新。此时Diff结束。
- 最理想的情况:只需在第一轮遍历进行组件
-
newChildren没遍历完,oldFiber遍历完- 已有的
DOM节点都复用了,这时还有新加入的节点,意味着本次更新有新节点插入,我们只需要遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement。 - 你可以在这里看到这段源码逻辑
- 已有的
-
newChildren遍历完,oldFiber没遍历完- 意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的
oldFiber,依次标记Deletion。 - 你可以在这里看到这段源码逻辑
- 意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的
-
newChildren和oldFiber都没遍历完- 这意味着有节点在这次更新中改变了位置。重点!!,下文详细讲解
- 你可以在这里看到这段源码逻辑
-
处理移动的节点
-
由于有节点改变了位置,所以不能再用位置索引
i对比前后的节点,那么我们需要使用key。 -
为了快速的找到
key对应的oldFiber,我们将所有还未处理的oldFiber存入以key为key,oldFiber为value的Map中。(这地方有些像vue2的diff)const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
-
接下来遍历剩余的
newChildren,通过newChildren[i].key就能在existingChildren中找到key相同的oldFiber。
-
-
标记节点是否移动
-
我们需要明确:节点是否移动是以什么为参照物?
-
参照物是:最后一个可复用的节点在
oldFiber中的位置索引(用变量lastPlacedIndex表示)。 -
方便理解,两个例子:
-
剩余
oldFiber生成map(key:index),遍历剩余newChildren,在oldFiber中匹配key找到对应老节点index -
在Demo中我们简化下书写,每个字母代表一个节点,字母的值代表节点的
key// 之前 abcd // 之后 acdb ===第一轮遍历开始=== a(之后)vs a(之前) key不变,可复用 此时 a 对应的oldFiber(之前的a)在之前的数组(abcd)中索引为0 所以 lastPlacedIndex = 0; 继续第一轮遍历... c(之后)vs b(之前) key改变,不能复用,跳出第一轮遍历 此时 lastPlacedIndex === 0; ===第一轮遍历结束=== ===第二轮遍历开始=== newChildren === cdb,没用完,不需要执行删除旧节点 oldFiber === bcd,没用完,不需要执行插入新节点 将剩余oldFiber(bcd)保存为map // 当前oldFiber:bcd // 当前newChildren:cdb 继续遍历剩余newChildren(在map中找相同key) key === c 在 oldFiber中存在 const oldIndex = c(之前).index; 此时 oldIndex === 2; // 之前节点为 abcd,所以c.index === 2 比较 oldIndex 与 lastPlacedIndex; 如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动 并将 lastPlacedIndex = oldIndex; 如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动 在例子中,oldIndex 2 > lastPlacedIndex 0, 则 lastPlacedIndex = 2; c节点位置不变 继续遍历剩余newChildren // 当前oldFiber:bd // 当前newChildren:db key === d 在 oldFiber中存在 const oldIndex = d(之前).index; oldIndex 3 > lastPlacedIndex 2 // 之前节点为 abcd,所以d.index === 3 则 lastPlacedIndex = 3; d节点位置不变 继续遍历剩余newChildren // 当前oldFiber:b // 当前newChildren:b key === b 在 oldFiber中存在 const oldIndex = b(之前).index; oldIndex 1 < lastPlacedIndex 3 // 之前节点为 abcd,所以b.index === 1 则 b节点需要向右移动 ===第二轮遍历结束===
// 之前 abcd // 之后 dabc ===第一轮遍历开始=== d(之后)vs a(之前) key改变,不能复用,跳出遍历 ===第一轮遍历结束=== ===第二轮遍历开始=== newChildren === dabc,没用完,不需要执行删除旧节点 oldFiber === abcd,没用完,不需要执行插入新节点 将剩余oldFiber(abcd)保存为map 继续遍历剩余newChildren // 当前oldFiber:abcd // 当前newChildren dabc key === d 在 oldFiber中存在 const oldIndex = d(之前).index; 此时 oldIndex === 3; // 之前节点为 abcd,所以d.index === 3 比较 oldIndex 与 lastPlacedIndex; oldIndex 3 > lastPlacedIndex 0 则 lastPlacedIndex = 3; d节点位置不变 继续遍历剩余newChildren // 当前oldFiber:abc // 当前newChildren abc key === a 在 oldFiber中存在 const oldIndex = a(之前).index; // 之前节点为 abcd,所以a.index === 0 此时 oldIndex === 0; 比较 oldIndex 与 lastPlacedIndex; oldIndex 0 < lastPlacedIndex 3 则 a节点需要向右移动 继续遍历剩余newChildren // 当前oldFiber:bc // 当前newChildren bc key === b 在 oldFiber中存在 const oldIndex = b(之前).index; // 之前节点为 abcd,所以b.index === 1 此时 oldIndex === 1; 比较 oldIndex 与 lastPlacedIndex; oldIndex 1 < lastPlacedIndex 3 则 b节点需要向右移动 继续遍历剩余newChildren // 当前oldFiber:c // 当前newChildren c key === c 在 oldFiber中存在 const oldIndex = c(之前).index; // 之前节点为 abcd,所以c.index === 2 此时 oldIndex === 2; 比较 oldIndex 与 lastPlacedIndex; oldIndex 2 < lastPlacedIndex 3 则 c节点需要向右移动 ===第二轮遍历结束===
- 考虑性能,我们要尽量减少将节点从后面移动到前面的操作。
-
-
-
-
-
- 当
React-Router
- BrowserRouter
React-router-dom v6BrowserRouter,通过createBrowserHistory创建history实例(history库提供),执行history.listen监听路由变化,返回Router组件
React-router- 提供
Router,Routes以及多个路由相关hooks Router注入两个context,并格式化props中相关参数。Routes将children转化为routes数组,并作为参数放在useRoutes中执行useRoutes内部读取context中数据(这也说明为什么useRoutes要放在<Router>下使用),并会通过分数规则判断路由匹配,交由_renderMatches渲染_renderMatches注入一个context,并渲染出对应组件
- 提供
React事件机制
参考
- 事件原理,
react中事件是合成事件,统一绑定在document,事件回调为dispatchEvent - 当用户点击时触发
document上的对应事件,从原生事件找到对应的合成事件,并从事件池中取出该合成事件的实例对象,并覆盖属性,作为事件对象,如果没有就创建一个(React17取消了事件对象的复用) - 从原生事件中找到点击对应的
dom节点,从dom节点找到最近的React组件实例,从而找到了一条由这个实例父节点不断向上组成的链, 这个链就是我们要触发合成事件的链, - 反向触发这条链,
父-> 子,模拟捕获阶段,触发所有props中含有onClickCaptures的实例 - 正向触发这条链,
子-> 父,模拟冒泡阶段,触发所有props中含有onClick的实例。 - 总结:
React会在派发事件时打开批量更新, 此时所有的setState都会变成异步。ReactonClick/onClickCapture, 实际上都发生在原生事件的冒泡阶段。(React17支持原生捕获事件)
Fiber
参考:
-
虚拟DOM在
React中有个正式的称呼——Fiber,用Fiber来取代React16虚拟DOM这一称呼 -
起源:
- 在
React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。 - 为了解决这个问题,
React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。
- 在
-
含义:
- 作为架构来说,之前
React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。 - 作为静态的数据结构来说,每个
Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。 - 作为动态的工作单元来说,每个
Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)
- 作为架构来说,之前
-
结构:
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // 作为静态数据结构的属性 this.tag = tag; // Fiber对应组件的类型 Function/Class/Host... this.key = key; // key属性 this.elementType = null; // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹 this.type = null; // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent(原生html标签),指DOM节点tagName this.stateNode = null; // Fiber对应的真实DOM节点 // 用于连接其他Fiber节点形成Fiber树 this.return = null; // 指向父级Fiber节点 this.child = null; // 指向子Fiber节点 this.sibling = null; // 指向右边第一个兄弟Fiber节点 this.index = 0; this.ref = null; // 作为动态的工作单元的属性 // 保存本次更新造成的状态改变相关信息 this.pendingProps = pendingProps; this.memorizedProps = null; this.updateQueue = null; //fiber 上的更新队列执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新最终会形成一个链表结构,最后做批量更新 this.memorizedState = null; this.dependencies = null; this.mode = mode; // Effect相关 // 保存本次更新会造成的DOM操作 this.effectTag = NoEffect; // 表示当前 fiber 要进行何种更新(更新、删除等) this.nextEffect = null; // 指向下个需要更新的fiber this.firstEffect = null; // 指向所有子节点里,需要更新的 fiber 里的第一个 this.lastEffect = null; // 指向所有子节点中需要更新的 fiber 的最后一个 // 调度优先级相关 this.lanes = NoLanes; this.childLanes = NoLanes; // 指向该fiber在另一次更新时对应的fiber(workInProgress fiber树) this.alternate = null; }
Fiber的工作原理
参考:
-
在
React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。 -
current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。-
currentFiber.alternate === workInProgressFiber; workInProgressFiber.alternate === currentFiber;
-
-
即当
workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。 -
流程梳理——
mount时function App() { const [num, add] = useState(0); return ( <p onClick={() => add(num + 1)}>{num}</p> ) } ReactDOM.render(<App/>, document.getElementById('root'));
-
首次执行
ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是<App/>所在组件树的根节点。-
之所以要区分
fiberRootNode与rootFiber,是因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个fiberRootNode。fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树。fiberRootNode.current = rootFiber;
-
页面中还没有挂载任何
DOM,fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即current Fiber树为空)。
-
-
进入
render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树(下图中右侧的树) -
图中右侧已构建完的
workInProgress Fiber树在commit阶段渲染到页面后,fiberRootNode的current指针指向workInProgress Fiber树使其变为current Fiber 树。
-
-
流程梳理——
update时
JSX
参考:
-
编译成
React.createElementexport function createElement(type, config, children) { let propName; const props = {}; let key = null; let ref = null; let self = null; let source = null; if (config != null) { // 将 config 处理后赋值给 props // ...省略 } const childrenLength = arguments.length - 2; // 处理 children,会被赋值给props.children // ...省略 // 处理 defaultProps // ...省略 return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, ); } const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { // 标记这是个 React Element $$typeof: REACT_ELEMENT_TYPE, type: type,// 执行组件自身,类组件指向类,函数组件指向函数 key: key, ref: ref, props: props, _owner: owner, }; return element; };
JSX被
ReactElement生成为一个element对象。JSX是一种描述当前组件内容的数据结构 -
element对象不包含他不包含组件schedule、reconcile、render所需的相关信息。比如如下信息就不包括在
JSX中:- 组件在更新中的
优先级 - 组件的
state - 组件被打上的用于Renderer的
标记
这些内容都包含在
Fiber节点中。 - 组件在更新中的
-
所以,在组件
mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。在
update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记
Render阶段流程
参考:
beginWork负责从父到子遍历,当子为null的时候,执行completeWork,如果当前有sibling,就返回sibling继续执行beginWork
-
render阶段开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。// performSyncWorkOnRoot会调用该方法 function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress); } } // performConcurrentWorkOnRoot会调用该方法 function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); } }
- 区别在于是否调用
shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。 workInProgress代表当前已创建的workInProgress fiber。performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。
- 区别在于是否调用
-
performUnitOfWork的工作可以分为两部分:“递”和“归”。通过遍历的方式实现可中断的递归-
递阶段
-
首先从
rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法 。该方法会根据传入的
Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
-
-
归阶段
-
在“归”阶段会调用completeWork 处理
Fiber节点。当某个
Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。如果不存在
兄弟Fiber,会进入父级Fiber的“归”阶段。“递”和“归”阶段会交错执行直到“归”到
rootFiber。至此,render阶段的工作就结束了
-
-
-
例子:
function App() { return ( <div> i am <span>KaSong</span> </div> ) } ReactDOM.render(<App />, document.getElementById("root"));
对应Fiber树结构:
1. rootFiber beginWork 2. App Fiber beginWork 3. div Fiber beginWork 4. "i am" Fiber beginWork 5. "i am" Fiber completeWork 6. span Fiber beginWork 7. span Fiber completeWork 8. div Fiber completeWork 9. App Fiber completeWork 10. rootFiber completeWork // 之所以没有 “KaSong” Fiber 的 beginWork/completeWork // 是因为作为一种性能优化手段,针对只有单一文本子节点的Fiber,React会特殊处理。
-
beginWork:function beginWork( current: Fiber | null, // 当前组件对应的Fiber节点 workInProgress: Fiber, // 当前组件对应的Fiber节点 renderLanes: Lanes, // 优先级相关 ): Fiber | null { // ...省略函数体 }
-
组件
mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current === null。所以我们可以通过current === null ?来区分组件是处于mount还是update。// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点) if (current !== null) { // ...省略 // 复用current return bailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes, ); } else { didReceiveUpdate = false; } // mount时:根据tag不同,创建不同的子Fiber节点 switch (workInProgress.tag) { case IndeterminateComponent: // ...省略 case LazyComponent: // ...省略 case FunctionComponent: // ...省略 case ClassComponent: // ...省略 case HostRoot: // ...省略 case HostComponent: // ...省略 case HostText: // ...省略 // ...省略其他类型 } }
-
因此,
beginWork的工作可以分为两部分:-
update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。-
我们可以看到,满足如下情况时就可以直接复用前一次更新的
子Fiber,不需要新建子Fiber:(didReceiveUpdate=true)oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变!includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够,会在讲解Scheduler时介绍
if (current !== null) { const oldProps = current.memoizedProps; const newProps = workInProgress.pendingProps; if ( oldProps !== newProps || hasLegacyContextChanged() || (__DEV__ ? workInProgress.type !== current.type : false) ) { didReceiveUpdate = true; } else if (!includesSomeLane(renderLanes, updateLanes)) { didReceiveUpdate = false; switch (workInProgress.tag) { // 省略处理 } return bailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes, ); } else { didReceiveUpdate = false; } } else { didReceiveUpdate = false; }
-
-
mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点- 根据
fiber.tag不同,进入不同类型Fiber的创建逻辑。
- 根据
-
对于我们常见的组件类型,如(
FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren方法。
-
-
-
reconcileChildren
-
Reconciler模块的核心部分 -
对于
mount的组件,他会创建新的子Fiber节点 -
对于
update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes ) { if (current === null) { // 对于mount的组件 workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderLanes, ); } else { // 对于update的组件 workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes, ); } }
-
最终他会生成新的子
Fiber节点并赋值给workInProgress.child,作为本次beginWork返回值 ,并作为下次performUnitOfWork执行时workInProgress的传参。 -
mountChildFibers与reconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。
-
-
effectTag-
render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中。你可以从这里 看到effectTag对应的DOM操作// DOM需要插入到页面中 export const Placement = /* */ 0b00000000000010; // DOM需要更新 export const Update = /* */ 0b00000000000100; // DOM需要插入到页面中并更新 export const PlacementAndUpdate = /* */ 0b00000000000110; // DOM需要删除 export const Deletion = /* */ 0b00000000001000;
-
通过二进制表示
effectTag,可以方便的使用位操作为fiber.effectTag赋值多个effect -
那么,如果要通知
Renderer将Fiber节点对应的DOM节点插入页面中,需要满足两个条件:fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点(fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag
-
我们知道,
mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?fiber.stateNode会在completeWork中创建- 假设
mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。 - 为了解决这个问题,在
mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。
-
-
beginWork流程图 -
completeWork:function completeWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ): Fiber | null { const newProps = workInProgress.pendingProps; switch (workInProgress.tag) { case IndeterminateComponent: case LazyComponent: case SimpleMemoComponent: case FunctionComponent: case ForwardRef: case Fragment: case Mode: case Profiler: case ContextConsumer: case MemoComponent: return null; case ClassComponent: { // ...省略 return null; } case HostRoot: { // ...省略 updateHostContainer(workInProgress); return null; } case HostComponent: { // ...省略 return null; }
-
类似
beginWork,completeWork也是针对不同fiber.tag调用不同的处理逻辑。 -
处理
HostComponent:-
和
beginWork一样,我们根据current === null ?判断是mount还是update。 -
同时针对
HostComponent,判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)case HostComponent: { popHostContext(workInProgress); const rootContainerInstance = getRootHostContainer(); const type = workInProgress.type; if (current !== null && workInProgress.stateNode != null) { // update的情况 // ...省略 } else { // mount的情况 // ...省略 } return null; }
-
mount的情况:
-
为
Fiber节点生成对应的DOM节点 -
将子孙
DOM节点插入刚生成的DOM节点中 -
与
update逻辑中的updateHostComponent类似的处理props的过程// mount的情况 // ...省略服务端渲染相关逻辑 const currentHostContext = getHostContext(); // 为fiber创建对应DOM节点 const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, ); // 将子孙DOM节点插入刚生成的DOM节点中 appendAllChildren(instance, workInProgress, false, false); // DOM节点赋值给fiber.stateNode workInProgress.stateNode = instance; // 与update逻辑中的updateHostComponent类似的处理props的过程 if ( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); }
-
mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢? -
原因就在于
completeWork中的appendAllChildren方法,每次调用时都会将已生成的子孙DOM节点插入当前生成的DOM节点下,当completeWork函数执行到rootfiber时,appendAllChildren已经构建好了一个完整dom的(未渲染)
-
-
update的情况:
-
当
update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:onClick、onChange等回调函数的注册- 处理
style prop - 处理
DANGEROUSLY_SET_INNER_HTML prop - 处理
children prop
-
我们去掉一些当前不需要关注的功能(比如
ref)。可以看到最主要的逻辑是调用updateHostComponent方法。if (current !== null && workInProgress.stateNode != null) { // update的情况 updateHostComponent( current, workInProgress, type, newProps, rootContainerInstance, ); }
-
在
updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。workInProgress.updateQueue = (updatePayload: any); // updatePayload=[props[key],props[value]] // 偶数索引的值为变化的prop key,奇数索引的值为变化的prop value。
-
你可以从这里看到
updateHostComponent方法定义。
-
-
-
effectList
-
作为
DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么? -
为了解决这个问题,在
completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。 -
effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。 -
类似
appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。nextEffect nextEffect rootFiber.firstEffect -----------> fiber -----------> fiber
-
-
completeWork流程图
-
-
-
结尾:
-
至此,
render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。commitRoot(root);
-
Commit阶段
参考:
-
rootFiber.firstEffect为开始保存了一条需要执行副作用的链表effectList,在commit阶段执行 -
除此之外,一些生命周期钩子(比如
componentDidXXX)、hook(比如useEffect)需要在commit阶段执行。 -
commit阶段的主要工作(即Renderer的工作流程)分为三部分:- before mutation阶段(执行
DOM操作前) - mutation阶段(执行
DOM操作) - layout阶段(执行
DOM操作后) - (你可以从这里 看到
commit阶段的完整代码)
- before mutation阶段(执行
-
在
before mutation阶段之前和layout阶段之后还有一些额外工作,涉及到比如useEffect的触发、优先级相关的重置、ref的绑定/解绑。 -
before mutation阶段之前
-
before mutation之前主要做一些变量赋值,状态重置的工作。 -
这一长串代码我们只需要关注最后赋值的
firstEffect,在commit的三个子阶段都会用到他。do { // 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务 flushPassiveEffects(); } while (rootWithPendingPassiveEffects !== null); // root指 fiberRootNode // root.finishedWork指当前应用的rootFiber const finishedWork = root.finishedWork; // 凡是变量名带lane的都是优先级相关 const lanes = root.finishedLanes; if (finishedWork === null) { return null; } root.finishedWork = null; root.finishedLanes = NoLanes; // 重置Scheduler绑定的回调函数 root.callbackNode = null; root.callbackId = NoLanes; let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); // 重置优先级相关变量 markRootFinished(root, remainingLanes); // 清除已完成的discrete updates,例如:用户鼠标点击触发的更新。 if (rootsWithPendingDiscreteUpdates !== null) { if ( !hasDiscreteLanes(remainingLanes) && rootsWithPendingDiscreteUpdates.has(root) ) { rootsWithPendingDiscreteUpdates.delete(root); } } // 重置全局变量 if (root === workInProgressRoot) { workInProgressRoot = null; workInProgress = null; workInProgressRootRenderLanes = NoLanes; } else { } // 将effectList赋值给firstEffect // 由于每个fiber的effectList只包含他的子孙节点 // 所以根节点如果有effectTag则不会被包含进来 // 所以这里将有effectTag的根节点插入到effectList尾部 // 这样才能保证有effect的fiber都在effectList中 let firstEffect; if (finishedWork.effectTag > PerformedWork) { if (finishedWork.lastEffect !== null) { finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; } else { firstEffect = finishedWork; } } else { // 根节点没有effectTag firstEffect = finishedWork.firstEffect; }
-




