1.主要功能是时间切片和优先级调度
1.时间切片
时间切片
的本质是模拟实现requestIdleCallback
浏览器一帧执行的任务,除去浏览器重排/重绘还有四个是可以用来执行我们的js的
只有requestAnimationFrame 是确定时机在 浏览器重排/重绘 之前执行的
一个task(宏任务) — 队列中全部job(微任务) — requestAnimationFrame — 浏览器重排/重绘 — requestIdleCallback
我们模拟实现的requestIdleCallback也没法在确定时机被调用
所以,退而求其次,Scheduler
的时间切片
功能是通过task
(宏任务)实现的。
如果有messagechannel就用messagechannel实现 ,没有就用settimeout实现
所以Scheduler
将需要被执行的回调函数作为MessageChannel
的回调执行。如果当前宿主环境不支持MessageChannel
,则使用setTimeout
。
所以时间片执行的回调函数在messagechannel执行 否则在settimeout回调执行
所以每个任务在每一帧宏任务里面就执行5ms,超过5ms shouldYieldToHost就会变为false,就会中断workloop,等到下一帧的宏任务执行时间在执行
2.优先级调度
schedule是独立于react包,他的优先级是独立于react的优先级的,那么schedule是怎么和react优先级联系上的呢?
1.优先级调度
因为schedule往外暴露啦一个unstable_runwithpriority方法, 这个函数
unstable_runWithPriority(priorityLevel, eventHandler) 作用 让eventHandler这个函数以priorityLevel优先级执行
schedule有五个优先级
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break;
default:
priorityLevel = NormalPriority;
复制代码
commitRoot:以同步的方式执行commitRootImpl这个函数
runWithPriority(
ImmediateSchedulerPriority,//优先级
commitRootImpl.bind(null, root, renderPriorityLevel),
);
复制代码
2.优先级意义
优先级对应过期时间的长短,以时间大小排序即优先级排序
var timeout;//!不同优先级不同 优先级越低时间timeout越大
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;//-1
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
var expirationTime = startTime + timeout;//!开始时间+timeout = 过期时间
复制代码
暴露scheduleCallback函数 该方法用于以某个优先级
注册回调函数。
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();//useEffect函数执行
return null;//将flushPassiveEffects以NormalSchedulerPriority注册这个函数 即创建task 加入最小堆中 加入调度 等到执行时机到来
});
}
复制代码
执行流程
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
复制代码
以schedulerPriorityLevel优先级注册这个函数 ,当这个函数执行的时候如果中断返回值则依然是这个函数 那么这个函数依然是新任务的callback继续执行 知道结束返回null 才弹出这个task
这就是为什么render阶段可中断恢复
3.react内部lane模型的工作原理
满足三个特性:1.可以表示不同的优先级 2.可以表示批处理 3.可以方便计算
//!不同的赛道 越下的赛道优先级越低 lanes就是批次
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputDiscreteHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /* */ 0b0000000000000000000000000011000;
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000000011000000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000;
export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111000000000;//!这三个赛道有一样的优先级 批的概念
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;//!优先级越低的赛道越批处理越多 因为他们容易被打断被留下来
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000;
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
const NonIdleLanes = /* */ 0b0000111111111111111111111111111;
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
const IdleLanes: Lanes = /* */ 0b0110000000000000000000000000000;
复制代码
4.异步可中断更新
react内部的lane和schedule结合流程
1.根据模式找到schedule的优先级
2.转成lane的优先级
3.根据lane的优先级找到lanes 然后找到优先级最高的没被占用的lane(赛道)
4.lane赋值给update.lane
5.然后给当前fiber.lanes合并这个lane 然后所有父节点的childlanes合并这个lane
6.将root的pendinglanes(root上需要执行还没执行的lane)加上这个lane
7.获取root的callbacknode属性即root当前正在执行的task的回调函数,如果有那么当前应用正在被执行任务
8.取到root上的pendinglanes,给上面所有的lane加上timestamp(过期时间),timestamp是怎么计算的呢?先获取这个lane对应的优先级,优先级越低 他的timestamp越高,所以所有pendinglanes都有一个过期时间,如果当前时间超过timestamp+starttime(即有任务过期),那么就把这个lane加入到root.expiredLanes里面(root上过期的lane), 这一步解决饿饥问题
9.然后获取root上最高lane对应的任务,包括expiredLanes和pendinglanes,如果有过期任务会先被获取,我们这次执行的任务可能是在后面,这次不会被执行,我们这次拿到最优先级的任务,过期任务优先,记为nextlanes为root下优先级最高的lanes
10.如果callbacknode(即root正在被执行的callback)存在,那么我们判断nextlanes和callbacknode对应的lanes谁的优先级高,如果nextlanes更高那么之前的任务就会被取消
11.把lane的优先级转回为schedule优先级,然后调用schedulecallback传入回调函数为performConcurrentworkOnroot,这和函数如果被时间到达中断啦则会返回这次函数,在schedule内部判断如果返回的是个函数 那么这个函数作为newtask的callback继续执行 这就是可中断恢复
if (root.callbackNode === originalCallbackNode) {//包过时间过啦和其他高优先级的performConcurrentworkOnroot都会走这 因为函数不变 task的callback是这个,优先级在schedule里面保存啦这个函数的优先级
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
复制代码
返回函数的情况:继续把这个函数=currentTask.callback这里面保存啦优先级。
currentTask.callback = continuationCallback;//!继续把这个函数当作新任务
markTaskYield(currentTask, currentTime);
复制代码
root.callbackNode 是当前root进行任务的回调函数,originalCallbackNode就是performConcurrentworkOnroot,这里就是判断如果新进来的函数不是originalCallbackNode,那么就是更高优先级任务进来啦,那么返回Null,不让performConcurrentworkOnroot作为下一个task,这个任务会pop除去。如果相等则是时间片到了的更新因为函数没变,所以就返回函数让下一个task继续。
12.时间片是根据当前时间-任务开始时间>5ms来判断是否超时?yieldInterval = Math.floor(1000 / fps);
5ms是根据fps来定的
13.commitRoot阶段:root.finishedLanes(是指已经完成工作的lanes)赋值为lanes,合并root的lanes和childlanes即为该root即子树中剩下的lanes记为remaininglanes,将remaininglanes和之前的penginglanes比对,少的就是完成的lanes,我们给他初始化为空方便下次使用,更新penginglanes为remaininglanes
5.batchupdates
之前的版本batchedUpdates
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;//!这个函数类似runwithpriority 执行fn的时候上下文包括BatchedContext 执行后又重置上下文
try {
return fn(a);//当fn里面检测到BatchedContext 就不会立即执行flushSyncCallbackQueue 而是在回调里面执行flushSyncCallbackQueue
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
复制代码
缺点就是:如果onclick是setState异步的,那么就没有BatchedContext这个上下文,那么就还是要执行那么多render次数。
现在版本的batchedUpdates
lane模型解决这个缺点:开启concurrentmode
//举例
handleClick(){
setTimeout(() => {
this.setState({
number:1
})
this.setState({
number:2
})
}, 0);
}
复制代码
每一次setState都会走获取lane这个函数requestUpdateLane,
这个函数有两个关键地方
if (currentEventWipLanes === NoLanes) {
currentEventWipLanes = workInProgressRootIncludedLanes;
}
复制代码
第一次setState的时候currentEventWipLanes,后面的setState时候currentEventWipLanes都有值且都等于workInProgressRootIncludedLanes
lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
复制代码
这个是根据schedulerLanePriority和currentEventWipLanes获取lane,显然同一个onclick下schedulerLanePriority和currentEventWipLanes都一样,最后取得lane也一样,这个函数就是取去除currentEventWipLanes后的schedulerLanePriority对应lanes的最高优先的lane.
最后有一个判断执行不执行render的判断
if (existingCallbackNode !== null) {
var existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// The priority hasn't changed. We can reuse the existing task. Exit.
return;
}
复制代码
第一次setState,existingCallbackNode是为null的
第二次setState existingCallbackNode不为null,且这两个的lane一样,所以existingCallbackPriority === newCallbackPriority两个任务的优先级也一样,所以后面setState不会将render阶段进行调度,所以只执行第一个setState 实现批处理。
原理:
只有开始进入render阶段后,workInProgressRootIncludedLanes = lanes; workInProgressRootIncludedLanes 才会被赋值为这次render的lanes。所以同一次的workInProgressRootIncludedLanes相同,经过啦render阶段workInProgressRootIncludedLanes才会被赋值为其他值。实现啦一次render阶段的批处理。
所以整体逻辑:
每次进入render阶段,workInProgressRootIncludedLanes 会被赋值为这次的lanes,所以同一个onclick的settimeout函数里面,只有第一个setState进入render阶段的调度,而同步的setState会因为
workInProgressRootIncludedLanes相同(因为都是用上次render阶段workInProgressRootIncludedLanes ,导致lane相同,而不会将render阶段加入调度。所以等这次render执行后workInProgressRootIncludedLanes赋值为这次的lane,下次执行setState就能有不同的workInProgressRootIncludedLanes ,就能进入render的调度。虽然render阶段调度只进入了一次,但是update还是都创建加入队列啦的,所以render只进入一次会依次执行两个update,所以值就是最后的新值。
主要是workInProgressRootIncludedLanes 这个变量。
6.高优先级更新如何插队
低优先级的lane为10去执行schecallback调度render阶段
高优先级(点击事件)获取的lane为8 会判断当前exitcallback是否存在(是否有任务在调度) 发现有则判断两个优先级,如果新的高那么旧的任务就会被取消,即低优先级的被取消。然后以lane=8的优先级调度render阶段 实现插队。
怎么清除低优先级更新已经造成的影响?进入render阶段有一个函数prepareFreshStak:
进入这个函数的条件是本次新的render的lane和已经在执行的lane不一致,即优先级不同的render阶段在执行(高优先级的执行render阶段),就会进入prepareFreshStack,这个函数清除上次的workingfiber树和
重置一些属性,以及workInProgressRootIncludedLanes=新的lanes。主要是清除之前创建好的fiber。重新去创建新的workingfiber去diff。
}
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);//清除之前的workingfiber树
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
复制代码
与第五点区别: 如果lane相同则和batchupdates一样的逻辑 直接return 第二次就不调度
总之主要这个优先级是在render开始阶段以什么优先级去执行render,所有组件的update还是都存在fiber里面的,和render阶段以什么优先级无关,只是说要不要执行这个update,不管执行不执行都存在。
7.suspense的实现
io的瓶颈的解决方案就是suspense,解决异步请求数据的时间问题,cpu的问题就是用异步可中断更新解决。
import React, {
Suspense,
useState,
unstable_useTransition as useTransition
} from "react";
import { wrapPromise } from "./utils";
function fetchTime() {
return wrapPromise(
new Promise((resolve) => {
setTimeout(() => {
resolve({ time: new Date().toLocaleString() });
}, 1000);
})
);
}
function Clock({ resource }) {
const { time } = resource.read();
return <h3>{time}</h3>;
}
function Button({ onClick, children }) {
const [startTransition, isPending] = useTransition({
timeoutMs: 2000
});
const btnOnClick = () => {
startTransition(() => {
onClick();//还有2000ms延迟的lane 和setpending:false 一致的优先级
});
};
return (
<>
<button disabled={isPending} onClick={btnOnClick}>
{children}
</button>
<span>{isPending && " loading"}</span>
</>
);
}
export default function App() {
const [time, setTime] = useState(fetchTime());
const load = () => {
setTime(fetchTime());
};
return (
<Suspense fallback={<div>Loading...</div>}>
<Button onClick={load}>加载</Button>
<Clock resource={time} />
</Suspense>
);
}
复制代码
useTransition实现
会触发lian两次更新 一次是高优先级的更新回调函数为pending=true,表示我们正在执行延迟任务执行,等待pending中,第二次是低优先级更新,回调函数为setpending=false.callback(即我们传入useTransition的函数),低优先级执行pending=false和我们想推迟的回调。 这就实现了回调函数会延迟执行。
updatesuspense实现
先保持suspense两个子组件为nextchildren 此例为button 和clock 但是最终返回的是primarychild,怎么得到呢?
创建一个offsetfiber 他的children是两个子组件,mode:”visible”根据模式是不是可见来判断要不要展示,最终返回的是offsetFiber,即我们suspense和children之间有一层offsetFiber子组件,通过offsetFiber判断两个子组件是否可见。
react在workloop函数下面会catch(error),我们看这个error是不是promise对象(发生在clock子组件上,因为他直接同步执行promise,同步方法执行异步,会抛出错误),是的话会一直向上抛出这个错误的error promise 找到最近的suspense组件,将这promise加入到suspense的fiber的updatequeue中,然后执行这个promise的resolve方法,触发root的更新使得视图重新渲染。(不大懂)