A background

Hello everyone, I am a alien, when it comes to update, is the front frame of a nagging question of this knowledge is also in the interview, the interviewer prefer q, so under the background of different technical framework, the means of processing updates each are not identical, today we are going to discuss, the mainstream of the front frame of batch processing way, and its internal implementation principle.

Here’s what you’ll learn from today’s lesson:

  • Batch update of mainstream front-end frameworks.
  • Vue and React batch update implementation.
  • Characteristics of macro and micro tasks.

1 A VUE case

Let’s start with a question. For example, in an update to vUE.

<template>
   <div>{{name}} Age: {{age}}<button @click="handleClick" >Click on the</button>
   </div>
</template>

<script>
export default {
    data(){
        return {
            age:0.name:' '}},methods: {handleClick(){
            this.name = 'alien'
            this.age = 18}}}</script>
Copy the code

The above is a very simple logical code, click the button, will trigger the name and age update. So the first question to think about is:

  • Normally, vUE’s data layer is handled in a reactive manner, so such as age and name can be interpreted as a layer of property proxy. The get properties (name and age) in the string template template are associated with the component’s render Watcher (Effect in Vue3.0).

  • A reassignment will trigger the set, and depending on the response, it will trigger the render Watcher to re-execute, and then it will re-update the component and render the view.

The problem is that in handleClick, we change the name and age properties at the same time. Normally, this will trigger the name and age set, respectively. If we do not do this, we will make the render Watcher execute twice. The result is that the component updates twice, but is that the result?

The result: The VUE underlayer updates components only once through batch processing.

2 React Case

After the example of batch updates in VUE, let’s look at batch updates in React. React

function Index(){
    const [ age , setAge ] = React.useState(0)
    const [ name, setName ] = React.useState(' ')
    return <div>Name: {name} Age: {age}<button onClick={()= >{setAge(18) setName('alien')}} > Click</button>
    </div>
}
Copy the code

Click the button to trigger the update, which triggers the useState update function twice. The React update process looks like this.

  • FiberRoot is found first.
  • Then the blending process is carried out. Execute the Index component to get the new Element.
  • Diff fiber, get the effectList.
  • Execute the Effect List to get the latest DOM and render it.

Normally, the Index component would execute twice. The reality is that render is executed only once.

3 Significance of batch processing

The above example illustrates that in mainstream frameworks, batch processing is used for updates. An update in context is merged into an update. So why do update batching?

React A batch is executed for performance reasons. React a batch is executed for performance reasons.

🌰 Example 1: Assuming no batch update:

/ —— js level ——

  • Step 1: A click event triggers a macro task.
  • Step 2: Execute setAge to update fiber state.
  • Step 3: Perform the Render phase, Index execution, and get the new element. Get effectlist.
  • Step 4: Perform the COMMIT phase and update the DOM.
  • Step 5: Run setName to update fiber status.
  • Step 6: Repeat steps 3 and 4.

/ —— browser render ——

  • Render the real DOM element after js is executed.

We can see that without batch update processing, there are many more steps, including the Render phase, the COMMIT phase, the DOM update, etc., which can cause a waste of performance. Let’s take a look at batch update.

🌰 Example 2: Batch updates exist.

/ —— js level ——

  • Step 1: A click event triggers a macro task.
  • Step 2: Batch process setAge and setName to update fiber state.
  • Step 3: Perform the Render phase, Index execution, and get the new element. Get effectlist.
  • Step 4: Perform the COMMIT phase and update the DOM.

/ —— browser render ——

  • Render the real DOM element after js is executed.

Update batching essentially optimizes many steps in the js execution context, reducing performance overhead.

Brief introduction of macro tasks and micro tasks

Before we talk about batch updates, let’s review macro and micro tasks, which are a must for front-end engineers.

Macro tasks can be understood as

For example, in the browser environment, the execution of the macro task does not affect the browser rendering and response. Let’s do an experiment.

function Index(){
    const [ number , setNumber ] = useState(0)
    useEffect(() = >{
        let timer
        function run(){
            timer = setTimeout(() = > {
                console.log('---- Macro task execution ----')
                run()
            }, 0)
        }
        run()
        return () = > clearTimeout(timer)
    },[])
    return <div>
        <button onClick={()= >SetNumber (number + 1)} > Click {number}</button>
    </div>
}
Copy the code

In the simple demo above, the setTimeout macro task is repeated by recursively calling the run function.

SetTimeout execution in this case does not affect the execution of the click event and the normal rendering of the page.

What are microtasks?

So let’s analyze the microtask again. In the js execution process, we hope that some tasks will not block the code execution, and the task can be completed in this round of event loop, so the concept of a microtask queue is introduced.

Compared with macro tasks, micro tasks have the following characteristics:

  • Microtasks are executed immediately after the current JS execution and block browser rendering and response.
  • After a macro task is complete, the microtask queue is cleared.

Common microtasks include Promise, queueMicrotask, MutationObserver in the browser environment, process.nexttick in the Node environment, etc.

Let’s do a similar experiment to look at microtasks:

function Index(){
    const [ number , setNumber ] = useState(0)
    useEffect(() = >{
        function run(){
            Promise.resolve().then(() = >{
                run()
            })
        }
        run()
    },[])
    return <div>
        <button onClick={()= >SetNumber (number + 1)} > Click {number}</button>
    </div>
}
Copy the code
  • In this case, the browser simply freezes and does not respond, confirming the above conclusion.

Three micro task | macro task to achieve batch update

With macro and micro tasks finished, let’s move on to the first batch update implementation, which is based on macro and micro tasks.

For example, we do not immediately execute the update task for each update. Instead, we put each update task into a queue to be updated. Then, after js execution is completed, we use a microtask to batch update the tasks in the queue. So degrade to a macro task. The reason why microtasks are preferred here is that they are executed before the next macro task.

Typical examples are vue update principle, vue.$nextTick principle, and scheduleMicrotask update principle in V18.

Using Vue as an example, let’s look at the implementation of nextTick:

runtime-core/src/scheduler.ts

const p = Promise.resolve() 
/* nextTick implementation, implemented with microtasks */
export function nextTick(fn? : () = >void) :Promise<void> {
  return fn ? p.then(fn) : p
}
Copy the code
  • So you can see the nextTick principle, which is essentiallyPromise.resolve()Create a microtask.

Take a look at the react V18 implementation.

react-reconciler/src/ReactFiberWorkLoop/ensureRootIsScheduled

function ensureRootIsScheduled(root, currentTime) {
     /* omit unnecessary logic */
     if (newCallbackPriority === SyncLane) {
        /* Supports microtasks */
        if (supportsMicrotasks) {
            /* Through microtask */scheduleMicrotask(flushSyncCallbacks); }}}Copy the code

Next, see how scheduleMicrotask is implemented.

/* Backward compatible */
var scheduleMicrotask = typeof queueMicrotask === 'function' ? queueMicrotask : typeof Promise! = ='undefined' ? function (callback) {
  return Promise.resolve(null).then(callback).catch(handleErrorInNextTick);
} : scheduleTimeout; 
Copy the code

ScheduleMicrotask also uses promise.resolve, and there is a setTimeout case for backward compatibility.

The general realization flow chart is as follows:

So let’s simulate the implementation of this.

class Scheduler {
    constructor(){
        this.callbacks = []
        /* Microtask batch processing */
        queueMicrotask(() = >{
            this.runTask()
        })
    }
    /* Add task */
    addTask(fn){
        this.callbacks.push(fn)
    }
    runTask(){
        console.log('------ Merge update start ------')
        while(this.callbacks.length > 0) {const cur = this.callbacks.shift()
            cur()
        }
        console.log('------ Merge update end ------')
        console.log('------ Start updating components ------')}}function nextTick(cb){
    const scheduler = new Scheduler()
    cb(scheduler.addTask.bind(scheduler))
}

/* Simulate an update */
function mockOnclick(){
   nextTick((add) = >{
       add(function(){
           console.log('First Update')})console.log('---- macro task logic ----')
       add(function(){
        console.log('Second Update')
       })
   })
}

mockOnclick()
Copy the code

Let’s simulate the implementation details:

  • A Scheduler is used to complete the process.
  • AddTask to queue each time.
  • Create a microtask with queueMicrotask to process these tasks in a unified manner.
  • MockOnclick simulates an update. Let’s use nextTick to simulate the processing logic of the update function.

Take a look at the print effect:

Batch update of controllable tasks

There is also another way to block tasks to make them manageable. This is typical of batchEventUpdate before React V17. Updates in this case come from intercepting events, such as the React event system.

React events are handled by the React event system, for example, onClick and onChange events. The outer layer is intercepted with a unified handler function. The events we bind are called within the execution context of that function.

So let’s say multiple updates are triggered in a single click event. Essentially, the outer layer is in the context of the React event system handler, in which case you can use a switch to prove that the current update is controllable and batch processing is possible. React is used once.

React’s underlying implementation logic:

react-dom/src/events/ReactDOMUpdateBatching.js


export function batchedEventUpdates(fn, a) {
  /* Enable batch update */
  const prevExecutionContext = executionContext;
  executionContext |= EventContext;
  try {
    /* An event handler that executes here, such as setState on a click event, will execute */ within this function
    return fn(a);
  } finally {
    /* Return does not affect finally execution */
    /* Complete an event, batch update */
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
        /* Perform the update immediately. * /flushSyncCallbackQueue(); }}}Copy the code

Enable isBatchingEventUpdates=true before the React event is executed. Enable isBatchingEventUpdates= false after the React event is executed. Turn off the switch, and then use this switch in scheduleUpdateOnFiber to determine whether to batch update.

For example, in a click event:

const [ age , setAge ] = React.useState(0)
const [ name, setName ] = React.useState(' ')
const handleClick=() = >{
    setAge(18)
    setName('alien')}Copy the code
  • So first handleClick is generated by clicking on events, so in the React system, the event proxy function is executed first and then executedbatchedEventUpdates. The batch update status is enabled at this time.
  • Next, setAge and setName will not be updated immediately in batch state.
  • At last,flushSyncCallbackQueueTo handle the update task immediately.

Let’s use a flow chart to illustrate the principle.

Let’s simulate a concrete implementation:

<body>  
    <button onclick="handleClick()" >Click on the</button>
</body>
<script>
  let  batchEventUpdate = false 
  let callbackQueue = []

  function flushSyncCallbackQueue(){
      console.log('----- Perform batch update -------')
      while(callbackQueue.length > 0) {const cur = callbackQueue.shift()
          cur()
      }
      console.log('----- Batch update end -------')}function wrapEvent(fn){
     return function (){
         /* Enable batch status update */
        batchEventUpdate = true
        fn()
        /* Execute the update task immediately */
        flushSyncCallbackQueue()
        /* Disable batch update status */
        batchEventUpdate = false}}function setState(fn){
      /* If in batch update state, then batch update */
      if(batchEventUpdate){
          callbackQueue.push(fn)
      }else{
          /* If not in batch update condition, then directly update. * /
          fn()
      }
  }

  function handleClick(){
      setState(() = >{
          console.log('update 1 - - -)})console.log('Context execution')
      setState(() = >{
          console.log('- update 2 -)})}/* Make handleClick controllable */
  handleClick = wrapEvent(handleClick)


</script>
Copy the code

Print result:

Analyze the core process:

  • The core of this approach is to make handleClick manageable through WrapEvents. First, wrapEvent is similar to the event handler function. It internally checks whether batch update is enabled by switching batchEventUpdate. Finally, it flushSyncCallbackQueue flushSyncCallbackQueue to flush the queue to be updated.

  • In batch update conditions, events are placed in the update queue; in non-batch update conditions, the update task is executed immediately.

Five summarizes

This section describes how mainstream frameworks implement update batching. Send rose 🌹, the hand has lingering fragrance, hope to see the feel of the harvest of the students, can give the author praise ➕ pay attention to a wave, in order to encourage me to continue to create front-end hard text.

The resources

  • React Advanced Practice guide