背景: 后端开发, 对 es6 和 vue 有一定了解, 最近正在折腾研究其原理.
问题概述: 在 async 函数中多执行了一个 nextTick, 导致代码执行时机发生变化
点击 btn 触发 onClick, onClick 内部先调用 doSomeThing, 然后执行一个 nextTick1, doSomeThing 为 async 函数, 里面 await 了一个 callback(注意 cb 是普通箭头函数), 里面也有一个 nextTick2.
问题代码预览: https://codepen.io/zymoplastic/pen/XWdrPpj
截取代码如下
<template>
<button @click="onClick(true)">多执行一个 nextTick</button>
<button @click="onClick(false)">少执行一个 nextTick</button>
</template>
{
data() {
return {}
},
methods: {
async doSomeThing(hasOneMoreTick) {
let cb = () => {
console.log('doSomeThing -- cb');
if (hasOneMoreTick) {
this.$nextTick(() => {
console.log('cb $nextTick done');
})
}
}
await cb();
console.log('doSomeThing -- close');
console.log('call destroy');
},
onClick(hasOneMoreTick) {
console.log(`------hasOneMoreTick: ${hasOneMoreTick} begin ------`);
this.doSomeThing(hasOneMoreTick);
this.$nextTick(() => {
console.log('onClick $nextTick done');
console.log('call destroy');
})
}
}
}
疑问:
1
sujin190 2020-08-04 18:16:29 +08:00
这个问题好像是$nextTick 是微任务,Promise 的 callback 是宏任务,不是一个任务队列,微任务优先级高于宏任务,只有微任务执行完成才会执行宏任务,看起来你的输出还是符合这个流程的
|
2
sujin190 2020-08-04 18:21:48 +08:00
|
3
ymcz852 2020-08-04 18:31:18 +08:00
其实关键在于 await cb() 的效果 === await this.$nextTick(() => { console.log('cb $nextTick done'); }) === await Promise.resolve.then(() => { console.log('cb $nextTick done') })
|
6
1490213 OP @sujin190 我看了 vue 内部实现, nextTick 在存在 Promise 的时候会优先使用 Promise, Promise.then 就是属于微任务
|
7
Zhuzhuchenyan 2020-08-04 18:44:59 +08:00
对以上两个回答都不敢沟通,给题主一个简单的测试环境
async function test() { let cb = () => { Promise.resolve().then(() => console.log('awaited')); }; await cb(); console.log('after await'); } test(); 以上代码加不加 await 对运行结果是有差异的,分别为, 添加 await:awaited,after await 不添加 await:after await,awaited 究其原因是因为当你给一个 function 添加 async 关键字并在其中使用 await 之后,此处就会产生一个 asynchrony context,所以以上代码最后可以理解为以下的等价代码(仅供参考执行顺序,并不完全严谨) Promise.resolve() .then(() => { let cb = () => { Promise.resolve().then(() => console.log('awaited')); }; cb(); }) .then(() => console.log('after await')); 所以题主的理解“ await 后面跟一个函数执行(没有返回 Promise 以及没有 then 属性), 应该是直接返回值, 语义上不应该对执行顺序产生影响”并不完全正确 |
8
rabbbit 2020-08-04 18:45:36 +08:00
async function a() {
await Promise.resolve(2).then(d => { console.log(d); }); console.log(1); } function b() { a(); Promise.resolve(3).then(d => { console.log(d) }) } b(); // 2 3 4 1 // 函数 a, 相当于(大概意思) function a() { return new Promise((resolve, reject) => { resolve( Promise.resolve(2).then(d => { console.log(d); }) ); }).then(() => { console.log(1) }) } |
9
Zhuzhuchenyan 2020-08-04 18:47:18 +08:00
对上条回复的补充,如果你配置了 tslint,你在 await 那一行会收到警告
'await' has no effect on the type of this expression. ts(80007) 这里 no effect 并不完全正确,因为他的确会存在潜在的执行顺序变化 |
10
1490213 OP @Zhuzhuchenyan 有一点我没理解, 我上面有两种情况, 就是不执行第二个 nextTick, 如果说 await 会产生 asynchrony context, 那还是一样的 await cb, 但顺序又不同了. 也就是说, 这里我对照试验的是 cb 里是否执行 nextTick(也就是 Promise), 而 await 是一直存在的
|
11
1490213 OP 顺便感慨一下, 一个月前, 我一个后端最开始学前端是轻松的, 照着组件库示例写代码很快就能拼出一个页面,
但是后面又写了了两周后, 才发现细节里面有很多, 细分领域也很广泛, 确实是不能小视, 当然, 可能很多人也就一直停留在拼页面的阶段了, 但是后端搞单体搬 CRUD 砖的难道就少了吗, 其实都一样的 |
12
Zhuzhuchenyan 2020-08-04 19:14:07 +08:00 via iPhone
@1490213 我明白你的担忧。仔细看源码,nexttick 维护了一个需要执行回调的队列,在合适得时候通过一个循环同步的执行。
所以第一种情况 A 先加入队列,然后 B 加入队列,在合适的时候执行了 A,此时浏览器根本没有空调用 await 之后的逻辑先执行了 B,然后才有空调用 await 之后的逻辑 第二种情况就比较简单了,你可以试着分析看。await 所带来的 async context (等价为 promise resolve)为什么会先于 next tick (也可以等价为 promise resolve)执行 |
13
1490213 OP @Zhuzhuchenyan 我大致知道了, 源代码里面是直接用的 resolve
``` if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) } } ``` 这个地方就直接加入了主线程栈外的任务队列, 然后后面它调用了 timerFunc, 然后加了一个"锁" pending, 后面再调用 nexttick, 只是往 callbacks 队列里添加内容罢了 然后, await cb() 类似于 `await Promise.resolve(void 0).then(() => { // code behind});`, 把 `code behind` 加入了 任务队列 此时主线程栈没有程序执行了, 于是从执行任务队列里面拿内容, 首先执行 nextTick 里面的 flushCallbacks, flushCallbacks 把 nextTick callbacks 里的内容执行了, 然后拿任务队列里面的 `code behind` 来执行 |
14
azcvcza 2020-08-05 09:31:33 +08:00
async 是 promise 的语法糖;使用 promise 的时候要求同时都是要包在 Promise(()=>{})里的
|