为什么用Vue.nextTick()
JS执行是单线程的,它是基于事件循环的。所谓单线程,就是同一时间只能处理一件事情。JS中的任务分为同步任务和异步任务,其中异步任务分为宏任务和微任务。
所有同步任务都在主线程上执行,形成一个执行栈。而异步任务则会形成任务队列,宏任务进入宏队列,微任务进入微队列。
执行顺序:
- 执行同步代码;
- 等待所有的同步任务执行完毕,开始读取任务队列的任务,先从微队列取队首任务放入执行栈中执行
- 继续取直到微队列任务完毕,如果执行过程中又产生了微任务则加入队列末尾,这个任务也会在这个周期执行
- 微队列和执行栈都为空,则取宏队列队首任务放入栈中执行
- 执行完毕,执行栈为空,重复执行
<template>
<div>
<p ref="dom">{{num}}</p>
<button @click="add">++</button>
</div>
</template>
<script>
export default {
data() {
return {
num:0
}
},
methods: {
add(){
this.num++
console.log(this.$refs.dom.innerText,'Vue数据改变后,视图并不会立即更新')
this.$nextTick(() => {
console.log(this.$refs.dom.innerText,'异步更新后')
})
setTimeout(() => {
console.log(this.$refs.dom.innerText)
})
Promise.resolve().then(res=>{
console.log(this.$refs.dom.innerText)
})
this.num++
console.log(this.$refs.dom.innerText,'Vue数据改变后,视图并不会立即更新')
}
},
mounted() {
},
}
</script>
<style>
</style>
由于Vue DOM更新是异步执行的,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。为了确保得到更新后的DOM,所以设置了
Vue.nextTick()方法。
那么,为什么要设计成异步的,其实很好理解,如果是同步的,当我们频繁的去改变状态值时,就会频繁的导致我们的dom更新,增加性能的消耗。
nextTick 的本质是为了利用 JavaScript 的异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
什么是Vue.nextTick()
官方文档解释如下:
在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。
实现原理
将传入的回调函数包装成异步任务,nextTick 提供了四种异步方法 ,因为微任务优先于宏任务执行,所以优先级为
Promise > MutationObserver > setImmediate > setTimeOu
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 <strong>isUsingMicroTask</strong> = false // nextTick 最终是否以<strong>微任务</strong>执行
const <strong>callbacks</strong> = [] // 存放调用 nextTick 时传入的回调函数
let <strong>pending</strong> = false // 标识当前是否有 nextTick 在执行,<strong>同一时间只能有一个执行</strong>
// 声明 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
})
}
}
// 判断当前环境优先支持的异步方法,优先选择微任务
// 优先级: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 = () => {
// <strong>用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务</strong>
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) }
}
// 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {
pending = false
<strong> const copies = callbacks.slice(0) // 拷贝一份</strong>
callbacks.length = 0 // 清空 callbacks
for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调
copies[i]()
}
}
// callbacks.slice(0) 将 callbacks 拷贝出来一份,
// 是因为考虑到 nextTick 回调中可能还会调用 nextTick 的情况,
// 如果 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
<strong>// nextTick 回调中的 nextTick 应该放在下一轮执行,
// 如果不将 callbacks 复制一份就可能一直循环</strong>