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 了。