基本使用
this.$nextTick()将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。
<template>
<section>
<h1 ref="hello">{{ value }}</h1>
<el-button type="danger" @click="get">点击</el-button>
</section>
</template>
<script>
export default {
data() {
return {
value: 'Hello World ~'
};
},
methods: {
get() {
this.value = '你好啊';
console.log(this.$refs['hello'].innerText); // Hello World ~
this.$nextTick(() => {
console.log(this.$refs['hello'].innerText); // 你好啊'
});
}
},
}
</script>
在进行讲解之前我们先掌握js的事件循环,请看我的另一篇文章前端八股文之事件循环
我们先看一个简单的例子,查看一下事件循环的执行优先级
<body>
<div id="app">test</div>
</body>
<script>
var dom = document.querySelector('#app')
dom.innerText = 'render'
alert(`sync-${dom.innerText}`)
new Promise((resolve) => {
alert(`promise-${dom.innerText}`)
resolve()
}).then(() => {
alert(`then-${dom.innerText}`)
})
setTimeout(() => {
alert(`setTimeout-${dom.innerText}`)
}, 0);
</script>
因为alert会阻断当前tick的执行,所以我们能很好的看到执行顺序;
1、script主体同步代码,弹出sync-render
2、script主体同步代码,弹出promise-render
3、微任务, 弹出then-render,此时页面仍为 <div id=“app”>test</div>
4、render UI, 页面渲染 <div id=“app”>render</div>
5、执行下一次tick,宏任务,弹出setTimeout-render
误区一
既然nexttick可以在页面渲染后再执行代码,那nexttick必然在渲染后进行的,可很多资料又说明渲染过程是放在一次tick后用进行的,甚至有些博主的说法自相矛盾
误区二
微任务优先于宏任务执行,其实不然,在一次tick内宏任务的优先级是高于微任务,宏任务包括了主体script的代码:例如上面例子的这两行代码
var dom = document.querySelector(’#app’);dom.innerText = ‘render’;
——————终极结论——————
dom结构的数据改变后,改变后的节点数据我们是可以通过代码拿到的,但渲染过程在整个tick结束后进行的,然后进行下一次tick,从宏任务开始
从vue双向绑定源码过程中看,改变data数据 – 触发watcher – 改变dom结构数据 – 重新渲染页面;
经过上面的解释我们可以这样理解双向绑定,改变data数据 – 触发watcher – 改变dom结构数据 – Vue.$nextTick的回调 – 重新渲染页面;
那现在只剩一个问题:为什么Vue.$nextTick 会在重新渲染页面之前执行;由上面代码的实验,我们推测$nextTick必然一种可能是微任务了,最后附源码
是的,nextTick就是把回掉方法全部放在微任务中了,不过vue做了兼容处理,前前后后改了兼容的几种方法,由Promise做首选微任务,如果不支持选择微任务MutationObserver,不得已会选择setImmediate(IE、Node.js 环境) > MessageChannel > setTimeout;
注意如果最后由宏任务进行回调的执行,那双向绑定则变成改变 data数据 – 触发watcher – 改变dom结构数据 – 重新渲染页面 – Vue.$nextTick的回调因为宏任务在下一次tick执行,渲染行为发生在本次tick的结尾
nextTick主体源码,有删减
let isUsingMicroTask = false
const callbacks = []//用来存储所有需要执行的回调函数
let pending = false//该变量的作用是表示状态,判断是否有正在执行的回调函数。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
if (typeof Promise !== 'undefined') {
const p = Promise.resolve()
timerFunc = () => { p.then(flushCallbacks) }
isUsingMicroTask = true
} else if (typeof MutationObserver !== 'undefined' && (
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//注意vue不是用MutationObserver检测dom是否改变,主要是利用这是微任务
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)
}
// timerFunc函数执行时会导致文本节点textNode的数据发生改变,因为MutationObserver对象在监听文本节点,
//所以进而也就会触发flushCallbacks回调函数
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}