React 16调度器原理

417 阅读8分钟

简介

调度器是React独立的一个包,React希望前端的调度相关代码都可以借助Scheduler这个包来做,但是现在的场景较少

几个问题?

1. 什么是调度?

把多个不同优先级任务在,安排不同的时机执行就是调度

2什么是任务?安排了什么任务

同一时间段,可能有多个区域虚拟DOM,需要被调和器计算出来。多个区域的虚拟DOM,他们DOM结构肯定没有啥关系,这里就需要用多个任务来执行计算。不同区域的更新就需要不同的任务来执行。

举个例子:一个网页的渲染完成后,头部有1万个节点,尾部有10个节点,中间有2个按钮,按钮1点击后,更新头部(再添加10万个节点),按钮2点击后,在添加10个节点。

先点按钮1(更新头部,)假设此时更新了5万个节点的时候,点击按钮2. 那此时到底是更新头部还是尾部了?这里安排哪边的节点先更新,哪边后更新,的过程就叫调度。

3. 调度了哪几类任务

刚刚举的例子实际上是,调度最重要的一种任务:计算Fiber树,也可以叫react 调和阶段的工作循环, 那还没有其他类型的任务了?有,事件相关的调度,和IO相关的调度

举个例子

栗子1:

有一名程序员,他叫小苏,他每天的事情是,他白天工作,晚上睡觉

原理:有一个浏览器,他叫Chrome,他每一帧的事情,分为JS计算(对应小苏白天工作),UI渲染(晚上睡觉)

栗子2: 小苏的工作根据优先级划分有4种

  1. 优先级最高的工作(项目已经延期了,需要立刻做)
  2. 高优先级的工作(还有250个工时)
  3. 正常优先级 (还有5000个工时)
  4. 低优先级 (还有10000个工时)

原理: React调度器任务根据优先级有4种

  1. ImmediatePriority (任务已经延期了,需要立即执行-1ms就过期)
  2. UserBlockingPriority (还有250ms延期)
  3. NormalPriority (还有5000ms延期)
  4. LowPriority (还有10000ms延期)

栗子3:

小苏早上看了看手头上的工作,找到了优先级最高的工作,项目A写1000个页面。

原理: React调度器工作时,找到当前优先级最高的任务,使用调和器计算出页面头部1000个DOM节点

栗子4:

小苏要做的项目A写1000个页面,这个工作已经延期了,那怎么办?白天干满12个小时,晚上加班接着干,不休息,直至把页面全写完。

原理:调度器调度的任务A计算头部的1000个DOM节点,这个任务已经过期了,那怎么办,在JS线程的5ms里,计算fiber, 本应该切换给UI线程渲染的时间片里,不要切换,继续计算fiber,直至把所有fiber都计算完,这时候才交给页面渲染,表现的效果就是页面卡住一段时间后,突然页面出现了很多dom节点

栗子5:

小苏要做的项目A写1000个页面,这个工作没有过期,是个不重要的工作,还有1万个工时,给他写页面。这个时候小苏就不着急了,他每天上班干嘛呢?

  1. 首先就是打卡,打完卡后,开始写页面,
  2. 写完一个页面,看一下当前时间减去打卡时间有没有超过8个小时,超过8个小时就下班走人,没超过就写下一个页面。

原理:调度器还有1000个任务,每个任务就是渲染一些新的内容。是个不重要的任务,还有1万毫秒,给他运行,这个时候调度器就不着急,他每一帧都干嘛了,

  1. 首先同样是打卡(源码里有这个函数performWorkUntilDeadline这个函数里currentTime = performance.now()) 记录了这一帧开始的时间,
  2. 然后开始干活(源码里搜scheduledHostCallback和workloop)
let currentTime
function workloop(initialTime){  // initialTime = performance.now()
    currentTime = initialTime // 开始干活了,打卡,之后会不停的比较当前时间和打卡时间
    currentTask = peek(taskQueue) // 小苏看看,来写哪一个页面
    if (shouldYieldToHost()){  // 如果干活满8小时了,就下班走人 
        break
    }
}

const yieldInterval = 5
// 再看看当前时间 是不大于之前记录的时间 加上 5ms 。
// 感受下,这是不是就像 小苏再看看现在几点了:是不是比早上打卡时间加上每天上班时间要长?如果是的话就下班,否则继续搬砖
function shouldYieldToHost(){
    return performance.now() >= currentTime + yieldInterval; 

}

通过上面几个例子我们大体上已经理解了调度器的基本思路,接下来我们从源码上,来找一找,我这样举例的依据

精简代码与逐行解释

以下我会用比较生活化的解释这些代码为什么这么设计

const channel = new MessageChannel();
const scheduledHostCallback = null
const taskQueue= []
const port = channel.port2;


channel.port1.onmessage = performWorkUntilDeadline;

// 这个是小苏安排活的老板
function unstable_scheduleCallback(priorityLevel, callback){
    // 在安排活之前先,把这个活的截止时间定好。要想定截止时间,得看,当前时间是啥时候,再看看这个活到底要干多久 所以
    // 1. 老板先看看当前时间
    var currentTime = performance.now()
    // 2. 老板看了下以往经验,什么工作量的活对应干多长时间。
    let timeoutMap = {
        ImmediatePriority: -1,
        UserBlockingPriority: 250,
        NORMAL_PRIORITY_TIMEOUT: 5000,
        LOW_PRIORITY_TIMEOUT: 10000,
        IDLE_PRIORITY: 1073741823
    }
    // 老板把活定好了
    let task = {
        callback,
        // 并且给这个活的截止时间也定好了
        expirationTime = startTime + timeoutMap[priorityLevel],
        priorityLevel,
    }
    // 老板把这个活安排给小苏了
    push(taskQueue, task)
    // flushWork内部是工作循环可以理解为: 给小苏安排好了一种叫flushWork的工作节奏(简单来说就是白天干活,晚上睡觉)所以说也可以安排别的工作方式,大家可以尝试一下,这个怎么写哦
    scheduledHostCallback = flushWork;
    //这个可以理解为: 今天先不着急。明天开始干
    port.postMessage(null);
}


// 这里是小苏自己每天自己的工作节奏哦,别和老板安排的scheduledHostCallback给搞混淆了哦
function performWorkUntilDeadline(){
    // 小苏先打卡
    let currentTime = performance.now()
    // startTime = currentTime
    // 看看老板安排的活还有没有没做完的
    const hasMoreWork = scheduledHostCallback(
        hasTimeRemaining,
        currentTime,
    );
    // 老板安排的活没了
    if (!hasMoreWork) {
        // 那老板给我安排的工作节奏scheduledHostCallback也没了,那就按照小苏自己的工作方式来了
        scheduledHostCallback = null;
    } else {
        port.postMessage(null);
    }
}

// 调度器内部这样写的,就贴过来了,其实可以忽略
function flushWork(hasTimeRemaining, currentTime){
    return workLoop(hasTimeRemaining, initialTime);
}

// 小苏工作循环:也就是打工人小苏 他的一天到底是怎样工作的。让我们来看看小苏的一天。
function workLoop(){
    // 小苏找到当前要干的活,可能是之前没干完的,也可能是老板临时插队进来的高优先级的活,总之先看看,今天要干啥
    currentTask = peek(taskQueue);
    //工作的前提: 今天只要有工作,就不停的工作,
    while(currentTask !== null){
        // 当前的工作没有过期
        // shouldYieldToHost 是看有没有下班的意思可以去前面看,逻辑就是看看当前时间减去早上来的时候打卡时间有没有超过一天的工作时长。
        // 所以这里的意思就是,只要当前的活没有过期,且下班时间到了,就下班走人,今天不干活了(break)
        if(currentTask.expirationTim>currentTime && shouldYieldToHost()) {
            break
        }
        const callback = currentTask.callback;
        // 过期了,所以会一直卡在while循环里,表示小苏今天别想下班了,老老实实把活干完了,在跳出while循环下班回家
        const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
        const continuationCallback = callback(didUserCallbackTimeout);
        if (typeof continuationCallback === 'function') {
            currentTask.callback = continuationCallback;
        }
    }    
    // 总的来说,工作完一天结束了。再总结下,今天的工作看看明天要干啥
    currentTask = peek(taskQueue);
    // 明天还有活,接着干
    if (currentTask !== null) {
        return true;
    // 明天没活了,好了,汇报给老板你所有的活都干完了。    
    } else {
        return false
    }
}

个人体会

总的来说,欧美人利用它们非常出色的抽象思维,利用非常生活化的思路,实现了调度器。我个人觉得,再抽象思维上,欧美人优于我,因为他们文字是表音文字,表音文字,同一个音可能会有多个意思,所以,为了防止逻辑混乱,必须要有非常严格的文字结构,也就间接的锻炼的欧美人的抽象思维,因为衍生出了,物理,化学,生物(浅谈则止,谈的多了,就是东西方哲学差异了)。而传统中国人的优势在于美感和全局,中国人的文字是表意文字,无需在意文字之间的结构顺序,大家都能理解,比如说,天净沙秋思,枯藤老树昏鸦,小桥流水人家,古道西风瘦马,夕阳西下,断肠人在天涯。短短10几个名词,就给我们带来一副生动的画卷,美而全,这是我们的天赋,这是欧美人不具备的。所以我们应该学习欧美人的抽象思维。也坚守自身的天赋。