最近项目使用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 | // Vue 2.6.10 |
首先明确,dialog的打开关闭是用一个data中的变量来控制的,它是reactive的,而button点击打开dialog的过程无非就是将该值设为true,而设置的过程必然会触发defineProperty中的set函数。触发过程如下
set function -> dep.notify() -> subs[].update() -> queueWatcher() -> nextTick()
也就是说每次赋值都会触发nextTick,或者说每次数据的更新后续的变更都是在nextTick中进行的(不知道这么说是不是准确)。