一个 bug 引发的 promise 面试题思考 (50% 的人不知道如何调整 promise 解决)
背景: 最近拿到一个vue2的历史仓库,在开发的时候发现一个诡异的问题。此仓库技术栈 vue2.0、vue-i18n 8.0.0版本。 具体现象是: 当用户点击 登出按钮,需要调用一个接口,在接口的 then 里面清除了 登录态,然后在 业务组件的 then里面执行跳转逻辑(通过 this.$i18n.path 拿到跳转的 path) 执行,结果报错了。 前面为分析问题的过程, 可以直接跳到面试题部分
下面抽象下具体的代码逻辑:
// common-logout.js 此方法是一个公共的方法,属于全局封装
// invokeClearCookie 是一个接口,返回一个 promise
const invokeClearCookie = () => { ... }
// 全局公共方法
Vue.prototype.$logout = (ctx) => {
let that = ctx || this
const clear = () => {
that.$store.commit('SET_CN_REWARDS_ENTER', false)
}
return invokeClearCookie().then((res) => {
clear()
return res
})
}
// 组件渲染的逻辑
<ComA v-if="islogin"/>
<ComB v-else/>
// ComA 组件 里面的 点击 退出按钮 逻辑 (点击退出 执行 logout方法)
methods: {
logout() {
// 退出登录
this.$logout(this).then((res) => {
if (+res.code == 0) {
this.$router.push(this.$i18n.path('/'))
}
})
},
}
执行逻辑分析:
- 当用户点击了 退出登录 按钮,执行了 ComA 组件的 logout 方法
- ComA 组件的 logout 方法 调用了 全局的 this.$logout
- 全局的 this.$logout 方法 调用了一个接口,返回了一个 promise
- 等
invokeClearCookie
方法的 接口返回来之后,首先执行的是 全局方法this.$logout
的then (也就是公共方法的then 比 ComA 组件的 then 执行时机要早, 因为本质上 业务组件ComA 的then 是基于 全局方法的 this.$logout then 的后续) - 执行 全局方法 $logout 的then 会触发一个 vuex 的 clear() 逻辑, 这个clear 逻辑就是让 islogin 变为 false (islogin 用在渲染 ComA 组件)
- 此时在 执行 ComA 组件 的 then 逻辑 (此时就出现问题了)
问题的原因
- 全局方法
this.$logout
的then 执行时机 比 业务组件的 then 执行时机要早 - 全局方法
this.$logout
的 then 里面执行了 vuex 的逻辑,本质上就是让 islogin 变为 false - 此时问题来了, 因为 islogin 变为false, 触发了 ComA 组件的卸载, 但是 ComA 组件的 then 还没有执行 (因为此时还在执行 全局方法
this.$logout
的 then,也就是 clear(), clear() 执行 是同步的, clear() 执行完立马 触发了 ComA的卸载,因为 v-if 条件变了) - 但是 此时 业务组件的 ComA 的 then 还没有执行
- vue-i18n 在 8.0.0 版本在 beforeDestroy 销毁了 _i18 属性
- 因为 vue-i18n 在 beforeDestroy 销毁了 _i18 属性,但是 此时 ComA 组件的 then 还没有执行,现在还在 执行 公共方法 then 里面的 clear逻辑 (这时候的函数调用栈是 clear -> ComA 卸载 -> vue-i18n 的 beforeDestroy)。
- 所以当 ComA 组件的 then 执行时,通过
this.$i18n.path
拼接属性就会报错 (因为 $i18n 已经被销毁了)
如何改动
- 组内也稍微讨论了下改动的方案,方案很多,这里不赘述如何改动,最终我们是升级 vue-i18n 版本规避这个问题。
- 但是引发了一个 promise 面试题,下面我抽象下,有兴趣的同学试一下
抽象出来的面试题
const invokeClearCookie = () => {
return new Promise((r) => {
setTimeout(() => {
r('我是promise')
}, 1000)
})
}
const $logout = (ctx) => {
const clear = () => {
console.log('我是 clear')
}
const p1 = invokeClearCookie()
return p1.then((res)=>{
console.log('p1 的 then 里面执行的 clear')
clear()
});
}
$logout().then((res) => {
console.log('out - res1', res)
return res;
}).then((res2) => {
console.log('out - res2', res2)
})
- 此时的打印结果是 'p1 的 then 里面执行的 clear' -> '我是 clear' -> 'out - res1' -> 'out - res2'
- 现在要求 打印结果变成 'out - res1' -> 'out - res2' -> 'p1 的 then 里面执行的 clear' -> '我是 clear', 该 如何调整 $logout 里面的 逻辑呢?
- 这样调整的目的: 让业务组件ComA 的 then 先执行,等业务组件的 then 的执行完成,在去执行 公共方法的 then,此时可以规避 _i18 属性被销毁的问题 (因为 公共方法的 then 推迟执行了,所以 销毁逻辑也推迟了)
答案:如何调整 promise 执行顺序 (保证业务组件的 then 先执行)
const invokeClearCookie = () => {
return new Promise((r) => {
setTimeout(() => {
r('我是promise')
}, 1000)
})
}
const $logout = (ctx) => {
const clear = () => {
console.log('我是 clear')
}
// ** 调整的关键 **
const p1 = invokeClearCookie()
// ** 调整的关键 **
p1.then((res) => {
// ** 调整的关键 **
return Promise.resolve(res);
}).then((res)=>{
console.log('p1 的 then 里面执行的 clear')
clear()
});
return p1
}
$logout().then((res) => {
console.log('out - res1', res)
return res;
}).then((res2) => {
console.log('out - res2', res2)
})
转载自:https://juejin.cn/post/7360970130913083455