写在前面:最近一段时间在学习React源码,写这篇文章的目的有二:
我又将这段时间分成如下几个阶段:
再次声明,本篇文章主要是为了分享学习经理,想要系统学习React源码的同学可以参照文末的优秀文章~
刚开始学习react源码之前,建议自己先手动实现一个简单的react。推荐跟着下面的视频教程进行学习。
react17源码训练营
照着视频教程撸的源码
# 拉取代码 1. git clone https://github.com/facebook/react.git # 也可以使用cnpm镜像 2. git clone https://github.com.cnpmjs.org/facebook/react # 或者使用码云的镜像 3. git clone https://gitee.com/mirrors/react.git
cd react yarn
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE
tips: 如果自己build有困难的话,可以直接使用我打包好的
打包好的地址
npx create-react-app my-app
step1: 在打包好的react目录下执行 yarn link step2: 在my-app项目目录下执行 yarn link react step3: 在打包好的react-dom目录下执行 yarn link step4: 在my-app项目目录下执行 yarn link react-dom
// 在react-dom/cjs/react-dom.development.js中加上自己的log // 1. 应用程序中 调用的入口函数 在这里 function render(element, container, callback) { console.log('react render函数执行了'); if (!isValidContainer(container)) { { throw Error( "Target container is not a DOM element." ); } } { var isModernRoot = isContainerMarkedAsRoot(container) && container._reactRootContainer === undefined; if (isModernRoot) { error('You are calling ReactDOM.render() on a container that was previously ' + 'passed to ReactDOM.createRoot(). This is not supported. ' + 'Did you mean to call root.render(element)?'); } } return legacyRenderSubtreeIntoContainer(null, element, container, false, callback); }
启动my-react项目,打开控制台… 可以看到,react,react-dom这两个包使用的是我们打包好的而不是node_modules里的。接下来我们就可以愉快地调试了!
最新的react架构可以分为三层:
scheduler
包, 核心职责只有 1 个, 就是执行回调.
react-reconciler
提供的回调函数, 包装到一个任务对象中.react-reconciler
包, 有 3 个核心职责:
HostConfig
协议(如: react-dom
), 保证在需要的时候, 能够正确调用渲染器的 api, 生成实际节点(如: dom
节点).react-dom
包(初次render
)和react
包(后续更新setState
)发起的更新请求.fiber
树的构造过程包装在一个回调函数中, 并将此回调函数传入到scheduler
包等待调度.react中最广为人知的是可中断渲染。之所以react16之后
UNSAFE_componentWillMount, UNSAFE_componentWIllReceiveProps
render阶段执行的声明周期函数是不安全的,是因为render阶段是可中断的。但是!只有在HostRootFiber.mode === ConcurrentRoot | BlockingRoot
才会开启。如果是legacy模式,即通过ReactDOM.render(<App/>, dom)
这种方式启动时,这种情况下无论是首次 render 还是后续 update 都只会进入同步工作循环, reconciliation没有机会中断, 所以生命周期函数只会调用一次。所以,虽然在react17中可中断渲染已经实现,但目前为止,还是实验性功能。
react-dom
包, 有 2 个核心职责:
react
应用的启动(通过ReactDOM.render
).HostConfig
协议(源码在 ReactDOMHostConfig.js 中), 能够将react-reconciler
包构造出来的fiber
树表现出来, 生成 dom 节点(浏览器中), 生成字符串(ssr).什么叫做双缓存:在内存中构建并直接替换的技术叫做双缓存。
在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;
React应用的根节点(fiberRootNode)通过改变current
指针在不同Fiber树
的指向来完成current树
和workInProgress树
的替换。
当workInProgress树构建完成交给Renderer渲染在页面上后,应用跟节点(fiberRootNode)指针指向
workInProgress Fiber树
,此时workInProgress 树
变为current Fuber树
每次状态更新都会产生新的
workInProgress Fiber
树,通过current
与workInProgress
的替换,完成DOM
更新。
class App extends React.Component { constructor(props) { super(props); this.state = { number: 1 } } render() { const { number } = this.state; return ( <div onClick={() => {this.setState({number: number+1})}}> classComponent: { number} </div> ) } } ReactDOM.render( <App />, document.getElementById('root') )
ReactDOM.render
会创建fiberRootNode
(源码中叫fiberRoot)和rootFiber
。其中fiberRootNode
是整个应用的根节点,rootFiber
是<App/>
所在组件的根节点。由于我们的应用中,可以调用多次ReactDOM.render
。那么rootFiber
就会有多个,但是fiberRootNode
仅有一个。fiberRootNode永远指向当前页面上渲染的Fiber 树
,即current Fiber树
fiberRootNode.current = rootFiber
render
阶段,根据组件返回的JSX
在内存中依次创建Fiber节点
并连接在一起构建Fiber树
,被称为workInProgress Fiber树
。(下图中右侧为内存中构建的树,左侧为页面显示的树)在构建workInProgress Fiber
树时会尝试复用current Fiber树
中已有的Fiber节点
内的属性,在首屏渲染时只有rootFiber
存在对应的current fiber
(即rootFiber.alternate
)。
workInProgress Fiber树
在 commit阶段
渲染到页面上后。此时DOM
更新为右侧对应的样子。fiberRootNode
的current
指针指向workInProgress Fiber树
使其变为current树对应的代码:
// commitRootImpl函数中 重点关注下这行代码!!! workInProgress树和current树的切换 root.current = finishedWork;
setState
触发更新。这一次会开启一次新的render阶段
并且构建一颗新的workInProgress 树
和mount
时一样,workInProgress fiber
的创建可以复用current Fiber
树对应的节点数据。
这个决定是否复用的过程就是
Diff
算法
workInProgress Fiber 树
在render
阶段完成构建后进入commit
阶段渲染到页面上。渲染完毕后,workInProgress Fiber
树变为current Fiber
树。先看数据结构, 其 type 类型的定义在ReactInternalTypes.js中:
export type Fiber = {| tag: WorkTag, // 标识fiber类型,根据ReactElemnt组件的type进行生成 key: null | string, // 该节点的唯一表示,用于diff算法的优化 elementType: any, // 一般来讲和ReactElement组件的 type 一致 type: any, // 一般来讲和fiber.elementType一致. 一些特殊情形下, 比如在开发环境下为了兼容热更新(HotReloading), 会对function, class, ForwardRef类型的ReactElement做一定的处理, 这种情况会区别于fiber.elementType stateNode: any, // 与fiber关联的局部状态节点(比如: HostComponent类型指向与fiber节点对应的 dom 节点; 根节点fiber.stateNode指向的是FiberRoot; class 类型节点其stateNode指向的是 class 实例). return: Fiber | null, // 该节点的父节点 child: Fiber | null, // 该节点的第一个子节点 sibling: Fiber | null, // 该节点的下一个子节点 index: number, // 该节点的下标 ref: | null | (((handle: mixed) => void) & { _stringRef: ?string, ... }) | RefObject, pendingProps: any, // 从`ReactElement`对象传入的 props. 用于和`fiber.memoizedProps`比较可以得出属性是否变动 memoizedProps: any, // 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中 updateQueue: mixed, // 存储state更新的队列, 当前节点的state改动之后, 都会创建一个update对象添加到这个队列中. memoizedState: any, // 用于输出的state, 最终渲染所使用的state dependencies: Dependencies | null, // 该fiber节点所依赖的(contexts, events)等 mode: TypeOfMode, // 二进制位Bitfield,继承至父节点,影响本fiber节点及其子树中所有节点. 与react应用的运行模式有关(有ConcurrentMode, BlockingMode, NoMode等选项). // Effect 副作用相关 flags: Flags, // 标志位 subtreeFlags: Flags, //替代16.x版本中的 firstEffect, nextEffect. 当设置了 enableNewReconciler=true才会启用 deletions: Array<Fiber> | null, // 存储将要被删除的子节点. 当设置了 enableNewReconciler=true才会启用 nextEffect: Fiber | null, // 单向链表, 指向下一个有副作用的fiber节点 firstEffect: Fiber | null, // 指向副作用链表中的第一个fiber节点 lastEffect: Fiber | null, // 指向副作用链表中的最后一个fiber节点 // 优先级相关 lanes: Lanes, // 本fiber节点的优先级 childLanes: Lanes, // 子节点的优先级 alternate: Fiber | null, // 指向内存中的另一个fiber, 每个被更新过fiber节点在内存中都是成对出现(current和workInProgress) // 性能统计相关(开启enableProfilerTimer后才会统计) // react-dev-tool会根据这些时间统计来评估性能 actualDuration?: number, // 本次更新过程, 本节点以及子树所消耗的总时间 actualStartTime?: number, // 标记本fiber节点开始构建的时间 selfBaseDuration?: number, // 用于最近一次生成本fiber节点所消耗的实现 treeBaseDuration?: number, // 生成子树所消耗的时间的总和 |};
当react程序触发状态更新的时候,我们首先会去创建一个update对象。
状态更新的流程: 触发状态更新 —> 创建Update对象 -> 从fiber到root(
markUpdateLaneFromFiberToRoot
) -> 调度更新(ensureRootIsScheduled
) -> render阶段(performSyncWorkOnRoot
或performConcurrentWorkOnRoot
) -> commit阶段(commitRoot
)
export type Update<State> = {| eventTime: number, // 发起update事件的时间(17.0.1中作为临时字段, 即将移出) lane: Lane, // update所属的优先级 tag: 0 | 1 | 2 | 3, // 共 4 种. UpdateState,ReplaceState,ForceUpdate,CaptureUpdate payload: any, // 载荷, 根据场景可以设置成一个回调函数或者对象(setState中的第一个参数) callback: (() => mixed) | null, // 回调函数(setState中的第二个参数) next: Update<State> | null, // 指向链表中的下一个, 由于UpdateQueue是一个环形链表, 最后一个update.next指向第一个update对象 |}; // =============== UpdateQueue ============== type SharedQueue<State> = {| pending: Update<State> | null, |}; export type UpdateQueue<State> = {| baseState: State, firstBaseUpdate: Update<State> | null, lastBaseUpdate: Update<State> | null, shared: SharedQueue<State>, effects: Array<Update<State>> | null, |};
fiber对象中有一个updateQueue,是一个链式队列,下面通过一张图来描述Fiber,UpdateQueue,Update对象之间的关系
Hook的出现使得function函数具有状态管理的能力,从react@16.8
版本之后,官方开始推荐Hook
语法。官方一共定义了14种Hook类型。
export type Hook = {| memoizedState: any, // 内存状态,用于输出给行程最终的fiber树 baseState: any, // 基础状态,当Hook.queue更新过后,baseState也会更新 baseQueue: Update<any, any> | null, // 基础状态队列, 在reconciler阶段会辅助状态合并. queue: UpdateQueue<any, any> | null, // 指向一个Update队列 next: Hook | null, // 指向该function组件的下一个Hook对象, 使得多个Hook之间也构成了一个链表. |}; // UpdateQueue和Update是为了保证Hook对象能够顺利更新, 与上文fiber.updateQueue中的UpdateQueue和Update是不一样的(且它们在不同的文件) type Update<S, A> = {| lane: Lane, action: A, eagerReducer: ((S, A) => S) | null, eagerState: S | null, next: Update<S, A>, priority?: ReactPriorityLevel, |}; //UpdateQueue和Update是为了保证Hook对象能够顺利更新, 与上文fiber.updateQueue中的UpdateQueue和Update是不一样的(且它们在不同的文件) type UpdateQueue<S, A> = {| pending: Update<S, A> | null, dispatch: (A => mixed) | null, lastRenderedReducer: ((S, A) => S) | null, lastRenderedState: S | null, |};
更多详细的高频对象解请参考图解React
打开浏览器的performance,我们可以看到react框架的调用栈,首次渲染时大体上可以分为三个部分
点击事件,触发setState更新时的调用栈
ReactDOM.render( <App />, document.getElementById('root') )
在之前的Fiber架构工作原理
中,我们提到,在mount
时会创建fiberRootNode
和rootFiber
对象。其实还创建了一个ReactDOMRoot
对象,并且调用其render
方法,开始渲染我们的react程序。
render阶段的主要任务是创建Fiber节点
并且构建Fiber树
。
render阶段开始于performSyncWorkOnRoot
或performConcurrentWorkOnRoot
方法的调用。这取决于本次更新是同步更新还是异步更新(由于我们渲染时调用的是render方法,那么就默认接下来的更新都是同步更新)。
// performSyncWorkOnRoot会调用该方法 function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress); } }
performUnitOfWork的工作可以分为两部分:“递”和“归”。
在构建Fiber树之前,我想用一个’王朝’的故事来描述深度优先遍历。说是当皇帝驾崩之后(当前Fiber节点处理完了),会将王位传给太子(第一个儿子也就是Fiber中的child),如果没有太子,就会传给自己的兄弟(兄弟也就是Fiber节点中的sibling),如果找不到兄弟节点时,又向上找父亲的兄弟,当找到的人又是刚开始那个皇帝时,说明后继无人。这个王朝也就覆灭了(该Fiber树的遍历也就结束了)
首先从rootFiber
开始向下深度优先遍历。为每个遍历到的Fiber节点
调用beginWork方法(后面会详细解释)
,该方法会根据传入的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"));
render阶段
会依次执行
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
/** * @desc beginWork的工作是传入当前Fiber节点,创建子Fiber节点,并根据diff算法给对应的Fiber打上effectTag * @params current 当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate * @params workInProgress 当前组件对应的Fiber节点 * @params renderLanes 优先级相关 */ function beginWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ): Fiber | null { }
Fiber节点
在上一次更新时的Fiber节点
,即fiberRootNode指向的节点Fiber节点
(jsx和state的共同结果)从性能上考虑,React程序在运行时候,不可能每次重新渲染都重新创建Fiber节点,相信大家多少也都听说过diff算法。所以在beginWork中,需要区分一下是首次渲染(mount)还是更新(update),减少不必要的渲染。
之前我们讲过,React中使用双缓存机制,但是在首次渲染的时候,current树是不存在的,可以作为我们判断是否是首次渲染的依据(即 current === null)。
function beginWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes ): Fiber | null { // 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: // ...省略 // ...省略其他类型 } }
从上面的代码中可以看出,当我们通过current===null
来确定是首次渲染时,我们需要根据Fiber中的tag
属性,创建不同的内容(所以,首屏渲染实际上是很耗时的,这也是单页面应用存在的一个问题)。
我们可以看到,满足如下情况时didReceiveUpdate === false
(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber)
oldProps === newProps && workInProgress.type === current.type
,即props
与fiber.type
不变!includesSomeLane(renderLanes, updateLanes)
,即当前Fiber节点优先级不够拿updateFunctionComponent
举例,如果我们经过一系列判断,后发现该Fiber节点
是可以复用的。那么,就不需要花费大量的操作去diff,直接复用现有的Fiber节点
。
function updateFunctionComponent { ... if (current !== null && !didReceiveUpdate) { bailoutHooks(current, workInProgress, renderLanes); return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } // React DevTools reads this flag. }
从该函数名就能看出这是Reconciler
模块的核心部分。
mount
的组件,他会创建子Fiber节点
update
的组件,他会将当前组件于上次更新时对应的Fiber
节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点
(复用)/** * reconcileChildren 调和函数 * 调和函数是 `updateXXX`函数中的一项重要逻辑, 它的作用是向下生成子节点, 并设置`fiber.flags`. * 初次创建时 `fiber`节点没有比较对象, 所以在向下生成子节点的时候没有任何多余的逻辑, 只管创建就行. * 对比更新时 需要把`ReactElement`对象与`旧fiber`对象进行比较, 来判断是否需要复用`旧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, ); } }
对于diff算法,这边不详细展开,想要深入了解的同学可以参考diff算法
我们知道,render阶段
的工作是在内存中进行的,当工作结束后需要通知渲染器Renderer
执行具体的DOM
操作。需要执行DOM
,执行哪种DOM操作呢?就需要根据fiber.flags
。
比如:
// DOM需要插入到页面中 export const Placement = /* */ 0b00000000000010; // DOM需要更新 export const Update = /* */ 0b00000000000100; // DOM需要插入到页面中并更新 export const PlacementAndUpdate = /* */ 0b00000000000110; // DOM需要删除 export const Deletion = /* */ 0b00000000001000;
function markUpdate(workInProgress) { workInProgress.flags |= Update; // 打上Update的标签(react16中的 effectTag), beginWork阶段还是completeWork阶段??? }
updateClassComponent{ ... // 打上Placement标签 workInProgress.flags |= Placement; }
/** * 在上一个节点diff完成之后,对他进行一些收尾工作。 * 1. 将需要更新的属性名放入到Fiber节点的updateQueue属性中 * 2. 生成EffectList(subtreeFlags) */ function completeWork(current, workInProgress, renderLanes) { var newProps = workInProgress.pendingProps; switch (workInProgress.tag) { ... } }
在beginWrok
中我们提到了执行了beginWork
之后会创建子Fiber节点
,该Fiber节点上可能存在effectTag
。
类似beginWork
,completeWork
也是针对不同fiber.tag
调用不同的处理逻辑。
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; } // ...省略
我们重点关注页面渲染所必须的HostComponent
(即原生DOM组件对应的Fiber节点)。
和beginWork一样,我们根据current === null ?判断是mount还是update。
当update时,Fiber节点已经存在对应DOM节点
,所以不需要生成DOM节点
。需要做的主要是处理props
,比如:
onClick
、onChange
等回调函数的注册style prop
DANGEROUSLY_SET_INNER_HTML prop
children prop
在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。
workInProgress.updateQueue = (updatePayload: any);
其中updatePayload
为数组形式,他的偶数索引的值为变化的prop key
,奇数索引的值为变化的prop value
。
可以看到,mount时的主要逻辑包括三个:
Fiber节点
生成对应的DOM节点
DOM节点
插入刚生成的DOM节点
中update
逻辑中的updateHostComponent
类似的处理props
的过程commitRoot
方法是commit阶段
工作的起点。fiberRootNode
会作为传参。
在rootFiber.firstEffect
上保存了一条需要执行副作用的Fiber节点
的单向链表effectList
,这些Fiber节点
的updateQueue
中保存了变化的props
。
这些副作用
对应的DOM操作
在commit
阶段执行。
除此之外,一些生命周期钩子(比如componentDidXXX, useEffect)需要在commit
阶段执行。
commit
阶段的主要工作(即Renderer的工作流程),主要分成三部分:
before mutation阶段的代码很短,整个过程就是遍历effectList并调用commitBeforeMutationEffects函数处理。
/** * 1. 处理DOM节点渲染/删除后的 autoFocus、blur逻辑 * 2. 调用getSnapshotBeforeUpdate生命周期钩子 * 3. 调度useEffect */ function commitBeforeMutationEffects(root, firstChild) { // 1. 处理DOM节点渲染/删除后的 autoFocus、blur逻辑 focusedInstanceHandle = prepareForCommit(root.containerInfo); nextEffect = firstChild; // 调用 getSnapshotBeforeUpdate useEffect 生命周期函数 commitBeforeMutationEffects_begin(); // We no longer need to track the active instance fiber var shouldFire = shouldFireAfterActiveInstanceBlur; shouldFireAfterActiveInstanceBlur = false; focusedInstanceHandle = null; return shouldFire; }
说到getSnapshotBeforeUpdate
这个生命周期函数,我们不得不想起componentWillXXX
钩子前新增的UNSAFE_
前缀。由于render阶段
是可中断/重新开始的,所以这些UNSAFE
的生命周期函数可能会重复执行,但是commit阶段
是同步的,所以不会遇到重复执行的问题。
类似before mutation阶段,mutation阶段也是遍历effectList,执行函数。这里执行的是commitMutationEffects。
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) { // 遍历effectList while (nextEffect !== null) { const effectTag = nextEffect.effectTag; // 根据 ContentReset effectTag重置文字节点 if (effectTag & ContentReset) { commitResetTextContent(nextEffect); } // 更新ref if (effectTag & Ref) { const current = nextEffect.alternate; if (current !== null) { commitDetachRef(current); } } // 根据 effectTag 分别处理 const primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating); switch (primaryEffectTag) { // 插入DOM case Placement: { commitPlacement(nextEffect); nextEffect.effectTag &= ~Placement; break; } // 插入DOM 并 更新DOM case PlacementAndUpdate: { // 插入 commitPlacement(nextEffect); nextEffect.effectTag &= ~Placement; // 更新 const current = nextEffect.alternate; commitWork(current, nextEffect); break; } // SSR case Hydrating: { nextEffect.effectTag &= ~Hydrating; break; } // SSR case HydratingAndUpdate: { nextEffect.effectTag &= ~Hydrating; const current = nextEffect.alternate; commitWork(current, nextEffect); break; } // 更新DOM case Update: { const current = nextEffect.alternate; commitWork(current, nextEffect); break; } // 删除DOM case Deletion: { commitDeletion(root, nextEffect, renderPriorityLevel); break; } } nextEffect = nextEffect.nextEffect; } }
commitMutationEffects
会遍历effectList
,对每个Fiber节点
执行如下三个操作:
ContentReset effectTag
重置文字节点ref
effectTag
分别处理,其中effectTag
包括(Placement
| Update
| Deletion
| Hydrating
)当Fiber节点
含有Placement effectTag
,意味着该Fiber节点
对应的DOM节点
需要插入到页面中。
调用的方法为commitPlacement
我们可以看下,最终调用的原生DOM操作
function appendChildToContainer(container, child) { var parentNode; if (container.nodeType === COMMENT_NODE) { parentNode = container.parentNode; parentNode.insertBefore(child, container); // 熟悉的原生DOM操作 } else { parentNode = container; parentNode.appendChild(child); // 熟悉的原生DOM操作 } }
当Fiber节点
含有Update effectTag
,意味着该Fiber节点
需要更新。调用的方法为commitWork
,他会根据Fiber.tag
分别处理。
当fiber.tag
为FunctionComponent
,会调用commitHookEffectListUnmount
。该方法会遍历effectList
,执行所有useLayoutEffect hook
的销毁函数。
useLayoutEffect(() => { // ...一些副作用逻辑 return () => { // ...这就是销毁函数 } })
当fiber.tag
为HostComponent
,会调用commitUpdate
。
最终会在updateDOMProperties
(opens new window)中将render
阶段 completeWork
(opens new window)中为Fiber节点
赋值的updateQueue
对应的内容渲染在页面上。
for (let i = 0; i < updatePayload.length; i += 2) { const propKey = updatePayload[i]; const propValue = updatePayload[i + 1]; // 处理 style if (propKey === STYLE) { setValueForStyles(domElement, propValue); // 处理 DANGEROUSLY_SET_INNER_HTML } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { setInnerHTML(domElement, propValue); // 处理 children } else if (propKey === CHILDREN) { setTextContent(domElement, propValue); } else { // 处理剩余 props setValueForProperty(domElement, propKey, propValue, isCustomComponentTag); } }
当Fiber节点
含有Deletion effectTag
,意味着该Fiber节点
对应的DOM
节点需要从页面中删除。调用的方法为commitDeletion
。
该方法会执行如下操作:
Fiber节点
及其子孙Fiber节点
中fiber.tag
为ClassComponent
的componentWillUnmount
(opens new window)生命周期钩子,从页面移除Fiber节点
对应DOM
节点该阶段之所以称为layout
,因为该阶段的代码都是在DOM
渲染完成(mutation
阶段完成)后执行的。
与前两个阶段类似,layout
阶段也是遍历effectList
,执行函数。
具体执行的函数是commitLayoutEffects
。commitLayoutEffects
主要做两件事
commitLayoutEffectOnFiber
(调用生命周期钩子和hook
相关操作)root.current = finishedWork; nextEffect = firstEffect; do { try { commitLayoutEffects(root, lanes); } catch (error) { invariant(nextEffect !== null, "Should be working on an effect."); captureCommitPhaseError(nextEffect, error); nextEffect = nextEffect.nextEffect; } } while (nextEffect !== null); nextEffect = null;
commitLayoutEffectOnFiber
方法会根据fiber.tag
对不同类型的节点分别处理。
对于classComponent
,他会根据是mount
还是update
调用componentDidMount
或componentDidUpdate
触发状态更新的this.setState如果赋值了第二个参数回调函数,也会在此时调用。
对于FunctionComponent
及相关类型,他会调用useLayoutEffect hook
的回调函数,调度useEffect
的销毁与回调函数
对于HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。
ReactDOM.render(<App />, document.querySelector("#root"), function() { console.log("i am mount~"); });
commitLayoutEffects
会做的第二件事是commitAttachRef
。
代码逻辑很简单:获取DOM实例,更新ref。
之前我们提过React中的双缓存技术,workInProgress Fiber
树在commit
阶段完成渲染后会变为current Fiber
树。这行代码的作用就是切换fiberRootNode
指向的current Fiber
树。
仅供参考
// 大概是这样的,由于使用了数组下标作为key // react的在diff时当判断componenet.type 和 key相同,那么就会复用之前的节点,顶多是个Update,那么这样一来,componentDidMount/useState中的初始值都无效了 list.map((item, key) => ( <Component key={key} dataSource={item}/> ))
React技术揭秘
图解React原理
react17源码解析
react17源码训练营