V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
zhanglintc
V2EX  ›  问与答

下面这段 js 代码的输出应该是什么?

  •  
  •   zhanglintc · 2023-01-13 17:16:13 +08:00 · 1283 次点击
    这是一个创建于 705 天前的主题,其中的信息可能已经有所发展或是发生改变。
    async function async1() {
        console.log(1)
        await async2()
        console.log(2)
    }
    
    async function async2() {
        console.log(3)
    }
    
    console.log(4)
    
    setTimeout(function() {
        console.log(5)
    }, 0)
    
    async1()
    
    new Promise(function (resolve) {
        console.log(6)
    })
    
    

    看到一道题,我不是特别了解 js ,我大概看了下:

    • 我感觉顺序应该是 413265
    • 但是我实际跑了一下是 413625

    为啥 6 在 2 前面呢?

    16 条回复    2023-01-15 21:10:51 +08:00
    baolongzhanshen
        1
    baolongzhanshen  
       2023-01-13 17:47:28 +08:00
    可以看一下 promise 相关的东西,大致就是 promise 构造函数中的代码是同步的。
    DoubleKing
        2
    DoubleKing  
       2023-01-13 17:47:38 +08:00
    进入微任务的队列顺序?
    tutou
        3
    tutou  
       2023-01-13 17:49:00 +08:00   ❤️ 2
    await async2()
    console.log(2)

    =>

    Promise.resolve(async2()).then(() => { console.log(2) })
    murmur
        4
    murmur  
       2023-01-13 17:52:18 +08:00   ❤️ 1
    啊,这些面试题是真的害死人,你买了一把枪,然后直接照着别人脑袋砸过去,然后不解为啥人没啥事不是枪威力很大么
    november
        5
    november  
       2023-01-13 17:58:22 +08:00
    @murmur 23333 ,很喜欢这个比喻。
    MossFox
        6
    MossFox  
       2023-01-13 18:02:45 +08:00
    Javascript Promises are Eager and Not Lazy: https://tusharf5.com/posts/js-promises-eager-not-lazy/
    (就是一楼所说的那个)

    同样的题目我记得在掘金上面看到过一个细致的题解,客户端似乎没有浏览历史记录,所以链接也没法找了。
    LancerXu
        7
    LancerXu  
       2023-01-13 18:02:52 +08:00   ❤️ 1
    await 后面的代码可以理解成.then 执行,所以被放到了微任务列表
    第一轮宏任务执行输出 6 后再回头执行当前微任务列表
    MossFox
        8
    MossFox  
       2023-01-13 18:04:52 +08:00   ❤️ 1
    zhanglintc
        9
    zhanglintc  
    OP
       2023-01-13 20:26:11 +08:00
    先看了 #8 @MossFox 回复的文章,再看 #3 @tutou 和 #7 @LancerXu 的回复就全理解了。
    zhanglintc
        10
    zhanglintc  
    OP
       2023-01-14 17:24:00 +08:00
    @MossFox @tutou @LancerXu 各位再帮忙看下:

    // 这个大概耗时 1.7 秒
    function sleep() {
    i=0
    // do a heavy job
    for (let j = 0; j < 1e9; j++) {
    i++;
    }
    console.log("sleep done")
    return 3
    }

    setTimeout(function(){console.log(1)}, 10) // 队列位置 1
    setTimeout(function(){console.log(2)}, 0) // 队列位置 2
    sleep();

    这里认为塞入微队列的顺序应该是书写顺序吧,那么就是 1 在 2 之前。
    然后 sleep 是一个耗时操作,测试大概在 1700 毫秒左右。
    那么宏队列结束后调用微队列,此时先出栈 1 ,且应该已经超过 10 毫秒,那么 1 可以直接输出。
    但是为什么还是先输出的 2 呢?

    还是说 setTimeout 的计时是在进入微队列循环操作后才开始考虑计时器的?

    期望输出:
    sleep done
    1
    2

    实际输出:
    sleep done
    2
    1
    MossFox
        11
    MossFox  
       2023-01-14 20:36:53 +08:00
    @zhanglintc

    我不确定自己的解释对不对,所以…… 如果有路过的人发现有不准确的地方,麻烦一定要指出来一下,感谢。

    setTimeOut 注册的是 marcotask (宏任务),它的行为是这样的:
    - 在执行到 setTimeOut 的时候,定时器会交给 JS 引擎去在指定的将来 n 毫秒的时候,将 callback function 推入宏任务队列 (**注意** 此时的任务队列是空的,等待定时器的过程不属于 JS 的任务队列里的任务,注册的回调函数才是)
    - 只有在当前的同步语句执行完成 (即当前的宏任务结束) 之后,宏任务队列中才会开始执行下一个任务

    也就是说,即使这里在两个 setTimeOut 执行结束后,阻塞了超过 10 ms ,实际上宏任务队列也是会按照注册的时间将任务推进去的——只不过到时间了的时候,当前的宏任务还没有结束,所以回调函数不会按预期执行 (但确实是按预期的时间顺序推到了队列里面,所以是打印 2 的任务在打印 1 的任务之前被推入)。

    (这整个代码块需要被视为是一整个宏任务,sleep() 执行完毕之前,宏任务队列里面的其他任务不会继续执行,但不代表宏任务队列不可以被推入新的任务。进入队列的时间不受 JS 阻塞的影响,JS 的阻塞影响的只有开始执行的时间)

    啊,以防对于前面描述的宏任务的概念有些理解不到位,这里放个参考链接:
    https://stackoverflow.com/questions/25915634/difference-between-microtask-and-macrotask-within-an-event-loop-context
    MossFox
        12
    MossFox  
       2023-01-14 20:42:33 +08:00
    更正:"此时的任务队列是空的" → "此时,当前的回调函数并没有直接进入任务队列"
    (正在执行的也是一个宏任务)
    zhanglintc
        13
    zhanglintc  
    OP
       2023-01-14 21:07:23 +08:00
    @MossFox #11 这个字有点多,链接更多,要花点时间再理解理解。

    不过我倒是试了下,如果改成这样的话

    ```
    setTimeout(function(){console.log(1)}, 10) // 队列位置 1
    sleep();
    setTimeout(function(){console.log(2)}, 0) // 队列位置
    ```

    是可以输出 1 ,2 的:
    sleep done
    1
    2
    zhanglintc
        14
    zhanglintc  
    OP
       2023-01-14 23:57:11 +08:00
    @MossFox https://stackoverflow.com/a/34691484/4156036

    看起来是给定的 delay 到期后,可能会有一个“事件”或者“中断”发生,然后 JS 引擎会立即处理它(也就是放到微任务)。但是这个暂时没找到资料来证明。

    但是如果说 JS 引擎内部有类似的“事件”机制的话,我感觉都能解释通了。

    上面的例子:
    setTimeout(function(){console.log(1)}, 10)
    setTimeout(function(){console.log(2)}, 0)

    第一个 10 毫秒后触发事件,第二个立即触发事件,那么自然第二个先入队列。
    到时候执行微任务的时候自然就是第二个先打印了。
    tutou
        15
    tutou  
       2023-01-15 11:17:05 +08:00
    @zhanglintc
    ```
    setTimeout(function(){console.log(1)}, 10) // 队列位置 1
    sleep();
    setTimeout(function(){console.log(2)}, 0) // 队列位置
    ```
    你这个 12 是 node 环境吧,你试试 chrome 环境。这东西能应对面试就行了,再研究下去就要看 v8 源码了。
    补充一题
    Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
    }).then((res) => {
    console.log(res)
    })

    Promise.resolve().then(() => {
    console.log(1);
    }).then(() => {
    console.log(2);
    }).then(() => {
    console.log(3);
    }).then(() => {
    console.log(5);
    }).then(() =>{
    console.log(6);
    })

    链接: https://juejin.cn/post/6945319439772434469#heading-31
    zhanglintc
        16
    zhanglintc  
    OP
       2023-01-15 21:10:51 +08:00
    @tutou #15 是的是 node 环境。换成 chrome 的确不一样了:

    Node:
    sleep done
    1
    2

    Chrome:
    sleep done
    2
    1

    那意思这个 timer 的时间实际上还是跟具体实现有关是吧?不能一概而论。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4712 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 10:02 · PVG 18:02 · LAX 02:02 · JFK 05:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.