React原理相关
MyPrototypeWhat opened this issue · 0 comments
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 v6
BrowserRouter
,通过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
都会变成异步。React
onClick
/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.createElement
export 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; }
-