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源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳 ‍ ‍ ‍

相关推荐
zhougl9961 小时前
html处理Base文件流
linux·前端·html
花花鱼2 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_2 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo3 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之5 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端5 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡5 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木6 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
Alo3656 小时前
面试考点复盘(二)
面试