共计 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 task
有setTimeout
、MessageChannel
、postMessage
、setImmediate
; - 常见的
micro task
有MutationObsever
和Promise.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 改喽
解析
- 同步形式:当把
data
中的name
批改之后,此时会触发name
的setter
中的dep.notify
告诉依赖本data
的render watcher
去update
,update
会把flushSchedulerQueue
函数传递给nextTick
,render watcher
在flushSchedulerQueue
函数运行时watcher.run
再走diff -> patch
那一套重渲染re-render
视图,这个过程中会从新依赖收集,这个过程是异步的;所以当咱们间接批改了name
之后打印,这时异步的改变还没有被patch
到视图上,所以获取视图上的DOM
元素还是原来的内容。 setter 前
:setter 前
为什么还打印原来的是原来内容呢,是因为nextTick
在被调用的时候把回调挨个push
进callbacks
数组,之后执行的时候也是for
循环进去挨个执行,所以是相似于队列这样一个概念,先入先出;在批改name
之后,触发把render watcher
填入schedulerQueue
队列并把他的执行函数flushSchedulerQueue
传递给nextTick
,此时callbacks
队列中曾经有了setter
前函数 了,因为这个cb
是在setter
前函数 之后被push
进callbacks
队列的,那么先入先出的执行callbacks
中回调的时候先执行setter
前函数,这时并未执行render watcher
的watcher.run
,所以打印DOM
元素依然是原来的内容。setter 后
:setter 后
这时曾经执行完flushSchedulerQueue
,这时render watcher
曾经把改变patch
到视图上,所以此时获取DOM
是改过之后的内容。Promise 形式
:相当于Promise.then
的形式执行这个函数,此时DOM
曾经更改。setTimeout 形式
:最初执行macro task
的工作,此时DOM
曾经更改。
留神,在执行 setter 前
函数 这个异步工作之前,同步的代码曾经执行结束,异步的工作都还未执行,所有的 $nextTick
函数也执行结束,所有回调都被 push
进了 callbacks
队列中期待执行,所以在 setter 前
函数执行的时候,此时callbacks
队列是这样的:[setter 前
函数,flushSchedulerQueue
,setter 后
函数,Promise
形式函数],它是一个 micro task
队列,执行结束之后执行 macro task
、setTimeout
,所以打印出下面的后果。
另外,如果浏览器的宏工作队列外面有 setImmediate
、MessageChannel
、setTimeout/setInterval
各种类型的工作,那么会依照下面的程序挨个依照增加进event loop
中的程序执行,所以如果浏览器反对 MessageChannel
,nextTick
执行的是 macroTimerFunc
,那么如果 macrotask queue
中同时有 nextTick
增加的工作和用户本人增加的 setTimeout
类型的工作,会优先执行 nextTick
中的工作,因为MessageChannel
的优先级比 setTimeout
的高,setImmediate
同理。
阐明
以上局部内容起源与本人温习时的网络查找,也次要用于集体学习,相当于记事本的存在,暂不列举链接文章。如果有作者看到,能够分割我将原文链接贴出。