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: 'SHERlocked93'    }},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>

执行后果为:

同步形式:SHERlocked93
setter前:SHERlocked93
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 同理。

阐明

以上局部内容起源与本人温习时的网络查找,也次要用于集体学习,相当于记事本的存在,暂不列举链接文章。如果有作者看到,能够分割我将原文链接贴出。