【JS】一道promise的偏题

1,036 阅读7分钟

该问题是本人在2021.5.28一个交流群里看到的 ----------2021.6.27 在知乎里也看到了这个问题知乎链接,首赞解释得比较清楚,下面是当时的探索历程,这句话是一个月后加的


问题

        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);
        }).then(() => {
            console.log(7);
        })

答案:在这里插入图片描述 得彻底搞清楚promise的原理才行

在这里插入图片描述

探寻历程

--时间 2021.6.22重新复习promise源码,依旧没有得到如何来的答案 思路如下

  • then1内部回调,因为前面是给的成功的p对象,所以会放入微队列

  • then2因为then1返回的是pending的p1对象,所以会把回调放到p1的callbacks数组里, 等待then1回调出结果后,then1内部根据结果执行resolve(v)或者reject(r), 在resolve(v)或者reject(r)内部,会把处理callbacks操作加入微队列

  • 第二段开始,除了then3会得到一个成功的p对象,后面的then都只能得到一个pending的对象 所以执行同步代码的时候, 只有then3的回调会主动放到微队列,然后把后面的then都放到前面对应的p对象的callbacks里

  • 同步结束,只有then1和then3的回调以此进入微队列,其余均是callbacks里

  • 第一次微队列,先执行then1的回调, 输出0, 然后得到一个成功的值为4 的p_success(自己瞎命名的)对象

  • then1内部得到成功的p对象,执行resolve(p_success),把放有 then2 的回调的callbacks处理函数放入新的微队列

  • then2,已经没啥事了,同步异步均跟他无关

  • 跳到then3的回调, 执行, 输出1 , 然后得到一个成功的,值为undefined的 p对象

  • then3内部,得到成功p对象,执行resolve(p),把放有 then4 的回调的callbacks处理函数放入新的微队列

到这里就卡住了,不知道为什么输出1后会继续输出2然后3,又跳到4,明明输出0后微队列里面就只有输出1,输出0后把输出4的回调放入了微队列,但是输出1后却不是执行的输出4,而是输出2,

太难了,而且,源码的教学基本上对于promise都是用setTimeout来模拟,我用queueMicrotask来模拟也没办法达到原版promise的效果,但是明确了一点

通过手写promise,知道了 new Promise()的时候,并不是异步执行的,

Promise.resolve()也不是异步执行的,所有关于Promise异步执行的只是then里的回调函数即参数

并且,then方法也不全是异步,只有里面的执行回调的部分是异步的,返回一个新Promise的代步不是异步的

但是在异步执行完毕之前,该promise的状态是pending,通过闭包等待异步执行完毕后改变状态

promise里面的代码,只有then里的回调是异步的,其他都是同步的,当异步处理的时候,有可能then没有得到结果来执行回调,就会保存回调到实例对象上,但等到闭包拿到结果后,也会是加入异步队列去处理这个回调函数,

看到一篇文章,里面说then的注册只会在上一个then返回来一个有状态的promise对象,如果再往下继续then,但是没有结果,就会去执行其他同步代码, 文章链接 在这里插入图片描述 但是我持保留意见,因为我在debug的时候,把每个then都let一个对象,可以看到每个对象其实都是生成了的,只不过都是pending状态,如下

let p1 = Promise
    .resolve()

let p2 = p1.then(() => {
    console.log(0);
    return p = Promise
        .resolve(4);
})
let p3 = p2.then((res) => {
    console.log(res)
})

let p4 = Promise
    .resolve()
let p5 = p4.then(() => {
    console.log(1);
})
let p6 = p5.then(() => {
    console.log(2);
})
let p7 = p6.then(() => {
    console.log(3);
})
let p8 = p7.then(() => {
    console.log(5);
})
let p9 = p8.then(() => {
    console.log(6);
})
let p91 = p9.then(() => {
    console.log(7);
})

在这里插入图片描述 在这里插入图片描述

先保留吧,确实做不出来目前,想了两天了

重要参考

-----2021.6.27,在b站教学视频里找到了答案,链接如下 饥人谷前端promise教学视频2p 13分钟左右 在这里插入图片描述 如图,视频中方老师说then方法是黑盒,最好不要去探究原理,因为promise实现者随时都能改掉内部规则,就像node11以下,微任务不支持插队,微任务1里有微任务3,那么3也会排在微任务2后面再进行,但是后来改了规则,微任务可以立即插队立即解决, 他的意思是只要记住规则就好,而且promiseA+规范里面也没写这个。(其实我本人不太赞同这个观点,我还是倾向于了解内部原理,这样对自己提升很大,当然方老师是站在教育培训者角度,面对学员当然是希望记住就行,先把面试通过都好说)

言归正传

那我就跟着方老师思路走先,在这种普通的链式调用情况下,上下两个promise的回调执行顺序是一上一下交替进行的

然后加入一个 return Promise.resolve(3.5) ,就会改变顺序,他说原理他也不知道,记住规律就行, 规律就是:原本是一上一下交替进行,现在上面加了一个return 一个Promise.resolve()操作,上面那个promise的下一个回调就会在原本该执行的地方推迟两个回调后执行,比如在图中3里面加了return Promise.resolve(),下一个5本来该是回调4执行后就执行,现在变成4执行后再等两个,这里5都得等,自然就是执行下面promise的两个后,也就是6、8之后执行5,然后接着上下交替

在这里插入图片描述 所以顺序由12345678910变成了1234 > 6 >8 >5 >10 >7 >9

后面验证又加了一个return Promise.resolve(),同样在7后面本该是 7 > 12 >9 >14 >16 变成 7 >12 >14 >16 >9 在这里插入图片描述 但是注意,这种情况适用于函数返回经过Promise.resolve()后的结果,如果只是写一个return ,而不是return Promise.resolve(),就不能适用这种规律,所以看下面的代码

        Promise.resolve().then(() => {
            console.log(0);
            return 
        }).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);
        }).then(() => {
            console.log(7);
        })

会输出在这里插入图片描述 因为只是一个return,是无法适用刚刚那个规律的,依然会一上一下

同样,在下面的例子里,利用resolve向外传递,也是可以适用的,类似于return 一个结果,因为promise的原理就是向外返回一个promise对象,return是根据后面的对象类别,而resolve则是返回一个成功的promise对象而已

        new Promise((resolve, reject) => {
            console.log(0);
            resolve(Promise.resolve())
        }).then(() => {
            console.log(4);
        })

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

在这里插入图片描述

规律总结

即是 then里面返回一个 Promise.resolve() , 或者 new Promise的excutor同步代码里resolve( Promise.resolve ( ) ),当然reject就直接失败了,会穿透,一般不考虑,成功的才会链式顺序调用 只要是在 能向外返回一个成功的promise对象的代码块里,比如再写一个 Promise.resolve(),这个 Promise.resolve()操作就会阻隔后面挨着的两次回调,之后再依次进行,或者直接 new Promise(excutor) , 在excutor里面resolve(value),也是一样的会阻隔两次

如下,我把

    Promise.resolve().then(() => {
        console.log(0);
        return Promise.resolve(4);
    }).then((res) => {
        console.log(res)
    })
    

改成了

  return new Promise((resolve, reject) => {
    resolve(4)
  })
    

依然是输出01234567

    Promise.resolve().then(() => {
      console.log(0);
      return new Promise((resolve, reject) => {
        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);
    }).then(() => {
      console.log(7);
    })

答案

视频中看到知乎的答案,这个不只是根据规律了,而是根据原理,知道了promise的resolve的运行逻辑,知道了为什么是这样的知乎 在这里插入图片描述 总结

resolve 的参数为 Promise 时,要额外消耗两个 microtask,第一个 microtask 是规范要求的 PromiseResolveThenableJob;第二个 microtask 的功能是把 resolvedPromise 的状态同步给 p0。

首赞的大佬写的很详细知乎链接 在这里插入图片描述 但是其实源码也是有可能变的,在node6 7 8 10 11等版本都是这个输出,但是node9就是挨着输出,视频里方老师讲的还是有一定道理的

回顾本题

所以可以知道这道题的答案了 没有return 的情况下

        Promise.resolve().then(() => {
            console.log(0);
        }).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);
        }).then(() => {
            console.log(7);
        })

答案:0 1 undefined 2 3 5 6 7 在这里插入图片描述 加上之后就把 undefined的位置变为4并且 在1 后卡两个回调 变成 0 1 2 3 4(原undefined) 5 6 7

        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);
        }).then(() => {
            console.log(7);
        })