Подробное объяснение исходного кода React Scheduler (1)

React.js
Следующий

Подробное объяснение исходного кода React Scheduler (2)

1. Введение

посколькуreact 16После выхода,react fiberЕсть много связанных статей, но большинство из них объясняютfiberструктура данных и дерево компонентовdiffКак перейти от рекурсии к обходу цикла. дляtime slicingВ описании обычно говорится, что использованиеrequestIdleCallbackЭтот API используется для планирования, но трудно найти подробное описание того, как планируются задачи.

Поэтому эта статья должна сделать это, шаг за шагом с точки зрения исходного кодаReact SchedulerЭто как добиться планирования задач.

Хотя названиеReact Scheduler, но содержание этой статьиreactне имеет значения, потому что планировщик задач на самом деле связан сreactНе беда, там просто описано, как выполнить какую-то задачу в нужное время, то есть даже если у вас нетreactОсновы также можно прочитать в этой статье.Если вы являетесь автором фреймворка, вы также можете узнать из этой статьиschedulerРеализация в собственной структуре для планирования задач.

  • В этой статье объясняется, чтоreact v16.7.0Исходный код версии, обратите внимание на своевременность.
  • исходный путьScheduler.js

2. Базовые знания

Далее, давайте взглянем на некоторые основы, которые вам нужно знать, чтобы прочитать эту статью.

1,window.performance.now

Это встроенные часы браузера, которые запускаются с момента загрузки страницы и возвращаются к текущему общему времени в единицахms. Это означает, что вы вызываете этот метод в консоли на 10-й минуте открытия страницы, и возвращается число около 600000 (неверно).

2,window.requestAnimationFrame

  • Этот метод должен быть очень распространенным, он позволяет нам вызывать указанную функцию в начале следующего кадра. Его выполнение следует частоте обновления системы.requestAnimationFrameМетод получает один параметр, функцию обратного вызова для выполнения. Эта функция обратного вызова будет передавать параметр по умолчанию, то есть продолжительность времени с момента открытия страницы до запуска функции обратного вызова в миллисекундах.

  • Можно понять, что система выполняет его непосредственно перед вызовом обратного вызоваperformance.now()Передано обратному вызову в качестве параметра. Таким образом, мы можем узнать текущее время выполнения при выполнении обратного вызова.

     requestAnimationFrame(function F(t) {
           console.log(t, '===='); //会不断打印执行回调的时间,如果刷新频率为60Hz,则相邻的t间隔时间大约为1000/60 = 16.7ms
           requestAnimationFrame(F)
       })
    
  • requestAnimationFrameЕсть функция, что когда обработка страницы не активирована,requestAnimationFrameостановит выполнение; когда страница станет активной позже,requestAnimationFrameОн будет продолжать выполняться с предыдущего места.

3.window.MessageChannel

Этот интерфейс позволяет нам создать новый канал сообщений и передать два егоMessagePort(port1,port2)Свойства отправляют данные. Пример кода выглядит следующим образом

    var channel = new MessageChannel();
    var port1 = channel.port1;
    var port2 = channel.port2;
    port1.onmessage = function(event){
        console.log(event.data)  // someData
    }
    port2.postMessage('someData')

Здесь следует отметить одну вещь,onmessageВремя вызова функции обратного вызова - после завершения краски кадра. НаблюдаетсяvueизnextTickтакже использоватьMessageChannelсделатьfallbackиз (предпочитаюsetImmediate).
react schedulerВнутри это используется для выполнения задач в оставшееся время после рендеринга кадра.

4, связанный список

По умолчанию все имеют базовое представление о связанных списках. Если у вас его нет, получите свои собственные знания.

Мы хотим представить здесь дважды циклический связанный список.

  • Двусвязный список означает, что каждый узел имеетpreviousа такжеnextДва атрибута, указывающих на передний и задний узлы соответственно.
  • Цикл означает, что следующий из последних узлов указывает на первый узел, а следующий узел первого узлаpreviousуказывает на последний узел, образуя кольцо人体蜈蚣.
  • Нам также нужна переменная firstNode для хранения первого узла.
  • Давайте на конкретном примере поговорим о вставке и удалении двусвязного списка. Предположим, есть группа людей, которым нужно выстроиться в очередь в соответствии с их возрастом.Дети стоят впереди, а взрослые сзади. Люди будут приходить снова и снова в процесс, и нам нужно подключить его к нужному месту. Если вы удалите его, вы будете рассматривать возможность удаления только главного человека каждый раз.
    //person的类型定义
    interface Person {
        name : string  //姓名
        age : number  //年龄,依赖这个属性排序
        next : Person  //紧跟在后面的人,默认是null
        previous : Person //前面相邻的那个人,默认是null
    }
    var firstNode = null; //一开始链表里没有节点
    
    //插入的逻辑
    function insertByAge(newPerson:Person){
        if(firstNode = null){
        
        //如果 firstNode为空,说明newPerson是第一个人,  
        //把它赋值给firstNode,并把next和previous属性指向自身,自成一个环。
          firstNode = newPerson.next = newPerson.previous = newPerson;
          
        } else { //队伍里有人了,新来的人要找准自己的位置
        
             var next = null; //记录newPerson插入到哪个人前边
             var person = firstNode; // person 在下边的循环中会从第一个人开始往后找
             
             do {
                  if (person.age > newPerson.age) {
                  //如果person的年龄比新来的人大,说明新来的人找到位置了,他恰好要排在person的前边,结束
                    next = person;
                    break;
                  }
                  //继续找后面的人
                  node = node.next;
            } while (node !== firstNode); //这里的while是为了防止无限循环,毕竟是环形的结构
            
            if(next === null){ //找了一圈发现 没有person的age比newPerson大,说明newPerson应该放到队伍的最后,也就是说newPerson的后面应该是firstNode。
                next = firstNode;
            }else if(next === firstNode){ //找第一个的时候就找到next了,说明newPerson要放到firstNode前面,这时候firstNode就要更新为newPerson
                firstNode = newPerson
            }
            
            //下面是newPerson的插入操作,给next及previous两个人的前后链接都关联到newPerson
            var previous = next.previous;
            previous.next = next.previous = newPerson; 
            newPerson.next = next;
            newPerson.previous = previous;
        }
        //插入成功
    }
    
    //删除第一个节点
    function deleteFirstPerson(){
        if(firstNode === null) return; //队伍里没有人,返回
        
        var next = firstNode.next; //第二个人
        if(firstNode === next) {
            //这时候只有一个人
            firstNode = null;
            next = null;
        } else {
            var lastPerson = firstNode.previous; //找到最后一个人
            firstNode = lastPerson.next = next; //更新新的第一人
            next.previout = lastPerson; //并在新的第一人和最后一人之间建立连接
        }
        
    }
    

из-заreact16Для записи данных используется большое количество связанных списков, особенноreact schedulerОперация внутренней задачи использует структуру дважды замкнутого связанного списка. Итак, поймите приведенный выше код, для пониманияreactПланирование задач станет проще.

3. Текст

Примечание. Чтобы разобраться в общем процессе работы, в следующем примере кода может быть небольшое количество удалений на основе исходного кода.

0. Несколько способов, которые не будут повторяться ниже

```
    getCurrentTime = function() {
        return performance.now();
        //如果不支持performance,利用 Date.now()做fallback
    }
```

1. Приоритет задачи

Приоритет определения задачи в React делится на 5 типов, чем меньше число, тем выше приоритет

   var ImmediatePriority = 1;  //最高优先级
   var UserBlockingPriority = 2; //用户阻塞型优先级
   var NormalPriority = 3; //普通优先级
   var LowPriority = 4; // 低优先级
   var IdlePriority = 5; // 空闲优先级

Эти 5 приоритетов поочередно соответствуют 5 срокам действия.

   // Max 31 bit integer. The max integer size in V8 for 32-bit systems.
   // Math.pow(2, 30) - 1
   var maxSigned31BitInt = 1073741823;

   // 立马过期 ==> ImmediatePriority
   var IMMEDIATE_PRIORITY_TIMEOUT = -1;
   // 250ms以后过期
   var USER_BLOCKING_PRIORITY = 250;
   //
   var NORMAL_PRIORITY_TIMEOUT = 5000;
   //
   var LOW_PRIORITY_TIMEOUT = 10000;
   // 永不过期
   var IDLE_PRIORITY = maxSigned31BitInt;

Когда каждая задача добавляется в связанный список, она проходитperformance.now() + timeoutЧтобы получить время истечения этой задачи, с течением времени текущее время будет все ближе и ближе к времени истечения, поэтому чем меньше время истечения, тем выше приоритет. Если время истечения меньше текущего времени, это означает, что срок действия задачи истек, и она не была выполнена, и ее нужно выполнить немедленно (asap).

надmaxSigned31BitInt, вы можете знать из аннотации, что это32битовая системаV8Самое большое целое число в движке.reactиспользовать это, чтобы сделатьIdlePriorityВремя окончания срока действия.

По грубому расчету это время составляет примерно12.427небо. То есть в крайних случаях ваша веб-страницаtabЕсли его можно держать открытым до 12 с половиной дней, срок действия задания может истечь.

2,function scheduleCallback()

  • Метод в коде называетсяunstable_scheduleCallback, что означает, что ток все еще нестабилен, вотscheduleCallbackкак имя.
  • Функция этого метода состоит в том, чтобы сортировать задачи со временем истечения в качестве приоритета, и процесс аналогичен процессу работы дважды кругового связанного списка выше.

код ниже

   function scheduleCallback(callback, options? : {timeout:number} ) {
       //to be coutinued
   }

Этот метод имеет два входных параметра, первый должен быть выполненcallback, временно можно понимать как задачу. Второй параметр является необязательным, и можно передать тайм-аут, чтобы определить, как долго задача истекла. Если он не пройден, время истечения будет определяться в соответствии с указанным выше приоритетом задачи.

  //这是一个全局变量,代表当前任务的优先级,默认为普通
  var currentPriorityLevel = NormalPriority
  
  function scheduleCallback(callback, options? : {timeout:number} ) {
      var startTime = getCurrentTime()
      if (
          typeof options === 'object' &&
          options !== null &&
          typeof options.timeout === 'number'
        ){
          //如果传了options, 就用入参的过期时间
          expirationTime = startTime + options.timeout;
        } else {
          //判断当前的优先级
          switch (currentPriorityLevel) {
            case ImmediatePriority:
              expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
              break;
            case UserBlockingPriority:
              expirationTime = startTime + USER_BLOCKING_PRIORITY;
              break;
            case IdlePriority:
              expirationTime = startTime + IDLE_PRIORITY;
              break;
            case LowPriority:
              expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
              break;
            case NormalPriority:
            default:
              expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
          }
        }
        
        //上面确定了当前任务的截止时间,下面创建一个任务节点,
        var newNode = {
          callback, //任务的具体内容
          priorityLevel: currentPriorityLevel, //任务优先级
          expirationTime, //任务的过期时间
          next: null, //下一个节点
          previous: null, //上一个节点
        };
      //to be coutinued
  }

Приведенный выше код определяет ток в соответствии с входными параметрами или текущим приоритетом.callbackвремя истечения срока действия и создать реальный узел задачи. Далее этот узел будетexpirationTimeСортировка вставляется в связанный список задач.

   // 代表任务链表的第一个节点
   var firstCallbackNode = null;
   
   function scheduleCallback(callback, options? : {timeout:number} ) {
       ...
       var newNode = {
           callback, //任务的具体内容
           priorityLevel: currentPriorityLevel, //任务优先级
           expirationTime, //任务的过期时间
           next: null, //下一个节点
           previous: null, //上一个节点
       };
       // 下面是按照 expirationTime 把 newNode 加入到任务队列里。参考基础知识里的person排队的例子
       
       if (firstCallbackNode === null) {
           firstCallbackNode = newNode.next = newNode.previous = newNode;
           ensureHostCallbackIsScheduled(); //这个方法先忽略,后面讲
       } else {
           var next = null;
           var node = firstCallbackNode;
           do {
             if (node.expirationTime > expirationTime) {
               next = node;
               break;
             }
             node = node.next;
           } while (node !== firstCallbackNode);

       if (next === null) {
         next = firstCallbackNode;
       } else if (next === firstCallbackNode) {
         firstCallbackNode = newNode;
         ensureHostCallbackIsScheduled(); //这个方法先忽略,后面讲
       }
   
       var previous = next.previous;
       previous.next = next.previous = newNode;
       newNode.next = next;
       newNode.previous = previous;
     }
   
     return newNode;
       
   }
  • Приведенная выше логика, кромеensureHostCallbackIsScheduledЭто логика вставки дважды кругового связанного списка, упомянутого выше.
  • На этом, как определить время истечения срока действия новой входящей задачи и как вставить ее в существующую очередь задач, закончено.
  • На данный момент у нас не может не возникнуть вопрос, мы расставили задачи по времени истечения, так когда задачи будут выполняться?
  • Ответ заключается в том, что есть два случая: 1 – когда добавляется первый узел задачи, чтобы начать выполнение задачи, и 2 – когда вновь добавленная задача заменяет предыдущий узел и становится новым первым узлом. Потому что 1 означает, что задача начинается с нуля и должна запускаться немедленно. 2 означает, что есть новая задача с наивысшим приоритетом, задача, которая должна была выполняться до этого, должна быть остановлена, а выполнение новой задачи должно быть перезапущено.
  • Вышеуказанные две ситуации соответствуютensureHostCallbackIsScheduledДве ветви выполнения метода. Итак, мы уже должны знать,ensureHostCallbackIsScheduledОн используется для запуска выполнения задачи в нужное время.
  • Что именно является подходящим временем? Его можно описать как время простоя после отрисовки каждого кадра. Это гарантирует, что частота отрисовки браузером каждого кадра соответствует частоте обновления системы и не будет пропускать кадры.

Затем нам нужно реализовать такую ​​функцию, как выполнять функцию в нужное время.

3 requestIdleCallback pollyfill

А пока, пожалуйста, забудьте на время об очереди задач выше и подумайте, как сделать что-нибудь в простое браузера после отрисовки каждого кадра.

ответ может бытьrequestIdleCallback, но по какой-то причине команда реагирования отказалась от этого API в пользу использованияrequestAnimationFrameа такжеMessageChannel pollyfillвзял одинrequestIdleCallback

1,function requestAnimationFrameWithTimeout()

Сначала введите суперфункцию, код выглядит следующим образом

    var requestAnimationFrameWithTimeout = function(callback) {
      rAFID = requestAnimationFrame(function(timestamp) {
        clearTimeout(rAFTimeoutID);
        callback(timestamp);
      });
      rAFTimeoutID = setTimeout(function() {
        cancelAnimationFrame(rAFID);
        callback(getCurrentTime());
      }, 100);
    }

Что означает этот код?

  • когда мы звонимrequestAnimationFrameWithTimeoutи пройти вcallbackКогда начнетсяrequestAnimationFrameс однимsetTimeout, оба будут выполнятьcallback. Но из-заrequestAnimationFrameПриоритет выполнения относительно высок, и он будет внутренне вызыватьclearTimeoutОтмените работу таймера ниже. так на страницеactiveпроизводительность в случаеrequestAnimationFrameсогласуется.

  • В этот момент каждый должен понять, что базовые знания в начале гласили:requestAnimationFrameНе работает, когда страница переключается в неактивную, тогдаrequestAnimationFrameWithTimeoutэквивалентно запуску100msТаймер берет на себя выполнение задачи. Эта частота выполнения не является ни высокой, ни низкой, что не только не влияет на энергопотребление процессора, но и обеспечивает эффективное выполнение задачи.

  • Ниже мы рассмотрим на данный моментrequestAnimationFrameWithTimeoutЭквивалентноrequestAnimationFrame

(Неосознанно длина страницы уже такая большая. Я напишу это здесь сегодня и обновлю, когда у меня будет возможность в следующий раз.)