Vue 源码分析记录 nextTick 作者: lambert 发表时间: 2020年8月31日 10:41 203 分类: Vue 源码解析 源码学习 个人 # 全局 API ## 1. nextTick 实现原理与目的 ### 目的:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。 ```js // 修改数据 vm.msg = 'Hello' // DOM 还没有更新 Vue.nextTick(function () { // DOM 更新了 }) // 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示) Vue.nextTick() .then(function () { // DOM 更新了 }) ``` ### 原因: ## [异步更新队列](https://cn.vuejs.org/v2/guide/reactivity.html#异步更新队列) 可能你还没有注意到,`Vue 在更新 DOM 时是**异步**执行的`。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。 然后,`在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作`。Vue 在内部对异步队列尝试使用原生的 `Promise.then`、`MutationObserver` 和 `setImmediate`,如果执行环境不支持,则会采用 `setTimeout(fn, 0)` 代替。 例如,当你设置 `vm.someData = 'new value'`,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。 `为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 `Vue.nextTick(callback)`。这样回调函数将在 DOM 更新完成后被调用。` 例如: ```js {{message}} var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message' // 更改数据 vm.$el.textContent === 'new message' // false Vue.nextTick(function () { vm.$el.textContent === 'new message' // true }) ``` 在组件内使用 `vm.$nextTick()` 实例方法特别方便,因为它不需要全局 `Vue`,并且回调函数中的 `this` 将自动绑定到当前的 Vue 实例上: ```js Vue.component('example', { template: '{{ message }}', data: function () { return { message: '未更新' } }, methods: { updateMessage: function () { this.message = '已更新' console.log(this.$el.textContent) // => '未更新' this.$nextTick(function () { console.log(this.$el.textContent) // => '已更新' }) } } }) ``` 因为 `$nextTick()` 返回一个 `Promise` 对象,所以你可以使用新的 [ES2017 async/await](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function) 语法完成相同的事情: ```js methods: { updateMessage: async function () { this.message = '已更新' console.log(this.$el.textContent) // => '未更新' await this.$nextTick() console.log(this.$el.textContent) // => '已更新' } } ``` ### 异步机制原理 1. Vue 在内部对异步队列尝试使用原生的 `Promise.then`、`MutationObserver` 和 `setImmediate`,如果执行环境不支持,则会采用 `setTimeout(fn, 0)` 代替。 ### 实现源码 ```js /** * Defer a task to execute it asynchronously. */ var nextTick = (function () { // 回调函数队列 var callbacks = []; var pending = false; var timerFunc; // next Tick 的处理方法,执行所有回调函数 function nextTickHandler () { pending = false; var copies = callbacks.slice(0); callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copies[i](); } } // An asynchronous deferring mechanism. // In pre 2.4, we used to use microtasks (Promise/MutationObserver) // but microtasks actually has too high a priority and fires in between // supposedly sequential events (e.g. #4521, #6690) or even between // bubbling of the same event (#6566). Technically setImmediate should be // the ideal choice, but it's not available everywhere; and the only polyfill // that consistently queues the callback after all DOM events triggered in the // same loop is by using MessageChannel. /* istanbul ignore if */ if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function () { setImmediate(nextTickHandler); }; } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { var channel = new MessageChannel(); var port = channel.port2; channel.port1.onmessage = nextTickHandler; timerFunc = function () { port.postMessage(1); }; } else /* istanbul ignore next */ if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex var p = Promise.resolve(); timerFunc = function () { p.then(nextTickHandler); }; } else { // fallback to setTimeout timerFunc = function () { setTimeout(nextTickHandler, 0); }; } return function queueNextTick (cb, ctx) { var _resolve; callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve, reject) { _resolve = resolve; }) } } })(); ```