相信大家在写 vue 项目的时候,一定会发现一个神奇的 api,Vue.nextTick
。为什么说它神奇呢,那是因为在你做某些操作不生效时,将操作写在Vue.nextTick
内,就神奇的生效了。那这是什么原因呢?
让我们一起来研究一下。
简述
vue 实现响应式并不是数据发生变化后 DOM 立即变化,而是按照一定策略异步执行 DOM 更新的
vue 在修改数据后,视图不会立刻进行更新,而是要等同一事件循环机制内所有数据变化完成后,再统一进行 DOM 更新
nextTick
可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。
事件循环机制
在讨论Vue.nextTick
之前,需要先搞清楚事件循环机制,算是实现的基石了,那我们来看一下。
在浏览器环境中,我们可以将我们的执行任务分为宏任务和微任务,
事件循环的顺序,决定 js 代码的执行顺序。事件循环如下:
用代码解释,浏览器中事件循环的顺序同如下代码:
for (macroTask of macroTaskQueue) {
// 1. 执行一个宏任务
handleMacroTask();
// 2. 执行所有的微任务
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}
复制代码
vue 数据驱动视图的处理(异步变化 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.nextTick
原理
由上一节我们知道,Vue 中 数据变化 => DOM 变化 是异步过程,一旦观察到数据变化,Vue 就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察到数据变化的 Watcher
(Vue 源码中的 Wacher 类是用来更新 Dep 类收集到的依赖的)推送进这个队列。
如果这个 watcher 被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和 DOM 操作。而在下一个事件循环时,Vue 会清空队列,并进行必要的 DOM 更新。
nextTick
的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback)
,JS 是单线程的,拥有事件循环机制,nextTick
的实现就是利用了事件循环的宏任务和微任务。
vue 中 next-tick.js 的源码如下
参考 vue 实战视频讲解:进入学习
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
// 首先定义一个 callbacks 数组用来存储 nextTick,在下一个 tick 处理这些回调函数之前,
// 所有的 cb 都会被存在这个 callbacks 数组中
const callbacks = []
// pending 是一个标记位,代表一个等待的状态
let pending = false
// 最后执行 flushCallbacks() 方法,遍历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
/*判断采用哪种异步回调方式由于微任务优先级高,首先尝试微任务模拟1.首先尝试使用Promise.then(微任务)2.尝试使用MuationObserver(微任务)回调3.尝试使用 setImmediate(宏任务)回调4.最后尝试使用setTimeout(宏任务)回调*/
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) ||
// PhantomJS and iOS 7.x
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)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
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(resolve => {
_resolve = resolve
})
}
}
复制代码
目前浏览器平台并没有实现 nextTick 方法,所以 Vue.js 源码中分别用 Promise、setTimeout、setImmediate 等方式在 microtask(或是 task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。
nextTick 的调用方式
回调函数方式:Vue.nextTick(callback)
Promise 方式:Vue.nextTick().then(callback)
实例方式:vm.$nextTick(callback)
Vue.nextTick
的应用
created 生命周期中操作 DOM
created 钩子函数执行的时候 DOM 其实并未进行挂载和渲染,此时就是无法操作 DOM 的,我们将操作 DOM 的代码中放到 nextTick 中,等待下一轮事件循环开始,DOM 就已经进行挂载好了,而与这个操作对应的就是 mounted 钩子函数,因为在 mounted 执行的时候所有的 DOM 挂载已完成。
created(){
vm.$nextTick(() => {
//不使用this.$nextTick()方法操作DOM会报错
this.$refs.test.innerHTML="created中操作了DOM"
});
}
复制代码
修改数据,获取 DOM 值
当我们修改了 data 里的数据时,并不能立刻通过操作 DOM 去获取到里面的值
<template>
<div class="test">
<p ref='msg' id="msg">{{msg}}</p>
</div>
</template>
<script>
export default { name: 'Test', data () { return { msg:"hello world", } }, methods: { changeMsg() { this.msg = "hello Vue" // vue数据改变,改变了DOM里的innerText
let msgEle = this.$refs.msg.innerText //后续js对dom的操作
console.log(msgEle) // hello world
// 输出可以看到data里的数据修改后DOM并没有立即更新,后续的DOM不是最新的
this.$nextTick(() => { console.log(this.$refs.msg.innerText) // hello Vue
}) this.$nextTick().then(() => { console.log(this.$refs.msg.innerText) // hello Vue
}) }, changeMsg2() { this.$nextTick(() => { console.log(this.$refs.msg.innerText) // 1.hello world
}) this.msg = "hello Vue" // 2.
console.log(this.$refs.msg.innerText) // hello world
this.$nextTick().then(() => { console.log(this.$refs.msg.innerText) // hello Vue
}) // nextTick中先添加的先执行,执行1后,才会执行2(Vue操作Dom的异步)
} }}
</script>
复制代码
v-show/v-if 由隐藏变为显示
点击按钮显示原本以 v-show=false 或 v-if 隐藏起来的输入框,并获取焦点或者获得宽高等的场景
评论