跳到主内容

Vue $nextTick()实现原理解析

· 6分钟阅读

在写 Vue 的时候,你或许会遇到更新数据的时候需要操作 DOM,比如在修改状态后通过 this.$refs.content.getBoundingClientRect() 想要获取最新的渲染后的元素的尺寸

<template>
<div>
<div className="container" ref="container" :style="{ width, height }"></div>
<button @click="changeSize">click</button>
</div>
</template>

<script>
export default {
name: "HelloWorld",
data() {
return {
width: "200px",
height: "200px",
};
},
methods: {
changeSize() {
this.width = "400px";
console.log(this.$refs.container.getBoundingClientRect());
},
},
};
</script>

<style scoped>
.container {
background-color: yellow;
}
</style>

上面的代码定义一个 div 和 button,div 通过 data 赋值 width 和 height,此时 div 的 width 为 200px,height 为 200px

button 绑定点击事件,改变 this.width 后立即获取 div 的尺寸,打印结果如下

打印结果

可以看见,getBoundingClientRect() 获取到的是更新后的元素尺寸,如果想要获取更新后的元素尺寸,可以通过 this.$nextTick 来实现,通过改变 changeSize 方法

changeSize() {
this.width = "400px";
console.log("立刻获取结果");
console.log(console.log(this.$refs.container.getBoundingClientRect()));
this.$nextTick(() => {
console.log("nextTick获取结果");
console.log(this.$refs.container.getBoundingClientRect());
});
},

打印结果如下

nextTick结果

异步更新

从上面的例子可以看出,Vue 在改变 data 的时候,不会立刻更新 DOM,所以无法获取到更新后的 div 尺寸,这是为什么呢?

当 data 更新时,Vue 会通知对应的 wather 进行重新计算操作,但是在一次事件循环里面,如果同一个 watcher 被触发多次,只会放进队列一次。

然后在下一次事件循环,Vue 才会执行队列的 watcher 进行更新和渲染,整个过程是异步的,是为了避免不必要的计算和渲染,能大大提高性能

nextTick 源码解析

接下来结合下 nextTick 源码分析 nextTick 是如何执行的?

Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this);
};

$nextTick 通过调用 nextTick 函数来把回调暂时用 callbacks 数组暂存起来

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;
});
}
}

callbacks 暂存的所有回调在 flushCallbacks 调用的时候会执行,那么 flushCallbacks 什么时候执行呢?我们继续看代码

function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}

flushCallbacks 的执行时机

let timerFunc;

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true;
} else if (
!isIE &&
typeof MutationObserver !== "undefined" &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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);
};
}

可以看出 flushCallback 是通过 Promise 或者 MutationObserver 或者 setImmediate 等微任务形式来执行,最后不到万不得已的情况下才用 setTimeout 来执行

总结

Vue 在状态改变的时候为了性能优化,采用异步更新,对一个事件循环周期上的多次赋值进行集中处理,避免了无效的 DOM diff 计算,当需要获取更新数据渲染后新的的 DOM 元素时,可以通过 $nextTick 获取。