vue nextTick 引发的思考
2020/3/16 11:31:27
本文主要是介绍vue nextTick 引发的思考,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
背景
最近做项目碰见一个这样的问题,伪代码(vue版本为2.6.x)如下:
<template> <div class="layout"> <TopBar /> <div class="main" v-if="isRouterAlive"> <slot /> </div> </div> </template> <script> export default { async created() { this.admin = await ajax('...') if (!admin) { this.$router.replace('/403') } this.$nextTick(() => { this.isRouterAlive = true }) } } </script> 复制代码
没有权限的时候,理想中的情况是这样的:
- 访问首页
- 调用接口获取用户权限,权限为false
- 路由跳转到403
this.isRouterAlive = true
,显示403页面
实际情况是这样的:
- 访问首页
- 调用接口获取用户权限,权限为false
this.isRouterAlive = true
,显示首页- 路由跳转到403,显示403页面
解决方案也很简单,直接await this.$router.replace('/403')
即可,但好学的我打算一探究竟!
分析
很显然,解决问题的关键是 this.$router.replace
和 this.$nextTick
,要分析这个问题必然要分析两者的执行逻辑。
此时我们知道 this.$router.replace
返回的是一个 promise
,this.$nextTick
中的timeFunc
实现优先级是 Promise
--> MutationObserver
--> setImmediate
--> setTimeout
,因此在浏览器环境下 this.$nextTick
也是基于 Promise
实现的,我们改造一下代码:
// 案例1 async created() { this.$router.replace('/403').then(() => { console.log(1) }) this.$nextTick(() => { console.log(2) this.isRouterAlive = true }) } // 2 // 1 复制代码
控制台先打印 2,再打印 1,很奇怪,为什么同样是 Promise
,nextTick
会优先输出?
是不是因为this.$router.replace
是个多层嵌套的 Promise
,导致后面跟随的输出1的 then
前面还有 隐藏的then
,即可以把上面的代码想象成:
function myReplace () { return new Promise((resolve) => { Promise.resolve().then(() => { resolve() }) }) } async created() { myReplace('/403').then(() => { console.log(1) }) Promise.resolve().then(() => { console.log(2) }) } // 2 // 1 复制代码
真的是类似这样吗?我们再改造一下代码:
// 案例2 async created() { this.$router.replace('/403').then(() => { console.log(1) }) setTimeout(() => { console.log(3) }) this.$nextTick(() => { console.log(2) this.isRouterAlive = true }) } // 2 // 3 // 1 复制代码
咦,为什么 3 比 1 先输出?this.$router.replace
不是返回 Promise
吗?Promise
不是微任务吗?setTimeout
不是宏任务吗?
this.$router.replace
是返回一个 Promise
没错,但是当只有 Promise
被 resolve
时,才会把输出 1 的 then
加入微任务队列,随后执行,而 3 比 1 先输出说明 this.$router.replace
内部是先执行一个类似 setTimeout
的宏任务,之后 resolve
,即:
function myReplace () { return new Promise((resolve) => { setTimeout(() => { resolve() }, 10) // 延迟一定时间 }) } async created() { myReplace('/403').then(() => { console.log(1) }) setTimeout(() => { console.log(3) }) Promise.resolve().then(() => { console.log(2) }) } // 2 // 3 // 1 复制代码
那么问题来了, this.$router.replace
内部执行的宏任务到底是什么?
没办法,只能打断点看源码调试了,具体函数调用栈这里就不赘述了。路由403在项目 router.ts
中的定义为:
{ path: '/403', name: '403', component: () => import('./views/403.vue') } 复制代码
this.$router.replace
函数接受一个 path
参数,在示例中即是 '/403' ,this.$router.replace
内会根据 path
参数找到匹配的 RouteRecord
(路由记录,记录路由的路径、参数、组件等等),之后调用resolveAsyncComponents
方法对匹配的路由记录进行加载处理,因为 403 组件采用异步加载的方式,所以需要先import('./views/403.vue')
,之后再进行路由跳转、视图更新。
动态 import
是也是基于 Promise
的,即:
function myReplace () { return new Promise((resolve) => { import('./views/403.vue').then(() => { resolve() }) }) } async created() { myReplace('/403').then(() => { console.log(1) }) setTimeout(() => { console.log(3) }) Promise.resolve().then(() => { console.log(2) }) } // 2 // 3 // 1 复制代码
因此, this.$router.replace
内部执行的宏任务是在 import
里。
那么问题又来了,import
内部执行的宏任务到底是什么?
动态 import
接受模块的 url 作为参数,因此不难猜测, import
内部需要对模块进行请求加载,所以import
内部执行的宏任务就是加载模块的 http 请求,即:
function myReplace () { return new Promise((resolve) => { return new Promise((resolve => { ajax('./views/403.vue').then(() => { resolve() }) })) }) } async created() { myReplace('/403').then(() => { console.log(1) }) setTimeout(() => { console.log(3) }) Promise.resolve().then(() => { console.log(2) }) } // 2 // 3 // 1 复制代码
分析到这,我们再回过头来看看背景中出现的问题就不难解释了:
async created() { this.admin = await ajax('...') if (!admin) { this.$router.replace('/403') } this.$nextTick(() => { this.isRouterAlive = true }) } 复制代码
- 访问首页
- 调用接口获取用户权限,权限为false
- 执行
this.$router.replace('/403')
,触发一个异步加载403模块的宏任务 - 执行
this.$nextTick
,添加一个微任务到微任务队列 - 执行微任务队列,
this.isRouterAlive = true
,显示首页 - 宏任务异步加载403模块加载完毕,路由跳转到403,显示403页面
到这就结束了吗?远远没有!
我们再改造下代码:
// 案例3 async created() { this.$router.replace('/403').then(() => { console.log(1) }) Promise.resolve().then(() => { console.log(2) }) this.$nextTick(() => { console.log(3) }) } 复制代码
当 403 模块是异步加载时,根据前面的分析,不难得出执行代码会依次输出 2、3、1。
**但是当 403 模块不是异步加载呢?**即路由403在项目 router.ts
中的定义为:
{ path: '/403', name: '403', component: Page403 // Page403为 import Page403 from './views/403.vue' } 复制代码
此时 this.$router.replace
内部并不需要执行 import
,根据前面的分析,代码等价于:
function myReplace () { return new Promise((resolve) => { resolve() }) } async created() { myReplace('/403').then(() => { console.log(1) }) Promise.resolve().then(() => { console.log(2) }) Promise.resolve().then(() => { console.log(3) }) } 复制代码
因此当 403 模块是同步加载时,按照前面的分析,执行代码应该依次输出 1、2、3。
但是实际情况下执行代码,输出顺序为 3、1、2。
咦,为什么会先输出 3 ?为什么先执行了 this.$nextTick
里的回调?
我们分析下 nextTick
的源码,我简化了一下:
const callbacks = [] let pending = false function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } let timerFunc = Promise.resolve().then(flushCallbacks) export function nextTick (cb?: Function) { callbacks.push(cb) if (!pending) { pending = true timerFunc() } } 复制代码
nextTick
中维护了一个全局的 callbacks
数组,第一次调用 nextTick
:
- 将回调函数放入
callbacks
中 pending
为false
,执行timeFunc
,添加微任务flushCallbacks
到微任务队列中
此后同一 tick
中再次调用nextTick
,只会将回调函数放入callbacks
中,并不会触发新的微任务。因此同一 tick
中多次调用nextTick
的回调函数最终会由第一次调用 nextTick
时添加的微任务 flushCallbacks
统一执行。
分析下如下代码:
// 案例4 async created() { this.$nextTick(() => { console.log(1) }) Promise.resolve().then(() => { console.log(2) }) this.$nextTick(() => { console.log(3) }) } // 1 // 3 // 2 复制代码
- 第一次调用
nextTick
,添加微任务flushCallbacks
到微任务队列中,此时callbacks
有一个输出1的回调函数 - 执行
Promise.resolve()
,添加一个输出2的微任务到微任务队列 - 第二次调用
nextTick
,将输出3的回调函数添加到callbacks
数组中 - 执行微任务队列,执行第一个微任务
flushCallbacks
,即依次执行callbacks
数组中的回调函数,依次输出 1,3 - 执行第二个微任务,输出 2
此时回过头再看案例3,当 403 模块是同步加载时,先输出了3,根据案例4,我们可以猜测到 this.$router.replace('/403')
内部调用过一次 nextTick
,将微任务 flushCallbacks
添加到了微任务队列的前面,因此会先输出3,即当 403 模块是同步加载时,案例3代码等价于:
function syncReplace () { return new Promise((resolve) => { this.$nextTick(() => {}) resolve() }) } async created() { syncReplace('/403').then(() => { console.log(1) }) Promise.resolve().then(() => { console.log(2) }) this.$nextTick(() => { console.log(3) }) } 复制代码
那么问题来了,this.$router.replace('/403')
内部为什么会调用 nextTick
?
我们打个断点,看下函数调用栈即可一清二楚:
如上图,当403模块是同步加载时,执行this.$router.replace('/403')
时路由会同步更新,update
过程会调用 queueWatcher
方法, queueWatcher
方法内部调用了 nextTick
(当403模块是同步加载时,执行this.$router.replace('/403')
时实际上会触发多次 update
(watcher
),nextTick
会被多次执行,这里暂不深究)。
至此,我们再分析一下案例3的执行过程(当 403 模块是同步加载时):
// 案例3 async created() { this.$router.replace('/403').then(() => { console.log(1) }) Promise.resolve().then(() => { console.log(2) }) this.$nextTick(() => { console.log(3) }) } 复制代码
- 执行
this.$router.replace('/403')
,路由更新,触发路由相关watcher
,第一次调用nextTick
,添加微任务flushCallbacks
到微任务队列中 - 将输出1的微任务加入微任务队列
- 执行
Promise.resolve()
,添加一个输出2的微任务到微任务队列 - 执行
nextTick
,将输出3的回调函数添加到微任务flushCallbacks
的callbacks
数组中 - 执行微任务队列,执行第一个微任务
flushCallbacks
,即依次执行callbacks
数组中的回调函数,输出 3 - 执行第二个微任务,输出 1
- 执行第二个微任务,输出 2
思考
思考以下代码的输出顺序?
思考题1:
<template> <div class="app"> {{msg}} </div> </template> <script> export default { data() { return { msg: 'aaa' } }, created() { this.msg = 'bbb' Promise.resolve().then(() => { console.log(1) }) this.$nextTick(() => { console.log(2) }) } } </script> 复制代码
思考题2:
<template> <div class="app"> {{msg}} </div> </template> <script> export default { data() { return { msg: 'aaa' } }, mounted() { this.msg = 'bbb' Promise.resolve().then(() => { console.log(1) }) this.$nextTick(() => { console.log(2) }) } } </script> 复制代码
这篇关于vue nextTick 引发的思考的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南