React 的挂载都发生了什么

前言

最近公司用的技术主要是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 ,先后创建了hostRootFiberfiberRootNode(代码中的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;

![6.png](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3a8f7310b8624983bdfcbc5aac3bbb36~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5a6d5LqM54i3:q75.awebp?rk3s=f64ab15b&x-expires=1744257394&x-signature=nUePAM7%2FEyywhGfTMA6FEj%2Bfs68%3D)
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、开始创建这个节点的子节点,并和这个节点通过 childreturn 建立关联关系

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 ,然后通过位运算(可以通俗的理解为收集)子元素的subtreeFlagsflags。收集完后,赋值给父节点上。

补一下内存图

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 这两行还是位运算,大概意思就是看 subtreeFlagsflags 是否有值,从内存图中可以看到我们的 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原理、事件原理、优先级 等变现出来(图换成文字),以及其他笔记。

欢迎大家批评指正,大家能一起学习进步,目的就达到啦。

谢谢!

相关推荐
安分小尧1 小时前
[特殊字符] 使用 Handsontable 构建一个支持 Excel 公式计算的动态表格
前端·javascript·react.js·typescript·excel
ElasticPDF-新国产PDF编辑器1 小时前
React 项目 PDF 批注插件库在线版 API 示例教程
react.js·pdf·json
帅帅哥的兜兜2 小时前
react中hooks使用
前端·javascript·react.js
拉不动的猪11 小时前
刷刷题49(react中几个常见的性能优化问题)
前端·react.js·面试
小满zs14 小时前
React-router v7 第二章(路由模式)
前端·react.js
大莲芒14 小时前
react 15-16-17-18各版本的核心区别、底层原理及演进逻辑的深度解析--react18
前端·javascript·react.js
编程社区管理员16 小时前
「2025最新版React+Ant Design+Router+TailwindCss全栈攻略:从零到实战,打造高颜值企业级应用
前端·react.js·前端框架
gongzemin16 小时前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
黄毛火烧雪下18 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js