V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
1490213
V2EX  ›  Vue.js

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

  •  
  •   1490213 · 2020-08-04 17:49:57 +08:00 · 3640 次点击
    这是一个创建于 1573 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景: 后端开发, 对 es6 和 vue 有一定了解, 最近正在折腾研究其原理.

    问题概述: 在 async 函数中多执行了一个 nextTick, 导致代码执行时机发生变化

    问题详细描述

    点击 btn 触发 onClick, onClick 内部先调用 doSomeThing, 然后执行一个 nextTick1, doSomeThing 为 async 函数, 里面 await 了一个 callback(注意 cb 是普通箭头函数), 里面也有一个 nextTick2.

    • 若 nextTick2 执行, 'doSomeThing -- close' 会在 nextTick1 回调('onClick $nextTick done')之后执行
    • 若 nextTick2 不执行, 'doSomeThing -- close' 会在 nextTick1 回调('onClick $nextTick done')之前执行

    问题代码预览: 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 会对代码执行的顺序有联系?
    14 条回复    2020-08-05 09:31:33 +08:00
    sujin190
        1
    sujin190  
       2020-08-04 18:16:29 +08:00
    这个问题好像是$nextTick 是微任务,Promise 的 callback 是宏任务,不是一个任务队列,微任务优先级高于宏任务,只有微任务执行完成才会执行宏任务,看起来你的输出还是符合这个流程的
    sujin190
        2
    sujin190  
       2020-08-04 18:21:48 +08:00
    ymcz852
        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') })
    1490213
        4
    1490213  
    OP
       2020-08-04 18:36:53 +08:00
    @ymcz852 老哥, 这个 cb 没有返回值, 返回的是 void
    ymcz852
        5
    ymcz852  
       2020-08-04 18:38:34 +08:00
    @1490213 和 cb 返回值没有关系,在于 cb 函数里有没有 promise
    1490213
        6
    1490213  
    OP
       2020-08-04 18:40:51 +08:00
    @sujin190 我看了 vue 内部实现, nextTick 在存在 Promise 的时候会优先使用 Promise, Promise.then 就是属于微任务
    Zhuzhuchenyan
        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 属性), 应该是直接返回值, 语义上不应该对执行顺序产生影响”并不完全正确
    rabbbit
        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)
     }) 
    }
    Zhuzhuchenyan
        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 并不完全正确,因为他的确会存在潜在的执行顺序变化
    1490213
        10
    1490213  
    OP
       2020-08-04 19:02:44 +08:00
    @Zhuzhuchenyan 有一点我没理解, 我上面有两种情况, 就是不执行第二个 nextTick, 如果说 await 会产生 asynchrony context, 那还是一样的 await cb, 但顺序又不同了. 也就是说, 这里我对照试验的是 cb 里是否执行 nextTick(也就是 Promise), 而 await 是一直存在的
    1490213
        11
    1490213  
    OP
       2020-08-04 19:12:27 +08:00
    顺便感慨一下, 一个月前, 我一个后端最开始学前端是轻松的, 照着组件库示例写代码很快就能拼出一个页面,
    但是后面又写了了两周后, 才发现细节里面有很多, 细分领域也很广泛, 确实是不能小视,
    当然, 可能很多人也就一直停留在拼页面的阶段了, 但是后端搞单体搬 CRUD 砖的难道就少了吗, 其实都一样的
    Zhuzhuchenyan
        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)执行
    1490213
        13
    1490213  
    OP
       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
        14
    azcvcza  
       2020-08-05 09:31:33 +08:00
    async 是 promise 的语法糖;使用 promise 的时候要求同时都是要包在 Promise(()=>{})里的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1030 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 18:50 · PVG 02:50 · LAX 10:50 · JFK 13:50
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.