关于javascript:VuenextTick原理

50次阅读

共计 6774 个字符,预计需要花费 17 分钟才能阅读完成。

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 同理。

阐明

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

正文完
 0