参考: Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!
看了大佬的文章,第一次记住了,隔了一段时间回想发现又忘了,写一篇文章来记录学习过程,以及简单实现一个nextTick。
文章的核心其实是纠正了一个常见的误区,先抛出一个问题: 为什么在Vue中修改了数据之后,要使用$nextTick才能获取到修改的DOM值 👇戳这里看示例代码
export default {
data() {
return {
inputVal: 1
}
},
mounted() {
this.inputVal = 2
console.log(document.getElementById('input').value) // 1
this.$nextTick(() => {
console.log(documen.getElementById('input').value) // 2
})
},
render() {
return h('input', {
attrs: {
id: 'input'
},
props: {
value: this.inputVal
}
})
}
}
问题一: DOM操作是同步的还是异步的?
是同步的!!!
DOM操作是同步的,在本质上,DOM也只是一个对象而已,这里的同步是指在修改DOM对象时,在数据层面上,你的下一行代码就可以拿到它的修改结果,但是视觉上可不一定,因为要想看到页面上渲染出对应的元素,还需要浏览器的进一步Render操作,这个Render才是异步,不能把概念搞混淆。
问题二:既然DOM操作是同步的,那为什么Vue还需要nextTick才可以拿到改变后的元素值?
因为Vue对DOM的操作行为是异步的!!!
也就是说 下面这段代码, Vue对input的DOM操作流程是这样的:
this.inputVal = 2 -> 把渲染Watcher添加到队列queueWatcher中 -> 调用nextTick(flushSchedulerQueue) 在下一个tick清空Watcher任务队列🌟异步🌟 -> 触发渲染Watcher,改变DOM
mounted() {
this.inputVal = 2 // 对DOM的操作在下一个tick才生效
console.log(document.getElementById('input').value) // 1
this.$nextTick(() => {
console.log(document.getElementById('input').value) // 2
})
},
Vue使用nextTick来实现任务的调度,它的本质是开启了一个微任务,this.$nextTick会把传进来的回调函数保存起来,但不会立即执行,而是推入队列中,在下一个tick执行
问题三:简单实现一个nextTick
结合源码,简化了一些逻辑,实现了一个简单的nextTick,它的原理也十分简单,调用nextTick时把cb保存起来,
// next-tick.js
const callbacks = [];
let pending = false;
// 执行所有callback
function flashCallbacks() {
pending = false;
// 赋值一份callbacks,如果在nextTick中又调用了nextTick,要将其放入下一个任务队列
let copies = callbacks.slice(0)
for(let i=0; i<copies.length; i++) {
copies[i]();
}
}
let timerFunc;
// timerFunc是真正需要执行的微任务
// TODO: 兼容性降级: Promise -> mutationObserver -> setImmediate -> setTimeout
let p = Promise.resolve();
timerFunc = () => {
p.then(() => {
flashCallbacks();
});
};
// nextTick
export function nextTick(cb, ctx) {
callbacks.push(() => {
cb.call(ctx);
});
if (!pending) {
pending = true;
timerFunc();
}
}
问题四:nextTick和普通的Promise有什么区别?
Promise每一次链式调用then都会开启一个新的微任务,而nextTick在同一个tick内执行时,会把所有的回调都添加到同一个微任务中。 看一个例子
// test.js
import { nextTick } from 'next-tick.js'
nextTick(() => {
console.log(1);
nextTick(() => {
console.log(4);
});
});
Promise.resolve()
.then(() => {
console.log(3);
})
.then(() => {
console.log(6);
});
nextTick(() => {
console.log(2);
nextTick(() => {
console.log(5);
});
});
最后输出的结果为
1
2
3
4
5
6