一、异步更新队列
可能你还没有留神到,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 更新实现后被调用。例如:
<div id="example">{{message}}</div>
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 实例上:
Vue.component('example', {template: '<span>{{ message}}</span>',
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 语法实现雷同的事件:
methods: {updateMessage: async function () {
this.message = '已更新';
console.log(this.$el.textContent); // => '未更新'
await this.$nextTick();
console.log(this.$el.textContent); // => '已更新'
}
}
nextTick
接管一个回调函数作为参数,并将这个回调函数提早到 DOM 更新后才执行;nextTick
是在下次 DOM 更新循环完结之后执行提早回调,在批改数据之后应用 nextTick,则能够在回调中获取更新后的 DOM
应用场景 :想要操作 基于最新数据的生成 DOM 时,就将这个操作放在 nextTick
的回调中;
二、前置常识
nextTick
函数的作用能够了解为异步执行传入的函数,这里先介绍一下什么是异步执行,从 JS 运行机制说起。
2.1 JS 运行机制
JS 的执行是单线程的,所谓的单线程就是事件工作要排队执行,前一个工作完结,才会执行后一个工作,这就是同步工作,为了防止前一个工作执行了很长时间还没完结,那下一个工作就不能执行的状况,引入了异步工作的概念。JS 运行机制简略来说能够按以下几个步骤。
- 所有同步工作都在主线程上执行,造成一个执行栈(execution context stack)。
- 主线程之外,还存在一个工作队列(task queue)。只有异步工作有了运行后果,会把其回调函数作为一个工作增加到工作队列中。
- 一旦执行栈中的所有同步工作执行结束,就会读取工作队列,看看外面有那些工作,将其增加到执行栈,开始执行。
- 主线程一直反复下面的第三步。也就是常说的事件循环(Event Loop)。
2.2 异步工作的类型
nextTick
函数异步执行传入的函数,是一个异步工作。异步工作分为两种类型。
主线程的执行过程就是一个 tick
,而所有的异步工作都是通过工作队列来一一执行。工作队列中寄存的是一个个的工作(task)。标准中规定 task 分为两大类,别离是宏工作(macro task)和微工作(micro task),并且每个 macro task
完结后,都要清空所有的 micro task
。
用一段代码形象介绍 task 的执行程序。
for (macroTask of macroTaskQueue) {handleMacroTask();
for (microTask of microTaskQueue) {handleMicroTask(microTask);
}
}
在浏览器环境中,常见的创立 macro task
的办法有
setTimeout
、setInterval
、postMessage
、MessageChannel
(队列优先于setTimeiout
执行)网络申请 IO
- 页面交互:DOM、鼠标、键盘、滚动事件
- 页面渲染
常见的创立 micro task
的办法
Promise.then
MutationObserve
process.nexttick
在 nextTick
函数要利用这些办法把通过参数 cb
传入的函数解决成异步工作。
三、nextTick 实现原理
将传入的回调函数包装成异步工作,异步工作又分微工作和宏工作,为了尽快执行所以优先选择微工作;nextTick
提供了四种异步办法 Promise.then
、MutationObserver
、setImmediate
、setTimeOut(fn,0)
3.1 Vue.nextTick 外部逻辑
在执行 initGlobalAPI(Vue)
初始化 Vue 全局 API 中,这么定义Vue.nextTick
:
function initGlobalAPI(Vue) {
//...
Vue.nextTick = nextTick;
}
能够看出是间接把 nextTick 函数赋值给 Vue.nextTick,就能够了,非常简单。
3.2 vm.$nextTick 外部逻辑
Vue.prototype.$nextTick = function (fn) {return nextTick(fn, this)
};
能够看出是 vm.$nextTick
外部也是调用 nextTick
函数。
3.3 源码解读
nextTick
的源码位于 src/core/util/next-tick.js
nextTick
源码次要分为两块:
import {noop} from 'shared/util'
import {handleError} from './error'
import {isIE, isIOS, isNative} from './env'
// 下面三行与外围代码关系不大,理解即可
// noop 示意一个无操作空函数,用作函数默认值,避免传入 undefined 导致报错
// handleError 谬误处理函数
// isIE, isIOS, isNative 环境判断函数,// isNative 判断是否原生反对,如果通过第三方实现反对也会返回 false
export let isUsingMicroTask = false // nextTick 最终是否以微工作执行
const callbacks = [] // 寄存调用 nextTick 时传入的回调函数
let pending = false // 标识以后是否有 nextTick 在执行,同一时间只能有一个执行
// 申明 nextTick 函数,接管一个回调函数和一个执行上下文作为参数
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
// 将传入的回调函数寄存到数组中,前面会遍历执行其中的回调
callbacks.push(() => {if (cb) { // 对传入的回调进行 try catch 谬误捕捉
try {cb.call(ctx)
} catch (e) {handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {_resolve(ctx)
}
})
// 如果以后没有在 pending 的回调,就执行 timeFunc 函数抉择以后环境优先反对的异步办法
if (!pending) {
pending = true
timerFunc()}
// 如果没有传入回调,并且以后环境反对 promise,就返回一个 promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {_resolve = resolve})
}
}
能够看到在 nextTick
函数中把通过参数 cb 传入的函数,做一下包装而后 push 到 callbacks
数组中。
而后用变量 pending 来保障执行一个事件循环中只执行一次 timerFunc()。
最初执行 if (!cb && typeof Promise !== 'undefined')
,判断参数 cb
不存在且浏览器反对 Promise,则返回一个 Promise 类实例化对象。例如 nextTick().then(() => {})
,当 _resolve
函数执行,就会执行 then 的逻辑中。
来看一下 timerFunc
函数的定义,先只看用 Promise 创立一个异步执行的 timerFunc
函数。
// 判断以后环境优先反对的异步办法,优先选择微工作
// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
// setTimeOut 最小提早也要 4ms,而 setImmediate 会在主线程执行完后立即执行
// setImmediate 在 IE10 和 node 中反对
// 屡次调用 nextTick 时 ,timerFunc 只会执行一次
let timerFunc
// 判断以后环境是否反对 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 反对 promise
const p = Promise.resolve()
timerFunc = () => {
// 用 promise.then 把 flushCallbacks 函数包裹成一个异步微工作
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
// 标记以后 nextTick 应用的微工作
isUsingMicroTask = true
// 如果不反对 promise,就判断是否反对 MutationObserver
// 不是 IE 环境,并且原生反对 MutationObserver,那也是一个微工作
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
// new 一个 MutationObserver 类
const observer = new MutationObserver(flushCallbacks)
// 创立一个文本节点
const textNode = document.createTextNode(String(counter))
// 监听这个文本节点,当数据发生变化就执行 flushCallbacks
observer.observe(textNode, { characterData: true})
timerFunc = () => {counter = (counter + 1) % 2
textNode.data = String(counter) // 数据更新
}
isUsingMicroTask = true // 标记以后 nextTick 应用的微工作
// 判断以后环境是否原生反对 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = () => {setImmediate(flushCallbacks) }
} else {
// 以上三种都不反对就抉择 setTimeout
timerFunc = () => { setTimeout(flushCallbacks, 0) }
}
其中 isNative
办法是如何定义,代码如下。
function isNative(Ctor) {return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
在其中发现 timerFunc
函数就是用各种异步执行的办法调用 flushCallbacks
函数。
来看一下 flushCallbacks
函数
// 如果屡次调用 nextTick,会顺次执行下面的办法,将 nextTick 的回调放在 callbacks 数组中
// 最初通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0) // 拷贝一份
callbacks.length = 0 // 清空 callbacks
for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调
copies[i]()}
}
// 为什么要拷贝一份 callbacks
// callbacks.slice(0) 将 callbacks 拷贝进去一份,// 是因为思考到 nextTick 回调中可能还会调用 nextTick 的状况,
// 如果 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中增加回调,// nextTick 回调中的 nextTick 应该放在下一轮执行,// 如果不将 callbacks 复制一份就可能始终循环
执行 pending = false
使下个事件循环中能 nextTick
函数中调用 timerFunc
函数。
执行 var copies = callbacks.slice(0);callbacks.length = 0
; 把要异步执行的函数汇合 callbacks
克隆到常量 copies
,而后把 callbacks
清空。
而后遍历 copies
执行每一项函数。回到 nextTick
中是把通过参数 cb
传入的函数包装后 push 到 callbacks
汇合中。来看一下怎么包装的。
function() {if (cb) {
try {cb.call(ctx);
} catch (e) {handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {_resolve(ctx);
}
}
逻辑很简略。若参数 cb 有值。在 try 语句中执行 cb.call(ctx)
,参数 ctx 是传入函数的参数。如果执行失败执行 handleError(e, ctx, 'nextTick')
。
若参数 cb 没有值。执行 _resolve(ctx)
,因为在 nextTick
函数中如何参数 cb 没有值,会返回一个 Promise 类实例化对象,那么执行 _resolve(ctx)
,就会执行 then 的逻辑中。
到这里 nextTick
函数的主线逻辑就很分明了。定义一个变量 callbacks
,把通过参数 cb 传入的函数用一个函数包装一下,在这个中会执行传入的函数,及解决执行失败和参数 cb 不存在的场景,而后 增加到 callbacks。
调用 timerFunc
函数,在其中遍历 callbacks
执行每个函数,因为 timerFunc
是一个异步执行的函数,且定义一个变量 pending
来保障一个事件循环中只调用一次 timerFunc
函数。这样就实现了 nextTick
函数异步执行传入的函数的作用了。
那么其中的要害还是怎么定义 timerFunc
函数。因为在各浏览器下对创立异步执行函数的办法各不相同,要做兼容解决,上面来介绍一下各种办法。
3.4 为什么优先应用微工作:
依照下面事件循环的执行程序,执行下一次宏工作之前会执行一次 UI 渲染,期待时长比微工作要多很多。所以在能应用微工作的时候优先应用微工作,不能应用微工作的时候才应用宏工作,优雅降级。*
四、参考文献
深刻响应式原理 — Vue.js
nextTick 实现原理,必拿下! – 掘金
🚩Vue 源码——nextTick 实现原理 – 掘金