本系列会实现一个简单的react
,包含最基础的首次渲染,更新,hook
,lane
模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘
本文致力于实现一个最简单的useTransition
,代码均已上传至github
,期待star!✨:
本文是系列文章,阅读的联系性非常重要!!
手写mini-react!超万字实现mount首次渲染流程🎉🎉
进击的hooks!实现react(反应)中的hooks架构和useState 🚀🚀
面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭
期待点赞!😁😁
食用前指南!本文涉及到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对象被创建后,有两种情况:
- 当前hook为函数组件的第一个hook,当前hook对象为链表的第一项,此时需要将hook链表保存在
fiber
节点的emoizedState
属性中。 - 当前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
当作参数传入,在执行任务前后变更中间状态。
- 设置
isPending
为true - 标记当前为
transition
更新阶段,在后续更新获取优先级时,降低优先级 - 执行任务
- 设置
isPending
为false - 任务执行完成,恢复进入
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-react
和antd
源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳