V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
1490213
V2EX  ›  Vue.js

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

  •  
  •   1490213 · Aug 4, 2020 · 4193 views
    This topic created in 2092 days ago, the information mentioned may be changed or developed.

    背景: 后端开发, 对 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 replies    2020-08-05 09:31:33 +08:00
    sujin190
        1
    sujin190  
       Aug 4, 2020
    这个问题好像是$nextTick 是微任务,Promise 的 callback 是宏任务,不是一个任务队列,微任务优先级高于宏任务,只有微任务执行完成才会执行宏任务,看起来你的输出还是符合这个流程的
    sujin190
        2
    sujin190  
       Aug 4, 2020
    ymcz852
        3
    ymcz852  
       Aug 4, 2020
    其实关键在于 await cb() 的效果 === await this.$nextTick(() => { console.log('cb $nextTick done'); }) === await Promise.resolve.then(() => { console.log('cb $nextTick done') })
    1490213
        4
    1490213  
    OP
       Aug 4, 2020
    @ymcz852 老哥, 这个 cb 没有返回值, 返回的是 void
    ymcz852
        5
    ymcz852  
       Aug 4, 2020
    @1490213 和 cb 返回值没有关系,在于 cb 函数里有没有 promise
    1490213
        6
    1490213  
    OP
       Aug 4, 2020
    @sujin190 我看了 vue 内部实现, nextTick 在存在 Promise 的时候会优先使用 Promise, Promise.then 就是属于微任务
    Zhuzhuchenyan
        7
    Zhuzhuchenyan  
       Aug 4, 2020
    对以上两个回答都不敢沟通,给题主一个简单的测试环境
    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  
       Aug 4, 2020
    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  
       Aug 4, 2020
    对上条回复的补充,如果你配置了 tslint,你在 await 那一行会收到警告

    'await' has no effect on the type of this expression. ts(80007)

    这里 no effect 并不完全正确,因为他的确会存在潜在的执行顺序变化
    1490213
        10
    1490213  
    OP
       Aug 4, 2020
    @Zhuzhuchenyan 有一点我没理解, 我上面有两种情况, 就是不执行第二个 nextTick, 如果说 await 会产生 asynchrony context, 那还是一样的 await cb, 但顺序又不同了. 也就是说, 这里我对照试验的是 cb 里是否执行 nextTick(也就是 Promise), 而 await 是一直存在的
    1490213
        11
    1490213  
    OP
       Aug 4, 2020
    顺便感慨一下, 一个月前, 我一个后端最开始学前端是轻松的, 照着组件库示例写代码很快就能拼出一个页面,
    但是后面又写了了两周后, 才发现细节里面有很多, 细分领域也很广泛, 确实是不能小视,
    当然, 可能很多人也就一直停留在拼页面的阶段了, 但是后端搞单体搬 CRUD 砖的难道就少了吗, 其实都一样的
    Zhuzhuchenyan
        12
    Zhuzhuchenyan  
       Aug 4, 2020 via iPhone
    @1490213 我明白你的担忧。仔细看源码,nexttick 维护了一个需要执行回调的队列,在合适得时候通过一个循环同步的执行。

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

    第二种情况就比较简单了,你可以试着分析看。await 所带来的 async context (等价为 promise resolve)为什么会先于 next tick (也可以等价为 promise resolve)执行
    1490213
        13
    1490213  
    OP
       Aug 4, 2020
    @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  
       Aug 5, 2020
    async 是 promise 的语法糖;使用 promise 的时候要求同时都是要包在 Promise(()=>{})里的
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   1441 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 54ms · UTC 23:56 · PVG 07:56 · LAX 16:56 · JFK 19:56
    ♥ Do have faith in what you're doing.