react能力值+1!useTransition是如何实现的?

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

本文致力于实现一个最简单的useTransition,代码均已上传至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的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!

一. 基本概念

‍ ​useTransition​主要是用于当有大数据量渲染时减少重复渲染次数,并且返回一个等待状态。

js 复制代码
const [isPending, startTransition] = useTransition()

通过一个例子看一下useTransition​的用法;

js 复制代码
import React, { useState, useTransition } from 'react';
 
export default function App() {
  const [val, setVal] = useState('');
  const [searchVal, setSearchVal] = useState([]);
  const [loading, startTransition] = useTransition();
 
  const handleChange = (e) => {
    setVal(e.target.value);
	// 真正的待执行函数
    startTransition(() => {
      setSearchVal(Array(100000).fill(e.target.value));
    });
  };
 
  return (
    <div className="App">
      <input value={val} onChange={handleChange} />
      {loading ? (
        <p>loading...</p>
      ) : (
        searchVal.map((item, index) => <div key={index}>{item}</div>)
      )}
    </div>
  );
}

当触发更新的时候,使用startTransition​传入真正的待执行函数,由startTransition​来执行,并且执行完毕后loading​会返回false。代表已经渲染完成。

useTransition​在不阻塞 UI 的情况下更新状态,比如在我们上边的这个例子中,如果直接执行渲染逻辑,由于需要渲染的数据量太大,在执行setSearchVal​后很可能会直接卡住,然后此时用户在执行的其他操作时都会没有响应。

useTransition​可以在不等待列表渲染完成的情况下完成操作。在列表渲染过程中依然保持响应。

在实现useTransition​之前,有必要了解一下react中的hook架构:

二. 数据共享层

hook​架构在实现时,脱离了react部分的逻辑,在内部实现了一个数据共享层,类似于提供一个接口。任何满足了规范的函数都可以通过数据共享层接入处理hook​的逻辑。这样就可以与宿主环境解耦,灵活性更高。

js 复制代码
// 内部数据共享层
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
	currentDispatcher
};

const currentDispatcher = {
	current: null
};

currentDispatcher​为我们本次实现的hook​。

所以对应到我们的render​流程以及hook​的应用,他们之间的调用关系是这样的:

hook​怎么知道当前是mount​还是update​?

我们在使用hook​时,react在内部通过currentDispatcher.current赋予不同的函数来处理不同阶段的调用,判断hook 是否在函数组件内部调用。

三. hook

hook​可以看做是函数组件和与其对应的fiber​节点进行沟通和的操作的纽带。在react​中处于不同阶段的fiber​节点会被赋予不同的处理函数执行hook​:

  • 初始化阶段 -----> HookDispatcherOnMount
  • 更新阶段 -----> HookDispatcherOnUpdate
js 复制代码
const HookDispatcherOnMount = {
	useTransition: mountTransition,
};

const HookDispatcherOnUpdate = {
	useTransition: updateTransition,
};

但是实现之前,还有几个问题需要解决:

如何确定fiber对应的hook上下文?

还记得我们在处理函数组件类型的fiber​节点时,调用renderWithHooks​函数进行处理,在我们在执行hook​相关的逻辑时,将当前fiber​节点信息保存在一个全局变量中:

js 复制代码
// 当前正在render的fiber
let currentlyRenderingFiber = null;
js 复制代码
export function renderWithHooks(wip: FiberNode) {
	// 赋值操作
	currentlyRenderingFiber = wip;
	// 重置
	wip.memoizedState = null;
	const current = wip.alternate;

	if (current !== null) {
		// update
		// hooks更新阶段
	} else {
		// mount
		// hooks初始化阶段
	}

	const Component = wip.type;
	const props = wip.pendingProps;
	const children = Component(props);

	// 重置操作
	// 处理完当前fiber节点后清空currentlyRenderingFiber
	currentlyRenderingFiber = null;

	return children;
}

将当前正在处理的fiber​节点保存在全局变量currentlyRenderingFiber​ 中,我们在处理hook​ 的初始化及更新逻辑中就可以获取到当前的fiber​节点信息。

hook是如何存在的?保存在什么地方?

注意hook​只存在于函数组件中,但是一个函数组件的fiber​节点时如何保存hook​信息呢?

答案是:memoizedState​。

fiber​节点中保存着非常多的属性,有作为构造fiber​链表,用于保存位置信息的属性,有作为保存更新队列的属性等等。

而对于函数组件类型的fiber​节点,memoizedState​属性保存hooks​信息。hook​在初始化时,会创建一个对象,保存此hook​所产生的计算值,更新队列,hook​链表。

js 复制代码
const hook = {
	// hooks计算产生的值 (初始化/更新)
	memoizedState: "";
	// 对此hook的更新行为
	updateQueue: "";
	// hooks链表指针
	next: null;
}

多个hook如何处理?

例如有以下代码:

js 复制代码
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  const [age, setAge] = useState(10);

  function handleClick() {
    setCount(count + 1);
  }

  function handleAgeClick() {
    setCount(age + 18);
  }

  return (
    <button onClick={handleClick}>
      add
    </button>
	<button onClick={handleAgeClick}>
      age
    </button>
  );
}

在某个函数组件中存在多个hook​,此时每个hook​的信息该如何保存呢?这就是上文中hook​对象中next​ 属性的作用,它是一个链表指针。在hook​对象中,next​ 属性指向下一个hook​。

换句话说,如果在一个函数组件中存在多个hook​,那么在该fiber​节点的memoizedState​属性中保存该节点的hooks​链表。

函数组件对应 fiber​ 用 memoizedState​ 保存 hook​ 信息,每一个 hook​ 执行都会产生一个 hook​ 对象,hook​ 对象中,保存着当前 hook​的信息,不同 hook​保存的形式不同。每一个 hook​ 通过 next​ 链表建立起关系。

四. ​useTransition​

定义useTransition

与之前实现的useState​和useEffect​一致,useTransition​同样使用resolveDispatcher​获取不同阶段的执行函数。

js 复制代码
export const useTransition = () => {
	const dispatcher = resolveDispatcher();
	return dispatcher.useTransition();
};

实现原理

前面的文章中已经实现了简易的调度功能,而useTransition​的实现也正是通过调度功能实现的。在更新时,为渲染列表的任务设置一个较低的优先级,所以当触发了渲染列表的任务后,有涉及到用户交互的功能被触发后(优先级较高),会中止渲染列表的任务,优先执行用户交互的操作。因为优先满足用户交互的行为,所以在视觉上会缓解卡顿的情况。

上图所示,在改变优先级前后,一共会触发三次更新,其中第一次设置setPending​会高优先级同步更新。callback​和第二次setPending​为低优先级更新。 ‍

五. mount

mount​阶段需要完成的任务:

  • 构建hook对象
  • 创建startTransition执行函数
  • 创建中间状态isPending
js 复制代码
function mountTransition() {
	// 通过useState定义中间状态isPending
	const [isPending, setPending] = mountState(false);
	// 创建hook对象,并添加到hooks链表中
	const hook = mountWorkInProgresHook();
	// 定义startTransition
	const start = startTransition.bind(null, setPending);
	// 执行函数保存在memoizedState属性中
	hook.memoizedState = start;
	// 返回中间状态与执行函数
	return [isPending, start];
}

mountState​是useState​在初始化阶段的形态,同理在更新阶段将会使用updateState​,两者的作用于useState​一致。

mountWorkInProgresHook

mountWorkInProgresHook​用于创建一个hook对象,由于函数组件类型的fiber​节点中保存hook的形式是一个链表。所以当一个hook对象被创建后,有两种情况:

  1. 当前hook为函数组件的第一个hook,当前hook对象为链表的第一项,此时需要将hook链表保存在fiber节点的emoizedState属性中。
  2. 当前hook为函数组件的后续hook,此时直接将当前hook对象连接到hook链表的最后一项(next属性)。
js 复制代码
function mountWorkInProgresHook() {
	const hook = {
		memoizedState: null,
		updateQueue: null,
		next: null,
		baseQueue: null,
		baseState: null
	};
	if (workInProgressHook === null) {
		// mount时 第一个hook
		if (currentlyRenderingFiber === null) {
			throw new Error('请在函数组件内调用hook');
		} else {
			workInProgressHook = hook;
			currentlyRenderingFiber.memoizedState = workInProgressHook;
		}
	} else {
		// mount时 后续的hook
		// 直接链接到hook链表的尾部
		workInProgressHook.next = hook;
		workInProgressHook = hook;
	}
	return workInProgressHook;
}

值得注意的是,不同类型的hook对象memoizedState​属性保存的值也是千差万别,useTransition​的memoizedState​属性保存执行函数。useState​保存具体的值,也就是执行结果。useEffect​则是一个保存具体信息(create​,destory​,deps​...)的对象。(参考前面的文章)

startTransition

startTransition​函数真正执行用户定义的执行函数,维护中间状态isPending​,所以需要将setPending​当作参数传入,在执行任务前后变更中间状态。

  1. 设置isPending为true
  2. 标记当前为transition更新阶段,在后续更新获取优先级时,降低优先级
  3. 执行任务
  4. 设置isPending为false
  5. 任务执行完成,恢复进入transition前的原标记

currentBatchConfig​是一个全局对象,用于在整个react更新开始时,判断是否为transition​环境,获取优先级的标记。

js 复制代码
const currentBatchConfig = {
	transition: null
};

js 复制代码
function startTransition(setPending, callback) {
	// 变更中间状态
	setPending(true);
	// 获取原transition标记
	const prevTransition = currentBatchConfig.transition;
	currentBatchConfig.transition = 1;
	// 执行真正的任务
	callback();
	// 执行完毕,变更isPending状态
	setPending(false);

	currentBatchConfig.transition = prevTransition;
}

六. update

update阶段调用 updateState​ 去更新 isPending​ 的状态。

js 复制代码
function updateTransition() {
	const [isPending] = updateState();
	// 更新hook链表
	const hook = updateWorkInProgresHook();
	const start = hook.memoizedState;
	return [isPending, start];
}

‍hook对象的更新过程的主旨是"复用"。获取current​树中对应的fiber​节点的memoizedState​属性,根据属性值生成新的hook对象和hook链表。

js 复制代码
function updateWorkInProgresHook() {
	let nextCurrentHook;

	if (currentHook === null) {
		// 这是这个FC update时的第一个hook
		const current = currentlyRenderingFiber?.alternate;
		if (current !== null) {
			nextCurrentHook = current?.memoizedState;
		} else {
			// mount
			nextCurrentHook = null;
		}
	} else {
		// 这个FC update时 后续的hook
		nextCurrentHook = currentHook.next;
	}

	if (nextCurrentHook === null) {
		throw new Error(
			`组件${currentlyRenderingFiber?.type}本次执行时的Hook比上次执行时多`
		);
	}

	currentHook = nextCurrentHook;
	// 创建新hook对象
	const newHook = {
		memoizedState: currentHook.memoizedState,
		updateQueue: currentHook.updateQueue,
		next: null,
		baseQueue: currentHook.baseQueue,
		baseState: currentHook.baseState
	};
	if (workInProgressHook === null) {
		// mount时 第一个hook
		if (currentlyRenderingFiber === null) {
			throw new Error('请在函数组件内调用hook');
		} else {
			workInProgressHook = newHook;
			currentlyRenderingFiber.memoizedState = workInProgressHook;
		}
	} else {
		// 后续的hook
		workInProgressHook.next = newHook;
		workInProgressHook = newHook;
	}
	return workInProgressHook;
}

七. 优先级变更

既然需要变更优先级,那么需要创建一个单独属于useTransition​的优先级,这个优先级级别比较低,这样才能在不影响交互行为下运行;

js 复制代码
// 同步优先级 最高
export const SyncLane = 0b00001;
// 输入框等交互优先级
export const InputContinuousLane = 0b00010;
// 默认优先级
export const DefaultLane = 0b00100;
// useTransition优先级
export const TransitionLane = 0b01000;
// 空闲
export const IdleLane = 0b10000;

TransitionLane​的优先级仅比空闲优先级高,可以随时被打断。

接下来在初始化/更新的入口处,我们需要判断currentBatchConfig.transition​是否有值?如果有值,需要强制将他的优先级变更为TransitionLane​。

首先看一下初始化和更新的流程入口,无论是初始化还是更新,首先要根据不同的触发环境获取对应的优先级:

requestUpdateLane的作用是在调度开始前设定本次更新的优先级,作为在后续的scheduler调度执行的依据。在requestUpdateLane​中判断当前更新是否是transition​触发的逻辑。

diff 复制代码
export function requestUpdateLane() {
	// 判断是否为transition逻辑执行的过程中
++	const isTransition = currentBatchConfig.transition !== null;
++	if (isTransition) {
++		return TransitionLane;
++	}

	// 从上下文环境中获取Scheduler优先级
	const currentSchedulerPriority = unstable_getCurrentPriorityLevel();
	const lane = schedulerPriorityToLane(currentSchedulerPriority);
	return lane;
}

‍ ‍ ‍ ‍写在最后

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

相关推荐
FIRE17 分钟前
uniapp小程序分享使用canvas自定义绘制 vue3
前端·小程序·uni-app
四喜花露水18 分钟前
vue elementui el-dropdown-item设置@click无效的解决方案
前端·vue.js·elementui
jokerest12340 分钟前
web——sqliabs靶场——第五关——报错注入和布尔盲注
前端
谢尔登1 小时前
前端开发调试之 PC 端调试
开发语言·前端
每天吃饭的羊1 小时前
在循环中只set一次
开发语言·前端·javascript
斗-匕1 小时前
面试击穿mysql
mysql·面试
_默_4 小时前
adminPage-vue3依赖DetailsModule版本说明:V1.2.1——1) - 新增span与labelSpan属性
前端·javascript·vue.js·npm·开源
也无晴也无风雨6 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang6 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
SRY122404198 小时前
javaSE面试题
java·开发语言·面试