Vue-nextTick原理

Posted 阳123456

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue-nextTick原理相关的知识,希望对你有一定的参考价值。

VUE-nextTick原理

1、JS Event Loop

介绍 Vue 的 nextTick 之前,先简单介绍一下 JS 的运行机制:JS 执行是单线程的,它是基于事件循环的。对于事件循环的理解,阮老师有一篇文章写的很清楚,大致分为以下几个步骤:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度被调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task

  • 在浏览器环境中,常见的 macro tasksetTimeoutMessageChannelpostMessagesetImmediate
  • 常见的 micro taskMutationObseverPromise.then

2、Vue 的 nextTick

Vue 的 nextTick,顾名思义,就是下一个 tick,Vue 内部实现了 nextTick,并把它作为一个全局 API 暴露出来,它支持传入一个回调函数,保证回调函数的执行时机是在下一个 tick。官网文档介绍了 Vue.nextTick 的使用场景:

Usage: Defer the callback to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update.
使用:在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的 DOM。
在 Vue.js 里是数据驱动视图变化,由于 JS 执行是单线程的,在一个 tick 的过程中,它可能会多次修改数据,但 Vue.js 并不会傻到每修改一次数据就去驱动一次视图变化,它会把这些数据的修改全部 push 到一个队列里,然后内部调用 一次 nextTick 去更新视图,所以数据到 DOM 视图的变化是需要在下一个 tick 才能完成。

3、源码

1.  /* @flow */
    
2.  /* globals MessageChannel */
    

4.  import { noop } from \'shared/util\'
    
5.  import { handleError } from \'./error\'
    
6.  import { isios, isNative } from \'./env\'
    

8.  const callbacks = []
    
9.  let pending = false
    

11.  function flushCallbacks () {
    
12.      pending = false
    
13.      const copies = callbacks.slice(0)
    
14.      callbacks.length = 0
    
15.      for (let i = 0; i < copies.length; i++) {
    
16.          copies[i]()
    
17.      }
    
18.  }
    

20.  // Here we have async deferring wrappers using both micro and macro tasks.
    
21.  // In < 2.4 we used micro tasks everywhere, but there are some scenarios where
    
22.  // micro tasks have too high a priority and fires in between supposedly
    
23.  // sequential events (e.g. #4521, #6690) or even between bubbling of the same
    
24.  // event (#6566). However, using macro tasks everywhere also has subtle problems
    
25.  // when state is changed right before repaint (e.g. #6813, out-in transitions).
    
26.  // Here we use micro task by default, but expose a way to force macro task when
    
27.  // needed (e.g. in event handlers attached by v-on).
    
28.  let microTimerFunc
    
29.  let macroTimerFunc
    
30.  let useMacroTask = false
    

32.  // Determine (macro) Task defer implementation.
    
33.  // Technically setImmediate should be the ideal choice, but it\'s only available
    
34.  // in IE. The only polyfill that consistently queues the callback after all DOM
    
35.  // events triggered in the same loop is by using MessageChannel.
    
36.  /* istanbul ignore if */
    
37.  if (typeof setImmediate !== \'undefined\' && isNative(setImmediate)) {
    
38.      macroTimerFunc = () => {
    
39.          setImmediate(flushCallbacks)
    
40.      }
    
41.  } else if (typeof MessageChannel !== \'undefined\' && (
    
42.      isNative(MessageChannel) ||
    
43.      // PhantomJS
    
44.      MessageChannel.toString() === \'[object MessageChannelConstructor]\'
    
45.  )) {
    
46.      const channel = new MessageChannel()
    
47.      const port = channel.port2
    
48.      channel.port1.onmessage = flushCallbacks
    
49.      macroTimerFunc = () => {
    
50.          port.postMessage(1)
    
51.      }
    
52.  } else {
    
53.      /* istanbul ignore next */
    
54.      macroTimerFunc = () => {
    
55.          setTimeout(flushCallbacks, 0)
    
56.      }
    
57.  }
    

59.  // Determine MicroTask defer implementation.
    
60.  /* istanbul ignore next, $flow-disable-line */
    
61.  if (typeof Promise !== \'undefined\' && isNative(Promise)) {
    
62.      const p = Promise.resolve()
    
63.      microTimerFunc = () => {
    
64.          p.then(flushCallbacks)
    
65.          // in problematic UIWebViews, Promise.then doesn\'t completely break, but
    
66.          // it can get stuck in a weird state where callbacks are pushed into the
    
67.          // microtask queue but the queue isn\'t being flushed, until the browser
    
68.          // needs to do some other work, e.g. handle a timer. Therefore we can
    
69.          // "force" the microtask queue to be flushed by adding an empty timer.
    
70.          if (isIOS) setTimeout(noop)
    
71.     }
    
72.   } else {
    
73.      // fallback to macro
    
74.      microTimerFunc = macroTimerFunc
    
75.  }
    

77.  /**
    
78.  * Wrap a function so that if any code inside triggers state change,
    
79.  * the changes are queued using a Task instead of a MicroTask.
    
80.  */
    
81.  export function withMacroTask (fn: Function): Function {
    
82.      return fn._withTask || (fn._withTask = function () {
    
83.          useMacroTask = true
    
84.          const res = fn.apply(null, arguments)
    
85.          useMacroTask = false
    
86.          return res
    
87.      })
    
88.  }
    

90.  export function nextTick (cb?: Function, ctx?: Object) {
    
91.       let _resolve
    
92.       callbacks.push(() => {
    
93.         if (cb) {
    
94.             try {
    
95.                 cb.call(ctx)
    
96.             } catch (e) {
    
97.                  handleError(e, ctx, \'nextTick\')
    
98.          }
    
99.      } else if (_resolve) {
    
100.            _resolve(ctx)
    
101.     }
    
102.  })
    
103.  if (!pending) {
    
104.      pending = true
    
105.      if (useMacroTask) {
    
106.          macroTimerFunc()
    
107.      } else {
    
108.          microTimerFunc()
    
109.     }
    
110.  }
    
111.  // $flow-disable-line
    
112.  if (!cb && typeof Promise !== \'undefined\') {
    
113.       return new Promise(resolve => {
    
114.            _resolve = resolve
    
115.       })
    
116.    }
    
117.  }

这段源码中 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。

这个强制指的是,原来在 Vue.js 在绑定 DOM 事件的时候,默认会给回调的 handler 函数调用 withMacroTask 方法做一层包装 handler = withMacroTask(handler),它保证整个回调函数执行过程中,遇到数据状态的改变,这些改变都会被推到 macro task 中。

对于 macro task 的执行,Vue.js 优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0

4、一个小例子
<div id="app">
    <span id=\'name\' ref=\'name\'>{{ name }}</span>
    <button @click=\'change\'>change name</button>
    <div id=\'content\'></div>
</div>
<script>
new Vue({
    el: \'#app\',
    data() {
        return {
            name: \'阳123456\'
    }
},
methods: {
    change() {
        const $name = this.$refs.name
        this.$nextTick(() => console.log(\'setter前:\' + $name.innerhtml))
        this.name = \' name改了 \'
        console.log(\'同步方式:\' + this.$refs.name.innerHTML)
        setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
        this.$nextTick(() => console.log(\'setter后:\' + $name.innerHTML))
        this.$nextTick().then(() => console.log(\'Promise方式:\' + $name.innerHTML))
     }
  }
})
</script>

执行结果为:

同步方式:阳123456
setter前:阳123456
setter后:name改了
Promise方式:name改了
setTimeout方式:name改了

解析

  1. 同步方式: 当把data中的name修改之后,此时会触发namesetter 中的 dep.notify 通知依赖本datarender watcherupdateupdate 会把 flushSchedulerQueue 函数传递给 nextTickrender watcherflushSchedulerQueue 函数运行时 watcher.run 再走 diff -> patch 那一套重渲染 re-render 视图,这个过程中会重新依赖收集,这个过程是异步的;所以当我们直接修改了name之后打印,这时异步的改动还没有被 patch 到视图上,所以获取视图上的DOM元素还是原来的内容。
  2. setter前setter前为什么还打印原来的是原来内容呢,是因为 nextTick 在被调用的时候把回调挨个pushcallbacks数组,之后执行的时候也是 for 循环出来挨个执行,所以是类似于队列这样一个概念,先入先出;在修改name之后,触发把render watcher填入 schedulerQueue 队列并把他的执行函数 flushSchedulerQueue 传递给 nextTick ,此时callbacks队列中已经有了 setter前函数 了,因为这个 cb 是在 setter前函数 之后被pushcallbacks队列的,那么先入先出的执行callbacks中回调的时候先执行 setter前函数,这时并未执行render watcherwatcher.run,所以打印DOM元素仍然是原来的内容。
  3. setter后setter后这时已经执行完 flushSchedulerQueue,这时render watcher已经把改动 patch 到视图上,所以此时获取DOM是改过之后的内容。
  4. Promise方式: 相当于 Promise.then 的方式执行这个函数,此时DOM已经更改。
  5. setTimeout方式: 最后执行macro task的任务,此时DOM已经更改。

注意,在执行 setter前 函数 这个异步任务之前,同步的代码已经执行完毕,异步的任务都还未执行,所有的 $nextTick 函数也执行完毕,所有回调都被push进了callbacks队列中等待执行,所以在setter前 函数执行的时候,此时callbacks队列是这样的:[setter前函数,flushSchedulerQueuesetter后函数,Promise方式函数],它是一个micro task队列,执行完毕之后执行macro tasksetTimeout,所以打印出上面的结果。
另外,如果浏览器的宏任务队列里面有setImmediateMessageChannelsetTimeout/setInterval 各种类型的任务,那么会按照上面的顺序挨个按照添加进event loop中的顺序执行,所以如果浏览器支持MessageChannelnextTick 执行的是 macroTimerFunc,那么如果 macrotask queue 中同时有 nextTick 添加的任务和用户自己添加的 setTimeout 类型的任务,会优先执行 nextTick 中的任务,因为MessageChannel 的优先级比 setTimeout的高,setImmediate 同理。

说明

以上部分内容来源与自己复习时的网络查找,也主要用于个人学习,相当于记事本的存在,暂不列举链接文章。如果有作者看到,可以联系我将原文链接贴出。

以上是关于Vue-nextTick原理的主要内容,如果未能解决你的问题,请参考以下文章

数据库系统原理 片段翻译

标签菜单android - 不正确的调用片段

如何在不单击的情况下显示片段

二级域名原理以及程序代码

深入理解PHP原理之Opcodes

HashMap原理:哈希函数的设计