事情是这样的, 这是一个在某天的默默的开发中, 笔者发现了一个惊天地泣鬼神的抓破脑壳都想不破的问题, 然后在这个月黑风高的晚上, 通过对源码的窥探终于发现原因的悲惨故事
我们先来看一个demo, 关于$nextTick的使用这里就不再赘述了
<div id='#app'>
{{ msg }}
div>
const vm = new Vue({
el: '#app',
data: {
msg: 'helloWorld'
}
})
// 1. 首先页面中一定会渲染出helloWorld
// 2. 第一个$nextTick
vm.$nextTick(() => {
console.log('我是第一个$nextTick的输出', vm.msg, vm.$el.innerHTML);
})
// 3. 更改msg的值
vm.msg = 'yes i do';
console.log(vm.msg, vm.$el.innerHTML);
// 4. 第二个$nextTick
vm.$nextTick(() => {
console.log('我是第二个$nextTick的输出',vm.msg, vm.$el.innerHTML);
})
console.log(vm.msg, vm.$el.innerHTML);
我们可以看到输出结果如下
笔者一开始是真实的一脸懵逼, 我当时唯一可以确定的是$nextTick是异步的, 当然Vue对于界面的更新本来就是异步的, 这个等于是在说废话, 但是两个nextTick的结果竟然不一样?说好的会延迟到下一次dom更新才执行呢? 于是笔者去Vue的官网仔细的翻了翻
来自vue官方:
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
翻译成人话就是 vue的每次数据更新都会在下一个事件循环(eventloop)中执行
那么结合我们上方的输出结果, 笔者短暂认为$nextTick是在下一次事件循环中dom更新完毕后执行
如果你不看源码 一定是百思不得其解的, 因为vue有时候确实设计的非常精妙
笔者来用自己的方法给你写一写你能够看的明白的$nextTick, 跟着注释看我相信你是不会迷路的
const nextTick = (function() {
let callbacks = []; // 最后所有在nextTick中传递过来的函数都会进入这个数组
let timerHandler = () => { // 这个函数用来延迟nexTick传递进来的函数的执行
// Promise.resolve这句话往这里一站, 你就知道这哥们后面的那行then代码要等待了,
const p = Promise.resolve();
p.then(releaseCallbacks); // 等同步任务执行完毕这哥们会执行
}
function releaseCallbacks() { // 作为p.then的回调 releaseCallbacks肯定也会在微队列中等待
for(let i = 0; i < callbacks.length; i++) {
callbacks[i](); // 所有存储进callbacks的函数挨个执行
}
callbacks.length = 0;
}
// 真正暴露给用户的回调函数
return function(cb) {
callbacks.push(cb);
timerHandler();
}
}())
调用我们自己的的nextTick方法,其他语句都不变 我们走一遍输出发现输出结果如下
确实发现所有交付给nextTick的函数都按照异步执行了, 但是并没有如我们所想象的那样, 相反连nextTick真正的作用都发挥不上了, 我们不再可以监听到msg被更改, 于是我们来看看被笔者进行注释过后的真正的$nextTick源码(当然, 前提是上面笔者的这份简化版源码你已经看懂了, 不然vue源码会更加头大)
export let isUsingMicroTask = false // 这是vue用来判断是否启用微任务的锁, 如果不懂没关系他不重要
const callbacks = [] // 同样, 最后所有在nextTick中传递过来的函数都会进入这个数组
let pending = false // 异步锁, 如果同步任务未执行完, 异步锁肯定是锁住的
function flushCallbacks () { // 最终执行callbacks的函数
pending = false // 重置异步锁
// 这里我们发现将callbacks复制了一份给copies, 最终循环操作的也是copies, 这是因为不想造成nextTick嵌套调用的冲突
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc // 相当于上面的timerHandler
// 判断当前环境支不支持原生的Promise构造函数
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 如果支持会走上面的timerHandler的流程
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]'
)) {
// 判断是不是IE
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)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 真正暴露出去的nextTick方法
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 这个是新加的, 如果没有传递cb参数则返回一个新的promoise出去
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
抛开一些兼容性写法和一些容错机制来说, vue的nextTick和我们写的nextTick没有什么差别, 但是为什么会产生截然不同的效果呢?
继续阅读源码笔者有发现, vue中还存在一个queueWatcher
方法, 如下
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
if (!config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);
}
}
}
你不用将他看懂, 但是笔者可以告诉你这哥们的作用就是用来更改nextTick的执行顺序的
本身我们执行nextTick他的效果跟一般的异步任务没什么太大的区别, 无非就是nextTick会被置于微任务, 而queueWatcher
方法和他带来的一些骚操作则改变了nextTick的运行轨迹
如果在$nextTick前没有更改vue监控的属性值的情况发生, 那么nextTick中的代码按照正常异步微任务走掉
如果在$nextTick前有更改了vue所监控的属性值的情况, 则queueWatcher会调换nextTick的执行顺序, nextTick将会在下一次事件循环vue刷新页面后执行
有些东西你不看源码是真的想破头都想不出来他到底是什么原因, 这也是我们作为开发者一直要追逐的事情, 共勉