Vue 初学者求问一个关于 nextTick 和 async/await 的问题

2020-08-04 17:49:57 +08:00
 1490213

背景: 后端开发, 对 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. 这个问题去掉 await 就好了, 但是据我查资料了解, await 后面跟一个函数执行(没有返回 Promise 以及没有 then 属性), 应该是直接返回值, 语义上不应该对执行顺序产生影响
  2. 看 Vue 代码发现 nextTick 内部优先使用 Promise 处理, 多调一次只是把 callback 放到了回调队列里面, 等待引擎的主线程执行完后, 在引擎的任务队列里统一处理所有的回调队列, 那为什么多了一个 callback 会对代码执行的顺序有联系?
3661 次点击
所在节点    Vue.js
14 条回复
sujin190
2020-08-04 18:16:29 +08:00
这个问题好像是$nextTick 是微任务,Promise 的 callback 是宏任务,不是一个任务队列,微任务优先级高于宏任务,只有微任务执行完成才会执行宏任务,看起来你的输出还是符合这个流程的
sujin190
2020-08-04 18:21:48 +08:00
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') })
1490213
2020-08-04 18:36:53 +08:00
@ymcz852 老哥, 这个 cb 没有返回值, 返回的是 void
ymcz852
2020-08-04 18:38:34 +08:00
@1490213 和 cb 返回值没有关系,在于 cb 函数里有没有 promise
1490213
2020-08-04 18:40:51 +08:00
@sujin190 我看了 vue 内部实现, nextTick 在存在 Promise 的时候会优先使用 Promise, Promise.then 就是属于微任务
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 属性), 应该是直接返回值, 语义上不应该对执行顺序产生影响”并不完全正确
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)
 }) 
}
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 并不完全正确,因为他的确会存在潜在的执行顺序变化
1490213
2020-08-04 19:02:44 +08:00
@Zhuzhuchenyan 有一点我没理解, 我上面有两种情况, 就是不执行第二个 nextTick, 如果说 await 会产生 asynchrony context, 那还是一样的 await cb, 但顺序又不同了. 也就是说, 这里我对照试验的是 cb 里是否执行 nextTick(也就是 Promise), 而 await 是一直存在的
1490213
2020-08-04 19:12:27 +08:00
顺便感慨一下, 一个月前, 我一个后端最开始学前端是轻松的, 照着组件库示例写代码很快就能拼出一个页面,
但是后面又写了了两周后, 才发现细节里面有很多, 细分领域也很广泛, 确实是不能小视,
当然, 可能很多人也就一直停留在拼页面的阶段了, 但是后端搞单体搬 CRUD 砖的难道就少了吗, 其实都一样的
Zhuzhuchenyan
2020-08-04 19:14:07 +08:00
@1490213 我明白你的担忧。仔细看源码,nexttick 维护了一个需要执行回调的队列,在合适得时候通过一个循环同步的执行。

所以第一种情况 A 先加入队列,然后 B 加入队列,在合适的时候执行了 A,此时浏览器根本没有空调用 await 之后的逻辑先执行了 B,然后才有空调用 await 之后的逻辑

第二种情况就比较简单了,你可以试着分析看。await 所带来的 async context (等价为 promise resolve)为什么会先于 next tick (也可以等价为 promise resolve)执行
1490213
2020-08-04 19:33:27 +08:00
@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` 来执行
azcvcza
2020-08-05 09:31:33 +08:00
async 是 promise 的语法糖;使用 promise 的时候要求同时都是要包在 Promise(()=>{})里的

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/695592

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX