几个Promise栗子看JS的运行机制

目录

同步/异步

第一个栗子

宏任务/微任务

 第二个栗子

完整的JS运行机制

第三个栗子

结论1

结论2

第四个栗子

第五个栗子 

第六个栗子

第七个栗子


今天又着重看了一下Promise,果然温故而知新,之前一直是懵懵懂懂的状态,赶紧记录下来。

首先搞清楚几个名词:

异步任务:是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。例如setTimeout、setInterval、事件监听、Ajax请求等等。

同步任务:会立即执行的任务。除了异步任务的任务

除此之外还可以分为宏任务微任务,这个在稍后介绍。

主线程在运行的时候会有执行栈和等待队列,执行栈里会放入所有同步任务,而如果遇到了异步任务,就会把该任务放到等待队列中,直到所有的同步任务执行完了(执行栈空了),就会去读取等待队列,执行等待队列里的回调函数。再次祭出这张神图:

所以,JS的运行过程是这样的,

同步/异步

第一个栗子

console.log('1')

setTimeout(function(){
    console.log('2')
}, 1000)

console.log('3')

//1 3 2

之所以会输出1,3,2,就是因为setTimeout 是异步任务先被放入Event Table,1秒之后被推入Event Queue中,而等待队列里的任务只有在主线程空闲时才会执行。

怎么知道主线程执行栈为空?

js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

以上是比较简单的同步异步的问题判断,接下来在加入promise之前先了解一下什么是宏任务什么是微任务

宏任务/微任务

宏任务:宿主(浏览器/node)发起的任务

微任务:js引擎发起的任务

宏任务:主代码块 > setImmediate > MessageChannel > setTimeout / setInterval

微任务:process.nextTick > Promise = MutationObserver

 第二个栗子

setTimeout(function () {
    new Promise(function (resolve, reject) {
        console.log('异步宏任务promise');
        resolve();
    }).then(function () {
        console.log('异步微任务then')
    })
    console.log('异步宏任务');
}, 0)
new Promise(function (resolve, reject) {
    console.log('同步宏任务promise');
    resolve();
}).then(function () {
    console.log('同步微任务then')
})
console.log('同步宏任务')
/*
同步宏任务promise
同步宏任务
同步微任务then
异步宏任务promise
异步宏任务
异步微任务then
*/

 由结果可以看出,先同步任务,后异步,先宏后微。注意,同步的微任务优于异步的宏任务

所以,JS的运行可以进行补充:

完整的JS运行机制

第三个栗子

function test(resolve, reject) {
    var timeOut = Math.random() * 2;
    console.log('set timeout to: ' + timeOut + ' seconds.');//2 
    setTimeout(function () {
        if (timeOut < 1) {
            console.log('call resolve()...'); //6
            resolve('200 OK');
            console.log('successfull task') //7
        }
        else {
            console.log('call reject()...');
            reject('timeout in ' + timeOut + ' seconds.');
        }
    }, timeOut * 1000);
    console.log('me or setTimeout')//3
}

setTimeout(function(){
    console.log('i am setTimeout')//5
}, 0) 
console.log('in') //1
var p = new Promise(test)
console.log('after promise')//4
p.then(function(reslut){
    console.log('then')//8
    console.log(reslut)//9
}).catch(function(reslut){
    console.log('reject')
    console.log(reslut)
})

/*
in
set timeout to: 0.16122192279302006 seconds.
me or setTimeout
after promise
i am setTimeout
call resolve()...
successfull task
then
200 OK
*/

这里有几点需要注意的:

结论1

1.Promise 传入的函数是同步执行的,then 方法也是同步执行的,但 then 中的回调会先放入微任务队列,等同步任务执行完毕后,再依次取出执行,换句话说只有then的回调是异步的

2.在执行then方法时,如果前面的 promise 已经是 resolved 状态,则会立即将回调推入微任务队列(但是执行回调还是要等到所有同步任务都结束后)。then只负责注册回调,触发回调是由 promise 的 resolve 来触发的。

3.执行then方法时,如果前面的 promise 是 pending 状态则会将回调存储在 promise 的内部,一直等到 promise 被 resolve 才将回调推入微任务队列

知道了以上结论后,再来看上面的栗子就不难理解了,让我们来一行行分析:

1.代码第1行先是一个test函数,此时test函数还没有执行,先不管;

2.第18行,遇到了setTimeout,这是一个异步任务,在等待0s后,将function(){console.log('i am setTimeout')}放入异步宏任务event queue中;

3.第21行,是同步任务,打印出in;

4.第22行,实例化了一个Promise,并把test函数传进去,test函数执行了;

5.由于Promise传入的函数是同步的,转到第3行,打印set timeout to:XXXXXXX;

6.第4行,又是一个setTimeout,由于此时timeOut<1,在等待timeOut*1000时间后,把第5-13行放入异步宏任务event queue;

7.第15行,同步任务,打印me or setTimeout;

8.test函数执行完毕,到第23行,打印after promise;

9.执行p.then,注意,此时p这个Promise没有执行resolve也没有reject,p还处于pending状态。所以then里的回调函数不会立即放入微任务队列中,then只是注册了回调

10.此时同步任务全部执行完毕了,开始查看有没有同步微任务,发现没有;

11.查看是否有异步宏任务,发现有。event queue里的第一个任务是第19行的回调函数,所以打印i am setTimeout;

12.event queue里的第二个任务是第5行开始的回调。第6行打印出 call resolve()...

13.第7行执行resolve('200 OK'),将p的状态变为成功态

14.第8行打印successful task。异步任务执行完

15.回到了第24行p.then,此时p已经是成功态,所以把p.then推入微任务队列,由于此时没有同步任务,也没有宏任务,所以执行第25行,打印then

16.第26行,打印200 OK

结论2

1.当一个 promise 被 resolve 时,会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中

2.对于 then 方法返回的 promise 它是没有 resolve 函数的,取而代之只要 then 中回调的代码执行完毕并获得同步返回值,这个 then 返回的 promise 就算被 resolve(同步返回值的意思换句话说,如果 then 中的回调返回了一个 内部promise,那么 then 返回的 promise 会等待这个 内部promise 被 resolve 后再 resolve)

3.对于普通的 promise 来说,当执行完 resolve 函数时,promise 状态就为成功态

第四个栗子

let p = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000);
});
p.then(() => {
  console.log("log: 外部第一个then");
});
p.then(() => {
  console.log("log: 外部第二个then");
});
p.then(() => {
  console.log("log: 外部第三个then");
});

 分析以上代码运行过程:

1.第一行是一个new Promise,promise里的回调函数是同步的,遇到了setTimeout,1s后把resolve放入event queue中

2.第四行是p.then,then注册了第五行的回调函数。此时p的状态还是pending,将第五行回调放在p内部,不会推入微任务队列(根据结论2.1)

3.第7行和第10行同理,8、9行的回调都被then注册,但是不会被推入微任务队列

4.同步任务结束,查看是否有微任务,发现没有

5.执行异步任务,查看event queue,有resolve,执行。此后p变为成功态

6.p被resolve了,把之前注册的3个回调都放入微任务队列

7.异步宏任务执行结束,查看是否有微任务,发现有三个。依次执行,打印出“外部第一个then”,“外部第二个then”,“外部第三个then”

第五个栗子 

new Promise((resolve, reject) => {
  resolve();
})
  .then(() =>
    new Promise((resolve, reject) => {
      resolve();
    }).then(() => {
      console.log("log: 内部第一个then");
    })
  )
  .then(() => {
    console.log("log: 外部第二个then");
  });
  
  // log: 内部第一个then
  // log: 外部第二个then

分析以上代码运行过程:

1.new 一个Promise,回调函数是同步的执行,碰到了resolve,外层Promise状态变为成功态

2.遇到then方法,注册回调,回调函数返回了一个新的Promise(成为内层Promise)。根据结论2.2,这里外部的第一个 then 的回调返回了一个 promise,所以外部第一个 then 返回的 promise 需要等到内部整个 promise (红框) 被 resolve 后才会被 resolve(执行黄框)

3.当打印 log: 内部第一个then 后,回调执行完毕,蓝框的 promise 被 resolve,然后外部第一个 then 返回的 promise 才被 resolve

4.随后遍历之前通过 then 给外部第一个 then 返回的 promise 注册的所有回调(黄框),放入微任务队列,等同步任务执行完毕后,依次取出执行,最终打印 log: 外部第二个then

第六个栗子

new Promise((resolve, reject) => {
  console.log("log: 外部promise");
  resolve();
})
  .then(() => {
    console.log("log: 外部第一个then");
    new Promise((resolve, reject) => {
      console.log("log: 内部promise");
      resolve();
    })
      .then(() => {
        console.log("log: 内部第一个then");
      })
      .then(() => {
        console.log("log: 内部第二个then");
      });
  })
  .then(() => {
    console.log("log: 外部第二个then");
  });
  
// log: 外部promise
// log: 外部第一个then
// log: 内部promise
// log: 内部第一个then
// log: 外部第二个then
// log: 内部第二个then

分析以上代码:

1.先new 一个Promise,它的回调函数是同步的,打印“外部promise”

2.第3行,该Promise被resolve,由pending态变为成功态。但由于此时 then 方法还未执行,所以遍历所有 then 方法注册的回调时什么也不会发生(结论2第一条)。此时主线程和微任务队列如下:

主线程:外部then1、外部then2

微任务队列:无

3.第5行,执行外部then1,由于该promise已经resolve了,所以会把外部then1的回调函数放入微任务队列中

主线程:外部then2

微任务:外部then1回调

4. 第18行,执行外部then2,由于外部then1的回调还没有执行,所以外部then1这个promise的状态还是pending,所以执行外部then2时,不会将外部then2的回调函数放入微任务队列也不会执行,而是由外部then2继续保存

主线程:空

微任务:外部then1回调

5. 此时主线程为空,去执行微任务队列中取任务。开始执行外部then1的回调

6.第6行,打印“外部第一个then”

7.第7行,实例化一个promise,立即执行回调函数,打印第8行的“内部promise”

8.第9行,内部promise被resolve

主线程:内部then1、内部then2

微任务:无

9.第11行,执行内部then1方法,由于此时这个then方法的promise已经被resolve了,所以会把该then方法的回调函数也就是11-13行放入微任务队列

主线程:内部then2

微任务:内部then1回调

 10.执行内部then2方法,由于此时内部then1这个promise还没有resolve,还是pending,所以内部then2的回调不会被加入微任务队列

主线程:无

微任务:内部then1回调

11.主线程执行完毕,外部then1的回调执行完毕。外部then1这个promise被resolve,由于当一个 promise 被 resolve 时,会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中。故而会把外部then2的回调放入微任务队列

主线程:无

微任务:内部then1回调、外部then2回调

12.去微任务队列中取任务,执行内部then1的回调

13.第12行,打印“内部第一个then”,内部then1的回调执行完了,内部then1这个promise被resolve,把内部then2的回调函数放入微任务队列

主线程:无

微任务:外部then2回调、内部then2回调

14.执行外部then2的回调,打印"外部第二个then",外部then2回调执行完毕,外部then2这个promise被resolve,同时遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中。外部then2没有链式调用了,所以结束

15.执行内部then2回调,打印“内部第二个then”,内部then2回调执行完毕,内部then2这个promise被resolve,没有链式调用了,结束。

16.全部结束

第七个栗子

包含async/await的运行顺序:当 async 函数执行到 await 时,会将 await 后面接的函数先执行一遍,然后跳出整个 async 函数,继续执行当前事件循环,当前事件循环结束之后,回到 async 函数中,如果这时 await 函数已经有了结果,那么就继续执行 async 函数,否则继续等待。如果 await 函数返回一个 promise 对象,此时把该对象添加到 microtasks 队列。

/* 注意 commonFunc 是一个普通函数 */
function commonFunc() {
    console.log('commonFunc start...');
    return 'commonFunc end';
}
/* fn1 是一个 async 函数,将被处理为一个 promise 对象*/
async function fn1() {
    console.log('fn1 start...');
    return 'fn1 end';
}
async function fn2() {
    console.log('fn2 start...'); // ① 第一个输出
    let res1 = await commonFunc();  // ② fn2 顺序执行到这里,先把 await 后面的函数执行一遍
                                    // 因此第二个输出了 commonFunc 里面的 'commonFunc start...'
                                    // 然后会第一次跳出整个 fn2,继续执行后面的task。
    console.log(res1);  // ④ task 执行完毕之后,回到 fn2
                        // commonFunc 只是一个普通的函数,将返回值赋值给 res1 后输出。
    let res2 = await fn1(); // ⑤ 同样,先执行一遍 fn1 ,输出 'fn1 start...'
                            // 由于 fn1 是一个 promise 对象,因此此时把 这个 promise 推到 micro tasks 队列中。
                            // 第二次跳出fn2.
    console.log(res2);  // ⑦ 执行完 micro tasks 队列首位的 promise 后,再次回到 fn2
                        // 此时 fn1 已经等待结束,输出 'fn1 end'
}
fn2(); // 这里执行了 fn2, 所以 'fn2 start...'第一个被输出
let promise = new Promise(resolve => {
    console.log('promise start...'); // ③ 第一次跳出 fn2 后,先执行到这里,所以第三个输出
                                     // 将这个 promise 推到 micro tasks 队列的首位
    resolve('this is a promise');
});
promise.then(value => console.log(value));  // ⑥ 第二次跳出 fn2 之后,task 已经执行完毕,开始执行 micro tasks 队列。
                                            // 由于这个 promise 位于 micro tasks 的首位,因此第6个输出。

这篇文章对async/await讲的很清楚https://www.cnblogs.com/fundebug/p/10095355.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值