跳到主要内容

认识

2023年12月21日
柏拉文
越努力,越幸运

一、认识


useTransition 用于在 createRoot 开启的并发特性模式下, 开启并发渲染。主要解决浏览器执行大量渲染工作问题时的卡顿问题。useTransition 返回一个数组, 第一个元素为 isPending, 表示当处于过渡状态的标志; 第二个是 startTransition , 一个方法, 里面的更新任务都会被标记成过渡更新任务,过渡更新任务在渲染并发场景下,会被降级更新优先级,中断更新, 所以它不会让页面卡顿,React 让这些任务,在浏览器空闲时间执行。

1.1 startTransition vs 节流防抖

节流防抖 也可以解决大量渲染工作同时渲染卡顿的问题, 但是节流防抖导致更新任务的执行时间变长了, 视图改变的时间也会延长。所以 startTransition节流防抖 的本质区别是: 一方面,节流防抖 本质上也是 setTimeout ,只不过控制了执行的频率,那么通过打印的内容就能发现,原理就是让 render 次数减少了。而 transitions 和它相比,并没有减少渲染的次数。另一方面,节流和防抖需要有效掌握 Delay Time 延时时间,如果时间过长,那么给人一种渲染滞后的感觉,如果时间过短,那么就类似于 setTimeout(fn,0) 还会造成前面的问题。而 startTransition 就不需要考虑这么多。

1.2 startTransition vs setTimeout

setTimeout 同样可以解决大量渲染工作同时渲染卡顿的问题, startTransition 相比 setTimeout 的优势和异同是: 一方面, startTransition 的处理逻辑和 setTimeout 有一个很重要的区别,setTimeout 是异步延时执行,而 startTransition 的回调函数是同步执行的。在 startTransition 之中任何更新,都会标记上 transitionReact 将在更新的时候,判断这个标记来决定是否完成此次更新。所以 Transition 可以理解成比 setTimeout 更早的更新。但是同时要保证 ui 的正常响应,在性能好的设备上,transition 两次更新的延迟会很小,但是在慢的设备上,延时会很大,但是不会影响 UI 的响应。另一方面,对于渲染并发的场景下,setTimeout 仍然会使页面卡顿。因为超时后,还会执行 setTimeout 的任务,它们与用户交互同样属于宏任务,所以仍然会阻止页面的交互。那么 transition 就不同了,在 conCurrent mode 下,startTransition 是可以中断渲染的 ,所以它不会让页面卡顿,React 让这些任务,在浏览器空闲时间执行

1.3 useTransition vs useDeferredValue

useDeferredValue 本质上和内部实现与 useTransition 一样都是标记成了过渡更新任务。但是, useTransition 是把 startTransition 内部的更新任务变成了过渡任务 transition,而 useDeferredValue 是把原值通过过渡任务得到新的值,这个值作为延时状态。 一个是处理一段逻辑,另一个是生产一个新的状态。useDeferredValue 还有一个不同点就是这个任务,本质上在 useEffect 内部执行,而 useEffect 内部逻辑是异步执行的 ,所以它一定程度上更滞后于 useTransitionuseDeferredValue = useEffect + transition

二、细节


2.1 hook

const hook = {
next: null,
baseQueue: null,
baseState: null,
updateQueue: null,
memoizedState: null
};

2.2 update

export const createUpdate = (
action,
lane,
hasEagerState = false,
eagerState = null
) => {
return {
lane,
action,
next: null,
hasEagerState,
eagerState
};
};

2.3 updateQueue

function createFCUpdateQueue() {
const updateQueue = createUpdateQueue();
updateQueue.lastEffect = null;
return updateQueue;
}
export const createUpdateQueue = () => {
return {
shared: {
pending: null
},
dispatch: null
};
};

updateQueue.shared.pending: 存储 update 对象

updateQueue.dispatch: 存储 dispatchAction.bind() 的值

2.4 hook.updateQueue

hook.updateQueue 用于存储 updateQueue

2.5 hook.memoizedState

hook.memoizedState 用于存储 state

2.6 fiber.memoizedState

fiber.memoizedState 用于存储 hooks 链表

三、执行函数组件


React.jsbeginWork 阶段, 遇到 FunctionComponent 类型的 Fiber, 会执行 updateFunctionComponentupdateFunctionComponent 内部调用 renderWithHooks。主要工作如下:

  1. 重置状态: 函数组件执行前, 重置当前正在处理的 hook、当前正在使用的 hook

  2. 记录当前组件 fiber: 通过 currentlyRenderingFiber 变量记录当前组件 fiber

  3. 选择对应阶段的 Hook: mount 阶段选择 HooksDispatcherOnMount 作为 Hooks Map, update 阶段选择 HooksDispatcherOnUpdate 作为 Hooks Map函数组件mount 阶段 创建 hook 数据结构, 存储到 fiber.memoizedState 或者 hook.next, 初次建立其 hooksfiber 之间的关系。函数组件 update 阶段 找到当前函数组件 fiber, 找到当前 hook, 更新 hook 状态。

  4. 执行函数组件: 执行我们真正函数组件,所有的 hooks 将依次执行, 每个 hook 内部要根据 currentlyRenderingFiber 读取对应的内容,

  5. 重置状态: 执行完 Component, 立马重置 currentlyRenderingFiber, 防止函数组件外部调用。置当前正在处理的 hook、当前正在使用的 hook

3.1 /packages/react-reconciler/fiberHooks.js

export function renderWithHooks(workInProgress, Component, lane) {
renderLane = lane;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;

const current = workInProgress.alternate;

if (current !== null) {
// update
currentDispatcher.current = HooksDispatcherOnUpdate;
} else {
// mount
currentDispatcher.current = HooksDispatcherOnMount;
}

const props = workInProgress.pendingProps;
const children = Component(props);

currentHook = null;
renderLane = NoLane;
workInProgressHook = null;
currentlyRenderingFiber = null;

return children;
}

3.2 /packages/react-reconciler/fiberHooks.js

/**
* @description: Mount 阶段 Hooks 实现
*/
const HooksDispatcherOnMount = {
use,
useRef: mountRef,
useMemo: mountMemo,
useState: mountState,
useEffect: mountEffect,
useContext: readContext,
useCallback: mountCallback,
useTransition: mountTransition
};

/**
* @description: Update 阶段 Hooks 实现
*/
const HooksDispatcherOnUpdate = {
use,
useRef: updateRef,
useMemo: updateMemo,
useState: updateState,
useEffect: updateEffect,
useContext: readContext,
useCallback: updateCallback,
useTransition: updateTransition
};

四、函数组件 mount 阶段 Hook


useTransition 创建一个 Hook, 通过调用 mountState 维护一个 isPending 状态, 定义一个 startTransition 方法, 并通过 .bindsetIsPending 方法传入, 将 startTransition.bind 方法保存到 hook.memoizedState , 返回 isPendingstartTransition 组成的数组即可。

function mountTransition() {
const [isPending, setPending] = mountState(false);
const hook = mountWorkInProgressHook();
const start = startTransition.bind(null, setPending);
hook.memoizedState = start;
return [isPending, start];
}

五、函数组件 update 阶段 Hook


useTransition 获取对应 Hook, 通过调用 updateState 维护一个 isPending 状态, 从 hook.memoizedState 上获取到 startTransition 方法, 返回 isPendingstartTransition 组成的数组即可。

function updateTransition() {
const [isPending] = updateState();
const hook = updateWorkInProgressHook();
const start = hook.memoizedState;
return [isPending, start];
}

六、执行回调函数, 并指定过渡任务优先级


startTransition 中, 会执行传入的 callback。执行 callback 前, 会更新全局配置 currentBatchConfig.transitiontrue, 表示当前任务为过度任务。当执行 callback 的过程中, 如果 callback 内部有更新任务。此时获取的任务优先级时, 会判断 currentBatchConfig.transition, 如果为 true, 那么此时的更新任务优先级就是 过渡任务优先级callback 执行完毕后, 将全局配置 currentBatchConfig.transition 恢复到之前状态。

6.1 startTransition

function startTransition(setPending, callback) {
setPending(true);
const prevTransition = currentBatchConfig.transition;
currentBatchConfig.transition = 1;
callback();
setPending(false);
currentBatchConfig.transition = prevTransition;
}

6.2 requestUpdateLane


export function requestUpdateLane() {
const isTransition = ReactCurrentBatchConfig.transition !== null;

if (isTransition) {
return TransitionLane;
}

const currentSchedulerPriority = unstable_getCurrentPriorityLevel();
const lane = schedulerPriorityToLane(currentSchedulerPriority);
return lane;
}