Table of Contents
1. Asynchronous update
2.2 nextTick实现
3. 一个例子
Home Web Front-end JS Tutorial Analysis of batch asynchronous update and nextTick principle in Vue source code

Analysis of batch asynchronous update and nextTick principle in Vue source code

Jul 20, 2018 am 11:53 AM
vue.js Source code analysis

This article introduces to you the analysis of batch asynchronous updates and nextTick principles in Vue source code. It has certain reference value. Friends in need can refer to it.

vue is already one-third of the domestic front-end web end. It is also one of my main technology stacks. I know it in my daily use and am curious about why. In addition, a large number of communities have emerged recently. For articles about reading vue source code, I will take this opportunity to draw some nutrients from everyone’s articles and discussions. At the same time, I will summarize some of my thoughts when reading the source code and produce some articles as a summary of my own thinking.

Target Vue version: 2.5.17-beta.0

vue source code comments: https://github.com/SHERlocked93/vue-analysis

Statement: in the article The syntax of the source code uses Flow, and the source code is abridged as needed (in order not to be confused@_@). If you want to see the full version, please enter the github address above. This article is a series of articles. The article address can be found at the bottom~

1. Asynchronous update

We have dispatch updates in the setter accessor in the reactive method defineReactive that relies on the collection principledep .notify() method, this method will notify the watchers collected in subs of dep that subscribe to their own changes one by one to perform updates. Let’s take a look at the implementation of the update method:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

// src/core/observer/watcher.js

 

/* Subscriber接口,当依赖发生改变的时候进行回调 */

update() {

  if (this.computed) {

    // 一个computed watcher有两种模式:activated lazy(默认)

    // 只有当它被至少一个订阅者依赖时才置activated,这通常是另一个计算属性或组件的render function

    if (this.dep.subs.length === 0) {       // 如果没人订阅这个计算属性的变化

      // lazy时,我们希望它只在必要时执行计算,所以我们只是简单地将观察者标记为dirty

      // 当计算属性被访问时,实际的计算在this.evaluate()中执行

      this.dirty = true

    else {

      // activated模式下,我们希望主动执行计算,但只有当值确实发生变化时才通知我们的订阅者

      this.getAndInvoke(() => {

        this.dep.notify()     // 通知渲染watcher重新渲染,通知依赖自己的所有watcher执行update

      })

    }

  else if (this.sync) {      // 同步

    this.run()

  else {

    queueWatcher(this)        // 异步推送到调度者观察者队列中,下一个tick时调用

  }

}

Copy after login

If it is not computed watcher or sync, the current watcher that calls update will be pushed to In the scheduler queue, it is called on the next tick. Take a look at queueWatcher:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

// src/core/observer/scheduler.js

 

/* 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则

 * 该watcher将被跳过,除非它是在队列正被flush时推送

 */

export function queueWatcher (watcher: Watcher) {

  const id = watcher.id

  if (has[id] == null) {     // 检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验

    has[id] = true

    queue.push(watcher)      // 如果没有正在flush,直接push到队列中

    if (!waiting) {          // 标记是否已传给nextTick

      waiting = true

      nextTick(flushSchedulerQueue)

    }

  }

}

 

/* 重置调度者状态 */

function resetSchedulerState () {

  queue.length = 0

  has = {}

  waiting = false

}

Copy after login

A hash map of has is used here to check whether the current watcher is Whether the id exists. If it exists, skip it. If it does not exist, push it to the queue queue and mark the hash table has for the next check to prevent repeated additions. This is a process of deduplication. It is more civilized than having to go to the queue every time to check for duplication. When rendering, the same watcher changes will not be repeated patch, so even if it is modified simultaneously a hundred times The data used in the view will only be updated with the last modification during asynchronous patch.

The waiting method here is used to mark whether flushSchedulerQueue has been passed to the mark bit of nextTick. If it has been passed, it will only be pushed to the queue. Do not pass flushSchedulerQueue to nextTick, and waiting will be set back to false# when resetSchedulerState resets the scheduler state. ## Allow flushSchedulerQueue to be passed to the callback of the next tick. In short, it is guaranteed that the flushSchedulerQueue callback is only allowed to be passed in once within a tick. Let's see what the callback flushSchedulerQueue passed to nextTick does:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

// src/core/observer/scheduler.js

 

/* nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers */

function flushSchedulerQueue () {

  flushing = true

  let watcher, id

 

  queue.sort((a, b) => a.id - b.id)                    // 排序

 

  for (index = 0; index  MAX_UPDATE_COUNT) {     // 持续执行了一百次watch代表可能存在死循环

        warn()                                  // 进入死循环的警告

        break

      }

    }

  }

  resetSchedulerState()           // 重置调度者状态

  callActivatedHooks()            // 使子组件状态都置成active同时调用activated钩子

  callUpdatedHooks()              // 调用updated钩子

}

Copy after login
is executed in the

nextTick method flushSchedulerQueue Method, this method executes the run method of the watcher in queue one by one. We see that first there is a queue.sort() method that sorts the watchers in the queue by ID from small to large. This can ensure:

  1. The order of component updates is from parent component to child component, because parent components are always created before child components.

  2. The user watchers (listener watcher) of a component run before the render watcher, because user watchers are often created earlier than the render watcher

  3. If a component is destroyed while the parent component watcher is running, its watcher execution will be skipped

In the for loop in the execution queue one by one,

index The length is not cached here because more watcher objects may be pushed into the queue during the execution of processing existing watcher objects.

Then the process of data modifications being reflected from the model layer to the view:

Data changes-> setter -> Dep -> Watcher -> nextTick -> patch -> Update view

2. nextTick principle

2.1 Macro task/micro task

Here let’s take a look at the method that contains each watcher execution and is passed in as a callback

nextTick After that, nextTick does something with this method. But first, you need to understand the concepts of EventLoop, macro task, and micro task in the browser. If you don’t understand, you can refer to the concepts in JS and Node.js. In this article about the event loop, here is a picture to show the execution relationship between the latter two in the main thread:

Analysis of batch asynchronous update and nextTick principle in Vue source code

Explain, when the main thread executes After completing the synchronization task:

  1. The engine first takes out the first task from the macrotask queue. After the execution is completed, it takes out all the tasks in the microtask queue and executes them all in order;

  2. Then remove one from the macrotask queue. After the execution is completed, remove all the microtask queues again;

  3. The cycle continues until two All tasks in the queue have been taken.

浏览器环境中常见的异步任务种类,按照优先级:

  • macro task :同步代码、setImmediateMessageChannelsetTimeout/setInterval

  • micro taskPromise.thenMutationObserver

有的文章把 micro task 叫微任务,macro task 叫宏任务,因为这两个单词拼写太像了 -。- ,所以后面的注释多用中文表示~

先来看看源码中对 micro task macro task 的实现: macroTimerFuncmicroTimerFunc

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

// src/core/util/next-tick.js

 

const callbacks = []     // 存放异步执行的回调

let pending = false      // 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送

 

/* 挨个同步执行callbacks中回调 */

function flushCallbacks() {

  pending = false

  const copies = callbacks.slice(0)

  callbacks.length = 0

  for (let i = 0; i  {

    setImmediate(flushCallbacks)

  }

else if (typeof MessageChannel !== 'undefined' && (

  isNative(MessageChannel) ||

  MessageChannel.toString() === '[object MessageChannelConstructor]'  // PhantomJS

)) {

  const channel = new MessageChannel()

  const port = channel.port2

  channel.port1.onmessage = flushCallbacks

  macroTimerFunc = () => {

    port.postMessage(1)

  }

else {

  macroTimerFunc = () => {

    setTimeout(flushCallbacks, 0)

  }

}

 

// 微任务

if (typeof Promise !== 'undefined' && isNative(Promise)) {

  const p = Promise.resolve()

  microTimerFunc = () => {

    p.then(flushCallbacks)

  }

else {

  microTimerFunc = macroTimerFunc      // fallback to macro

}

Copy after login

flushCallbacks 这个方法就是挨个同步的去执行callbacks中的回调函数们,callbacks中的回调函数是在调用 nextTick 的时候添加进去的;那么怎么去使用 micro taskmacro task 去执行 flushCallbacks 呢,这里他们的实现 macroTimerFuncmicroTimerFunc 使用浏览器中宏任务/微任务的API对flushCallbacks 方法进行了一层包装。比如宏任务方法 macroTimerFunc=()=>{ setImmediate(flushCallbacks) },这样在触发宏任务执行的时候 macroTimerFunc() 就可以在浏览器中的下一个宏任务loop的时候消费这些保存在callbacks数组中的回调了,微任务同理。同时也可以看出传给 nextTick 的异步回调函数是被压成了一个同步任务在一个tick执行完的,而不是开启多个异步任务。

注意这里有个比较难理解的地方,第一次调用 nextTick 的时候 pending 为false,此时已经push到浏览器event loop中一个宏任务或微任务的task,如果在没有flush掉的情况下继续往callbacks里面添加,那么在执行这个占位queue的时候会执行之后添加的回调,所以 macroTimerFuncmicroTimerFunc 相当于task queue的占位,以后 pending 为true则继续往占位queue里面添加,event loop轮到这个task queue的时候将一并执行。执行 flushCallbackspending 置false,允许下一轮执行 nextTick 时往event loop占位。

可以看到上面 macroTimerFuncmicroTimerFunc 进行了在不同浏览器兼容性下的平稳退化,或者说降级策略

  1. macroTimerFuncsetImmediate -> MessageChannel -> setTimeout 。首先检测是否原生支持 setImmediate ,这个方法只在 IE、Edge 浏览器中原生实现,然后检测是否支持 MessageChannel,如果对 MessageChannel 不了解可以参考一下这篇文章,还不支持的话最后使用 setTimeout
    为什么优先使用 setImmediate MessageChannel 而不直接使用 setTimeout 呢,是因为HTML5规定setTimeout执行的最小延时为4ms,而嵌套的timeout表现为10ms,为了尽可能快的让回调执行,没有最小延时限制的前两者显然要优于 setTimeout

  2. microTimerFuncPromise.then -> macroTimerFunc 。首先检查是否支持 Promise,如果支持的话通过 Promise.then 来调用 flushCallbacks 方法,否则退化为 macroTimerFunc
    vue2.5之后 nextTick 中因为兼容性原因删除了微任务平稳退化的 MutationObserver 的方式。

2.2 nextTick实现

最后来看看我们平常用到的 nextTick 方法到底是如何实现的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

// src/core/util/next-tick.js

 

export function nextTick(cb?: Function, ctx?: Object) {

  let _resolve

  callbacks.push(() => {

    if (cb) {

      try {

        cb.call(ctx)

      catch (e) {

        handleError(e, ctx, 'nextTick')

      }

    else if (_resolve) {

      _resolve(ctx)

    }

  })

  if (!pending) {

    pending = true

    if (useMacroTask) {

      macroTimerFunc()

    else {

      microTimerFunc()

    }

  }

  if (!cb && typeof Promise !== 'undefined') {

    return new Promise(resolve => {

      _resolve = resolve

    })

  }

}

 

/* 强制使用macrotask的方法 */

export function withMacroTask(fn: Function): Function {

  return fn._withTask || (fn._withTask = function() {

    useMacroTask = true

    const res = fn.apply(null, arguments)

    useMacroTask = false

    return res

  })

}

Copy after login

nextTick 在这里分为三个部分,我们一起来看一下;

  1. 首先 nextTick 把传入的 cb 回调函数用 try-catch 包裹后放在一个匿名函数中推入callbacks数组中,这么做是因为防止单个 cb 如果执行错误不至于让整个JS线程挂掉,每个 cb 都包裹是防止这些回调函数如果执行错误不会相互影响,比如前一个抛错了后一个仍然可以执行。

  2. 然后检查 pending 状态,这个跟之前介绍的 queueWatcher 中的 waiting 是一个意思,它是一个标记位,一开始是 false 在进入 macroTimerFuncmicroTimerFunc 方法前被置为 true,因此下次调用 nextTick 就不会进入 macroTimerFuncmicroTimerFunc 方法,这两个方法中会在下一个 macro/micro tick 时候 flushCallbacks 异步的去执行callbacks队列中收集的任务,而 flushCallbacks 方法在执行一开始会把 pendingfalse,因此下一次调用 nextTick 时候又能开启新一轮的 macroTimerFuncmicroTimerFunc,这样就形成了vue中的 event loop

  3. 最后检查是否传入了 cb,因为 nextTick 还支持Promise化的调用:nextTick().then(() => {}),所以如果没有传入 cb 就直接return了一个Promise实例,并且把resolve传递给_resolve,这样后者执行的时候就跳到我们调用的时候传递进 then 的方法中。

Vue源码中 next-tick.js 文件还有一段重要的注释,这里就翻译一下:

在vue2.5之前的版本中,nextTick基本上基于 micro task 来实现的,但是在某些情况下 micro task 具有太高的优先级,并且可能在连续顺序事件之间(例如#4521,#6690)或者甚至在同一事件的事件冒泡过程中之间触发(#6566)。但是如果全部都改成 macro task,对一些有重绘和动画的场景也会有性能影响,如 issue #6813。vue2.5之后版本提供的解决办法是默认使用 micro task,但在需要时(例如在v-on附加的事件处理程序中)强制使用 macro task

为什么默认优先使用 micro task 呢,是利用其高优先级的特性,保证队列中的微任务在一次循环全部执行完毕。

强制 macro task 的方法是在绑定 DOM 事件的时候,默认会给回调的 handler 函数调用 withMacroTask 方法做一层包装 handler = withMacroTask(handler),它保证整个回调函数执行过程中,遇到数据状态的改变,这些改变都会被推到 macro task 中。以上实现在 src/platforms/web/runtime/modules/events.js 的 add 方法中,可以自己看一看具体代码。

刚好在写这篇文章的时候思否上有人问了个问题 vue 2.4 和2.5 版本的@input事件不一样 ,这个问题的原因也是因为2.5之前版本的DOM事件采用 micro task ,而之后采用 macro task,解决的途径参考 中介绍的几个办法,这里就提供一个在mounted钩子中用 addEventListener 添加原生事件的方法来实现,参见 CodePen。

3. 一个例子

说这么多,不如来个例子,执行参见 CodePen

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

<p>

  <span>{{ name }}</span>

  <button>change name</button>

  </p><p></p>

 

<script>

  new Vue({

    el: &#39;#app&#39;,

    data() {

      return {

        name: &#39;SHERlocked93&#39;

      }

    },

    methods: {

      change() {

        const $name = this.$refs.name

        this.$nextTick(() => console.log(&#39;setter前:&#39; + $name.innerHTML))

        this.name = &#39; name改喽 &#39;

        console.log(&#39;同步方式:&#39; + this.$refs.name.innerHTML)

        setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))

        this.$nextTick(() => console.log(&#39;setter后:&#39; + $name.innerHTML))

        this.$nextTick().then(() => console.log(&#39;Promise方式:&#39; + $name.innerHTML))

      }

    }

  })

</script>

Copy after login

执行以下看看结果:

1

2

3

4

5

同步方式:SHERlocked93 

setter前:SHERlocked93 

setter后:name改喽 

Promise方式:name改喽 

setTimeout方式:name改喽

Copy after login

为什么是这样的结果呢,解释一下:

  1. 同步方式: 当把data中的name修改之后,此时会触发name的 setter 中的 dep.notify 通知依赖本data的render watcher去 updateupdate 会把 flushSchedulerQueue 函数传递给 nextTick,render watcher在 flushSchedulerQueue 函数运行时 watcher.run 再走 diff -> patch 那一套重渲染 re-render 视图,这个过程中会重新依赖收集,这个过程是异步的;所以当我们直接修改了name之后打印,这时异步的改动还没有被 patch 到视图上,所以获取视图上的DOM元素还是原来的内容。

  2. Before setter: Why is the original content printed before setter? It’s because nextTick pushes the callbacks one by one into callbacks when called. When the array is executed later, it is also for looped out and executed one by one, so it is a concept similar to a queue, first in, first out; after modifying the name, trigger the render watcher to be filled in schedulerQueue Queue and pass its execution function flushSchedulerQueue to nextTick. At this time, there is already a setter pre-function in the callbacks queue, because this cb is pushed into the callbacks queue after the setter pre-function, so when executing callbacks in callbacks on a first-in, first-out basis, the pre-setter function is executed first, and render is not executed at this time. watcher's watcher.run, so the printed DOM element is still the original content.

  3. After the setter: The setter has been executed at this time flushSchedulerQueue, and the render watcher has already changed the patch to the view, so getting the DOM at this time is the modified content.

  4. Promise method: is equivalent to Promise.then to execute this function. At this time, the DOM has changed.

  5. setTimeout method: Finally execute the macro task, when the DOM has changed.

Note that before executing the setter pre-function this asynchronous task, the synchronous code has been executed, and the asynchronous tasks have not yet been executed. All The $nextTick function has also been executed, and all callbacks have been pushed into the callbacks queue to wait for execution, so when the setter pre-function is executed, the callbacks queue is like this: [ Function before setter, flushSchedulerQueue, Function after setter, Promise mode function], it is a micro task queue, and execute macro task after completion setTimeout, so print out the above result.

In addition, if there are various types of tasks such as setImmediate, MessageChannel, setTimeout/setInterval in the browser's macro task queue, then Follow the above order and execute it one by one in the order added to the event loop, so if the browser supports MessageChannel, nextTick executes macroTimerFunc, then if the macrotask queue If there are both tasks added by nextTick and tasks of type setTimeout added by the user, the tasks in nextTick will be executed first because MessageChannel## The priority of # is higher than that of setTimeout, and the same is true for setImmediate.

Related recommendations:


Analysis of how to use mixins in Vue

##Vue2 .0Custom instructions and instance attributes and methods

The above is the detailed content of Analysis of batch asynchronous update and nextTick principle in Vue source code. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

In-depth discussion of how vite parses .env files In-depth discussion of how vite parses .env files Jan 24, 2023 am 05:30 AM

When using the Vue framework to develop front-end projects, we will deploy multiple environments when deploying. Often the interface domain names called by development, testing and online environments are different. How can we make the distinction? That is using environment variables and patterns.

Detailed graphic explanation of how to integrate the Ace code editor in a Vue project Detailed graphic explanation of how to integrate the Ace code editor in a Vue project Apr 24, 2023 am 10:52 AM

Ace is an embeddable code editor written in JavaScript. It matches the functionality and performance of native editors like Sublime, Vim, and TextMate. It can be easily embedded into any web page and JavaScript application. Ace is maintained as the main editor for the Cloud9 IDE and is the successor to the Mozilla Skywriter (Bespin) project.

What is the difference between componentization and modularization in vue What is the difference between componentization and modularization in vue Dec 15, 2022 pm 12:54 PM

The difference between componentization and modularization: Modularization is divided from the perspective of code logic; it facilitates code layered development and ensures that the functions of each functional module are consistent. Componentization is planning from the perspective of UI interface; componentization of the front end facilitates the reuse of UI components.

Explore how to write unit tests in Vue3 Explore how to write unit tests in Vue3 Apr 25, 2023 pm 07:41 PM

Vue.js has become a very popular framework in front-end development today. As Vue.js continues to evolve, unit testing is becoming more and more important. Today we’ll explore how to write unit tests in Vue.js 3 and provide some best practices and common problems and solutions.

Let's talk in depth about reactive() in vue3 Let's talk in depth about reactive() in vue3 Jan 06, 2023 pm 09:21 PM

Foreword: In the development of vue3, reactive provides a method to implement responsive data. This is a frequently used API in daily development. In this article, the author will explore its internal operating mechanism.

A simple comparison of JSX syntax and template syntax in Vue (analysis of advantages and disadvantages) A simple comparison of JSX syntax and template syntax in Vue (analysis of advantages and disadvantages) Mar 23, 2023 pm 07:53 PM

In Vue.js, developers can use two different syntaxes to create user interfaces: JSX syntax and template syntax. Both syntaxes have their own advantages and disadvantages. Let’s discuss their differences, advantages and disadvantages.

A brief analysis of how to handle exceptions in Vue3 dynamic components A brief analysis of how to handle exceptions in Vue3 dynamic components Dec 02, 2022 pm 09:11 PM

How to handle exceptions in Vue3 dynamic components? The following article will talk about Vue3 dynamic component exception handling methods. I hope it will be helpful to everyone!

How to query the current vue version How to query the current vue version Dec 19, 2022 pm 04:55 PM

There are two ways to query the current Vue version: 1. In the cmd console, execute the "npm list vue" command to query the version. The output result is the version number information of Vue; 2. Find and open the package.json file in the project and search You can see the version information of vue in the "dependencies" item.

See all articles