简述
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。
我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新。
注意!!
- vue 实现响应式并不是数据发生变化后 DOM 立即变化,而是按照一定策略异步执行 DOM 更新的
- vue 在修改数据后,视图不会立刻进行更新,而是要等同一事件循环机制内所有数据变化完成后,再统一进行DOM更新
- nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。
事件循环机制
- 宏任务: 包括整体代码 script,setTimeout,setInterval 、setImmediate、 I/O 操作、UI 渲染
- 微任务: Promise.then、 MutationObserver(MutationObserver 可以观察整个 文档、DOM 树的一部分 或 具体 dom 元素,主要是观察元素的 属性、子节点、文本 的变化,并且可以在 DOM 被修改时异步执行回调。)
具体示例分析
示例
<template>
<div>
<div>{{count}}</div>
<div @click="handleClick">click</div>
</div>
</template>
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 10000; i++) {
this.count++;
}
}
}
}
分析
- 当点击按钮时,count会被循环改变10000次。那么每次count+1,都会触发count的setter方法,然后修改真实DOM。 按此逻辑,这整个过程,DOM会被更新10000次,我们都知道DOM的操作是非常昂贵的,而且这样的操作完全没有必要。所以vue内部在派发更新时做了优化。
- 也就是,并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列
queueWatcher
里,然后在 nextTick 后执行flushSchedulerQueue
处理。 - 当 count 增加 10000 次时,vue内部会先将对应的 Watcher 对象给 push 进一个队列 queue 中去,等下一个 tick 的时候再去执行。并不需要在下一个 tick 的时候执行 10000 个同样的 Watcher 对象去修改界面,而是只需要执行一个 Watcher 对象,使其将界面上的 0 变成 10000 即可。
理论知识
在 Vue中 数据变化 => DOM 变化这是异步的过程,一旦观察到数据变化,Vue就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察,得到数据变化的 Watcher(Vue源码中的Watcher类是用来更新Dep类收集到的依赖的)推送进这个队列。
如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
nextTick的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用Vue.nextTick(callback)
,JS是单线程的,拥有事件循环机制,nextTick的实现就是利用了事件循环的宏任务和微任务。
队列数据结构
队列从一端存入数据,另一端调取数据,其原则称为“先进先出”原则。(first in first out,简称“FIFO”)
- 入队列:进行插入操作的一端称为队尾。
- 出队列:进行删除操作的一端称为队头。
图解:根据队列的先进先出原则,(a1,a2,a3,a4,a5)中,由于 a1 最先从队尾进入队列,所以可以最先从队头出队列,对于 a2 来说,只有 a1 出队之后,a2 才能出队。
队列的高级使用模式有下列
- 消息队列
- 高性能队列
- 优先队列
- 延时队列
两种方式实现队列
-
顺序存储
使用顺序存储结构表示队列时,首先申请足够大的内存空间建立一个数组,除此之外,为了满足队列从队尾存入数据元素,从队头删除数据元素,还需要定义两个指针分别作为头指针和尾指针。
-
链式存储
队列的链式存储结构表示为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表,不过它只能尾进头出而已。说白一点就是:链式存储是在链表的基础上,按照“先进先出”的原则操作数据元素。
回头再看 $nextTick 源码
// vue/src/core/util/next-tick.js
const callbacks = []
let pending = false
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将传入的回调保存到数组callbacks中
callbacks.push(() => {
cb.call(ctx)
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
我们可以发现我们使用 this.$nextTick 的时候传入的回调函数会保存在一个数组 callbacks 中,然后通过pending控制 timerFunc 函数在某个时机执行
timerFunc 函数做了什么?
// vue/src/core/util/next-tick.js
// 将 callbacks 中的全部回调函数拷贝一份,然后依次执行
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
// isNative 检查一个值是否是原生 function
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
我们可以发现代码中生成了 timerFunc 函数,然后把回调作为 microTask
或 macroTask
参与到事件循环中来,
并且依次为promise -> MutationObserver -> setImmediate -> setTimeout这样的顺序进行降级,并且通过 flushCallbacks
方法将callbacks中的全部回调拷贝一份,然后依次执行。
/**
* Perform no operation.
* Stubbing args to make Flow happy without leaving useless transpiled code
* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
*/
function noop(a, b, c) { }