爆肝第五篇! 实现react调度更新与Lane模型✌️✌️

hey🖐! 我是小黄瓜😊😊。一枚小透明,期待关注➕ 点赞,共同成长~

写在前面

本系列会实现一个简单的react,包含最基础的首次渲染,更新,hooklane模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘

本文致力于实现一个最简单的更新调度流程,代码均已上传至github,期待star!✨:

github.com/kongyich/ti...

本文是系列文章,阅读的联系性非常重要!!

手写mini-react!超万字实现mount首次渲染流程🎉🎉

更新!更新!实现react更新及diff流程

深入react源码!react事件模型是如何实现的❓

深入react源码!实现react事件模型🎉🎉

期待点赞!😁😁

食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!

在之前实现的更新流程中,我们每一次调setstate​都会触发一次更新,例如:

js 复制代码
const App = () => {
  const [count, setCount] = useState(0);

  const add = () => {
      setCount((count) => count + 1);
      setCount((count) => count + 1);
      setCount((count) => count + 1);
  };

  return (
    <h3 onClick={add}>
      {count}
    </h3>
  );
};

在上面的例子中,我们调用了三次setCount​ 方法来触发更新流程,但是其实我们只是连续的调用三次来更新count​的值而已,像在上面的例子中这种情况,其实只需要最后一次setcount​来触发更新流程就可以了。

在实现此功能之前,先来了解一下js中的任务执行机制。

执行栈与任务队列

我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。

而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。

当代码第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。

当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码全部执行完毕。

事件循环

一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。

以上的过程说的都是同步代码的执行。那么当一个异步代码(如发送ajax请求数据)执行后会如何呢?前文提过,js的另一大特点是非阻塞,实现这一点得益于js中的一个机制:事件队列(Task Queue)。

js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。

被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为"事件循环(Event Loop)。

不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)。

以下事件属于宏任务:

  • setInterval()
  • setTimeout()

以下事件属于微任务:

  • new Promise()
  • new MutaionObserver()

在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。

如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈...如此反复,进入循环。

当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

批处理

目前我们实现的更新流程有两个问题:

  • 从触发更新到render流程,再到commit都是同步执行的
  • 多次触发更新会重复多次更新流程

那么有没有可能在进行多次触发更新之后,只进行一次更新流程呢。这就是涉及到多个更新任务按批次处理,React​批处理的时机既有宏任务,也有微任务。

首先我们如果要将更新合并,在前面的文章中我们实现了从构造fiber​到最后生成DOM节点全过程,其中涉及到两个阶段:

  • render阶段
  • commit阶段

在触发更新后我们立即调用scheduleUpdateOnFiber​ 方法,直接调用workLoop​函数开始生成fiber​节点,进入render​阶段。而现在我们要在开始render​之前加入调度流程。

‍ 也就是说在如下触发多次更新后,我们会对这些同一批次的更新统一调度,对多次更新任务添加优先级,能够合并在一次微任务中触发所有更新,决定哪个优先级优先进入render​阶段。

  • 增加优先级机制
  • 合并更新任务在一次微任务中进行触发
  • 高优先级任务优先进入render阶段

updateContainer

首先创建用于每次更新的各种优先级,由于目前的实现比较简单,所以只实现同步优先级:

js 复制代码
export const SyncLane = 0b0001;
export const NoLane = 0b0000;
export const NoLanes = 0b0000;

其中SyncLane​ 为同步优先级。NoLane​ 代表无优先级,为默认值。NoLanes​ 代表未来多个优先级的集合。

在初始化流程的开端,updateContainer​函数为整个初始化渲染流程的入口,所以我们要为他增加处理优先级。

js 复制代码
export function createRoot(container) {
	const root = createContainer(container);

	return {
		render(element: ReactElementType) {
			initEvent(container, 'click');
			// 开始处理
			return updateContainer(element, root);
		}
	};
}

‍ ​updateContainer​函数在加入调度任务之前需要创建一个更新任务,然后立即开始render​流程。

现在我们加入了优先级调度的能力,所以在开启render​流程之前,为更新任务添加优先级。

js 复制代码
export function updateContainer(
	element,
	root
) {
	const hostRootFiber = root.current;
	const lane = requestUpdateLane(); // 新增
	// 创建更新任务
	const update = createUpdate(element, lane);
	enqueueUpdate(
		hostRootFiber.updateQueue,
		update
	);
	// 开始调度
	scheduleUpdateOnFiber(hostRootFiber, lane);
	return element;
}

由于目前只有同步更新的优先级,所以现阶段requestUpdateLane​ 函数直接返回同步优先级:

js 复制代码
export function requestUpdateLane() {
	return SyncLane;
}

相应的,我们在之前每次触发更新创建的update​对象中,也需要加入lane​属性。next​ 属性在以后的多个更新任务中标识下一个更新任务(链表结构):

js 复制代码
export const createUpdate = (action, lane) => {
	return {
		action,
		lane,
		next
	};
};

在节点的shared.pending​ 属性中,保存更新任务的方式也要变更,在之前我们保存更新任务是直接将update​属性保存到updateQueue.shared.pending​中,因为每次创建更新任务都会触发更新。

但是现在我们需要在该属性保存多个更新任务,而update​对象中的next​属性帮助我们生成一条链表:

js 复制代码
export const enqueueUpdate = (
	updateQueue,
	update
) => {
	const pending = updateQueue.shared.pending;
	// pending中的第一个更新任务
	if (pending === null) {
		// 第一个更新任务
		update.next = update;
	} else {
		// 后续更新任务
		update.next = pending.next;
		pending.next = update;
	}
	// 保存链表
	updateQueue.shared.pending = update;
};

不同于之前实现的链表结构,本次实现的更新链表,是环形链表:

尾节点与头节点通过next​指针相连,这样可以更加快速的获取头节点,将pending​ 赋值为尾部节点,使用pending.next​就可以获取头节点。

例如我们使用字母来表示一个更新任务,多个更新通过链表结构:

js 复制代码
// 只有一个更新任务
pending = a -> a

// 多个更新任务
pending = b -> a -> b
pending = c -> a -> b -> c

scheduleUpdateOnFiber

scheduleUpdateOnFiber​开始调度任务,在由更新任务触发的流程中,我们之前实现了一个markUpdateFromFiberToRoot​函数,是指从当前fiber​节点开始,向上查找到根节点,然后从根节点开始render​流程。

本次加入调度流程后,将本次的调度优先级合并到根节点,便于筛选最高优先级的更新任务。

js 复制代码
export function scheduleUpdateOnFiber(fiber, lane) {
	// 查找根节点
	const root = markUpdateFromFiberToRoot(fiber);
	// 合成优先级 
	markRootUpdated(root, lane);
	// 根据优先级开始执行更新
	ensureRootIsScheduled(root);
}

markRootUpdated​ 函数负责合并所有更新优先级到根节点。

js 复制代码
function markRootUpdated(root, lane) {
	root.pendingLanes = mergeLanes(root.pendingLanes, lane);
}

// 合并lane
export function mergeLanes(laneA, laneB) {
	return laneA | laneB;
}

在创建应用根节点时,新增pendingLanes​ 属性,保存整棵fiber​树下所有的优先级:

js 复制代码
// 无优先级
const NoLane = 0b0000;
const NoLanes = 0b0000;

export class FiberRootNode {
	container;
	current;
	finishedWork;
	pendingLanes;
	finishedLane;
	constructor(container, hostRootFiber) {
		// ...省略
		// 正在处理中的调度优先级
		this.pendingLanes = NoLanes;
		// 已经处理完毕的调度优先级
		this.finishedLane = NoLane;
	}
}

接下来挑选当前根节点中保存的拥有最高优先级的权限,保存在执行队列中,在微任务开始时开始调用:

js 复制代码
function ensureRootIsScheduled(root) {
	// 获取该fiber下最高优先级任务
	const updateLane = getHighestPriorityLane(root.pendingLanes);
	// 没有优先级,退出调度
	if (updateLane === NoLane) {
		return;
	}
	if (updateLane === SyncLane) {
		// 同步优先级 用微任务调度
		// [performSyncWorkOnRoot, performSyncWorkOnRoot, performSyncWorkOnRoot]
		// 构造执行列表
		scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root, updateLane));
		// 启用微任务执行列表
		scheduleMicroTask(flushSyncCallbacks);
	} else {
		// 其他优先级 用宏任务调度
	}
}

获取当前根节点中pendingLanes​属性中保存的最高优先级:

js 复制代码
export function getHighestPriorityLane(lanes) {
	return lanes & -lanes;
}

由于在每次更新触发后可能存在多个高优先级任务,所以创建一个全局变量,保存这些更新任务,将开始执行workLoop​的入口函数performSyncWorkOnRoot​当作列表项:

js 复制代码
// 保存当前轮次微任务需要执行的更新任务
let syncQueue= null;

export function scheduleSyncCallback(callback) {
	if (syncQueue === null) {
		syncQueue = [callback];
	} else {
		syncQueue.push(callback);
	}
}

当前宿主环境如果支持微任务,则使用微任务去调度执行,如果不支持,则使用宏任务,开始一个定时器。

创建一个全局变量isFlushingSyncQueue​表示当前正在调度执行更新任务,避免在执行过程中重新执行新的任务。

js 复制代码
let isFlushingSyncQueue = false;

export const scheduleMicroTask =
	typeof queueMicrotask === 'function'
		? queueMicrotask
		: typeof Promise === 'function'
		// 使用Promise().then()创建微任务
		? (callback: (...args: any) => void) => Promise.resolve(null).then(callback)
		: setTimeout;

// 微任务中调用所有更新任务
export function flushSyncCallbacks() {
	if (!isFlushingSyncQueue && syncQueue) {
		isFlushingSyncQueue = true;
		try {
			syncQueue.forEach((callback) => callback());
		} catch (e) {
			console.error('flushSyncCallbacks报错', e);
		} finally {
			isFlushingSyncQueue = false;
		}
	}
}

performSyncWorkOnRoot​ 函数则是我们开始render​阶段的入口,也是在每一次微任务执行队列中保存的执行函数。

在进入performSyncWorkOnRoot​后首先判断优先级是否为同步优先级(目前只有同步优先级),如果不是同步优先级,则说明是无优先级或者比同步优先级更低。比同步优先级更低重新发起调度,如果是无优先级直接退出。

js 复制代码
function performSyncWorkOnRoot(root, lane) {
	const nextLane = getHighestPriorityLane(root.pendingLanes);

	if (nextLane !== SyncLane) {
		// 其他比SyncLane低的优先级
		// NoLane
		ensureRootIsScheduled(root);
		return;
	}

	// 初始化时,保存当前执行时的优先级
	prepareFreshStack(root, lane);

	do {
		try {
			workLoop();
			break;
		} catch (e) {
			// ...省略
		}
	} while (true);

	// render阶段结束后,重置调度权限
	root.finishedLane = lane;
	wipRootRenderLane = NoLane;

	// 开启commit流程
	commitRoot(root);
}

接下来在初始化workInProgress​树时,将本次执行的更新任务的优先级保存到全局变量wipRootRenderLane​,便于在执行更新函数时筛选同优先级的任务执行。

js 复制代码
function prepareFreshStack(root, lane) {
	// 创建workInProgress树
	workInProgress = createWorkInProgress(root.current, {});
	// 保存本次更新的优先级
	wipRootRenderLane = lane;
}

processUpdateQueue

接下来在beginwork​流程中,判断如果是根节点的处理流程,需要执行update​链表,传入当前优先级,renderLane​ 参数就是上文中全局变量wipRootRenderLane​。

js 复制代码
// 递归中的递阶段
export const beginWork = (wip, renderLane) => {
	// 比较,返回子fiberNode
	switch (wip.tag) {
		case HostRoot:
			return updateHostRoot(wip, renderLane);
		case HostComponent:
			// ...省略
		case HostText:
			return null;
		case FunctionComponent:
			// ...省略
		default:
			break;
	}
	return null;
};

根节点的执行逻辑:

js 复制代码
function updateHostRoot(wip: FiberNode, renderLane: Lane) {
	// ...省略
	const { memoizedState } = processUpdateQueue(baseState, pending, renderLane);
	// ...省略
}

由于在单次更新时直接执行保存在updateQueue.shared.pending​ 中,只需要取出更新函数,然后执行即可。

现在增加批处理功能后保存的更新函数为环形链表,所以需要循环执行。

使用wipRootRenderLane​全局变量中保存的当前执行任务的优先级,然后挑选update​链表中此优先级的任务,执行。

  • 使用尾节点的next指针获取头节点
  • 循环使用next指针获取后续节点依次执行
js 复制代码
export const processUpdateQueue = (
	baseState,
	pendingUpdate,
	renderLane
) => {
	const result = {
		memoizedState: baseState
	};

	if (pendingUpdate !== null) {
		// 第一个update,头节点
		const first = pendingUpdate.next;
		let pending = pendingUpdate.next;
		// 循环执行所有update
		do {
			const updateLane = pending.lane;
			// 同优先级的任务执行
			if (updateLane === renderLane) {
				const action = pending.action;
				if (action instanceof Function) {
					baseState = action(baseState);
				} else {
					baseState = action;
				}
			} else {
				console.error('不应该进入updateLane !== renderLane逻辑');
			}
			pending = pending.next;
		} while (pending !== first);
	}
	result.memoizedState = baseState;
	return result;
};

update

通过hook​触发的更新,整个执行流程也是类似的:

js 复制代码
function dispatchSetState(
	fiber,
	updateQueue,
	action
) {
	// 设置更新优先级
	const lane = requestUpdateLane();
	// 创建更新函数
	const update = createUpdate(action, lane);
	enqueueUpdate(updateQueue, update);
	// 开始调度
	scheduleUpdateOnFiber(fiber, lane);
}

beginWork​阶段处理函数组件的逻辑中,提前将当前执行中的优先级保存在全局变量中,这样在hook​执行时,便可以通过全局变量获取当前的优先级:

js 复制代码
// 递归中的递阶段
export const beginWork = (wip, renderLane) => {
	// 比较,返回子fiberNode
	switch (wip.tag) {
		case HostRoot:
			// ...省略
		case HostComponent:
			// ...省略
		case HostText:
			return null;
		case FunctionComponent:
			return updateFunctionComponent(wip, renderLane);
			// ...省略
		default:
			break;
	}
	return null;
};


function updateFunctionComponent(wip, renderLane) {
	// 函数组件执行逻辑
	const nextChildren = renderWithHooks(wip, renderLane);
	reconcileChildren(wip, nextChildren);
	return wip.child;
}

有关于hook​的逻辑详见上篇hook​的文章。

js 复制代码
// 保存当前优先级
let renderLane = NoLane;

export function renderWithHooks(wip, lane) {
	// 保存当前优先级
	renderLane = lane;
	// 定义hook函数
	if (current !== null) {
		// update
		currentDispatcher.current = HooksDispatcherOnUpdate;
	} else {
		// mount
		currentDispatcher.current = HooksDispatcherOnMount;
	}
	// 执行函数组件
	const Component = wip.type;
	const props = wip.pendingProps;
	const children = Component(props);

	// 重置操作
	// ...省略
	// 重置优先级
	renderLane = NoLane;
	return children;
}

js 复制代码
function updateState() {
	// ... 省略
	if (pending !== null) {
		// 通过全局变量renderLane执行更新链表
		const { memoizedState } = processUpdateQueue(
			hook.memoizedState,
			pending,
			renderLane
		);
		hook.memoizedState = memoizedState;
	}

	return [hook.memoizedState, queue.dispatch];
}

commit​阶段开始之前,在finishedLane​ 属性中保存lane​:

js 复制代码
function performSyncWorkOnRoot(root, lane) {
	// ...省略

	root.finishedLane = lane;
	wipRootRenderLane = NoLane;

	// 开启commit流程
	commitRoot(root);
}

最后在本次更新commit​节点结束之后,删除本次的优先级:

js 复制代码
function commitRoot(root) {
	// ...省略
	const lane = root.finishedLane;

	// 省略

	// 重置
	root.finishedWork = null;
	root.finishedLane = NoLane;
	// 删除此次优先级标识
	markRootFinished(root, lane);
}

export function markRootFinished(root, lane) {
	root.pendingLanes &= ~lane;
}

写在最后

未来可能会更新实现mini-reactantd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

相关推荐
前端fighter26 分钟前
js基本数据新增的Symbol到底是啥呢?
前端·javascript·面试
GISer_Jing41 分钟前
从0开始分享一个React项目:React-ant-admin
前端·react.js·前端框架
川石教育44 分钟前
Vue前端开发子组件向父组件传参
前端·vue.js·前端开发·vue前端开发·vue组件传参
豆子熊.1 小时前
外包干了3年,技术退步明显...
软件测试·selenium·测试工具·面试·职场和发展
GISer_Jing1 小时前
Vue前端进阶面试题目(二)
前端·vue.js·面试
乐闻x2 小时前
Pinia 实战教程:构建高效的 Vue 3 状态管理系统
前端·javascript·vue.js
weixin_431449682 小时前
web组态软件
前端·物联网·低代码·编辑器·组态
橘子味小白菜2 小时前
el-table的树形结构后端返回的id没有唯一键怎么办
前端·vue.js
前端Hardy3 小时前
HTML&CSS:比赛记分卡
前端·javascript·css·3d·html
疯狂的沙粒3 小时前
Vue项目开发 element-UI 前端实现 1到10排列选择的按钮
前端·vue.js·ui