
本文共 3328 字,大约阅读时间需要 11 分钟。
前言
我们知道,在Vue中,修改响应式数据是异步的。即如果修改后想获取到DOM的更新,需要在nextTick回调函数中才能得到。这样做的主要目的是为了节省性能。
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的
当你设置 vm.someData = ‘new value’,该组件不会立即重新渲染
源码解析
以下是nextTick的源码:
// 各种错误判断和兼容代码略过了。function nextTick (cb, ctx) { // 你传入的回调函数会被依次放入到callbacks数组中。 callbacks.push(function () { cb.call(ctx); }); if (!pending) { pending = true; timerFunc(); }}
// Vue.nextTick 和 this.$nextTick是等价的。Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this)};Vue.nextTick = nextTick;
可见,nextTick函数传入的回调都被push到一个数组中了,那么这个数组callbacks
中的回到函数是什么时间执行的呢?
其实当你第一次调用nextTick
就已经执行了,但是被放入到微任务队列中
// 这是上边nextTick中 timerFunc的实现 var p = Promise.resolve();timerFunc = function () { p.then(flushCallbacks);};
可以看到第一次调用nextTick就会执行timerFunc
函数,而p
已经是fullfiled
的状态,也就是会把flushCallbacks
回调函数放入到微任务队列中。当执行栈中的代码执行完毕就会立即执行微任务队列中的flushCallbacks
。由于这个过程是异步的,即callbacks可能还会再此期间被推入更多的回调,届时一起执行。
// flushCallbacks 就是这么简单,原封不动搬过来了// 说白了就是callbacks中的挨个执行。function flushCallbacks () { pending = false; var copies = callbacks.slice(0); callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copies[i](); }}
好了,既然到了这里,那么问题来了。为什么nextTick就能获取到DOM的更新呢?不难想象,那一定是响应式数据已经更新过了。
响应式数据的原理
那么再来看下当给响应式数据赋值直到DOM更新都发生了什么。

setter
会拦截响应式数据的设置,任何数据获取的地方,包括Watch,Computed,以及模板,都是一个Watcher
。当数据变更的时候,被通知(Notify
)进行相应的重新渲染(re-render
)或其他逻辑。 其实不妨先和大家直说了,如上图所示,响应式数据的改变当然也是异步的。思路和nextTick基本是一致的,都是准备一个队列,当第一次进行赋值的时候,将执行的时机延迟到微任务队列中,在此期间,任何对于响应式数据的变更,都会放入到同一队列中,当执行时机到来的时候,一起执行。更有甚者,这个异步实现的方法同样是nextTick
,见如下源码:
// 当赋值发生的时候会触发每个Watcher的更新,但更新并不是立即执行的,而是放入到队列中,见下面的queueWatcher函数Watcher.prototype.update = function update() { if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); }};
将需要更新的Watcher放入到一个队列中,使用同样的实现即nextTick方法去实现异步,当执行到微任务队列时候一并更新。
function queueWatcher (watcher) { var id = watcher.id; if (has[id] == null) { has[id] = true; if (!flushing) { queue.push(watcher); } else { var i = queue.length - 1; while (i > index && queue[i].id > watcher.id) { i--; } queue.splice(i + 1, 0, watcher); } // 这里异步的实现竟然和nextTick调用同一个方法。 // 这里可以看到同一个Watcher if (!waiting) { waiting = true; nextTick(flushSchedulerQueue); } }}
既然二者是相同的实现,那么这就解释了为什么当为响应式数据赋值之后,虽然他是异步的,但nextTick回调中却可以得到更新的数据。因为待执行的nextTick异步队列在响应式数据赋值Watcher的异步队列之后。固nextTick可以得到DOM更新。
DOM更新并非渲染
这里有必要说一句,DOM更新并非是指页面渲染,DOM更新之后,会和渲染引擎(webkit、Blink)进行交互,由后者完成更新,就是通常所说的重排重绘合成的浏览器渲染流程。但我们可以获取到DOM更新的时机就是在变更DOM之后。
下面这个页面alert可以弹出ok,但页面仍旧是一片空白,即渲染未发生,但已经可以获取到DOM更新了。
Document
举例印证
最后,我们来尝试做一个简单的例子来印证我们的理解。我们知道,可以得到DOM更新的原因是响应式数据改变的回调队列在nextTick的回调队列之前被调用。那请思考一下两个场景:
假设我们有以下的响应式数据
{ { foo}} { { bar}}data() { return { foo: 'foo', bar: 'bar' }}mounted() { // 第一种情况 this.$nextTick(() => { console.log(this.$refs.foo.innerHTML) // ?? }) this.foo = 'foo更新了' // 第二种情况 this.bar = 'bar 更新了' this.$nextTick(() => { console.log(this.$refs.foo.innerHTML) // ?? }) this.foo = 'foo更新了'}
- 先调用nextTick,后改变响应式数据。可以获取到DOM更新么?
- 先改变其他的响应式数据,后调用nextTick,再改变响应式数据bar,nextTick中可以得到DOM更新么?
具体结果可以看,这里我就不贴了,留给各位自行思考。
参考
github vue:
vue 文档:备注
本文vue源码基于vue 2.6.12
发表评论
最新留言
关于作者
