最近在写 Vue 时遇到了一个问题,大体场景是:有一个组件 A 用于对话框中,也可用在对话框之外。A 初始化后会请求数据,当它用于对话框中时,可能会接收外部传入的参数,需要根据这些参数去请求数据。
我们知道大部分 Dialog 组件在实现时,只有当对话框首次被打开时才会去渲染其内部的 content 部分。于是问题是:当组件 A 处于对话框中,并且首次打开对话框时,A 先被初始化,然后对话框向 A 透传外部传入的参数,由 A 发起数据请求。
事故现场
1 | export const A = { |
A 的 mounted
先被触发,调用 reload
函数;接着外部开始调用 receiveContext
,再次调用 reload
,也就出现了在对话框首次打开时会重复请求。
P.S. 别问我为什么不用 props
透传,因为我用 dispatch action 触发 receiveContext
。
无论我在哪儿怎么加 nextTick
,都避免不了在第一次渲染时重复请求的问题。于是发现自己对 nextTick 的本质还没搞透。
修复后
先直接给出修复后的代码,再慢慢探究原因。
1 | export const A = { |
Event Loop
都知道 js 是单线程的,为什么要设计成单线程,读者不妨可以思考一下🤔
同时 js 又是非阻塞的,这就是 Event Loop 的功劳了。
- 主线程运行的时候会生成堆(heap)和栈(stack);
- js 从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中;
- 当程序调用外部的API时,比如 ajax、setTimeout 等,会将此类异步任务挂起,继续执行执行栈中的任务,等异步任务返回结果后,再按照执行顺序排列到事件队列中;
- 主线程先将执行栈中的同步任务清空,然后检查事件队列中是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到事件队列中。
- 主线程每次将执行栈清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个过程是循环往复的,就叫做 Event Loop 事件循环。
macro task + micro task
在 Event Loop 为了区分异步任务的执行优先级,js 设计出了 macro task (宏任务) 与 micro task (微任务) 这两个概念。
常见的 task 有
macro task: setTimeout,setInterval,setImmediate,I/O (磁盘读写或网络通信),UI 交互事件
micro task: process.nextTick,Promise.then
事件循环会将遇到的异步任务排列到对应的 macro task 及 micro task 队列中:
- 当执行栈中的任务清空,主线程会先检查 micro task 队列中是否有任务,如果有,就将 micro task 队列中的任务依次执行,直到队列为空;
- 然后再检查 macro task 队列中是否有任务,如果有,则每次取出【第一个】macro task 加入到执行栈中;
- 再次清空执行栈,重新检查微任务(重复第一步)
测试代码
test 1
1 | console.log(1); |
test 2
1 | console.log(1); |
test 3
1 | console.log(1); |
test 4
1 | console.log(1); |
以上所有示例都按 1 2 3 4 5 6 的顺序输出。
最后一例中,关于 4
和 5
的输出顺序,还是有点“滑稽”。
Vue nextTick
v2.4.4
1 | /** |
可以看到,Vue 优先使用 Promise.resolve()
来实现 nextTick,对于不支持 Promise 的设备则使用 MutationObserver
,再次之则降级为 setTimeout
。
v2.5.17
1 | const callbacks = [] |
【参考资料】
http://hcysun.me/vue-design/art/8vue-reactive-dep-watch.html#nexttick-的实现
https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver