Exploring the inner of React -- postMessage & Scheduler

Keywords: Javascript React Attribute less

postMessage & Scheduler

Write in front

  • This article contains a certain amount of source code explanation, in which the author writes some contents to replace the official notes (that is, writing is almost equal to not writing that kind). If readers prefer the original code,

Moveable step Official warehouse , read in combination. For this reason, the reading experience of horizontal screen or PC may be better (the code may need to slide left and right)

  • This paper does not explicitly involve the content of react fiber adjuster and Algebraic Effects, but in fact, they are closely related. It can be understood that the content of this paper is the cornerstone of realizing the former two.

Interested readers can move <Fiber & Algebraic Effects> Do some pre reading.

start

Last year, on September 27, 2019 release In the Scheduler, React starts a new scheduling task scheme test:

  • The old scheme: align the task scheduling with the frame through the requestAnimationFrame (hereinafter referred to as cAF, and the related requestIdleCallback referred to as rIC)
  • New scheme: scheduling tasks by high frequency (short interval) call postMessage

Emm x1... All of a sudden, a lot of problems
So let's explore what happened in this "little" release

chance

Through the commit-message We summarize the following:

  1. Since the rAF relies on the refresh rate of the display, you need to see the face of vsync cycle when using the rAF
  2. In order to execute as many tasks as possible in each frame, the message event with 5ms interval is used to initiate the scheduling, that is, the postMessage mode
  3. The main risk of this scheme is that more frequent scheduling of tasks will intensify the resource contention between the main thread and other browser tasks
  4. Compared with rAF and setTimeout, it needs to be further determined how much the browser throttles the message events under the background tag. The experiment assumes that it has the same priority as the timer

In short, it is to give up the frame alignment strategy composed of two API s, namely, rAF and rIC, to control the scheduling frequency artificially, improve the task processing speed, and optimize the performance of React runtime

postMessage


So, what is postMessage? Does it refer to postMessage in iframe communication mechanism?

No, it's right

Emm x2... Okay, it's a little riddle. Let's solve it

incorrect

That's not true, because postMessage itself is initiated by an object created by the MessageChannel interface

The MessageChannel interface of the Channel Message API allows us to create a new message channel and communicate through the two messageports of the channel

This channel also works for web workers - so it's useful
Let's see how it communicates:

const ch = new MessageChannel()

ch.port1.onmessage = function(msgEvent) {
  console.log('port1 got ' + msgEvent.data)
  ch.port1.postMessage('Ok, r.i.p Floyd')
}

ch.port2.onmessage = function(msgEvent) {
  console.log(msgEvent.data)
}

ch.port2.postMessage('port2!')

// Output:
// port1 got port2!
// Ok, r.i.p Floyd.

Very simple, nothing special
Emm x3...
Ah... I seldom use it directly. How about its compatibility?



Oh! Although it's 10, IE can be all green!

Yeah

In fact, the communication between iframe and parent document in modern browsers is the message channel used. You can even:

// Suppose < iframe id = "childframe" SRC = "XXX" / >

const ch = new MessageChannel()
const childFrame = document.querySelector('#childFrame')

ch.port2.onmessage = function(msgEvent) {
  console.log(msgEvent.data)
  console.log('There\'s no father exists ever')
}

childFrame.contentWindow.postMessage('Father I can\'t breathe!', '*', [ch.port2])

// Output:
// Father I can't breathe
// There's no father exists ever

Well, we already know what this postMessage is. Let's see how it works

work

Before we talk about how postMessage works, let's talk about the Scheduler

Scheduler

Scheduler is a package developed by the React team for transaction scheduling, which is built into the React project. The vision of the team is to make the package independent of React and become a more widely used tool after incubation
What we are going to explore next is all within the scope of this package

Message channel found

In the source code of the Scheduler, by searching for the word "postMessage", we can easily focus on SchedulerHostConfig.default.js In the document, we cut off part of the content:

In the full source code, there is an if else branch to implement two different sets of API s. For non DOM or no message channel JavaScript environment (such as JavaScript core), the following content is implemented by setTimeout. Students who are interested in it can take a look at a quite simple Hack. This article will not elaborate, but only focus on the source code under else branch.
The above is also the reason why this file is called xxxConfig. It really has configuration logic

const performWorkUntilDeadline = () => {
    if (scheduledHostCallback !== null) {
      const currentTime = getCurrentTime();
      // Yield after `yieldInterval` ms, regardless of where we are in the vsync
      // cycle. This means there's always time remaining at the beginning of
      // the message event.
      deadline = currentTime + yieldInterval;
      const hasTimeRemaining = true;
      try {
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        if (!hasMoreWork) {
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
        } else {
          // If there's more work, schedule the next message event at the end
          // of the preceding one.
          port.postMessage(null);
        }
      } catch (error) {
        // If a scheduler task throws, exit the current browser task so the
        // error can be observed.
        port.postMessage(null);
        throw error;
      }
    } else {
      isMessageLoopRunning = false;
    }
    // Yielding to the browser will give it a chance to paint, so we can
    // reset this.
    needsPaint = false;
  };

  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;

  requestHostCallback = function(callback) {
    scheduledHostCallback = callback;
    if (!isMessageLoopRunning) {
      isMessageLoopRunning = true;
      port.postMessage(null);
    }
  };

The logic of this line of code is very simple:

  1. Define a MessageChannel named channel, and define a port variable to point to its port2 port
  2. Use the predefined performWorkUntilDeadline method as the message event processing function of the port1 port of the channel
  3. In requestHostCallback, call the previously defined port variable, that is, the port2 port of channel -- the postMessage method on the send message.
  4. The performWorkUntilDeadline method starts working

Now, let's take a look at the elements in this small piece of code

requestHostCallback (hereinafter referred to as rHC)

Remember rAF and rIC? They used to be the core API of scheduling mechanism, so since rHC looks like them, it must be the one on duty now
Indeed, let's go inside the code body and try:

requestHostCallback = function(callback) {
    // Assign the incoming callback to scheduledHostCallback
    // Use the analogy 'requestanimationframe (() = > {/ * dosomething * /})',
    // We can infer that scheduledHostCallback is the currently scheduled task
    scheduledHostCallback = callback;
  
      // Ismessageloopranning flag whether the current message cycle is on
    // What's the message loop for? Is to constantly check whether there is any new news - that is, new tasks
    if (!isMessageLoopRunning) {
      // If the current message loop is off, the rHC has the power to turn it on
      isMessageLoopRunning = true;
      // After opening, the port2 port of the channel will receive a message, that is, it will start performing work until deadline
      port.postMessage(null);
    } // What happens to else?
  };

Now we know that the role of rHC is to:

  • Prepare the task to be performed currently (scheduledHostCallback)
  • Turn on message cycle scheduling
  • Call performWorkUntilDeadline

performWorkUntilDeadline

Now, it seems that rHC is engaged in business, and performWorkUntilDealine is engaged in business
It's true that we go directly into the body of the code to taste:

const performWorkUntilDeadline = () => {
      // [A] : first check if the current scheduledHostCallback exists
    // In other words, is there anything that needs to be done
    if (scheduledHostCallback !== null) {
      const currentTime = getCurrentTime();
      // Ah, deadline!
      // It seems that as of yield interval, how much is it?
      // According to the previous content, it should be 5ms. We will verify it later
      deadline = currentTime + yieldInterval;
      // Well, the fresh deadline, in other words, how much time is left
      // With the definition of the remaining time shown, no matter what node we are in the vsync cycle, we have time when we receive the message (task)
      const hasTimeRemaining = true; // The word time remaining is reminiscent of rIC
      try {
        // Well, it seems that this scheduledHostCallback is not simple. We will study it later
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        if (!hasMoreWork) {
            // If the last task is completed, close the message loop and clean the references to the scheduledHostCallback
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
        } else {
          // [C] : if there are still tasks to be done, continue to send messages to the port2 port of the channel with the port
          // Obviously, this is a recursive operation
          // So, if there is no task, obviously it will not come here. Why judge the scheduledHostCallback? look-behind 
          port.postMessage(null);
        }
      } catch (error) {
        // If the current task is executed except for the fault, the next task will be executed and an error will be thrown
        port.postMessage(null);
        throw error;
      }
    } else {
      // [B] : it's OK to do it, so there's no need to check the messages in a circular way
      isMessageLoopRunning = false;
    }
    // Yielding to the browser will give it a chance to paint, so we can
    // reset this.
    needsPaint = false;
  };

Now it's much clearer. Let's use a schematic diagram to show:

Two dashed arrows represent the reference relationship. According to the analysis in the code, we can now know that all task scheduling is initiated by the port - the port2 port of the channel - by calling the postMessage method. Whether this task is to be executed seems to be related to yield interval and hasTimeRemaning. Let's see them:

  • Yield interval: in the complete source code, there are two places:
// It's defined as 5ms. It's not discussed at all
const yieldInterval = 5

// however
// This method is actually a public API provided by the Scheduler package to developers,
// Allows developers to set scheduling intervals based on different device refresh rates
// In fact, it is the consideration of adjusting measures to local conditions

forceFrameRate = function(fps) {
      // Up to 125 fps
    // My (pretended to have) 144hz electric competition screen has been offended
    if (fps < 0 || fps > 125) {
      // Using console['error'] to evade Babel and ESLint
      console['error'](
        'forceFrameRate takes a positive int between 0 and 125, ' +
          'forcing framerates higher than 125 fps is not unsupported',
      );
      return;
    }
    if (fps > 0) {
      yieldInterval = Math.floor(1000 / fps);
    } else {
      // Obviously, if there is no transmission or a negative transmission, it will be reset to 5ms, which improves some robustness
      // reset the framerate
      yieldInterval = 5;
    }
  };
  • hasTimeRemaning: refer to the general usage of rIC:
function doWorks() {
  // todo
}

function doMoreWorks() {
     // todo more 
}

function todo() {
      requestIdleCallback(() => {
      // The most important thing is to have time
           if (e.timeRemaining()) {
        doMoreWorks()
      }
   })
   doWorks()
}

Emm x4... There are two questions marked in red in the picture above:

  • what happened? : in fact, this place is to provide a new scheduledHostCallback for performWorkUntilDeadline. In this way, performWorkUntilDeadline "always has something to do" until no more tasks are registered by rHC
  • But How? Next, let's answer this question. Everything starts with the Scheduler

Scheduler

Aha, this time we gave the Scheduler a bigger title to show its leading role 🐶 ...
This time, we start from the entrance, step by step, and return to the But How? This question goes up

Again in front

  • According to the README file of the scheduler, its current API is not the final solution, so its entry file Scheduler.js The exposed interfaces are all unstable d_ Prefix. For simplicity, the following descriptions of interface names omit the prefix
  • The source code also contains some profiling related logic, which is mainly used to assist debugging and auditing, and has little to do with the operation mode. Therefore, the following will ignore these contents and focus on the interpretation of the core logic

scheduleCallback -- give the task to the Scheduler

Our journey starts from this interface, which is the key to the Scheduler magic 🔑 ~
This interface is used to register a callback function -- that is, the task we want to execute -- into the task queue of the Scheduler according to the given priority and additional settings, and start task scheduling:

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime(); // [A] How does getCurrentTime get the current time?

  var startTime; // The callback function is given a start time and is delayed according to the delay defined in options
  // Given a timer for the callback function, and according to the timeout definition in options, determine whether to directly use custom or use the timeoutForPriorityLevel method to output the time interval
  // [B] : so what does timeoutForPriorityLevel do?
  var timeout;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
    timeout =
      typeof options.timeout === 'number'
        ? options.timeout
        : timeoutForPriorityLevel(priorityLevel); // [C] Where does this priority level come from?
  } else {
    timeout = timeoutForPriorityLevel(priorityLevel);
    startTime = currentTime;
  }
  
  // Define an expiration time, which will be encountered later
  var expirationTime = startTime + timeout;

  // Ah, from here we can see what a task looks like in the Scheduler
  var newTask = {
    id: taskIdCounter++, // Scheduler.js  A taskIdCounter is defined as the producer of taskId globally in
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,  // [D] : I've seen all the previous ones. Is this sortIndex used for sorting?
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) {
    // Remember the delay attribute in options, which gives the possibility that the task start time is greater than the current time
    // Well, the previous definition sortIndex reappears, in this case it is assigned as startTime,
    newTask.sortIndex = startTime;
    // [E] : a timer queue appears here
    // If the start time is greater than the current time, push it into the timer queue
    // Obviously, for tasks to be performed in the future, it is necessary to put them in a "to be activated" queue
    push(timerQueue, newTask);
    // The logic here will be discussed later, entering the else branch first
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // expirationTime is the value of sortIndex. Logically, it can be confirmed that sortIndex is used for sorting
    newTask.sortIndex = expirationTime;
    // [F] This time, the task is push ed into the task queue. It seems that the timer queue and the task queue are isomorphic?
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Logically, this is to determine whether the current process is in progress, that is, whether the performWorkUntilDeadline is in a recursive execution state. If not, turn on the schedule
    // [G] Emm x5... What's this flushWork for?
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

ok, let's break down some problems marked with [X] in the above notes to make the function more solid:

  • A: How does getCurrentTime get the current time?

    • Solution: as mentioned before schedulerHostConfig.default.js According to the performance object and performance.now Whether the method exists, distinguish whether it is used Date.now Still use performance.now To get the current time, because the latter is more accurate and absolute than the former. For details, please refer to here
  • B C: let's take a look directly Scheduler.js The contents of the timeoutForPriorityLevel method in are as follows:
// ...other code
var maxSigned31BitInt = 1073741823;

/**
 * The following variables are globally defined, equivalent to system constants (environment variables)
 */
// Immediate execution
// Obviously, if you don't define deley, according to the logic immediately following the [B] comment, expirationTime is equal to currentTime - 1
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// In the future, you must enter else branch and push to task queue to enter performWorkUntilDealine immediately
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// The lowest priority never seems to be timed out. Let's see later when it will be executed
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

// ...other code

// As you can see, priorityLevel is also obviously constant by the system
function timeoutForPriorityLevel(priorityLevel) {
  switch (priorityLevel) {
    case ImmediatePriority:
      return IMMEDIATE_PRIORITY_TIMEOUT;
    case UserBlockingPriority:
      return USER_BLOCKING_PRIORITY_TIMEOUT;
    case IdlePriority:
      return IDLE_PRIORITY_TIMEOUT;
    case LowPriority:
      return LOW_PRIORITY_TIMEOUT;
    case NormalPriority:
    default:
      return NORMAL_PRIORITY_TIMEOUT;
  }
}

// ...other code

Where priorityLevel is defined in schedulerPriorities.js , very intuitive:

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

// Aha, in the future, we may use symbols,
// In that case, is it necessary to abstract a rule for size comparison?
// TODO: Use symbols?
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

It seems that the timing of task execution is determined by current time, delay and priority_ PRIORITY_ The increment of time length is determined by shedulerPriorities.js Each value in

  • CDE: these three points are very relevant, so they are put together directly

    • sortIndex: sort index. According to the previous content and [B] explanation, we can know that the value of this attribute is either startTime or expirationTime. Obviously, the smaller it is, the earlier it is. Therefore, sorting with this value will inevitably rank the priority of tasks
    • timerQueue and taskQueue: Well, sortIndex must be used to sort in these two isomorphic queues_ Seeing this, students familiar with the data structure should have guessed that the data structure of these two queues may be the standard solution for handling priority transactions - the minimum priority queue_

Sure enough, we traced the push method to a method called schedulerMinHeap.js The minimum priority queue is based on Min heap. Let's see later what push has done to the queue.

  • F:  flushWork! Listen to this name is very smooth, right? This name has told us very well, it is to deal with all the current tasks one by one! How does it do it? Leave a suspense and jump out of scheduleCallback first

Minimum heap

In essence, the minimum heap is a complete binary tree. After sorting, the element values of all non terminal nodes are not greater than their left and right nodes, which is as follows:

principle

The scheduler uses arrays to implement the minimum heap. Now let's simply analyze its working principle

PUSH

We push an element with a value of 5 into the above minimum heap, and its workflow is as follows:

As you can see, during the push process, we call the siftUp method to arrange the element with a value of 5 to the desired position, and it becomes the tree on the right. The relevant codes are as follows:

type Heap = Array<Node>;
type Node = {|
  id: number,
  sortIndex: number,
|};

export function push(heap: Heap, node: Node): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

function siftUp(heap, node, i) {
  let index = i;
  while (true) {
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    if (parent !== undefined && compare(parent, node) > 0) {
      // The parent is larger. Swap positions.
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // The parent is smaller. Exit.
      return;
    }
  }
}

function compare(a, b) {
  // Compare sort index first, then task id.
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

It can be seen that in siftUp, the calculation of the position of the parent node is also optimized by using the shift operator (> > 1 is equivalent to dividing by 2 and then removing the tail), so as to improve the calculation efficiency

POP

Then, we need to take an element from it (in the Scheduler, a task is scheduled to execute). The workflow is as follows:

When we take out the first element, that is, the lowest value and the highest priority, the tree loses its top, so it is bound to need to reorganize its branches and leaves. The siftDown method is used to reorganize the remaining elements, so that they remain a minimum heap. The relevant codes are as follows:

export function pop(heap: Heap): Node | null {
  const first = heap[0];
  if (first !== undefined) {
    const last = heap.pop();
    if (last !== first) {
      heap[0] = last;
      siftDown(heap, last, 0);
    }
    return first;
  } else {
    return null;
  }
}

function siftDown(heap, node, i) {
  let index = i;
  const length = heap.length;
  while (index < length) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    // If the left or right node is smaller, swap with the smaller of those.
    if (left !== undefined && compare(left, node) < 0) {
      if (right !== undefined && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (right !== undefined && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // Neither child is smaller. Exit.
      return;
    }
  }
}

The combination of Emm x5... And the code in the PUSH part is the standard implementation of a minimum heap
The rest of the land, SchedulerMinHeap.js A peek method is also provided in the source code to view the top elements:

export function peek(heap: Heap): Node | null {
  const first = heap[0];
  return first === undefined ? null : first;
}

Its function is obviously to take the first element, peek peek ~ we will meet it soon

flushWork

Now, let's see how the Scheduler flush es all tasks:

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // [A] Why reset these States?
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  // [B] : logically speaking, when the task itself does not throw an error, flushWork returns the result of workLoop. What does workLoop do?
  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if (currentTask !== null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        throw error;
      }
    } else {
      // This official note is specially left. It tells us that in the production environment, flushWork will not catch the errors thrown in the workloop,
           // Because in the development mode or debugging process, this error will generally cause a white page and give the developer a hint. Obviously, this function cannot affect the user
      // No catch in prod codepath.
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    // Terminate the current schedule if a task execution error occurs
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

Now let's analyze ABC in this code~

  • A: Why reset these States?

Since rHC does not necessarily execute the incoming callback function immediately, the isHostCallbackScheduled state may be maintained for a period of time; when flushWork starts to process tasks, it needs to release the state to support other tasks to be scheduled in; the same is true for isHostTimeoutScheduled. We will soon encounter what kind of timeout this is

  • B: workLoop, Emm x6... It's almost the end of this journey. Like a series of novels, this method will answer many questions

workLoop

As the name implies, this method must contain a loop for processing tasks, so what happens in this loop?

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // [A] What is this method for?
  advanceTimers(currentTime);
  // peek the task at the top of the task queue
  currentTask = peek(taskQueue);
  // As long as the currentTask exists, the loop will continue
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // dealine arrived, but the current task has not expired, so let it execute again in the next scheduling cycle
      // [B] How is it judged by shouldYieldToHost?
      break;
    }
    const callback = currentTask.callback;
    if (callback !== null) {
      // If the callback is not null, the current task is available
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      // Judge whether the current task is overdue
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      markTaskRun(currentTask, currentTime);
      // [C]: continuationCallback? What's the meaning of this? Let the task continue?
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
          // It seems that if the continuationCallback is established, it will replace the current callback
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        // If the continuationCallback fails, the current task will pop,
        // Logically, it should judge that the current task has been completed
        // Emm x7... So the task that schedule comes in should actually follow this rule
        // [D] Let's emphasize the problem later
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      // Here comes the advanced timers
      advanceTimers(currentTime);
    } else {
      // If the current task is no longer available, pop it out
      pop(taskQueue);
    }
    // Get a peek task out of taskQueue again
    // Note that if the previous continuation callback is true, taskQueue will not pop,
    // So the task from peek is still the current task, but the callback is already a continuation callback
    currentTask = peek(taskQueue);
  }
  // Bingo! Isn't that to check if there are any more tasks?
  // Finally return to the hasMoreWork logic in performWorkUntilDealine!
  if (currentTask !== null) {
    return true;
  } else {
    // [E] Well, it's not so simple here. What did you do?
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

We finally solved the previous But How question
Now, let's parse the ABC in the above code to see how this loop works

  • A: There are two times of advanced timers in the above code. What is it used for? As soon as you see the code above:
function advanceTimers(currentTime) {
  // In fact, the following official note is very clear. It is to transfer the queued tasks in timerQueue to taskQueue as needed
  // Check for tasks that are no longer delayed and add them to the queue.
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }
    timer = peek(timerQueue);
  }
}

In fact, this code is quite simple. It judges whether a timer has reached the execution time according to startTime and currentTime, and then transfers it to taskQueue. It can be summarized as follows:

Therefore, the function of the first call to workLoop is to sort out the tasks that need to be performed at present;
The second call is due to the time consumed after the task in the while statement is executed. When you enter the while statement again, of course, you need to reorganize the taskQueue

  • B: shouldYieldToHost and hasTimeRemaning together determine whether there is still time to execute the task. If not, break out the while loop, so as to maintain a cycle scheduling of 5ms - ah, and solve a problem. The source code of shouldYieldToHost is a little bit unexpected. Let's see:
if (
    enableIsInputPending &&
    navigator !== undefined &&
    navigator.scheduling !== undefined &&
    navigator.scheduling.isInputPending !== undefined
  ) {
    const scheduling = navigator.scheduling;
    shouldYieldToHost = function() {
      const currentTime = getCurrentTime();
      if (currentTime >= deadline) {
        // There's no time left. We may want to yield control of the main
        // thread, so the browser can perform high priority tasks. The main ones
        // are painting and user input. If there's a pending paint or a pending
        // input, then we should yield. But if there's neither, then we can
        // yield less often while remaining responsive. We'll eventually yield
        // regardless, since there could be a pending paint that wasn't
        // accompanied by a call to `requestPaint`, or other main thread tasks
        // like network events.
        // No more time. We may need to temporarily give control of the main thread, so the browser can perform high priority tasks.
        // The so-called high priority tasks are mainly "drawing" and "user input". If there is currently a drawing or input in progress, then
        // We should give up resources to give priority to their execution; if not, we can give up fewer resources to keep the response.
        // However, after all, there are painting status updates not initiated by 'requestPaint', and other main thread tasks such as network requests,
        // We will eventually give up resources at some critical point
        if (needsPaint || scheduling.isInputPending()) {
          // There is either a pending paint or a pending input.
          return true;
        }
        // There's no pending input. Only yield if we've reached the max
        // yield interval.
        return currentTime >= maxYieldInterval;
      } else {
        // There's still time left in the frame.
        return false;
      }
    };

    requestPaint = function() {
      needsPaint = true;
    };
  } else {
    // `isInputPending` is not available. Since we have no way of knowing if
    // there's pending input, always yield at the end of the frame.
    shouldYieldToHost = function() {
      return getCurrentTime() >= deadline;
    };

    // Since we yield every frame regardless, `requestPaint` has no effect.
    requestPaint = function() {};
  }

As you can see, for support navigator.scheduling For attribute environment, React takes a further consideration, that is, browser drawing and user input should take precedence, which is actually React Explained in the Scheduling part of the design concept The connotation of
Of course, since this attribute is not generally supported, the definition in else branch is simply to judge whether it exceeds the deadline
Considering the robustness of the API, requestPaint also has different definitions according to the situation

  • C: Let's take a closer look at the assignment of continuationCallback -- continuationCallback= Callback (didusercallbacktimeout), which transfers the overdue status of a task to the task itself. If the task supports different behaviors according to the overdue status - for example, in the overdue status, cache the current execution results, and reuse the cached results until the next time the schedule does not expire to continue to perform the subsequent logic, then the new processor will be returned And assign it to continuationCallback. This is where the Fiber Reconciler implementation in React is most closely related. If the callback itself does not process the overdue state, the returned things need to be controlled as non function values logically, that is to say, the type of continuationCallback = = 'function' is judged to be false. Because callback does not necessarily have special treatment for overdue status, its execution time may be much longer than expected, so it is more necessary to execute advance timers later.
  • D: As mentioned earlier, the incoming callback must follow the rules consistent with the logic related to the continuation callback. Since the Scheduler has not been officially promoted independently of React, there is no relevant document to explain it explicitly, so we must pay attention to this when using the Scheduler directly
  • E: In fact, this is to sort out the remaining tasks in timer again. Let's see what requestHostTimeout and handleTimeout have done

Now, if you look at the requestHostTimeout name, you know it must come from SchedulerHostConfig.default.js This document 🙂 :

// It is very simple to execute the callback in the timer phase of the next round of browser eventloop. If the specific time is passed in, say otherwise  
requestHostTimeout = function(callback, ms) {
    taskTimeoutID = setTimeout(() => {
      callback(getCurrentTime());
    }, ms);
  };

// The relevant cancel method is to directly clear the timer and reset the taskTimoutID
cancelHostTimeout = function() {
  clearTimeout(taskTimeoutID);
  taskTimeoutID = -1;
};

Look at handleTimeout again. Its definition is Scheduler.js Medium:

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  // Here again, task
  advanceTimers(currentTime);

  // If isHostCallbackScheduled is set to true again at this time
  // It means that a new task has been registered
  // Logically, these tasks will be delayed again
  if (!isHostCallbackScheduled) {
    // The new task of flush entering taskQueue
    if (peek(taskQueue) !== null) {
      // If the advanceTimer in this method has a push task to taskqueue
      // Then start flush ing them directly
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      // If taskQueue is still empty, call the method recursively
      // Until all tasks in timerQueue are cleared
      // (I think this recursion should not have a chance to stop for frequent interaction applications.)
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        // startTime - currentTime, that is XXX_ PRIORITY_ The value of timeout!
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

It can be summarized as the aftermath work of workLoop
Now, we can summarize a rough schematic diagram of workLoop:

Emm x7... It's very long, but it doesn't have much content
At this point, the core operation mode of the Scheduler is split
There are some other methods in the source code, some of which are used to cancel the current scheduling cycle (i.e. recursive process), some of which are tool interfaces provided for developers to use. Interested students can Poke here Learn more

summary

Due to a large number of source code pasted in, the length of this article is relatively long, but in fact, it explains two problems

How does postMessage work?

It is mainly to implement a recursive message sending receiving processing process through the method of performWorkUntilDeadline to realize task processing

How are tasks handled?

Everything revolves around two minimum priority queues:

  • taskQueue
  • timerQueue

Tasks are preset according to certain priority rules, and the main purpose of these presets is to confirm the time out for priority level.
When a series of tasks are not processed (flushWork), a while loop will be generated to continuously process the contents in the queue. During this period, the deferred tasks will be gradually sorted from the timerQueue (advance timers) to the taskQueue, so that the tasks can be executed in order according to the preset priority. Even for higher-level task callback implementations, you can "continue callback" tasks.
And one of the principles in the whole process is that all tasks should not occupy the browser task closest to user perception (needspainiting & isinputpending). Of course, this can be done to the extreme and also with the implementation of browser( navigator.scheduling )About

Overview

Now, we can integrate the previous schematic diagrams and add the schematic diagrams of two queues to get a large overview of the operation principle:

Ah, it's really big... In fact, it's mainly blank
In general, compared with the old implementation (rIC and rAF), postMessage is more independent and less dependent on the operation process of the device itself, which not only improves the efficiency of task processing, but also reduces the risk of application errors caused by uncontrollable factors, which is quite a correct attempt. Although it does not have an explicit impact on each React application, and even does not need a deep understanding of it by the developers, perhaps we know its operation principle, which adds the idea of code optimization and troubleshooting.
However, as mentioned earlier, some of this implementation is still in the experimental stage, so we need to consider carefully if we want to implement something directly using the Scheduler.
Emm x8... Can it be used for rendering management of bullet screen applications? After all, the priority of airplane gift notification is higher than that of plain text, and your gift is higher than Ah, it's a bit of a fight. Please forget the content after the dash.
Interested students can practice it and help the Scheduler experiment~


Finally, if there are any mistakes in the understanding of this paper, I would like to point out that 🙏

Posted by literom on Mon, 15 Jun 2020 19:21:15 -0700