TL; DR

This article includes:

  1. Scheduler introduction — Time sharding
  2. Time sharding should choose microtask or macro task
  3. Why not setTimeout(fn, 0)
  4. Why not select requestAnimationFrame(FN)

Scheduler introduction — Time sharding

If “the component Render process is time consuming” or “there are many virtual DOM nodes participating in the mediation phase”, it can take a long time to complete the mediation phase for all the components at once.

In order to avoid page delays resulting from long execution of the reconcile phase, the React team came up with the Fiber architecture and Scheduler task scheduling.

The intent of the Fiber architecture is to “be able to perform each virtual DOM harmonization phase independently,” rather than executing the harmonization phase of the entire virtual DOM tree each time.

Scheduler’s main function is time sharding, which returns the main thread to the browser at regular intervals to avoid long periods of occupation.

React interacts with Scheduler

If only React and Scheduler interactions are considered, the component update process is as follows:

  1. React component status updates, which saves a task to the Scheduler, which is the React update algorithm.
  2. Scheduler schedules the task and executes the React update algorithm.
  3. React will ask the Scheduler if it needs to pause after updating a Fiber during the reconcile phase. If no pause is required, repeat Step 3 to continue updating the next Fiber.
  4. If the Scheduler indicates that a pause is needed, React returns a function that tells the Scheduler that the task is not finished. Scheduler schedules the task at some point in the future.

In the first step, the Scheduler needs to expose the pushTask() method that React uses to save the task.

In the second step, the Scheduler needs to expose the scheduleTask() method for scheduling tasks.

In the third step, the Scheduler needs to expose the shouldYield() method that React uses to decide if the task needs to be paused.

In the fourth step, the Scheduler determines whether the return value after the task is executed is a function; if so, the task is incomplete and needs to be scheduled in the future.

This process can be expressed in pseudocode as follows:

const scheduler = {
  pushTask() {
    // 1
  },

  scheduleTask() {
    // 2. Select a task and execute it
    const task = pickTask()
    const hasMoreTask = task()

    if (hasMoreTask) {
      // 4. Continue scheduling in future}},shouldYield() {
    // 3. The call is called by the caller, and the caller determines whether to suspend}},// When the user clicks to change the component state, the pseudo-code is as follows
const handleClick = () = > {
  // Generate tasks when the React component is updated
  const task = () = > {
    const fiber = root
    while(! scheduler.shouldYield() && fiber) {// Reconciliation () Performs the reconciliation phase for the current fiber
      // Return the next fiber
      fiber = reconciliation(fiber)
    }
  }

  scheduler.pushTask(task)

  // React will execute scheduler.scheduletAsk () at some future time
  // This assumes scheduler.scheduletAsk () is executed immediately
  scheduler.scheduleTask()
}
Copy the code

Scheduler is a universal design, not just for React

The last section was about React interacting with Scheduler. In fact, Scheduler is a generic design that can be applied to any task scheduling.

For example (for example), let’s say we want to calculate the sum of 1000 integers. The code for a one-time walk is as follows:

let sum = 0
for (let i = 0; i < 1000; ++i) {
  sum += arr[i]
}
Copy the code

Assuming it takes a millisecond to perform an addition, the entire process takes a second, resulting in a one-second page lag. If you change the procedure to a task scheduled by Scheduler, the code is as follows:

const task = () = > {
  let pos = 0
  let sum = 0
  const continuousExec = () = > {
    for(; ! scheduler.shouldYield() && pos <1000; ++pos) {
      sum += arr[i]
    }

    if (pos === 1000) {
      return
    }

    return continuousExec
  }

  return continuousExec()
}
Copy the code

When scheduler.shouldyield () returns true, the task is paused, and the browser can update the page to avoid a page stacker.

The Scheduler approach can be understood to mean that the currently executing function returns execution rights to the caller, who can continue executing the function in the future. This scheduling is exactly the same as the Generator Function, so it is easier to implement the Scheduler using the Generator Function. But the React team didn’t use the generator function implementation, mainly because the generator function is stateful, and React wants to re-execute the task stateless. Refer to the official explanation.

Relationship to MessageChannel

So what does Scheduler have to do with MessageChannel?

The key is that when scheduler.shouldyield () returns true, the scheduler needs to satisfy the following function points:

  1. Pause JS execution and return the main thread to the browser, giving it a chance to update the page
  2. Continue scheduling tasks at some point in the future to execute tasks that were not completed last time

To meet these two requirements, a macro task is scheduled, because the macro task is executed in the next event loop and does not block the page update. The microtask is executed before the current page update, as if it were executed synchronously, without leaving the main thread. The event loop can be seen in the following image, which is taken from further exploration of the event loop.

The purpose of using MessageChannel is to generate macro tasks. The code for using MessageChannel in Scheduler looks like this:

const channel = new MessageChannel()
const port = channel.port2

// A macro task is added every time port.postMessage() is called
// The macro task is to call the scheduler. ScheduleTask method
channel.port1.onmessage = scheduler.scheduleTask

const scheduler = {
  scheduleTask() {
    // Select a task and execute it
    const task = pickTask()
    const continuousTask = task()

    // If the current macro task is not completed, the next macro task continues
    if (continuousTask) {
      port.postMessage(null)}}}Copy the code

Why not setTimeout(fn, 0)

SetTimeout (fn, 0) is the most common way to create macro tasks. Why didn’t React use it to implement Scheduler?

The reason is that when you recursively execute setTimeout(fn, 0), the last interval is 4 milliseconds instead of the first 1 millisecond. The following code can be executed in the browser:

var count = 0

var startVal = +new Date(a)console.log("start time".0.0)
function func() {
  setTimeout(() = > {
    console.log("exec time", ++count, +new Date() - startVal)
    if (count === 50) {
      return
    }
    func()
  }, 0)
}

func()
Copy the code

The running result is:

If the Scheduler is implemented using setTimeout(fn, 0), 4 milliseconds will be wasted. Since 60fps requires no more than 16.66ms between frames, 4ms is a waste that cannot be ignored.

If you are interested, you can try setInterval(fn, 0), which is the same as setTimeout.

// setInterval 0ms
var count = 0
var startVal = +new Date(a)var timer = setInterval(() = > {
  console.log("exec time", ++count, +new Date() - startVal)
  if (count >= 50) {
    clearInterval(timer)
  }
}, 0)
Copy the code

Why not select requestAnimationFrame(FN)

We know that rAF() is called before the page is updated.

If the first task scheduling is not triggered by rAF(), such as directly executing scheduler.scheduletask (), then a rAF() callback is executed before this page update, which is the second task scheduling. So using the rAF() implementation results in two tasks being performed before this page update.

Why two times instead of three or four times? Because rAF() is called again in the callback of rAF(), the second callback of rAF() is executed before the next frame, rather than before the current frame.

Another reason is that rAF() is triggered at an indefinite interval, and if the browser takes 10ms to update the page, then 10ms is wasted.

Existing WEB technologies do not specify when and when a browser should update a page, so it is generally assumed that after a macro task is completed, the browser decides whether or not it should update the page currently. If the page needs to be updated, executerAF()To update the page. Otherwise, the next macro task is executed.

conclusion

The React Scheduler uses MessageChannel for the following reasons:

  1. Return the main thread to the browser so the browser can update the page.
  2. The browser updates the page and continues to perform the unfinished task.

Why not use microtasks?

  1. The microtask will all be executed before the page is updated, so the purpose of “returning the main thread to the browser” is not met.

Why not use setTimeout(fn, 0)?

  1. recursivesetTimeout()The call will make the call interval 4ms, resulting in a waste of 4ms.

Why not use rAF()?

  1. If the last task scheduling is notrAF()Is triggered, resulting in two task schedules before the current frame is updated.
  2. The page update time is uncertain, and if the browser takes 10ms to update the page, then 10ms is wasted.

It’s a good idea

  1. The React performance optimization | including principle, technique, Demo, tools to use
  2. Talk about useSWR, improving development effectiveness – including useSWR design ideas, pros and cons, and best practices
  3. Why does React use the Lane technical solution

, recruiting

The writer is in Chengdu – Bytedance – private cloud direction, the main technology stack is React + Node.js. Team expansion speed is fast, the technical atmosphere within the group is active. Public cloud private cloud is just starting, there are many technical challenges, the future is foreseeable.

Interested can resume through this link: job.toutiao.com/s/e69g1rQ

You can also add my wechat moonball_cxy, chat together, make a friend.

Original is not easy, don’t forget to encourage oh ❤️