Deep analysis of batch asynchronous update strategy of Vue source code

Time:2021-10-22

Vue asynchronous update source code will involve the concepts of event loop, macro task and micro task, so let’s understand these concepts first.

1、 Event loop, macro task, micro task

1. Event loop: the working mechanism customized by the browser to coordinate tasks such as event processing, script execution, network request and rendering.

2. Macro task: represents a discrete and independent work unit. The browser completes a macro task and re renders the page before the next macro task starts. It mainly includes creating document objects, parsing HTML, executing mainline JS code and various events, such as page loading, input, network events and timers.

3. Micro task: a micro task is a smaller task that is executed immediately after the execution of the current macro task. If there are micro tasks, the browser will re render after completing the micro task. Examples of micro tasks include promise callback functions, DOM changes, etc.

Execution process: after executing macro tasks = > executing micro tasks = > page re rendering = > executing a new round of macro tasks

Example of task execution sequence:

//The first macro task enters the main thread
console.log('1');
//Drop into macro event queue
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
//Micro event 1
process.nextTick(function() {
    console.log('6');
})
//The main thread executes directly
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    //Micro event 2
    console.log('8')
})
//Drop into macro event queue
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
 
// 1,7,6,8,2,4,3,5,9,11,10,12

Resolution:

First macro task

  1. The first macro task enters the main thread and prints 1
  2. SetTimeout dropped to macro task queue
  3. Process.nexttick is dropped to the micro task queue
  4. Execute new promise directly and print 7
  5. Promise then event dropped to micro task queue
  6. SetTimeout dropped to macro task queue

After the first macro task is executed, start to execute the micro task

  1. Execute process.nexttick and print 6
  2. Execute promise then event, print 8

After the micro task is executed, empty the micro task queue, render the page, and enter the next macro task setTimeout

  1. Execute print 2
  2. Process.nexttick is dropped to the micro task queue
  3. Execute new promise directly and print 4
  4. Promise then event dropped to micro task queue

After the execution of the second macro task, start to execute the micro task

  1. Execute process.nexttick and print 3
  2. Execute promise then event, print 5

After the micro task is executed, empty the micro task queue, render the page, enter the next macro task setTimeout, repeat the above similar process, and print 9,11,10,12

2、 Vue asynchronous batch update process

1. Parsing: when data changes are detected, Vue will open a queue, store the relevant Watcher in the queue, store the callback function in the callbacks queue, execute the callback function asynchronously, and traverse the watcher queue for rendering.

Asynchronous: Vue executes asynchronously when updating dom. As long as it listens for data changes, Vue will open a queue and buffer   All data that occurs in the same event loop   Change of.

Batch: if the same watcher is triggered multiple times, it will only be pushed into the queue once. De duplication can avoid unnecessary calculations and DOM operations. Then, in the next event loop “tick”, Vue refreshes the queue to perform the actual work.

Asynchronous policy: Vue’s internal attempts to use native promise.then, mutationobserver, and   Setimmediate. If the execution environment does not support it, the   setTimeout(fn, 0)   Replace. That is, I will try to use the micro task mode first, but not the macro task mode.

Asynchronous batch update flowchart:

 

3、 Vue batch asynchronous update source code

Asynchronous update: the whole process is equivalent to putting smelly socks in a basin and washing them together at last.

1. When a data is updated, the following codes will be executed successively:

(1) Trigger data. Set()

(2) Call dep.notify(): traverse all relevant watchers and call watcher. Update().

core/oberver/index.js:

notify () {
    const subs = this.subs.slice()
    //If asynchronous is not running, the sub is not sorted in the scheduler
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      //Sort to ensure that they are executed in the correct order
      subs.sort((a, b) => a.id - b.id)
    }
    //Traverse the relevant watcher and call the watcher update
    for (let i = 0, l = subs.length; i < l; i++) { 
      subs[i].update()
    }
}

(3) Execute watcher. Update(): judge whether to update immediately or asynchronously. If the update is asynchronous, call queuewatcher (this), queue the watcher and update it later.

core/oberver/watcher.js:

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      //Render now
      this.run()
    } else {
      //The watcher enters the queue and performs rendering together later
      queueWatcher(this)
    }
}

(4) execute queueWatcher (this): Watcher after adding weights to the queue, add it to the queue, invoke nextTick (flushSchedulerQueue) to execute the asynchronous queue, and import callback function flushSchedulerQueue.

core/oberver/scheduler.js:

function queueWatcher (watcher: Watcher) {
  //Has ID to determine whether the watcher is already in the queue and avoid adding the same watcher to a queue
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    //Flushing ID, a new watcher that may be generated when processing watcher rendering.
    if (!flushing) {
      //Add current watcher to asynchronous queue
      queue.push(watcher)
    } else {
      //When a new watcher is generated, it is added to the sorting position
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    //The waiting ID allows all watchers to be updated in one tick.
    if (!waiting) {
      waiting = true
 
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      //Execute asynchronous queue and pass in callback
      nextTick(flushSchedulerQueue)
    }
  }
}

(5) execute nextTick (CB): add the flushSchedulerQueue function transferred to the callbacks queue, and invoke timerFunc to start the asynchronous execution task.

core/util/next-tick.js:

function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  //The callbacks here are queues (callback arrays). The incoming flushschedulerqueue method is processed and added to the callback array
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    //Start asynchronous execution task. This method will select different asynchronous strategies according to browser compatibility
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

(6) Timerfunc(): select different asynchronous methods to execute flushcallbacks according to browser compatibility. Since the macro task takes more time than the micro task, select the micro task method first, and then use the macro task method when it can’t,

core/util/next-tick.js:

let timerFunc
 
//If promise is supported, flush callbacks are executed asynchronously by promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  //The asynchronous mode of setTimeout can no longer be used
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

(7) Flushcallbacks: asynchronously executes all functions in the callbacks queue

core/util/next-tick.js:

//Loop the callbacks queue, execute all the functions in it, flush scheduler queue, and empty the queue
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

(8) Flushscheduler queue(): traverse the watcher queue and execute watcher. Run()

Watcher. Run (): real rendering

function flushSchedulerQueue() {
  currentFlushTimestamp = getNow();
  flushing = true;
  let watcher, id;
 
  //Sort, render the parent node first, and then the child node
  //This can avoid unnecessary child node rendering, such as the child node with V - if false in the parent node
  queue.sort((a, b) => a.id - b.id);
 
  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  //Traverse all watchers for batch updates.
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    //Real update function
    watcher.run();
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== "production" && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1;
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          "You may have an infinite update loop " +
            (watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`),
          watcher.vm
        );
        break;
      }
    }
  }
 
  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice();
  const updatedQueue = queue.slice();
 
  resetSchedulerState();
 
  // call component updated and activated hooks
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);
 
  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit("flush");
  }
}

(9) Updatecomponent(): after a series of turns, the watcher. Run() executes updatecomponent, and the updatecomponent executes render() to re render the component and then execute_ Update (vnode), and then execute   Patch () updates the interface.

(10)_ Update (): execute different patches according to whether there are vnodes.

4、 Vue.nexttick (callback)

1. Vue. Nexttick (callback) function: get the updated real DOM element.

Because Vue executes asynchronously when updating DOM, the modified DOM elements cannot be obtained immediately after modifying data. In order to get the modified DOM element, it can be used immediately after the data changes   Vue.nextTick(callback)。

2. Why   Vue.$nextTick   Can I get the updated DOM?

Because Vue. $nexttick is actually a call   nextTick   Method to execute the callback function in the asynchronous queue.


Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn, this);
};

3. Use   Vue.$nextTick

Example 1:

<template>
  <p>{{foo}}</p>
</template>
<script>
 
export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    this.foo = 'foo1';
    //Vue updates the DOM asynchronously, so the DOM is not updated here
    console.log('test.innerHTML:' + test.innerHTML);
 
    this.$nextTick(() => {
      // nextTick callback is called after DOM update, so DOM has been updated here.
      console.log('nextTick:test.innerHTML:' + test.innerHTML); 
    })
  }
}
</script>
Execution results:
test.innerHTML:foo
nextTick:test.innerHTML:foo1

Example 2:

<template>
  <p>{{foo}}</p>
</template>
<script>
 
export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    this.foo = 'foo1';
    //Vue updates the DOM asynchronously, so the DOM is not updated here
    console.log('1.test.innerHTML:' + test.innerHTML); 
 
    this.$nextTick(() => {
      // nextTick callback is called after DOM update, so DOM has been updated here.
      console.log('nextTick:test.innerHTML:' + test.innerHTML); 
    })
 
    this.foo = 'foo2';
    //The DOM is not updated here and is executed before the asynchronous callback function
    console.log('2.test.innerHTML:' + test.innerHTML); 
  }
}
</script>
Execution results:
1.test.innerHTML:foo
2.test.innerHTML:foo
nextTick:test.innerHTML:foo2

Example 3:

<template>
  <p>{{foo}}</p>
</template>
<script>
 
export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    this.$nextTick(() => {
      //The nexttick callback is put into the callbacks queue before the update is triggered,
      //The watcher. Update and a series of subsequent operations were not triggered at all, so the last watcher. Run () was not executed to render
      //So the DOM is not updated here
      console.log('nextTick:test.innerHTML:' + test.innerHTML);
    })
    this.foo = 'foo1';
    //Vue updates the DOM asynchronously, so the DOM is not updated here
    console.log('1.test.innerHTML:' + test.innerHTML); 
    this.foo = 'foo2';
    //The DOM is not updated here and is executed before the asynchronous callback function
    console.log('2.test.innerHTML:' + test.innerHTML); 
  }
}
</script>
Execution results:
1.test.innerHTML:foo
2.test.innerHTML:foo
nextTick:test.innerHTML:foo

4. Nexttick and other asynchronous methods

Nexttick is a simulated asynchronous task, so promise and setTimeout can be used to achieve effects similar to this. $nexttick.

Example 1:

<template>
  <p>{{foo}}</p>
</template>
<script>
 
export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    this.$nextTick(() => {
      //The nexttick callback is put into the callbacks queue before the update is triggered,
      //The watcher. Update and a series of subsequent operations were not triggered at all, so the last watcher. Run () was not executed to render
      //So the DOM is not updated here
      console.log('nextTick:test.innerHTML:' + test.innerHTML); 
    })
    this.foo = 'foo1';
    //Vue updates the DOM asynchronously, so the DOM is not updated here
    console.log('1.test.innerHTML:' + test.innerHTML); 
    this.foo = 'foo2';
    //The DOM is not updated here and is executed before the asynchronous callback function
    console.log('2.test.innerHTML:' + test.innerHTML); 
 
    Promise.resolve().then(() => {
      console.log('Promise:test.innerHTML:' + test.innerHTML); 
    });
    setTimeout(() => {
        console.log('setTimeout:test.innerHTML:' + test.innerHTML);
    });
  }
}
</script>
Execution results:
1.test.innerHTML:foo
2.test.innerHTML:foo
nextTick:test.innerHTML:foo
Promise:test.innerHTML:foo2
setTimeout:test.innerHTML:foo2

Example 2:

​
<template>
  <p>{{foo}}</p>
</template>
<script>
 
export default{
  data(){
    return {
      foo: 'foo'
    }
  },
  mounted() {
    let test  = document.querySelector('#test');
    //Promise and setTimeout still wait until the DOM is updated
    Promise.resolve().then(() => {
      console.log('Promise:test.innerHTML:' + test.innerHTML); 
    });
    setTimeout(() => {
        console.log('setTimeout:test.innerHTML:' + test.innerHTML);
    });
    this.$nextTick(() => {
      //The nexttick callback is put into the callbacks queue before the update is triggered,
      //The watcher. Update and a series of subsequent operations were not triggered at all, so the last watcher. Run () was not executed to render
      //So the DOM is not updated here
      console.log('nextTick:test.innerHTML:' + test.innerHTML); 
    })
    this.foo = 'foo1';
    //Vue updates the DOM asynchronously, so the DOM is not updated here
    console.log('1.test.innerHTML:' + test.innerHTML); 
    this.foo = 'foo2';
    //The DOM is not updated here and is executed before the asynchronous callback function
    console.log('2.test.innerHTML:' + test.innerHTML); 
  }
}
</script>
Execution results:
1.test.innerHTML:foo
2.test.innerHTML:foo
nextTick:test.innerHTML:foo
Promise:test.innerHTML:foo2
setTimeout:test.innerHTML:foo2

summary

This is the end of this article about the batch asynchronous update strategy of Vue source code. For more information about Vue batch asynchronous update strategy, please search the previous articles of developeppaer or continue to browse the relevant articles below. I hope you will support developeppaer in the future!