老大react说:schedule,我们今年的小目标是一个亿

老大react说:schedule,我们今年的小目标是一个亿

hello,这里是潇晨,今天来讲个故事

讲个故事:

从前,有家z公司,z公司的ceo叫react,它收下有个小弟或者叫小leader,schedule

schedule每天负责消化老大react画的大饼,然后将拆解成一个个小小的task,给下面的小弟去完成,并且负责划分优先级,调度小弟们的任务排序。

schedule怎样给这些任务划分优先级呢,它想了个最简单的办法,用deadline或者过期时间,给这些task划分优先级,过期时间越短说明这个任务越紧急,赶紧分配苦力(下面的小弟)去完成,过期时间越长,说明这个task越不紧急,可以以后慢慢干,还有一类task已经过了它的deadline,这个过期的任务优先级最高,没办法,延期之后也是要完成的,可怜了程序员小哥哥了。

image-20211129163824936

于是小leader,scheduler把老板的饼掰碎了,然后给这些小task按照deadline排了个优先级,于是程序员小哥哥开始接任务了

程序员小哥哥A接受了task1task2,于是他给自己排了个任务清单,按照优先级先做task1,然后做task2,于是小哥进入密集开发中(render阶段),正在做task1

image-20211129163900829

但是天又不测风云,老板根据业务的需要,给scheduler下达了一个非常紧急的需求,苦了程序员小哥了,scheduler说,唉,没办法呀,加加班,把这个非常紧急的需求现插个队吧,程序员小哥单线程的,每次只能做一个task,于是插个队,加加班做最紧急的需求task0吧。

image-20211129163918924

接下来就是密集的加班中。。。(这一阶段称为render阶段)

终于在不屑的努力下,最终程序员小哥还是加班加点的把所有任务完成了,交给测试验证(commit阶段),

image-20211129164136855

以上情况是有紧急任务时候的打断,还有一种情况是老板给的大饼很难消化,但是这个task2还没到达deadline,程序员小哥在做这个任务的时候遇到了困难,所以就先放一放吧,反正是个艰巨的任务,在空闲的时候在做吧,先完成优先级高的task0task1,有时间在做task2

image-20211129171041192

进入正题:

当我们在类似下面的搜索框组件进行搜索时会发现,组件分为搜索部分和搜索结果展示列表,我们期望输入框能立刻响应,搜素列表可以有等待的时间,如果搜索列表数据量很大,在进行渲染的时候,我们又输入了一些文字,因为用户输入事件的优先级是很高的,所以就要停止结果列表的渲染,这就引出了不同任务之间的优先级和调度

react源码15.5

Scheduler

我们知道如果应用占用较长的js执行时间,比如超过了设备一帧的时间,那么设备的绘制就会出现不流畅的现象。

Scheduler主要的功能是时间切片和调度优先级,react在对比节点差异的时候会占用一定的js执行时间,Scheduler内部借助MessageChannel实现了在浏览器绘制之前指定一个时间片,如果react在指定时间内没执行完差异的对比,Scheduler就会强制交出执行权给浏览器

react源码15.3

时间切片

在浏览器的一帧中js的执行时间如下
react源码15.1
`requestIdleCallback`是在浏览器重绘重排之后,如果还有空闲就可以执行未完成的任务,所以为了不影响重绘重排,可以在浏览器在`requestIdleCallback`中执行耗性能的计算,但是由于`requestIdleCallback`存在兼容和触发时机不稳定的问题,`scheduler`中采用`MessageChannel`来实现`requestIdleCallback`,如果当前环境不支持`MessageChannel`就采用`setTimeout`。 

在`performUnitOfWork`(`render`阶段的起点)之后会执行render阶段和`commit`阶段,如果在浏览器的一帧中,`cup`的计算还没完成,就会让出js执行权给浏览器,这个判断在`workLoopConcurrent`函数中,`shouldYield`就是用来判断剩余的时间有没有用尽。在源码中每个时间片时5ms,这个值会根据设备的`fps`调整。
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {//如果fiber链还没遍历完,没有被暂停或者打断
    performUnitOfWork(workInProgress);//执行render阶段
  }
}
function forceFrameRate(fps) {//计算时间片
  if (fps < 0 || fps > 125) {
    console['error'](
      'forceFrameRate takes a positive int between 0 and 125, ' +
        'forcing frame rates higher than 125 fps is not supported',
    );
    return;
  }
  if (fps > 0) {
    yieldInterval = Math.floor(1000 / fps);
  } else {
    yieldInterval = 5;//时间片默认5ms
  }
}

任务的暂停

shouldYield函数中有一段,所以可以知道,如果当前时间大于任务开始的时间+yieldInterval,就打断了任务的进行。

//deadline = currentTime + yieldInterval,deadline是在performWorkUntilDeadline函数中计算出来的
if (currentTime >= deadline) {
  //...
    return true
}

调度优先级

    在`Scheduler`中有两个函数可以创建具有优先级的任务
  • runWithPriority:以一个优先级执行callback,如果是同步的任务,优先级就是ImmediateSchedulerPriority

    function unstable_runWithPriority(priorityLevel, eventHandler) {
      switch (priorityLevel) {//5种优先级
        case ImmediatePriority:
        case UserBlockingPriority:
        case NormalPriority:
        case LowPriority:
        case IdlePriority:
          break;
        default:
          priorityLevel = NormalPriority;
      }
    
      var previousPriorityLevel = currentPriorityLevel;//保存当前的优先级
      currentPriorityLevel = priorityLevel;//priorityLevel赋值给currentPriorityLevel
    
      try {
        return eventHandler();//回调函数
      } finally {
        currentPriorityLevel = previousPriorityLevel;//还原之前的优先级
      }
    }
    
  • scheduleCallback:以一个优先级注册callback,在适当的时机执行,因为涉及过期时间的计算,所以scheduleCallbackrunWithPriority的粒度更细。

    • scheduleCallback中优先级意味着过期时间,优先级越高priorityLevel就越小,过期时间离当前时间就越近,var expirationTime = startTime + timeout;例如IMMEDIATE_PRIORITY_TIMEOUT=-1,那var expirationTime = startTime + (-1);就小于当前时间了,所以要立即执行。

    • scheduleCallback调度的过程用到了小顶堆,所以我们可以在O(1)的复杂度找到优先级最高的task,不了解可以查阅资料,或者查阅我的leetcode算法精讲系列,在源码中小顶堆存放着任务,每次peek都能取到离过期时间最近的task

    • scheduleCallback中,未过期任务task存放在timerQueue中,过期任务存放在taskQueue中。

      新建`newTask`任务之后,判断`newTask`是否过期,没过期就加入`timerQueue`中,如果此时`taskQueue`中还没有过期任务,`timerQueue`中离过期时间最近的task正好是`newTask`,则设置个定时器,到了过期时间就加入`taskQueue`中。
      
      当`timerQueue`中有任务,就取出最早过期的任务执行。
      
function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime();

  var startTime;//开始时间
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority://优先级越高timeout越小
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;//-1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;//250
      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;//优先级越高 过期时间越小

  var newTask = {//新建task
    id: taskIdCounter++,
    callback//回调函数
    priorityLevel,
    startTime,//开始时间
    expirationTime,//过期时间
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) {//没有过期
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);//加入timerQueue
    //taskQueue中还没有过期任务,timerQueue中离过期时间最近的task正好是newTask
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      //定时器,到了过期时间就加入taskQueue中
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);//加入taskQueue
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);//执行过期的任务
    }
  }

  return newTask;
}
react源码15.2

视频讲解(高效学习):点击学习

往期react源码解析文章:

1.开篇介绍和面试题

2.react的设计理念

3.react源码架构

4.源码目录结构和调试

5.jsx&核心api

6.legacy和concurrent模式入口函数

7.Fiber架构

8.render阶段

9.diff算法

10.commit阶段

11.生命周期

12.状态更新流程

13.hooks源码

14.手写hooks

15.scheduler&Lane

16.concurrent模式

17.context

18事件系统

19.手写迷你版react

20.总结&第一章的面试题解答

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容