4.Vue升级引发nextTick的bug(一)

最近项目使用qiankun做了个简单的重构和拆分,顺手把Vue版本往上升级了一丢丢(2.5.5 -> 2.6.10)。出现了一点问题,记录之。

简单介绍一下场景,也很简单,就是点击button,出现dialog,而点击其他区域,关闭该dialog,应该是个非常常见的需求。

Vue2.5.2事件流如下:

button clicked -> document clicked(冒泡) -> dialog render -> directive bind function 触发 -> document.addEventListener 触发

其中document.addEventListener中的callback,就是关闭该dialog。点击其他区域关闭弹窗,合情合理,符合需求。

代码在Vue2.5.2下工作正常,升级到Vue2.6.10之后,点击button后没有显示dialog,经过各种排查,发现原来是nextTick搞的鬼。

因为click事件是宏任务,如果不清楚可以点击 证明:Click是一个EventLoop宏任务 查看证明过程。

而Vue2.5.2中的nextTick是宏任务 -> 微任务 -> 宏任务交替触发,第一次使用宏任务。而Vue2.6.10中nextTick全部使用微任务。

重新解释一下 Vue2.5.2事件流:

button clicked (触发nextTick) -> document clicked(冒泡) -> nextTick 触发(dialog render -> directive bind function 触发 -> document.addEventListener 触发 )

而根据EventLoop中微任务优先于宏任务执行的逻辑,Vue2.6.10事件流:

button clicked (触发nextTick) -> nextTick 触发(dialog render -> directive bind function 触发 -> document.addEventListener 触发 ) -> document clicked(冒泡 触发listener函数)-> callback 触发 关闭dialog

所以表现上弹窗没打开。慢动作是打开了,又关闭了。Bug由此产生。


进一步刨根问题一下为啥会触发nextTick,如果清楚的同学就不用往下看了。
先看一下nextTick在不同Vue版本中的注释

1
2
3
4
5
6
7
8
9
// Vue 2.5.2
// Here we have async deferring wrappers using both micro and macro tasks.
// In < 2.4 we used micro tasks everywhere, but there are some scenarios where
// micro tasks have too high a priority and fires in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using macro tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use micro task by default, but expose a way to force macro task when
// needed (e.g. in event handlers attached by v-on).

1
2
3
4
5
6
7
8
9
10
11
12
// Vue 2.6.10
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).

首先明确,dialog的打开关闭是用一个data中的变量来控制的,它是reactive的,而button点击打开dialog的过程无非就是将该值设为true,而设置的过程必然会触发defineProperty中的set函数。触发过程如下

set function -> dep.notify() -> subs[].update() -> queueWatcher() -> nextTick()

也就是说每次赋值都会触发nextTick,或者说每次数据的更新后续的变更都是在nextTick中进行的(不知道这么说是不是准确)。

https://unpkg.com/vue@2.5.2/dist/vue.js