面试被问react性能优化?直接实现 bailout 和 eagerState 优化策略 🚀🚀

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

本文致力于实现一个最简单的优化策略执行过程,代码均已上传至github,期待star!✨:

github.com/kongyich/ti...

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

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

进击的hooks!实现react(反应)(反应)中的hooks架构和useState 🚀🚀

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

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

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

面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭

手写mini-react!实现react并发更新机制 🎉🎉

进击的hooks!useEffect执行机制大揭秘 🥳🥳

react(反应)(反应)能力值+1!useTransition是如何实现的?

面试官:说一下 useRef 是如何实现的?我:? 😭

进击的hooks!useContext 执行机制解析 🚀🚀

面试官惊呼内行!万字解析React Suspense实现原理 🚀🚀

期待点赞!😁😁

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

一. 性能优化策略

react中的性能优化的一般思路:将 「变化的部分」「不变的部分」 分离。

什么是 「变化的部分」

  • State
  • Props
  • Context

命中 「性能优化」 的组件可以不通过reconcile​生成wip.child​,而是直接复用上次更新生成的wip.child​。

总结起来有两点:

  • 性能优化的思路是将 「变化的部分」「不变的部分」 分离
  • 命中性能优化的组件的子组件 (而不是他本身)不需要render

源码内部有哪些性能优化策略?

存在两种性能优化策略:

  1. bailout策略:减少不必要的子组件render
  2. eagerState策略:不必要的更新,没必要开启后续调度流程

下面分别是用几个例子感受一下这两种策略:

  • 例一

先来看一个小例子,这面这一段代码实现每点击一次对num​进行累加:

js 复制代码
import { useState } from "react";
export default function App() {
  const [num, update] = useState(0);
  console.log("App render", num);
  return (
    <div>
      <button onClick={() => update(num + 1)}> + 1</button>
      <p>num is: {num}</p>
      <ExpensiveSubtree />
    </div>
  );
}
function ExpensiveSubtree() {
  console.log("Expensive render");
  return <p>i am child</p>;
}

可以看到,每点击一次,无论是父组件<App />​还是子组件<ExpensiveSubtree />​都会重新渲染一遍:

但是其实我们只要认真分析一下代码就会发现,需要变动的部分就只有父组件涉及到num​的地方需要更新重新渲染,子组件<ExpensiveSubtree />​与上次更新相比没有发生任何变化,子组件根本就不需要重新渲染。

那么根据上面 react 的优化策略的原则改造一下我们的案例,将 「变化的部分」「不变的部分」 分离,我们将<App />​组件中的更新num​的操作分离出来:

js 复制代码
import { useState } from "react";
export default function App() {
  console.log("App render");
  return (
    <div>
      <Num />
      <ExpensiveSubtree />
    </div>
  );
}

function Num() {
  const [num, update] = useState(0);
  return (
    <div>
      <button onClick={() => update(num + 1)}> + 1</button>
      <p>num is: {num}</p>
    </div>
  );
}
function ExpensiveSubtree() {
  console.log("Expensive render");
  return <p>i am child</p>;
}

可以看到,当满足了react性能优化的策略之后,只会重新渲染与更新数据相关的组件。

除了首次加载触发的渲染逻辑,再次点击按钮,<App />​和<ExpensiveSubtree />​不会重新渲染。

为什么<App />​组件也不会重新渲染呢,这是因为它作为HostRoot​(根组件)节点的子组件,也命中了性能优化策略。

  • 例二

再来看第二个小例子:

js 复制代码
import { useState } from "react";
export default function App() {
  console.log("App render");
  const [num, update] = useState(0);
  return (
    <div title={num}>
      <button onClick={() => update(num + 1)}> + 1</button>
      <p>num is: {num}</p>
      <ExpensiveSubtree />
    </div>
  );
}

function ExpensiveSubtree() {
  console.log("Expensive render");
  return <p>i am child</p>;
}

现在父组件里面也应用到了num​状态的变更,应该怎么改造,才能使ExpensiveSubtree​组件不会执行多余的渲染动作呢?

<App />​组件中具有副作用的部分拆分出来,将子组件作为参数传入,类似于插槽的用法,使用children​进行渲染。

js 复制代码
import { useState } from "react";
export default function App() {
  console.log("App render");

  return (
    <Wrapper>
      <ExpensiveSubtree />
    </Wrapper>
  );
}

function Wrapper({ children }) {
  const [num, update] = useState(0);
  return (
    <div title={num}>
      <button onClick={() => update(num + 1)}> + 1</button>
      <p>num is: {num}</p>
      {children}
    </div>
  );
}

function ExpensiveSubtree() {
  console.log("Expensive render");
  return <p>i am child</p>;
}

由于使用children​渲染的方式是使用组件的props​属性来获取的,而Wrapper​组件的props​属性是在<App />​组件的return​里面被设置的,所以当<App />​组件满足性能优化的策略时,<App />​组件return​的子级实际上是上次更新的结果,所以ExpensiveSubtree​组件也是复用上次的结果。Wrapper​组件中props​的children​属性也是不会变的。

可以看到,依然可以实现性能优化的目的:

  • 例三
js 复制代码
import { useState } from "react";
import ReactDOM from "react-dom/client";

export default function App() {
  const [num, update] = useState(0);
  console.log("App render ", num);
  return (
    <div
      onClick={() => {
        update(1);
      }}
    >
      <Cpn />
    </div>
  );
}

function Cpn() {
  console.log("cpn render");
  return <div>cpn</div>;
}

在这个例子中,首次渲染的时候两个组件都会渲染,在使用update​方法触发组件更新时,第一次会触发数据的更新和组件的重新渲染。但是第一次再使用update​方法触发更新时组件不会再更新了,这是因为由于每次数据的变动都是相同的值,并没有使num​变化,数据没有更新。所以组件不会再次出发渲染流程。

但是这里有一个问题,在上面的上个例子中,父组件中的更新行为都已经被拆分出父组件,所以父组件命中性能优化策略,同时子组件也不会重新渲染。但是我们的例三中父组件明明存在数据更新,为什么子组件还能避免重复更新呢?这是因为react会进行两种情况的判断一种是没有状态的情况,对应例一和例二。还有一种是具有状态变化,但是更新前后没有变化,所以也会命中性能优化策略,子组件也不会重新渲染。

那么两种情况有什么不同之处呢?

第一种情况因为父组件都没有状态变化,所以不需要render​的过程。

第二种情况因为具备状态变化,所以需要render​的过程,需要计算一下更新前后的值有没有变化,到底需不需要重新渲染。所以这也就是为什么例三中的App​组件为什么最后会打印一次。而子组件则不会被打印。

‍ 这个过程就是前面的bailout​策略。后续之所以怎么点击都不会再次更新是因为发现每次更新的值都是1,没有必要为本次更新开启调度流程,也不会进入render​阶段,这就是另一种eagerState​策略,也就是针对不必要的更新,没必要开启后续调度流程。

二. lanes 工作流程

上图是简洁的 react 执行流程。

由于bailout​策略中需要涉及到判断state​的逻辑,所以需要判断当前fiber​是否存在未执行的update​。

那么如何才能得知当前是否还有未执行的update​呢?众所周知,每一次发生的更新都会创建一个update​对象保存到fiber​节点的更新队列中。

js 复制代码
// useState dispatch
function dispatchSetState(
	fiber,
	updateQueue,
	action
) {
	// 定义lane优先级
	const lane = requestUpdateLane();
	// 创建update对象
	const update = createUpdate(action, lane);
	// 添加到update队列
	enqueueUpdate(updateQueue, update);
	// 开始调度更新
	scheduleUpdateOnFiber(fiber, lane);
}

由于在整个更新流程中 react 不是每一次都是同步更新,而是根据不同的触发行为划分不同的执行时机,这也就是在前面几篇文章中实现的lane​调度的逻辑。

在之前lane​调度的基础上,为fiber​节点新增lanes​的逻辑:

fiber.lanes​保存一个fiberNode​中 「所有未执行更新对应的lane」。所以利用lanes​的逻辑,我们只需要判断fiber​节点下的lanes​是否还有lane​(未执行完的优先级)。

每次更新时的enqueueUpdate​中处理lanes​的逻辑:

diff 复制代码
// useState dispatch
function dispatchSetState(
	fiber,
	updateQueue,
	action
) {
	// 定义lane优先级
	const lane = requestUpdateLane();
	// 创建update对象
	const update = createUpdate(action, lane);
	// 添加到update队列
	// 为enqueueUpdate传入本次更新优先级lane和fiber节点
++	enqueueUpdate(updateQueue, update, fiber, lane);
	// 开始调度更新
	scheduleUpdateOnFiber(fiber, lane);
}

enqueueUpdate​的作用主要是将本次更新的update​对象添加到fiber​节点的shared.pending​属性中,并组成一条环形链表。

在处理完更新对象后,将本次更新的lane​合并到fiber​的lanes​属性中。值得注意的是,由于我们经常需要通过双缓存树current​中对应的节点进行重置数据,所以在合并优先级的时候,需要将current​树中的节点也进行优先级的合并。

js 复制代码
export const enqueueUpdate = (
	updateQueue,
	update,
	fiber,
	lane
) => {
	const pending = updateQueue.shared.pending;
	// ...
	updateQueue.shared.pending = update;
	// 合并lane优先级
	fiber.lanes = mergeLanes(fiber.lanes, lane);
	// 合并current树中fiber节点的优先级
	const alternate = fiber.alternate;
	if (alternate !== null) {
		alternate.lanes = mergeLanes(alternate.lanes, lane);
	}
};

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

在创建处理完update​对象后,最终会在beginWork​函数中,也就是render​阶段处理fiber​节点的起点根据不同类型进行消费。通过update​来计算state​。

但是由于更新优先级的存在,某些优先级不够的更新可能会被跳过,所以对应的lane​也就不会被消费。

在执行update​时,如果遇到被跳过的更新,执行传入的回调函数:

js 复制代码
export const processUpdateQueue = (
	baseState,
	pendingUpdate,
	renderLane,
	onSkipUpdate
) => {
	const result = {
		memoizedState,
		baseState,
		baseQueue
	};

	if (pendingUpdate !== null) {
		// ...

		do {
			const updateLane = pending.lane;
			if (!isSubsetOfLanes(renderLane, updateLane)) {
				// 优先级不够 被跳过
				// 克隆当前的update对象
				const clone = createUpdate(pending.action, pending.lane);
				// 将本次的更新对象传入回调函数
				onSkipUpdate?.(clone);

				// ...
			} else {
				// ...
			}
			pending = pending.next;
		} while (pending !== first);

		// ...
	}
	return result;
};

在使用useState​对数据进行更新时,dispatch​触发update​更新任务,调用processUpdateQueue​执行更新任务。

js 复制代码
export const beginWork = (wip, renderLane) => {
	// 	清空lanes
	wip.lanes = NoLanes;
	// ...
}

由于在执行到beginWork​时fiber​节点的lanes​已经被清空。在通过dispatch​触发更新时,如果有因为优先级不足而没有被执行的update​对象,它的lane​会被重新加入到fiber​节点的lanes​属性中。

js 复制代码
function updateState() {
	// 找到当前useState对应的hook数据
	const hook = updateWorkInProgressHook();

	// ...


	if (baseQueue !== null) {
		const prevState = hook.memoizedState;
		const {
			memoizedState,
			baseQueue: newBaseQueue,
			baseState: newBaseState
		} = processUpdateQueue(baseState, baseQueue, renderLane, (update) => {
			const skippedLane = update.lane;
			const fiber = currentlyRenderingFiber;
			// NoLanes
			// 重新加入被跳过的update更新对象的lane
			fiber.lanes = mergeLanes(fiber.lanes, skippedLane);
		});

		// ...
	}

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

除了在fiber​节点中增加lanes​属性记录所有触发的优先级之外。再增加一个属性childLanes​,用于保存当前fiber​节点下所有子节点的lane​,保存的时机可以选择在render​流程的回溯时,也就是在completeWork​中。根据element​创建fiber​节点时会执行beginWork​函数不断创建子fiber​节点,当处理到最后一个子节点时,查找兄弟节点,如果没有兄弟节点,执行completeWork​函数,回到父级。然后不断重复这个过程,直到最后回到根节点。

所以在收集fiber​节点所有子节点的lane​时,可以选择在completeWork​回溯时进行收集,因为他会经过每个子节点最后聚拢在父节点。

在此之前我们已经利用completeWork​函数收集每个子fiber​节点的副作用标记。

js 复制代码
export const completeWork = (wip) => {
	// 递归回溯

	const newProps = wip.pendingProps;
	const current = wip.alternate;

	switch (wip.tag) {
		case HostComponent:
			// ...
			bubbleProperties(wip);
			return null;
		case HostText:
			// ...
			bubbleProperties(wip);
			return null;
		case HostRoot:
		case FunctionComponent:
		case Fragment:
			bubbleProperties(wip);
			return null;
		default:
			if (__DEV__) {
				console.warn('未处理的completeWork情况', wip);
			}
			break;
	}
};

completeWork​根据不同的类型执行不同的处理,最后都会调用bubbleProperties​收集副作用和lane​到父fiber​中。

js 复制代码
function bubbleProperties(wip) {
	let subtreeFlags = NoFlags;
	let child = wip.child;
	let newChildLanes = NoLanes;

	while (child !== null) {
		subtreeFlags |= child.subtreeFlags;
		subtreeFlags |= child.flags;
		// 收集childLanes
		// 合并子级的lanes和子级的所有childLanes
		newChildLanes = mergeLanes(
			newChildLanes,
			mergeLanes(child.lanes, child.childLanes)
		);

		child.return = wip;
		child = child.sibling;
	}
	// 收集副作用标记
	wip.subtreeFlags |= subtreeFlags;
	wip.childLanes = newChildLanes;
}

在每次触发更新时,例如在dispatch​时会从发生更新的函数组件的fiber​节点开始向上查找到根节点,然后从根节点开始生成完成的workInProgress​树。由于在触发更新时也会生成一个对应的lane​,所以在向上查找根节点的过程也需要保存childLanes​。

scheduleUpdateOnFiber​为一次更新render​阶段的入口函数。

js 复制代码
export function scheduleUpdateOnFiber(fiber, lane) {
	// fiberRootNode 由触发更新的节点开始查找根节点
	const root = markUpdateLaneFromFiberToRoot(fiber, lane);

	// ...
}

在向上合并的同时,也需要更新current​树的childLanes​属性。

js 复制代码
export function markUpdateLaneFromFiberToRoot(fiber, lane) {
	let node = fiber;
	let parent = node.return;
	// 不断向上查找父节点
	while (parent !== null) {
		// 合并
		parent.childLanes = mergeLanes(parent.childLanes, lane);
		// 合并current树childLanes
		const alternate = parent.alternate;
		if (alternate !== null) {
			alternate.childLanes = mergeLanes(alternate.childLanes, lane);
		}

		node = parent;
		parent = node.return;
	}
	if (node.tag === HostRoot) {
		return node.stateNode;
	}
	return null;
}

三. bailout 策略

命中 「性能优化」bailout​策略)的组件可以不通过reconcile​生成wip.child​,而是直接复用上次更新生成的wip.child​。

bailout​策略存在于beginWork​中

bailout​四要素:

  1. props不变

比较props​变化是通过 「全等比较」 ,使用React.memo​后会变为 「浅比较」

  1. state不变

两种情况可能造成state​不变:

  • 不存在update
  • 存在update,但计算得出的state没变化
  1. context不变
  2. type不变

workLoop​函数控制当前正在处理哪一个fiber​节点,workInProgress​相当于一个fiber​指针(全局变量),指向当前正在处理的fiber​节点。

每次beginWork​处理完一个fiber​节点,因为beginWork​函数的作用就是根据element​对象生成子节点的fiber​节点,所以在beginWork​函数执行完毕后,会将生成好的子级fiber​节点return​。然后会将workInProgress​指针指向beginWork​函数返回的fiber​节点,如果没有子级说明已经到达最底部,返回null​。如果不为null​,workLoop​会继续调用performUnitOfWork​,就相当于开启子级的beginWork​。

js 复制代码
function workLoop() {
	// workInProgress不为null,继续调用
	while (workInProgress !== null) {
		performUnitOfWork(workInProgress);
	}
}

function performUnitOfWork(fiber) {
	// 返回的next就是生成的子级fiber
	const next = beginWork(fiber, wipRootRenderLane);
	fiber.memoizedProps = fiber.pendingProps;
	// 如果为null,开始completeWork流程
	// 不为null,继续生成子fiber
	if (next === null) {
		completeUnitOfWork(fiber);
	} else {
		workInProgress = next;
	}
}

beginWork​函数开始真正开始创建fiber​节点,所以我们的bailout​策略在beginWork​执行之前进行判断。

bailout​策略的判定

js 复制代码
// 是否需要执行更新
let didReceiveUpdate = false;

export const beginWork = (wip, renderLane) => {
	// bailout策略
	didReceiveUpdate = false;
	const current = wip.alternate;

	if (current !== null) {
		const oldProps = current.memoizedProps;
		const newProps = wip.pendingProps;
		// 判断 props type 属性是否一致
		if (oldProps !== newProps || current.type !== wip.type) {
			didReceiveUpdate = true;
		} else {
			// 是否有更新?
			const hasScheduledStateOrContext = checkScheduledUpdateOrContext(
				current,
				renderLane
			);
			if (!hasScheduledStateOrContext) {
				// 命中bailout策略
				// state和context不变
				didReceiveUpdate = false;

				return bailouOnAlreadyFinishedWork(wip, renderLane);
			}
		}
	}

	wip.lanes = NoLanes;

	// 比较,返回子fiberNode
	switch (wip.tag) {
		case HostRoot:
			return updateHostRoot(wip, renderLane);
		case HostComponent:
			return updateHostComponent(wip);
		case FunctionComponent:
			return updateFunctionComponent(wip, wip.type, renderLane);
		case HostText:

			// ...

	}
	return null;
};

获取current​树的对应属性,首先判断 props​和 type​ 属性是否一致。查看属性是否发生了变化。如果两个属性不一致,说明需要执行更新didReceiveUpdate = true​)。

js 复制代码
if (oldProps !== newProps || current.type !== wip.type){}

如果一致,需要验证当前的fiber​节点是否有需要更新的update​对象:

本次更新要执行的优先级,当前的fiber​中也存在这个优先级代表的update​。

hasScheduledStateOrContext​为true​说明有需要的更新任务,state​和context​可能会被更新,所以不会命中bailout​策略。如果没有当前优先级的更新任务,则说明state​和context​没有更新。

js 复制代码
function checkScheduledUpdateOrContext(
	current,
	renderLane
) {
	const updateLanes = current.lanes

	if (includeSomeLanes(updateLanes, renderLane)) {
		return true;
	}
	return false;
}

export function includeSomeLanes(set, subset) {
	return (set & subset) !== NoLanes;
}

如果不存在更新任务,命中bailout​策略:

js 复制代码
function bailouOnAlreadyFinishedWork(wip, renderLane) {
	// 本次更新不在子节点的lanes中
	if (!includeSomeLanes(wip.childLanes, renderLane)) {
		if (__DEV__) {
			console.warn('bailout整棵子树', wip);
		}
		return null;
	}
	// 处理当前未bailout
	if (__DEV__) {
		console.warn('bailout一个fiber', wip);
	}
	// 克隆子节点及子节点的兄弟节点
	cloneChildFibers(wip);
	return wip.child;
}

此时需要判断如果此fiber​下的所有子树是否还存在与本次更新同一个lane​的更新任务,如果没有,直接返回null​,代表执行优化程度高的执行逻辑。workLoop​不会继续向下处理fiber​节点。

bailouOnAlreadyFinishedWork​存在两种优化路径:优化程度高的话,整个子树不需要更新操作,跳过子树的beginwork​ 流程。

优化程度低的话,只需要复用这个命中策略的fiber​节点的子节点,所以克隆子节点并返回

如果子树还存在与本次相同lane​的更新任务,执行优化程度低的逻辑将子节点克隆复用:

js 复制代码
export function cloneChildFibers(wip) {
	// child  sibling
	if (wip.child === null) {
		return;
	}
	let currentChild = wip.child;
	// 
	let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
	wip.child = newChild;
	newChild.return = wip;
	// 复用兄弟节点
	while (currentChild.sibling !== null) {
		currentChild = currentChild.sibling;
		newChild = newChild.sibling = createWorkInProgress(
			newChild,
			newChild.pendingProps
		);
		newChild.return = wip;
	}
}

当然,是不是被跳过的更新没有机会命中bailout​策略了呢?在HostRoot​和FunctionComponent​类型各自的处理函数中,也存在相应的处理逻辑:

HostRoot​类型代表处理根节点,就是我们初始的<App />​函数,所以也会存在状态的变更:

js 复制代码
function updateHostRoot(wip, renderLane) {

	// ...

	const prevChildren = wip.memoizedState;

	const nextChildren = wip.memoizedState;
	if (prevChildren === nextChildren) {
		return bailouOnAlreadyFinishedWork(wip, renderLane);
	}
	// 处理子节点
	reconcileChildren(wip, nextChildren);
	return wip.child;
}

FunctionComponent​:

当类型为函数组件类型时,在函数内部可能存在useState​等触发更新的hook。也就是函数内部存在update​,但计算得出的state​没变化这种情况。所以也需要判断是否会命中bailout​策略。

js 复制代码
function updateFunctionComponent(
	wip,
	Component,
	renderLane
) {
	// ...
	// render
	// 执行函数组件
	const nextChildren = renderWithHooks(wip, Component, renderLane);

	// ...
	return wip.child;
}

函数组件在renderWithHooks​中被执行,比如存在useState​这个hook,在useState​更新阶段会执行updateState​函数更新内部保存的值:

js 复制代码
function updateState() {
	// 找到当前useState对应的hook数据
	const hook = updateWorkInProgressHook();

	// 计算新state的逻辑

	if (baseQueue !== null) {
		const prevState = hook.memoizedState;
		// 计算本次更新最新的state
		const {
			memoizedState,
			baseQueue: newBaseQueue,
			baseState: newBaseState
		} = processUpdateQueue();

		// 和上次更新相比,state是否发生了变化
		// 如果不一致,没有命中,标记didReceiveUpdate为需要更新
		if (!Object.is(prevState, memoizedState)) {
			markWipReceivedUpdate();
		}

		// ...
	}

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

// 标记需要执行更新
export function markWipReceivedUpdate() {
	didReceiveUpdate = true;
}

renderWithHooks​函数组件执行完之后,说明state​值已经更新,是否命中bailout​策略已经有了定论,根据didReceiveUpdate​的值对子节点执行不同的处理逻辑。

diff 复制代码
function updateFunctionComponent(
	wip,
	Component,
	renderLane
) {
	// ...
	// render
	// 执行函数组件
	const nextChildren = renderWithHooks(wip, Component, renderLane);

	const current = wip.alternate;
++	if (current !== null && !didReceiveUpdate) {
++		// 命中bailout,复用子节点
++		bailoutHook(wip, renderLane);
++		return bailouOnAlreadyFinishedWork(wip, renderLane);
	}
	// 未命中bailout策略,生成子节点fiber
	reconcileChildren(wip, nextChildren);
	return wip.child;
}

不过在函数组件中当命中bailout​策略后,需要重置各种属性,手动移除lane​:

js 复制代码
export function bailoutHook(wip, renderLane) {
	const current = wip.alternate;
	wip.updateQueue = current.updateQueue;
	wip.flags &= ~PassiveEffect;
	// 移除lane
	current.lanes = removeLanes(current.lanes, renderLane);
}

// 移除lane
export function removeLanes(set, subet) {
	return set & ~subet;
}

四. eagerState 策略

状态更新前后没有变化,那么没有必要触发更新,为此需要做:

  1. 计算更新后的状态
  2. 与更新前的状态做比较

通常情况下, 「根据update计算state」 发生在beginWork​,而我们需要在 「触发更新时」 计算状态:

只有满足 「当前fiberNode没有其他更新」 才尝试进入eagerState​策略。

useState​在触发更新时使用dispatch​派发更新,内部调用dispatchSetState​。

在没有加入eagerState​策略时,调用dispatch​更新状态首先创建一个更新对象update​并加入fiber​节点的更新队列,随后通过scheduleUpdateOnFiber​开始调度更新。

所以eagerState​策略首先要在dispatch​发起后先判断状态有没有变化,如果没有变化且lanes​中不存在优先级(没有待执行的更新任务),就不会发起调度任务。

js 复制代码
function dispatchSetState(
	fiber,
	updateQueue,
	action
) {
	const lane = requestUpdateLane();
	// 创建update对象
	const update = createUpdate(action, lane);

	// eager策略
	const current = fiber.alternate;
	if (
		fiber.lanes === NoLanes &&
		(current === null || current.lanes === NoLanes)
	) {
		// 1. 更新前的状态 2.计算状态的方法
		// 上次更新的state
		const currentState = updateQueue.lastRenderedState;
		// 本次计算后新的state
		const eagarState = basicStateReducer(currentState, action);
		update.hasEagerState = true;
		update.eagerState = eagarState;
		// 两次state是否一致
		if (Object.is(currentState, eagarState)) {
			// 加入更新队列,不携带lane
			enqueueUpdate(updateQueue, update, fiber, NoLane);
			// 命中eagerState
			if (__DEV__) {
				console.warn('命中eagerState', fiber);
			}
			return;
		}
	}
	// 没命中
	// 加入更新队列,携带lane
	enqueueUpdate(updateQueue, update, fiber, lane);
	// 调度更新
	scheduleUpdateOnFiber(fiber, lane);
}

basicStateReducer​针对用户传入dispatch​的更新函数或者值对state​进行更新。

js 复制代码
export function basicStateReducer(
	state,
	action
) {
	// 函数 -> 执行
	if (action instanceof Function) {
		return action(state);
	} else {
	// 值 -> 直接返回
		return action;
	}
}

其中lastRenderedState​属性是在上次执行更新流程的hook函数时被保存。

js 复制代码
function updateState() {
	// 找到当前useState对应的hook数据
	const hook = updateWorkInProgressHook();

	// ...

	if (baseQueue !== null) {
		const prevState = hook.memoizedState;
		const {
			memoizedState,
			baseQueue: newBaseQueue,
			baseState: newBaseState
		} = processUpdateQueue(baseState, baseQueue, renderLane, (update) => {
			// ...
		});

		// ...

		hook.memoizedState = memoizedState;
		hook.baseState = newBaseState;
		hook.baseQueue = newBaseQueue;
		// 保存更新后的state状态
		queue.lastRenderedState = memoizedState;
	}

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

‍ ‍写在最后

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

相关推荐
abc800211703441 分钟前
前端Bug 修复手册
前端·bug
Best_Liu~44 分钟前
el-table实现固定列,及解决固定列导致部分滚动条无法拖动的问题
前端·javascript·vue.js
_斯洛伐克2 小时前
下降npm版本
前端·vue.js
苏十八3 小时前
前端进阶:Vue.js
前端·javascript·vue.js·前端框架·npm·node.js·ecmascript
码农爱java3 小时前
Spring Boot 中的监视器是什么?有什么作用?
java·spring boot·后端·面试·monitor·监视器
st紫月3 小时前
用MySQL+node+vue做一个学生信息管理系统(四):制作增加、删除、修改的组件和对应的路由
前端·vue.js·mysql
乐容4 小时前
vue3使用pinia中的actions,需要调用接口的话
前端·javascript·vue.js
似水明俊德4 小时前
ASP.NET Core Blazor 5:Blazor表单和数据
java·前端·javascript·html·asp.net
至天5 小时前
UniApp 中 Web/H5 正确使用反向代理解决跨域问题
前端·uni-app·vue3·vue2·vite·反向代理
与墨学长5 小时前
Rust破界:前端革新与Vite重构的深度透视(中)
开发语言·前端·rust·前端框架·wasm