前言
最近公司用的技术主要是React,本着知其然知其所以然的原则,学习了下react的内部原理,并将心得整理出来,与大家一同学习和成长。
目标
本篇的目标是梳理一下,react从代码中的 jsx => 屏幕渲染 这一过程都经历了什么,也就是 react的挂载过程。
正题
废话少说,我们先从一个最简单的栗子 hello-react 开始,剔除掉不影响主流程的代码,用最简洁的语言,直入主题。
请系好安全带,准备发车!
hello-react!
tsx
import ReactDOM from 'react-dom/client';
const App = (
<div>hello,React!</div>
)
ReactDOM.createRoot(document.getElementById('root')!).render(
App
);
先准备一排大家都熟悉的代码。简单的不能在简单了哈。
很明显, 第一站是 ReactDOM.createRoot
初始化
createRoot
tsx
export function createRoot(container: Container) {
const root = createContainer(container);
return {
render(element: ReactElementType) {
return updateContainer(element, root);
}
};
}
该方法 传入的是#root, 里面主要调用了createContainer这个方法,并返回了一个render方法。
createContainer
tsx
export function createContainer(container: Container) {
const hostRootFiber = new FiberNode(HostRoot, {}, null);
const root = new FiberRootNode(container, hostRootFiber);
hostRootFiber.updateQueue = createUpdateQueue();
return root;
}
该方法传入 #root ,先后创建了hostRootFiber 、fiberRootNode(代码中的root),并创建了一个空的更新队列(updateQueue)。
我们来逐个讲解
hostRootFiber:
在内存中创建了一个tag为3(HostRoot) fiber空对象, 接下来我讲列举部分属性(注意,这只是部分,源码实际更多)。有点小多,我们暂时只需要扫一眼,有个大概眼缘就行。
js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
) {
// 节点类型FunctionComponent | HostRoot | HostComponent | HostText | Fragment;
this.tag = tag;
this.type = null;
// 指向对应的DOM节点
this.stateNode = null;
// 用于连接其他Fiber节点形成Fiber树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
// 本次渲染待处理的 props
this.pendingProps = pendingProps;
// 上一次渲染的 props
this.memoizedProps = null;
// 上一次渲染的状态
this.memoizedState = null;
// 更新队列
this.updateQueue = null;
// 标记当前节点的副作用
this.flags = NoLanes;
// 标记所有子节点的副作用
this.subtreeFlags = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
fiberRootNode:
在内存中创建了一个新对象,并和#root,以及 hostRootFiber 通过如下方法 建立关联关系
tsx
fiberRootNode.current = hostRootFiber
hostRootFiber.stateNode = fiberRootNode
fiberRootNode.container = #root
fiberRootNode大概是这样,同样,只需要先扫一眼,留个印象即可
tsx
class FiberRootNode {
container: #root;
// 前面创建的hostRootFiber
current: hostRootFiber;
// 已经完成wip树
finishedWork: FiberNode | null;
// 待处理任务优先级集合
pendingLanes: Lanes;
// 已提交任务的优先级
finishedLane: Lane;
}
createUpdateQueue
创建一个更新队列
css
hostRootFiber = {
updataQuene:{
shared:{
pending:null
}
}
}
我们画个图,来帮我们直观的看下,上面三行代码实际上干了个啥
createContainer 方法执行完毕后,返回我们刚刚创建的 fiberRootNode,
回到createRoot方法里,此时进入render方法,啊,抱歉,render在本篇中只是在摸鱼,实际上是进入 updateContainer 方法
updateContainer
tsx
export function updateContainer(
element: ReactElementType | null,
root: FiberRootNode
) {
const hostRootFiber = root.current;
const lane = requestUpdateLane();
const update = createUpdate<ReactElementType | null>(element, lane);
enqueueUpdate(
hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>,
update
);
scheduleUpdateOnFiber(hostRootFiber, lane);
return element;
}
传入的element 就是我们实际业务中App组件,在我们的栗子中对应就是 hello,React! 的Element对象 ---------element对象就是描述dom 层级结构属性信息的js对象
代码量有点多了是不是,莫慌,我们来逐行解释下
一、根据上面的内存图,我们可以轻松看到 hostRootFiber 就是我们内存图中的 hostRootFiber。
二、requestUpdateLane 就是定义了一个lane 的优先级 这个值为1,暂时先这样理解,我们在以后的文章中在详细讲解,此处影响不大。
三、createUpdate, 实际上就是创建了一个更新对象,样子是这样的
ini
update = {
action:App的element对象,
lane:1,
next:null
}
四、enqueueUpdate 这个方法需要重点看下,它涉及到了 react的更新机制和环状链表机制,但是,在当前栗子中,并不是特别重要,我们简单看下就行。在之后文章中,在仔细的盘下这个方法(先把帽子丢到篱笆外面去...)
tsx
export const enqueueUpdate = <State>(
updateQueue: UpdateQueue<State>,
update: Update<State>
) => {
const pending = updateQueue.shared.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
updateQueue.shared.pending = update;
};
顺着代码,脑子转起来,我们的pending肯定是空的,对不,所以 update.next = update(next指向了自己),然后updateQueue.shared.pending = update
先不要管next为啥指向自己,结论就是形成了下面的样子
tsx
hostRootFiber = {
updataQuene:{
shared:{
pending:{
action:App,
lane:1,
next: {
action:App,
lane:1,
next:...(无限套娃)
}
}
}
}
}
在来画个图,更清晰一点
看看上面的图,其实很简答的对不,就是建立了两个节点,挂了些属性,还有通过current和 stateNode互相扯皮
我们进入下一站,scheduleUpdateOnFiber方法
scheduleUpdateOnFiber
tsx
export function scheduleUpdateOnFiber(fiber: FiberNode, lane: Lane) {
const root = markUpdateFromFiberToRoot(fiber);
markRootUpdated(root, lane);
ensureRootIsScheduled(root);
}
第一行 : markUpdateFromFiberToRoot,其实就是通过不断的向上递归,直到找到根元素,其实就是我们的root,代码大概是这样的
tsx
let node = fiber;
let parent = node.return;
while (parent !== null) {
node = parent;
parent = node.return;
}
if (node.tag === HostRoot) {
return node.stateNode;
}
return null;
我们这里直接传入了root,tag就是HostRoot(3) ,所以进来逛下街,就返回了 root.stateNode。
这里我们需要留意一下,React更新永远是从根节点开始的,就是这个缘故。
第二行:markRootUpdated(root, lane) 实际就是 给root节点上的pendingLanes属性赋值为1(ctrl + f 去找找这个属性吧)
第三行:ensureRootIsScheduled,记好安全带,准备下一站
ensureRootIsScheduled
tsx
function ensureRootIsScheduled(root: FiberRootNode) {
const updateLane = getHighestPriorityLane(root.pendingLanes);
if (updateLane === NoLane) {
return;
}
if (updateLane === SyncLane) {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root, updateLane));
scheduleMicroTask(flushSyncCallbacks);
}
}
第一行:getHighestPriorityLane 内部通过位运算得到我们的最高优先级,也就是我们前面得到的优先级 1 即updateLane = 1
第二行:NoLane 实际上是0 ,1 能等于0吗,So
第三行:这才是我们开车的方向 SyncLane是 1,我们进入 里面的方法。又是长长的两大坨方法...
功夫在高也怕菜刀,代码在长,也怕逐行分解
先看括号里面的方法 performSyncWorkOnRoot.bind。bind我们都知道,是返回了一个方法,这个方法不执行,因此我们这里先跳过,回到scheduleSyncCallback 方法里。
scheduleSyncCallback
tsx
let syncQueue: ((...args: any) => void)[] | null = null;
export function scheduleSyncCallback(callback: (...args: any) => void) {
if (syncQueue === null) {
syncQueue = [callback];
} else {
syncQueue.push(callback);
}
}
这个函数简直是送分题,外面一个全局数组syncQueue(初始为空),然后不断收集回调函数。
名字很长,实则不堪一击,哼哼,下一个
flushSyncCallbacks
tsx
let isFlushingSyncQueue = false;
export function flushSyncCallbacks() {
if (!isFlushingSyncQueue && syncQueue) {
isFlushingSyncQueue = true;
try {
syncQueue.forEach((callback) => callback());
} catch (e) {
console.error('flushSyncCallbacks报错', e);
} finally {
isFlushingSyncQueue = false;
syncQueue = null;
}
}
}
依然很简单对不,现实全局变量isFlushingSyncQueue 为false, syncQueue 已经在上面的函数里赋值了,进入if判断。将isFlushingSyncQueue 之为true后,开始循环调用回调方法。执行完后,isFlushingSyncQueue 再次改为 false,同步任务队列syncQueue 置空。
依然不够打,在来
scheduleMicroTask
这个方法是react 选用微任务的方式
tsx
function scheduleMicroTask(callback: () => void) {
// 优先使用现代浏览器 API
if (typeof queueMicrotask === 'function') {
queueMicrotask(callback);
}
// 降级方案:Promise/MutationObserver
else if (typeof Promise !== 'undefined') {
Promise.resolve().then(callback);
}
// 极端降级:setTimeout
else {
setTimeout(callback, 0);
}
}
有没有想起Vue 中的nextTick?
Vue(2.x)
tsx
function nextTick(callback: () => void) {
if (typeof Promise !== 'undefined') {
Promise.resolve().then(callback);
}
else if (typeof MutationObserver !== 'undefined') {
let counter = 1;
const observer = new MutationObserver(callback);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, { characterData: true });
microTimerFunc = (fn) => {
counter = (counter + 1) % 2;
textNode.data = String(counter); // 触发 MutationObserver 回调
};
}
// 极端降级:setTimeout
else {
setTimeout(callback, 0);
}
}
是不是又想起了 手撕 Promise的微任务实现?咳咳,题跑太远了,回到主题
熟悉事件循环的老司机都知道,微任务队列要先等当前宏任务执行完成后在开始执行。因此,需要耐心等待当前渲染任务执行完毕
...
三年后,魔童降世
...
flushSyncCallbacks 开始运行,循环遍历每个回调函数,我们现在再来看塞到队列的这个函数 performSyncWorkOnRoot.bind(null, root, updateLane)。
初始化Wip
performSyncWorkOnRoot
tsx
function performSyncWorkOnRoot(root: FiberRootNode, lane: Lane) {
const nextLane = getHighestPriorityLane(root.pendingLanes);
if (nextLane !== SyncLane) {
ensureRootIsScheduled(root);
return;
}
prepareFreshStack(root, lane);
do {
try {
workLoop();
break;
} catch (e) {
workInProgress = null;
}
} while (true);
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLane = lane;
wipRootRenderLane = NoLane;
commitRoot(root);
}
注意啦,这是react内部一个非常重要的函数,一个函数里面包含了 创建 wip、递归、commit。react挂载所有的方法都在这个函数里触发。
我们依然逐行来分析
第一行 getHighestPriorityLane 依然是获取root上的最高优先级任务(这个函数我们前面提到过哦),得到nextLane = 1
第二行 SyncLane 已经老熟人了,这家伙是1,所以聪明的老司机已经知道前面应该直行还是绕行了
第三行 prepareFreshStack 创建wip!我们来看 prepareFreshStack
prepareFreshStack
tsx
let workInProgress: FiberNode | null = null;
let wipRootRenderLane: Lane = NoLane;
function prepareFreshStack(root: FiberRootNode, lane: Lane) {
workInProgress = createWorkInProgress(root.current, {});
wipRootRenderLane = lane;
}
貌似很短?就一个createWorkInProgress方法,但很重要,这里定义了一个全局指针workInProgress,将这个指针指向了createWorkInProgress方法的返回值
createWorkInProgress
tsx
export const createWorkInProgress = (
current: FiberNode,
pendingProps: Props
): FiberNode => {
let wip = current.alternate;
if (wip === null) {
wip = new FiberNode(current.tag, pendingProps, current.key);
wip.stateNode = current.stateNode;
wip.alternate = current;
current.alternate = wip;
} else {
wip.pendingProps = pendingProps;
wip.flags = NoFlags;
wip.subtreeFlags = NoFlags;
wip.deletions = null;
}
wip.type = current.type;
wip.updateQueue = current.updateQueue;
wip.child = current.child;
wip.memoizedProps = current.memoizedProps;
wip.memoizedState = current.memoizedState;
return wip;
};
在接下来的诸多方法中,请诸位务必清楚每个函数的传参是什么东东,这样才能抓紧坐稳,跟上老司机的步伐
...
createWorkInProgress 两个参数,我们通过鼠标滚轮往回滚啊滚...发现这个 current 就是 root.current。而root就是我们最开始创建的 fiberRootNode(忘记的童鞋请看最近的内存图)。pengingProps是 空对象。
第一行: 通过内存图可知,wip = null ,因此我们选择 进入1号房间,开始创建逻辑
第二行-结束: 开始创建新的 FiberNode,我们完全可以理解为把 hostRootFiber完完全全的复制了一份。然后添加了一些指针将其关联起来,请看内存图,这里用图说明更清晰些。
补充内存图
接下来,请往回滑倒你的鼠标滚轮,一直到 performSyncWorkOnRoot 这个方法里,我们已经知道了,在进入这个do循环前,我们复制了一份hostRootFiber,同时有一个全局指针 workInProgress指向了 这个hostRootFiber(wip)
补充内存图
tsx
do {
try {
workLoop();
break;
} catch (e) {
workInProgress = null;
}
} while (true);
这里的do while 看似死循环,实际就一行代码workLoop() 。do while(true) 加 try catch 的作用就是保证 workLoop能完整执行一次。执行完后就退出
workLoop
tsx
function workLoop() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
这里是真循环了啊,只要workInProgress 有值,就会一直进入performUnitOfWork方法
performUnitOfWork
tsx
function performUnitOfWork(fiber: FiberNode) {
const next = beginWork(fiber, wipRootRenderLane);
fiber.memoizedProps = fiber.pendingProps;
if (next === null) {
completeUnitOfWork(fiber);
} else {
workInProgress = next;
}
}
首先明确传参,fiber 是全局指针workInprogress 指向的值,也就是我们前面刚刚创建的wip
递
beginWork
tsx
export const beginWork = (wip: FiberNode, renderLane: Lane) => {
switch (wip.tag) {
case HostRoot:
return updateHostRoot(wip, renderLane);
case HostComponent:
return updateHostComponent(wip);
case HostText:
return null;
case FunctionComponent:
return updateFunctionComponent(wip, renderLane);
case Fragment:
return updateFragment(wip);
default:
break;
}
return null;
};
参数: wip 是 workInprogress renderLane 是前面的全局变量 1
这个方法太简单了,就是根据不同的tag类型,进入不同方法。我们wip type类型是 HostRoot(3),前面有提到过哦
updateHostRoot
tsx
function updateHostRoot(wip: FiberNode, renderLane: Lane) {
const baseState = wip.memoizedState;
const updateQueue = wip.updateQueue as UpdateQueue<Element>;
const pending = updateQueue.shared.pending;
updateQueue.shared.pending = null;
const { memoizedState } = processUpdateQueue(baseState, pending, renderLane);
wip.memoizedState = memoizedState;
const nextChildren = wip.memoizedState;
reconcileChildren(wip, nextChildren);
return wip.child;
}
参数说明: 略...
memoizedState 为{}
updateQueue 是有值的哦,它是 shared.pending.action:App的element对象。 没印象的童鞋,滚轮转起来。
读取到pengding 后,就把 wip上原来的 updateQueue 清掉了
进入processUpdateQueue(baseState, pending, renderLane)
processUpdateQueue
tsx
export const processUpdateQueue = <State>(
baseState: State,
pendingUpdate: Update<State> | null,
renderLane: Lane
): { memoizedState: State } => {
const result: ReturnType<typeof processUpdateQueue<State>> = {
memoizedState: baseState
};
if (pendingUpdate !== null) {
const first = pendingUpdate.next;
let pending = pendingUpdate.next as Update<any>;
do {
const updateLane = pending.lane;
if (updateLane === renderLane) {
const action = pending.action;
if (action instanceof Function) {
baseState = action(baseState);
} else {
baseState = action;
}
}
pending = pending.next as Update<any>;
} while (pending !== first);
}
result.memoizedState = baseState;
return result;
};
参数:baseState: {} pendingUpdate就是 action:App,renderLane:1
还记得前面说过的react 环状链表机制吗
第一行 : 创建一个新对象,memoizedState: {}
第二行:pendingUpdate 是有值的,它的next在我们这个demo中就是自己哦,因此pengding 实际上等于 next。do while循环,先执行一次代码。
updateLane 为1 符合if条件,请进。 得到action ,action就是我们的App的element对象,baseState赋值。 pending 赋值为自己的next,实际上还是自己 。 pengding 实际上等于 next,因此退出循环,给result赋值,并返回result。
车速有点快,没跟上的在读一遍。
我们在返回到updateHostRoot ,看接下来的代码,剩下的未执行代码粘过来了,够贴心吧
tsx
function updateHostRoot(wip: FiberNode, renderLane: Lane) {
...
const { memoizedState } = processUpdateQueue(baseState, pending, renderLane);
wip.memoizedState = memoizedState;
const nextChildren = wip.memoizedState;
reconcileChildren(wip, nextChildren);
return wip.child;
}
memoizedState 就是我们的App element对象
tsx
// App element对象
{
$$typeof: Symbol(react.element)
key: null
props: {children: 'hello,React!'}
ref: null
type: "div"
}
进入下一个函数reconcileChildren
reconcileChildren
tsx
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
const current = wip.alternate;
if (current !== null) {
wip.child = reconcileChildFibers(wip, current?.child, children);
} else {
wip.child = mountChildFibers(wip, null, children);
}
}
参数:wip:workInprogress,children: 我们上面创建的App element对象
第一行: current 是有值的哦,它是hostRootFiber,不懂的去看下内存图
我们进入第一个路口 wip.child = reconcileChildFibers()
reconcileChildFibers
tsx
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
function ChildReconciler(shouldTrackEffects) {
return function reconcileChildFibers( returnFiber: FiberNode,currentFiber: FiberNode | null, newChild?: any){
return placeSingleChild(
reconcileSingleElement(returnFiber, currentFiber, newChild)
);
}
}
参数:returnFiber: workInprogress, currentFiber:null,newChild:我们上面创建的App element对象
闭包 + 工厂函数...
我们先确认此时的shouldTrackEffects 是true,然后从最里面的函数看起 reconcileSingleElement
在mount阶段,reconcileSingleElement 就是基于newChild 去创建新的fiber对象,同时 newChild.return = returnFiber。
placeSingleChild
tsx
function placeSingleChild(fiber: FiberNode) {
if (shouldTrackEffects && fiber.alternate === null) {
fiber.flags |= Placement;
}
return fiber;
}
参数: fiber对象是我们前面刚刚创建的新fiber
shouldTrackEffects 此时是true,并且没有alternate 属性,因此我们给当前fiber打上Placement标记。
更新内存图
经过 多层 return ,啊,头晕,我们回到reconcileChildren里。得到 wip.child = newFiber。
至此updateHostRoot 函数执行完成,我们还需要在回返。滚轮动起来...
回到performUnitOfWork 这个函数
tsx
const next = beginWork(fiber, wipRootRenderLane);
fiber.memoizedProps = fiber.pendingProps;

if (next === null) {
completeUnitOfWork(fiber);
} else {
workInProgress = next;
}
beginwork的返回值就是我们刚创建的 fiber对象,并赋值给next
注意此时fiber是workInprogress 。 memoizedProps,pendingProps这两个属性分别是 上一次更新的属性,本地渲染的属性
next 不为null. 我们更改workInProgress 的指针,继续递的操作轮回
...
接下来,因为大部分是重复的操作,我们只讲不同的地方,相同的地方一句话带过。以图为主,文字为辅
指针下移到Div,开始beginWork(Div)
在beginWork中,我们tag类型是HostComponent。进入updateHostComponent
updateHostComponent
tsx
function updateHostComponent(wip: FiberNode) {
const nextProps = wip.pendingProps;
const nextChildren = nextProps.children;
reconcileChildren(wip, nextChildren);
return wip.child;
}
其实逻辑是一样的对不,最终还是进入到reconcileChildren,并返回新的child
啰嗦一嘴
nextProps:{children:'hello,React!'}
nextChildren:'hello,React!'
再次回到reconcileChildren方法中,这次我们wip没有 alternate了,因此进入mountChildFibers 方法里,这个方法的区别就是 shouldTrackEffects = false。
依然创建新fiber,然后在placeSingleChild 中,因为 shouldTrackEffects 和 没有 alternate指针。fiber的flags不变,默认是 0
指针继续下移到 hello,react
再次开始 beginWork(hello,react)
这次我们的wip.tag是 HostText ,直接返回null
javascript
export const beginWork = (wip: FiberNode, renderLane: Lane) => {
switch (wip.tag) {
case HostText:
return null;
}
return null;
};
小节
总结下递这个阶段都干了什么
1、workInprogress 指向某个fiber节点
2、开始创建这个节点的子节点,并和这个节点通过 child 和 return 建立关联关系
3、给新的子节点打上标记
4、返回新的子节点,继续递
归
终于要回家了吗,车太颠了
...
tsx
function performUnitOfWork(fiber: FiberNode) {
const next = beginWork(fiber, wipRootRenderLane);
fiber.memoizedProps = fiber.pendingProps;
if (next === null) {
completeUnitOfWork(fiber);
} else {
workInProgress = next;
}
}
当递到文本节点后, next 就变成null了,我们开始归的阶段,进入 completeUnitOfWork
completeUnitOfWork
tsx
function completeUnitOfWork(fiber: FiberNode) {
let node: FiberNode | null = fiber;
do {
completeWork(node);
const sibling = node.sibling;
if (sibling !== null) {
workInProgress = sibling;
return;
}
node = node.return;
workInProgress = node;
} while (node !== null);
}
我们先整体梳理这个函数
这是一个do while循环。先去执行 completeWork后,开始判断兄弟节点,我们的栗子里是没有兄弟节点的,但是我们可以发现,如果有兄弟节点的话,会将 workInProgress 指向 兄弟节点,然后退出循环,退出循环干什么去! 还记得我们的workLoop吗,只要workInProgress 不为空,就会开始递归过程。
铺垫了这么多的字,只是想告诉大家,递归 这个过程是交互进行的,先递 到最左侧最底层(深度优先遍历),然后开始归 ,如果存在兄弟 节点,则又开始递的操作。如果没有兄弟节点,node就成了自己的父节点,继续completeWork,直到回到根节点(wip)为止。
好了我们来盘一盘completeWork 做了什么吧
completeWork
tsx
export const completeWork = (wip: FiberNode) => {
const newProps = wip.pendingProps;
switch (wip.tag) {
case HostComponent:
const instance = createInstance(wip.type, newProps)
appendAllChildren(instance, wip);
wip.stateNode = instance;
bubbleProperties(wip);
return null;
case HostText:
const instance = createTextInstance(newProps.content);
wip.stateNode = instance;
bubbleProperties(wip);
return null;
case HostRoot:
case FunctionComponent:
case Fragment:
bubbleProperties(wip);
return null;
default:
console.warn('未处理的completeWork情况', wip);
break;
}
};
参数: wip依然是我们的workInprogress
这个函数和beginwork简直太像了,根据不同的tag类型做不同的事。
当前tag类型是 HostText。 createTextInstance,顾名思义,创建文本节点DOM,通过stateNode 关联到wip上,然后进入bubbleProperties方法
bubbleProperties
tsx
function bubbleProperties(wip: FiberNode) {
let subtreeFlags = NoFlags;
let child = wip.child;
while (child !== null) {
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
child.return = wip;
child = child.sibling;
}
wip.subtreeFlags |= subtreeFlags;
}
这个函数代码看起来很简单,实际也很简单,哈哈哈
快速的过一下: 定义了初始属性subtreeFlags ,然后通过位运算(可以通俗的理解为收集)子元素的subtreeFlags 和 flags。收集完后,赋值给父节点上。
补一下内存图
subtreeFlags没啥变化对不,因为我们当前在最底层,没有子元素的flags让我们来收集不是
ok。我们在回到completeUnitOfWork 方法中,我们没有兄弟节点了。return到 Div里继续开始completeWork,此时的workInprogress指向的是Div
ini
case HostComponent:
const instance = createInstance(wip.type, newProps)
appendAllChildren(instance, wip);
wip.stateNode = instance;
bubbleProperties(wip);
return null;
这次多了一个appendAllChildren(instance, wip)
appendAllChildren
tsx
function appendAllChildren(parent: Container | Instance, wip: FiberNode) {
let node = wip.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node?.stateNode);
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === wip) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === wip) {
return;
}
node = node?.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
参数 : 来源于completeWork 的instance, 而instance 是刚刚创建的DOM节点。 wip 是workInprogress指针实际上也就是 刚创建的DOM节点对应的fiber对象。从参数来看,自己插自己?
虚惊一场... 进入代码后发现,取值来源于 wip.child.stateNode。这就对了么
我们来分析下这个函数都做了什么
首先是拿到 wip.child赋值给 node 。去看内存图得到 node 就是 我们的 'hello,react'
进入循环后,就被第一行俘虏了。appendInitialChild 就是 parent.append(node?.stateNode),很合理!
node 肯定不等于 wip . node的爹才等于 wip呢,对不对
node.sibling 是空的,然后再次被下面的条件俘虏,退出
进入bubbleProperties 。这个方法前面已经讲过了,不懂的童鞋鼠标滚轮滚起来...
我们接下来直接用内存图完成剩下的归操作
当wrokInprogress 指向 wip,执行完 completeWork后,我们的workInprogress已经指到了null。
还记得我们的workLoop吗,因为workInprogress为空,它终于完成了工作,可以下班休息了,这钱太难赚了...
回到我们最开始的方法 performSyncWorkOnRoot
tsx
function performSyncWorkOnRoot(root: FiberRootNode, lane: Lane) {
...workLoop执行完毕,下班回家了...
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLane = lane;
wipRootRenderLane = NoLane;
commitRoot(root);
}
需要重温下这个两个参数,root 就是内存图里的FiberRootNode,lane :1。
finishedWork 就是我们刚才创建的fiber树,将其赋值给了root
root.finishedLane = 1
wipRootRenderLane = 0
终于进入到我们今天最后一站commitRoot,醒醒了,马上到家了。我们在补充下这个阶段的内存图
小结
总结下归这个阶段都做了什么
1、创建workInprogress 节点对应的 dom节点(内存中)
2、收集子元素上的副作用,同时将子元素插入到dom节点中
3、结束后发现有兄弟节点,立马回到递的怀抱
Commit
commitRoot
tsx
function commitRoot(root: FiberRootNode) {
const finishedWork = root.finishedWork;
if (finishedWork === null) {
return;
}
const lane = root.finishedLane;
root.finishedWork = null;
root.finishedLane = NoLane;
markRootFinished(root, lane);
const subtreeHasEffect =
(finishedWork.subtreeFlags & MutationMask) !== NoFlags;
const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
if (subtreeHasEffect || rootHasEffect) {
commitMutationEffects(finishedWork, root);
root.current = finishedWork;
} else {
root.current = finishedWork;
}
rootDoesHasPassiveEffects = false;
ensureRootIsScheduled(root);
}
参数: root 内存图中的fiberRootNode
代码量有点大,加油胜利就在眼前,我们一行行解读
第一行: finishedWork 就是我们前面创建的fiber树,内存图中右侧那一坨
第二行:妖怪,滚一边去
第三到第五行: 获取finishedLane(值为1),同时将root上的finishedWork 和finishedLane都置空
markRootFinished,就是将root.pendingLanes也置空(内部进行位运算)
subtreeHasEffect,rootHasEffect 这两行还是位运算,大概意思就是看 subtreeFlags 和 flags 是否有值,从内存图中可以看到我们的 subtreeFlags 是 1,flag 是 0。 flag 不为0 表示自己不用动,subtreeFlags 不为0 标识自己的子组件有变化。
因此,我们进入第一个路口,commitMutationEffects
commitMutationEffects
tsx
export const commitMutationEffects = (
finishedWork: FiberNode,
root: FiberRootNode
) => {
nextEffect = finishedWork;
while (nextEffect !== null) {
const child: FiberNode | null = nextEffect.child;
if (
(nextEffect.subtreeFlags & (MutationMask | PassiveMask)) !== NoFlags &&
child !== null
) {
nextEffect = child;
} else {
up: while (nextEffect !== null) {
commitMutaitonEffectsOnFiber(nextEffect, root);
const sibling: FiberNode | null = nextEffect.sibling;
if (sibling !== null) {
nextEffect = sibling;
break up;
}
nextEffect = nextEffect.return;
}
}
}
};
吐了,双层循环
第一层循环:就是一直找到子元素没有变动的节点。也就是我下面没有任何变化,有事冲我来。好,有种,请你(div节点)进入第二层循环。
commitMutaitonEffectsOnFiber
ts
const commitMutaitonEffectsOnFiber = (
finishedWork: FiberNode,
root: FiberRootNode
) => {
const flags = finishedWork.flags;
if ((flags & Placement) !== NoFlags) {
commitPlacement(finishedWork);
finishedWork.flags &= ~Placement;
}
if ((flags & Update) !== NoFlags) {
commitUpdate(finishedWork);
finishedWork.flags &= ~Update;
}
if ((flags & ChildDeletion) !== NoFlags) {
const deletions = finishedWork.deletions;
if (deletions !== null) {
deletions.forEach((childToDelete) => {
commitDeletion(childToDelete, root);
});
}
finishedWork.flags &= ~ChildDeletion;
}
if ((flags & PassiveEffect) !== NoFlags) {
commitPassiveEffect(finishedWork, root, 'update');
finishedWork.flags &= ~PassiveEffect;
}
};
参数:finishedWork 这里是 Div节点,root 内存图中的fiberRootNode
函数内部很简单,还是位运算。简单来说就是如果flag 有新增,就进入新增的方法,完成后,就把新增的标记删除,如果flag 有更新的标记,就进入编辑的方法,然后就把更新的标记删除... 巴拉巴拉
我们当前节点的标记是1 也就是 Placement
进入commitPlacement(finishedWork)
commitPlacement
tsx
const commitPlacement = (finishedWork: FiberNode) => {
const hostParent = getHostParent(finishedWork);
const sibling = getHostSibling(finishedWork);
if (hostParent !== null) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, hostParent, sibling);
}
};
参数: finishedWork 是Div节点
先看参数名字,也能知道接下来的操作了不是
getHostParent找到父节点
getHostSibling找到兄弟节点
insertOrAppendPlacementNodeIntoContainer 把这些都插入进去。
getHostParent
tsx
function getHostParent(fiber: FiberNode): Container | null {
let parent = fiber.return;
while (parent) {
if (parentTag === HostComponent) {
return parent.stateNode as Container;
}
if (parentTag === HostRoot) {
return (parent.stateNode as FiberRootNode).container;
}
parent = parent.return;
}
return null;
}
代码太简单了,不解读了
getHostSibling
tsx
function getHostSibling(fiber: FiberNode) {
let node: FiberNode = fiber;
findSibling: while (true) {
while (node.sibling === null) {
const parent = node.return;
if (
parent === null ||
parent.tag === HostComponent ||
parent.tag === HostRoot
) {
return null;
}
node = parent;
}
node.sibling.return = node.return;
node = node.sibling;
while (node.tag !== HostText && node.tag !== HostComponent) {
// 向下遍历
if ((node.flags & Placement) !== NoFlags) {
continue findSibling;
}
if (node.child === null) {
continue findSibling;
} else {
node.child.return = node;
node = node.child;
}
}
if ((node.flags & Placement) === NoFlags) {
return node.stateNode;
}
}
}
代码略微复杂,实际上在我们这个demo中在 第一层的判断中就return null了。因为我们这里不涉及兄弟节点。
insertOrAppendPlacementNodeIntoContainer
tsx
function insertOrAppendPlacementNodeIntoContainer(
finishedWork: FiberNode,
hostParent: Container,
before?: Instance
) {
// fiber host
if (finishedWork.tag === HostComponent || finishedWork.tag === HostText) {
if (before) {
insertChildToContainer(finishedWork.stateNode, hostParent, before);
} else {
appendChildToContainer(hostParent, finishedWork.stateNode);
}
return;
}
const child = finishedWork.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, hostParent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, hostParent);
sibling = sibling.sibling;
}
}
}
参数 ;finishedWork 是div节点 hostParent 是#root,before是空
这个方法名字很长,内容也不少,但我们只进入appendChildToContainer 之后就退出了,这个方法其实就是 #root.apendChild(div)
至此,我们页面终于出现了hello,react!
虽然从效果来看,我们的功能已经实现,但我们的代码还没跑完,那位童鞋先别起来,车还没停,让我们继续!
我们回到commitMutaitonEffectsOnFiber 这个函数栈中,但接下来没有其他操作了。所以在回到 commitMutationEffects的循环中。
tsx
up: while (nextEffect !== null) {
commitMutaitonEffectsOnFiber(nextEffect, root);
const sibling: FiberNode | null = nextEffect.sibling;
if (sibling !== null) {
nextEffect = sibling;
break up;
}
nextEffect = nextEffect.return;
}
没有sibling 所以,nextEffect 变成了 Wip对不对,说不对的往上翻看内存图
于是再次进入 commitMutaitonEffectsOnFiber 方法,根据内存图可知我们wip节点flag是 空的,所以,就是在commitMutaitonEffectsOnFiber 方法里逛了个寂寞。再次出来后,nextEffect 就是null了,退出循环,回到 commitRoot的调用栈中
tsx
function commitRoot(root: FiberRootNode) {
...
if (subtreeHasEffect || rootHasEffect) {
// 我们刚刚从这里出来
commitMutationEffects(finishedWork, root);
root.current = finishedWork;
} else {
root.current = finishedWork;
}
rootDoesHasPassiveEffects = false;
ensureRootIsScheduled(root);
}
root的current指向了finishedWork。 这就是react的双缓存机制,current指向已经渲染的树,正在渲染的树放在内存构建中。构建好后在将指针指回来
来个完结的内存示意图
rootDoesHasPassiveEffects 置为false,还要进入ensureRootIsScheduled这个方法,这个方法我们在前面讲过了
ts
function ensureRootIsScheduled(root: FiberRootNode) {
const updateLane = getHighestPriorityLane(root.pendingLanes);
if (updateLane === NoLane) {
return;
}
if (updateLane === SyncLane) {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root, updateLane));
scheduleMicroTask(flushSyncCallbacks);
}
}
updateLane 是 0 了哦,看上面的内存图,所以,这个方法进去后就return出来了。
没有一行代码是白写的,没有一碗米饭是白干的...
至此,我们的微任务执行完成(flushSyncCallbacks这个方法)。isFlushingSyncQueue = false; syncQueue = null。这俩家伙当前demo没用到,我们在以后的篇幅在说,总之,没有白吃的米饭就对了。
老节...
在下车前在啰嗦一下,总结一下react 为了 一句 #root.append(jsx)都发生了什么
1、初始化hostRootFiber 和 fiberRootNode ,创建更新队列
2、 创建wip,即react的双缓冲机制雏形
3、开始递、归
4、将内存中创建好的DOM插入到root中
后记
非常感谢大家能看到这里,大家辛苦了!
本篇其实是由学习笔记改写。原型是自己学习时画一张完整的路程图。后来想着将图放在自己电脑中也没啥意思,不如改成文字梳理的方法共享出来。最开始以为很简单,实际下笔时,发现茶壶倒饺子,除了标题,一个字都憋不出来。好在随着一个字一个字的敲下去,慢慢的找到了思路,才写出了这篇。
后续也会将react的其他流程如 函数模式挂载、更新、Diff、Hook原理、事件原理、优先级 等变现出来(图换成文字),以及其他笔记。
欢迎大家批评指正,大家能一起学习进步,目的就达到啦。
谢谢!