介绍 Vue 的 nextTick
以前,先简单介绍一下 JS 的运行机制:JS 执行是单线程的,它是基于事件循环的。对于事件循环的理解,阮老师有一篇文章写的很清楚,大体分为如下几个步骤:html
(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack
)。vue
(2)主线程以外,还存在一个"任务队列"(task queue
)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。ios
(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。数组
(4)主线程不断重复上面的第三步。浏览器
主线程的执行过程就是一个 tick,而全部的异步结果都是经过 “任务队列” 来调度被调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task
和 micro task,而且每一个 macro task
结束后,都要清空全部的 micro task
。网络
macro task
有 setTimeout
、MessageChannel
、postMessage
、setImmediate
;micro task
有 MutationObsever
和 Promise.then
。Vue 的 nextTick
,顾名思义,就是下一个 tick
,Vue 内部实现了 nextTick
,并把它做为一个全局 API 暴露出来,它支持传入一个回调函数,保证回调函数的执行时机是在下一个 tick
。官网文档介绍了 Vue.nextTick
的使用场景:app
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 才能完成。
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
中。async
对于 macro task 的执行,Vue.js 优先检测是否支持原生 setImmediate
,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel
,若是也不支持的话就会降级为 setTimeout 0
。ide
<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
同理。
以上部份内容来源与本身复习时的网络查找,也主要用于我的学习,至关于记事本的存在,暂不列举连接文章。若是有做者看到,能够联系我将原文连接贴出。