Vue 是异步更新的,这个我们都知道,所以我们一般会使用 nextTick 来确保更新完后执行一些业务逻辑。我一直认为自己已经懂了,但是当我看到这个题目后,发现我似乎又不懂了:
题目:下面的打印顺序?注释掉 this.name = 'b' 以后呢?
答案:第一问 2 1,第二问 1 2。
1 | <template> |
你答对了吗?
我们还是到源码里找答案吧。
更新逻辑的入口应该去哪找呢,自然我们会想到 defineProperty 的 set 方法,没错,就是它:
1 | set: function reactiveSetter(newVal) { |
这里将新值赋值给了 val 并调用了 dep.notify() 来通知 Watcher 进行更新:
1 | update () { |
这里会走到 queueWatcher(this):
1 | const id = watcher.id |
首先通过 has[id] 判断当前 Wathcer 是不是已经入队过,如果已入队过就不再处理,避免每次修改数据都会进行更新。
然后判断当前是不是正在刷新队列,如果没有则将 Watcher 入队。我们先不管其他逻辑,也不急着分析 nextTick,我们只需要知道 flushSchedulerQueue 总是会在接下来的某个时刻执行就行了,看看它做了啥:
1 | function flushSchedulerQueue() { |
这里先对 Watcher 按照 id 从小到大进行排序,因为用户 Watcher 是在 Vue 初始化的时候生成的,渲染 Wathcer 是在 $mount 的时候生成的,所以用户 Watcher 会在组件的渲染 Watcher 之前执行。然后就是遍历执行 watcher.run():
1 | run () { |
这里先对比所观察的值有没有变化,这个值就是 watch 对象的 key,比如下面的 name 和 obj.age:
1 | watch: { |
如果变化了,或者观察的是一个对象,又或者传递了 deep 参数,并且是用户 Watcher,就会执行回调函数。
现在,让我们先回到 queueWathcer,看看下面这段代码是怎么回事:
1 | } else { |
当我们进入 else 的话说明刷新队列的时候又有 Watcher 被触发了更新,例如:当执行某个 watch 方法时,对响应式数据进行赋值触发了另外一个 Watcher 的更新。我们通过下面这个例子来梳理下这个流程:
1 | <div id="demo"> |
该例子中会有三个 Wathcer,当在 mounted 中修改 this.name 时,此时 name 的 Watcher 和组件渲染 Watcher 都会入队。然后,在“下一帧”的时候,会执行这些 Watcher,按照刚才的分析,首先会执行 name 的 Watcher,这里对 this.age 进行了赋值,此时会触发 age 的 Watcher 入队,因为该 Watcher 之前没有入队过,且当前正在刷新队列,所以会走到:
1 | } else { |
又因为 age 的 Watcher id 小于组件渲染 Wathcer 的 id,所以该 Watcher 会插入到当前的队列中。
现在,是时候看看 nextTick 了:
1 | import {noop} from 'shared/util' |
这个文件首先是经过一系列的特性判断来决定使用哪个 API 来实现异步,并最终以 timerFunc 方法提供给 nextTick 来调用。而 nextTick 中会把传入的回调函数放入 callbacks,且第一次调用的时候因为 pending 为 false,所以会执行 timerFunc 开启一个宏/微任务,最终会在将来执行 flushCallbacks 这个方法,该方法就是把 callbacks 中的函数都执行一遍,并重置 pending 为 false。
到此,异步更新逻辑分析的就差不多了。现在,让我们来回答一下一开始的那个问题:
题目:下面的打印顺序?注释掉 this.name = 'b' 以后呢?
1 | <template> |
第一问。因为 this.name = 'b' 会触发 Watcher 的更新,此时会开启一个异步的任务,在最新的 Chrome 浏览器中会使用微任务(我们叫它 task1 吧)来实现。 Promise.resolve().then 中的回调函数也会放到微任务队列当中,并放在 task1 的后面。当执行 this.$nextTick 时,会把回调函数放到 callbacks,但是他的执行还是在 task1 的任务之中的。所以这里打印顺序就是 2 1。
第二问。Promise.resolve().then 执行的时候回调函数会被放入微任务队列中, 然后执行 this.$nextTick 的时候又会开启一个微任务,放在微任务队列的队尾。所以下一次清空微任务队列的时候,打印的顺序就是 1 2 了。