Promise与宏任务、微任务

        最近在摸鱼的时候,遇到了一个需求:用户可以接连上传多张图片,但是uni.upload一次只能上传一张图片,我当时想着是设置一个Promise,在一个循环中挨个上传,最后做一个整体resolve,但是这样并不合理。后来听得说是可以使用Promise.all方法进行多次请求,但是我只是略懂皮毛。因此决定写下几篇文章,好好学一学Promise的功能与诸多方法。本文将着重介绍Promise的执行顺序和宏任务、微任务概念。

一、基本执行概念  

      首先了解一些基本概念。

Promise与宏任务、微任务_第1张图片

         在一个代码段中,我们可以写下输出、赋值等基本语句作为整个脚本的宏任务,也可以写下setTimeout、setInterval等和时间相关的宏任务、还可以通过Promise.then创建微任务。这些任务也有一个相应的优先级:

        script语句作为基本宏任务优先直接执行,初次遇到setTimeout等代码将推入宏任务队列,初次遇到promise的内容时,promise内部代码是同步的,因此作为script语句的宏任务正常直接执行,它的.then或者时.catch方法将被压入微任务队列。不过,只有当promise状态不为pedding的时候才可以成功压入队列。        

        整体流程说来就是:我们输入完成一个代码段后进行运算时,赋值、输出等基本语句以及Promise内部代码时从上往下依次执行,遇到其他宏任务时推入宏任务队列,遇到微任务时推入微任务队列。当一段宏任务代码执行完毕后查看微任务队列中是否有任务,如果有则优先执行全部的微任务,之后再执行下一条宏任务,这段宏任务执行完毕后再检查微任务,再...........循环往复,直至所有任务执行完毕。

        了解了基本概念后我们可以看几道例题加深印象。

 1.1  promise基础题

1.1.1  题目一

const promise1 = new Promise((resolve, reject) => {
  console.log('promise1')
})

promise1.then(() => {
  console.log(2);
});

console.log('1', promise1);

        代码段从上至下,优先执行script语句中new Promise中的同步代码,再执行下面的输出1,由于Promise并未做任何处理,状态还是pedding,因此promise.then并未执行。

最终结果为:

'promise1'
'1' Promise{}

1.1.2  题目二

const fn = () => (new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
}))
fn().then(res => {
  console.log(res)
})
console.log('start')

        代码段从上至下,首先定义乐了一个函数fn,fn的返回值是一个promise,注意这里是定义,并非执行,所以没有任何输出。然后执行到fn().then语句,需要注意的是,这里是fn(),意思是执行fn函数,那么开始调用fn,输出1并将返回的promise状态改为resolved。在看到.then方法,推入微任务队列,接着执行下面的start输出。执行至此,第一个宏任务执行完毕,接下来查看微任务队列,发现有一个任务输出success微任务队列清空,再查找宏任务队列中没有任务,执行完毕。

最终结果为:

1
'start'
'success'

1.2  promise结合setTimeout

1.2.1 题目一

console.log('start')
setTimeout(() => {
  console.log('time')
})
Promise.resolve().then(() => {
  console.log('resolve')
})
console.log('end')

        代码段从上至下,先输出start,遇到setTimeout时将其推入宏任务队列,遇到Promise.resolve().then()的时候将其推入微任务队列,然后输出end。当前宏任务执行完毕,开始检查有无微任务,有,输出resolve。清空微任务队列后,再检查宏任务队列有无任务,有,输出time。该宏任务执行完毕后检查有无微任务,无;再检查有无宏任务,无,执行完毕。

最终结果为:

'start'
'end'
'resolve'
'time'

1.2.2 题目二

const promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    console.log("timerStart");
    resolve("success");
    console.log("timerEnd");
  }, 0);
  console.log(2);
});
promise.then((res) => {
  console.log(res);
});
console.log(4);

        代码从上至下,牢记!Promise内部的代码仍然是同步执行代码,它的then和catch才会被推入微任务队列。因此输出1,将setTimeout推入宏任务队列,再输出2。接下来看到Promise.then,然而我们此时并不能将其推入微任务队列,因为它此时还是pedding状态,定义它的promise还并未执行resolve或者是reject,先不执行它。因此接下来输出的是4。接下来查看微任务队列,无,再查看宏任务,有,输出timerStart,将该promise状态改为resolved并将之前的promise.then推入微任务队列,输出timerEnd。该宏任务执行完毕。执行完一段宏任务后查看微任务队列,有,输出success。

最终结果为:

1
2
4
"timerStart"
"timerEnd"
"success"

1.2.3  题目三

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("success");
    console.log("timer1");
  }, 1000);
  console.log("promise1里的内容");
});
const promise2 = promise1.then(() => {
  throw new Error("error!!!");
});
console.log("promise1", promise1);
console.log("promise2", promise2);
setTimeout(() => {
  console.log("timer2");
  console.log("promise1", promise1);
  console.log("promise2", promise2);
}, 2000);

        需要注意的是,这里和1.1.2有所不同。在1.1.2中代码是const fn = () => (new Promise(),意思是在调用fn时返回一个promise,但在这里是promise1 = new promise(),根据之前介绍的,这里promise里的同步代码正常进行。

        代码从上至下,遇到setTimeout,放入宏任务队列,接下来输出"promise1里的内容";再往下,promise2这一行要使用到promise1.then,由于1的状态是pedding,因此无法执行。再往下,输出"promise1 "和"promise2 ";再往下遇到第二个setTimeout,放入宏任务队列。至此,第一轮的宏任务执行完毕。

        在执行完一段宏任务后检查是否有微任务,无,执行宏任务队列中第一个任务。因此将promise1的状态就改为resolved并将之前的promise2推入微任务队列,再输出timer1。该宏任务执行完毕,查看微任务队列,有,进行err的输出。微任务队列全部执行完毕后,执行下一个宏任务队列,输出timer2,"promise1 ”,"promise2 ",执行完毕。

 最终结果为:

'promise1里的内容'
'promise1' Promise{}
'promise2' Promise{}
'timer1'
test5.html:102 Uncaught (in promise) Error: error!!! at test.html:102
'timer2'
'promise1' Promise{: "success"}
'promise2' Promise{: Error: error!!!}

1.3  Promise中的then、catch、finally

        进行这部分的题目练习前需要看一些概念。

Promise与宏任务、微任务_第2张图片

1.3.1  题目一

const promise = new Promise((resolve, reject) => {
  reject("error");
  resolve("success2");
});
promise
.then(res => {
    console.log("then1: ", res);
  }).then(res => {
    console.log("then2: ", res);
  }).catch(err => {
    console.log("catch: ", err);
  }).then(res => {
    console.log("then3: ", res);
  })

        promise的状态只能改变一次,因此执行了reject后便不会执行接下来的resolve。又因为catch不管被连接到哪里,都能捕获上层未捕捉过的错误,于是接来下进入catch方法进行错误的捕获。在执行完catch后会接着执行下面的then3,因为.then.catch都会返回一个新的Promise。但是由于catch返回的promise没有返回值,所以打印出来的是undefined。

最终结果为:

"catch: " "error"
"then3: " undefined

1.3.2  题目二

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('timer')
    resolve('success')
  }, 1000)
})
const start = Date.now();
promise.then(res => {
  console.log(res, Date.now() - start)
})
promise.then(res => {
  console.log(res, Date.now() - start)
})

  Promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。  如果足够快的话,也可能做到两个都是1001。

 最终结果为:

'timer'
'success' 1001
'success' 1002

1.3.3  题目三

Promise.resolve().then(() => {
  return new Error('error!!!')
}).then(res => {
  console.log("then: ", res)
}).catch(err => {
  console.log("catch: ", err)
})

        也许这看起来像是在原promise中返回了一个Error,然后执行catch。但是,不要被表面的Error迷惑了,这里是return Error,返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')也被包裹成了return Promise.resolve(new Error('error!!!'))

最终结果为:

"then: " "Error: error!!!"

        所以,如果是想要抛出一个错误,推荐用一下两种方法

return Promise.reject(new Error('error!!!'));
// or
throw new Error('error!!!')

1.3.4  题目四

function promise1 () {
  let p = new Promise((resolve) => {
    console.log('promise1');
    resolve('1')
  })
  return p;
}
function promise2 () {
  return new Promise((resolve, reject) => {
    reject('error')
  })
}
promise1()
  .then(res => console.log(res))
  .catch(err => console.log(err))
  .finally(() => console.log('finally1'))

promise2()
  .then(res => console.log(res))
  .catch(err => console.log(err))
  .finally(() => console.log('finally2'))

 解析:

Promise与宏任务、微任务_第3张图片         这里主要想介绍如果是连续的.then方法,在一次宏任务中只会执行一次,在执行完第一个then方法后,后续的then会被推入微任务队列中,可以理解为链式调用后面的内容需要等前一个调用执行完才会执行,就像是这里的finally()会等promise1().then()执行完才会将finally()加入微任务队列。

最终结果为:

'promise1'
'1'
'error'
'finally1'
'finally2'

1.4  Promise中的all和race

        通俗来说,.all()的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调;.race()的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。

Promise与宏任务、微任务_第4张图片

1.4.1 题目一

function runAsync (x) {
    const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
    return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log(res))

        假设现在要处理三个数据的Promise,首先定义了一个函数renAsync,然后在这个promise里面抽象化某一个数据的执行方法。然后进入Promise.all语句。根据概念可知,all方法里面可以包裹多个Promise,因此将多个Promise放在数组[runAsync(1), runAsync(2), runAsync(3)]中,最后可以将他们的结果以数组的格式展现出来。有了all,我们就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。

最终结果为:

// 在间隔一秒后同时打印出1,2,3,还有一个数组[1,2,3]
1
2
3
[1, 2, 3]

1.5 async/await

 1.5.1 题目一

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
async1();
console.log('start')

        代码从上往下看,定义了两个函数先不用管,看到调用了async1,执行。输出async1 start后看到await,意思是等待async2函数的执行,因此输出async2。需要注意,在这执行完后跳出async1,执行宏任务的同步代码start。在这一轮宏任务执行完毕后再来执行await后面的async1 end。我们可以理解为紧跟着await后面的语句相当于放到了new Promise中(await async2),下一行及之后的语句相当于放在Promise.then中(console.log(async1 end))

async function async1() {
  console.log("async1 start");
  // 原来代码
  // await async2();
  // console.log("async1 end");
  
  // 转换后代码
  new Promise(resolve => {
    console.log("async2")
    resolve()
  }).then(res => console.log("async1 end"))
}
async function async2() {
  console.log("async2");
}
async1();
console.log("start")

最终结果为:

'async1 start'
'async2'
'start'
'async1 end'

        如果将这里的await async2换成一个new Promise

async function async1() {
  console.log("async1 start");
  new Promise(resolve => {
    console.log('promise')
  })
  console.log("async1 end");
}
async1();
console.log("start")
'async start'
'promise'
'async1 end'
'start'

        我们可以清晰的看到,promise并不会阻塞后面同步代码end的执行。对比一下我们可以加深对await的理解:await的目标函数会被转化为一个new promise,该行语句的后面语句将会作为.then方法推入微任务队列中执行。需要注意的是,await语句的末尾在转换过后要添加上resolve()来改变.then方法中的状态。

1.5.2 题目二

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  setTimeout(() => {
    console.log('timer')
  }, 0)
  console.log("async2");
}
async1();
console.log("start")

        和题目一差不多类型,这次添加上了定时器,我们还是可以将这个await换成promise理解。

async function async1() {
  console.log("async1 start");
  new Promise(resolve=>{
    setTimeout(() => {
      console.log('timer')
    }, 0)
    resolve()
    console.log("async2");
  }).then(()=>{
     console.log("async1 end");
  })
async1();
console.log("start")

        当async1执行的时候首先输出start,遇到promise的同步任务,进去执行。遇到setTimeout,推入宏任务队列,继续往下遇到async2输出。之后再接着script语句的宏任务往下走输出start。这一轮宏任务执行完毕查看微任务队列,遇到已经被resolve后的then,输出end。再查看宏任务队列,发现定时器的任务,输出timer。

最终结果为:

'async1 start'
'async2'
'start'
'async1 end'
'timer'

1.5.3 题目三

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
  setTimeout(() => {
    console.log('timer1')
  }, 0)
}
async function async2() {
  setTimeout(() => {
    console.log('timer2')
  }, 0)
  console.log("async2");
}
async1();
setTimeout(() => {
  console.log('timer3')
}, 0)
console.log("start")

        这里做一道综合一点的题,也许一时半会还不能肉眼判别,所以还是来进行转换。

async function async1() {
  console.log("async1 start");
  new Promise(resolve=>{
    setTimeout(() => {
      console.log('timer2')
    }, 0)
    console.log("async2");
    resolve()
  }).then(resolve=>{
     console.log("async1 end");
     setTimeout(() => {
       console.log('timer1')
     }, 0)
  })
}

async1();
setTimeout(() => {
  console.log('timer3')
}, 0)
console.log("start")

最终结果为:

'async1 start'
'async2'
'start'
'async1 end'
'timer2'
'timer3'
'timer1'

1.5.4 题目四

async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
    resolve('promise1 resolve')
  }).then(res => console.log(res))
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')

         这里将计时器换成了一个promise,我们再来转换看看。

async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
    resolve('promise1 resolve')
  }).then(res => console.log(res))
  .then(resolve=>{
     console.log('async1 success');
     resolve()
     return 'async1 end'
  })
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')

最终结果为:

'script start'
'async1 start'
'promise1'
'script end'
'promise1 resolve'
'async1 success'
'async1 end'

1.5.5 题目五

async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
    resolve('promise resolve')
  })
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => {
  console.log(res)
})
new Promise(resolve => {
  console.log('promise2')
  setTimeout(() => {
    console.log('timer')
  })
})

        继续转换。

async function async1 () {    // 2
  console.log('async1 start');    // 3
  await new Promise(resolve => {
    console.log('promise1')    // 4
    resolve('promise resolve') // 5 转换该promise的状态
  }).then(resolve=>{    // 6 推入微任务队列 //10 第一轮宏任务结束,执行该微任务
    console.log('async1 success');    // 11
    return 'async1 end'    //12 返回新的promise.resolve('async1 end')
  })
}


console.log('srcipt start')    // 1

async1().then(res => {    // 2 调用async1() //7 pedding状态保持不变,不推入队列
  console.log(res)    //13 状态改变为resolve
})


new Promise(resolve => {
  console.log('promise2')    // 8
  setTimeout(() => {         // 9 推入宏任务队列
    console.log('timer')     // 14
  })
})

最终结果为:

'script start'
'async1 start'
'promise1'
'promise2'
'async1 success'
'async1 end'
'timer'

1.5.6 题目六

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(function() {
  console.log("setTimeout");
}, 0);

async1();

new Promise(function(resolve) {
  console.log("promise1");
  resolve();
}).then(function() {
  console.log("promise2");
});
console.log('script end')

        还是继续转换。

async function async1() {    // 3
  console.log("async1 start");    // 4
  await async2();    // 5 
  console.log("async1 end");    // 6 推入微任务队列    // 11
}

async function async2() {
  console.log("async2");    // 5 同步执行
}

console.log("script start");    // 1

setTimeout(function() {    // 2 推入宏任务队列
  console.log("setTimeout");    // 13
}, 0);

async1();    // 3

new Promise(function(resolve) {    
  console.log("promise1");    // 7
  resolve();    // 8 改变当前promise状态
}).then(function() {    // 9 推入微任务队列
  console.log("promise2");    // 12
});
console.log('script end')    // 10

        当你做出了前面的题目后再来看这道题,会感觉有些轻松。不过这道题是头条的一道面试题喔。

1.5.7 题目七

        为这一系列的题目收个尾。

async function testSometing() {
  console.log("执行testSometing");
  return "testSometing";
}

async function testAsync() {
  console.log("执行testAsync");
  return Promise.resolve("hello async");
}

async function test() {
  console.log("test start...");
  const v1 = await testSometing();
  console.log(v1);
  const v2 = await testAsync();
  console.log(v2);
  console.log(v1, v2);
}

test();

var promise = new Promise(resolve => {
  console.log("promise start...");
  resolve("promise");
});
promise.then(val => console.log(val));

console.log("test end...");

        也许熟练了可以直接答出来,不过我们这里还是做一个转换。

async function test() {    // 1
  console.log("test start...");    // 2
  const v1 = new Promise(resolve=>{
    console.log("执行testSometing");    // 3
    return "testSometing";    // 4 将当前promise状态改为resolve
    resolve()
  }).then(res=>{    // 5 推入微任务队列
     console.log(v1);    // 10 
     const v2 = new Promise(resolve=>{
         console.log("执行testAsync");    // 11
         return Promise.resolve("hello async");    // 12
         resolve()
     }).then(res=>{    // 13 推入微任务队列
          console.log(v2);    // 15
          console.log(v1, v2);    // 16
        })
  })
}

test();    // 1

var promise = new Promise(resolve => {
  console.log("promise start...");    // 6
  resolve("promise");    // 7 将当前promise状态改为resolve
});

promise.then(val => console.log(val));    // 8 推入微任务队列    // 14

console.log("test end...");    // 9

最好的理解方式就是看文档或者做题,当把这些题刷完后对于promise的理解可以更加深刻。

参考文章:掘金

你可能感兴趣的:(es6)