JS基础整理(3)—来说说Promise和事件循环吧

Promise是什么?为什么要使用?

为什么使用Promise

这篇关于promise的blog其实已经是3年前写的了,但是一直在草稿状态。因为当时的项目开始使用ES6,我第一次接触到promise这个概念,当时还花了一点时间去理解。

现在每一个前端工作者肯定非常熟悉promise,它是用于处理异步的!那么,为什么要用promise呢?

首先看一个项目上的例子:

let submit = function(params){
  validate(params, res=>{
    if(res.data === "TRUE"){
      submitData(params, res=>{
        if(res.data === "TRUE"){
          // other actions
        }
      })
    }
  })
}

以上例子,实现一个表单提交功能,在真正把数据提交到后台之前,先要做一次校验,校验通过才允许用户提交。

再来看一下:

// 以下三个函数模拟异步方法
function job1(fn){
  setTimeout(() => { fn("job1 success"); }, 150);
}
function job2(fn){
  setTimeout(() => { fn("job2 success"); }, 200);
}
function job3(fn){
  setTimeout(() => { fn("job3 success!"); }, 100);
}
(function(){
  job1((res=>{ console.log(res); }));
  job2((res=>{ console.log(res); }));
  job3((res=>{ console.log(res); }));
})();

以上输出:
job3 success
job1 success
job2 success

如果我们的需求是,job1, job2, job3必须按顺序执行,代码得改成:

(function(){
  job1((res=>{
    console.log(res);
    job2((res=>{
      console.log(res);
      job3((res=>{
        console.log(res);
      }));
    }));
  }));
})();

这里和上面的例子,都使用了嵌套的写法,如果逻辑再复杂一点,嵌套层数会更多,容易陷入回调地狱(callback hell)

AjaxNode.js回调地狱例子就非常经典了。而promise就是为了解决这个问题。

promise是如何处理的呢?

如果可以写成 job1.then(job2).then(job3)... 是不是好多了?

把异步方法修改为Promise

function job1(){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      console.log("job1 success");
      resolve("job1 success");
    }, 150);
  })
  
}
function job2(){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      console.log("job2 success");
      resolve("job2 success");
    }, 200);
  })
}
function job3(){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      console.log("job3 success");
      resolve("job3 success");
    }, 100);
  })
}

这时候就可以使用链式方法调用了

(function(){
  job1().then(job2).then(job3).then(res=>{console.log(res);})
})();

那么,一开始的例子也可以改写成

let submit = function(params){
  validate(params)
    .then(submitData(params))
    .then(res=>{ });
}

下面,我们一起来看看Promise是怎样实现的

什么是Promise

定义

Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。

状态

一个 Promise 必然处于这几种状态之一:
pending(进行中)
fulfilled(已成功)
rejected(已失败)

状态的变化只有两种方法:pending变成fulfilledpending变成rejected,状态变化时,有以下的方法来处理:

方法

then(onFulfilled, onRejected) 添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来resolve
catch(onRejected) 添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的promise
finally(onFinally) 添加一个事件处理回调于当前promise对象,并且在原promise对象解析完毕后,返回一个新的promise对象。回调会在当前promise运行完毕后被调用,无论当前promise的状态是完成(fulfilled)还是失败(rejected)

// MDN上的例子
const myPromise =
  (new Promise(myExecutorFunc))
  .then(onFulfilledA,onRejectedA)
  .then(onFulfilledB,onRejectedB)
  .then(onFulfilledC,onRejectedC);

或者使用以下写法

const myPromise =
  (new Promise(myExecutorFunc))
  .then(onFulfilledA)
  .then(onFulfilledB)
  .then(onFulfilledC)
  .catch(onRejectedAny);

上面的例子,就可以写成:

let onFulfilled = (data)=>{ console.log("Fulfilled: ", data); }
let onRejected = (error)=>{ console.log("Error: ", error); }
let onFinally = ()=>{ console.log("Finally."); }

(function(){
  job1().then(job2).then(job3).then(onFulfilled)
  .catch(onRejected)
  .finally(onFinally);
})();

输出:
job1 success
job2 success
job3 success
Fulfilled: job3 success
Finally.

假如其中一个job有error,那么输出是
job1 success
job2 error
Error: job2 error
Finally.

可以看出,无论当前promise的状态是完成(fulfilled)还是失败(rejected)finally()都会被调用。

再来看看另一种写法:

(function(){
  job1()
  .then(job2)
  .then(job3)
  .then(onFulfilled,onRejected)
  .finally(onFinally);
})();

使用then(onFulfilled,onRejected) 代替catch(onRejected),输出和以上例子一样,所以,catch(onRejected) 其实是把then(onFulfilled,onRejected)的预留参数onFulfilled省略了,没有本质上的区别。

再来做一点修改

(function(){
  job1()
  .then(job2,onRejected)
  .then(job3,onRejected)
  .then(onFulfilled,onRejected)
  .finally(onFinally);
})();

输出:
job1 success (第二行 job1 的输出)
job2 error (第三行 job2的输出)
Error: job2 error (第四行 onRejected 的输出)
Fulfilled: undefined (第五行 onFulfilled 的输出)
Finally. (第六行 onFinally 的输出)

job2的promise调用了reject方法,状态变成rejected,所以在then()的时候调用了onRejected,但是promise的方法都会返回一个新的promise,所以在第五行的时候,then()对应的promise是上一行onRejected()返回的promise, 会调用onFulfilled()

任何不是 throw 的终止都会创建一个"已决议(resolved)"状态,而以 throw 终止则会创建一个"已拒绝"状态。

如果我们把onRejected()修改一下

let onRejected = (error)=>{
  console.log("Error: ", error);
  throw new Error(error);
}

那么,上面的输出就变成:
job1 success (第二行 job1 的输出)
job2 error (第三行 job2的输出)
Error: job2 error (第四行 onRejected 的输出)
Error: Error: job2 error (第五行 onRejected 的输出) *
at onRejected (.../test.js:34:9)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
Finally. 
(第六行 onFinally 的输出)*

静态方法

有一个使用得比较多的方法是Promise.all(),先来看代码

(function() {
  let p1 = job1();
  let p2 = job2();
  let p3 = job3();
  Promise.all([p1, p2, p3]).then(values=>{
    console.log(values); // 
  })
})();

输出:
job3 success
job1 success
job2 success
[ 'job1 success', 'job2 success', 'job3 success' ]

Promise.all()方法接收一个promiseiterable类型(注:ArrayMapSet都属于ES6的iterable类型)的输入,并且只返回一个Promise实例, 那个输入的所有promise的resolve回调的结果是一个数组。

但是这里注意一下,和上面的对比,job1、job2、job3不是按顺序执行的。

我们是不是还可能用上面then(onFulfilled,onRejected)或者catch(onRejected)来使用呢?

(function() {
  let p1 = job1();
  let p2 = job2();
  let p3 = job3();
  Promise.all([p1, p2, p3]).then(onFulfilled, onRejected).finally(onFinally);
})();
// 或者
(function() {
  let p1 = job1();
  let p2 = job2();
  let p3 = job3();
  Promise.all([p1, p2, p3]).then(onFulfilled).catch(onRejected).finally(onFinally);
})();

输入都是:
Error: job2 error
Finally.

Promise.all 在任意一个传入的 promise 失败时返回失败。

因为job2的状态是失败了,所以最后调用的是onRejected

Promise与事件循环

当涉及异步事件的时候,事件循环就成是了个很让人头大的问题。先来看看概念:

  • 宏任务
    • 主代码块
    • setTimeout
    • setInterval
    • setImmediate ()-Node
    • requestAnimationFrame ()-浏览器
  • 微任务
    • process.nextTick ()-Node
    • Promise.then()
    • catch
    • finally
    • Object.observe
    • MutationObserver

为了更好了看出执行顺序,我们先来修改一下上面的job的定义

function job1(){
  return new Promise((resolve, reject)=>{
    console.log("job1 start...")
    setTimeout(() => {
      console.log("job1 success");
      resolve(1);
    }, 150); //定时器,150ms后执行
  })
}
function job2(){
  return new Promise((resolve, reject)=>{
    console.log("job2 start...")
    setTimeout(() => {
      console.log("job2 success");
      resolve(2);
    }, 100); //定时器,100ms后执行
  })
}
function job3(){
  return new Promise((resolve, reject)=>{
    console.log("job3 start...")
    setTimeout(() => {
      console.log("job3 success");
      resolve(3);
    },0);
  })
}

调用方法如下

console.log("***** START ******");
let p1 = job1();
let p2 = p1.then(job2);
let p3 = p2.then(job3);
let p = p3.then(onFulfilled);
console.log(p1, p2, p3, p);


setTimeout(() => {
  console.log('500ms: the stack is now empty');
  console.log(p1, p2, p3, p);
},500);
setTimeout(() => {
  console.log('0ms...');
},0);
setTimeout(() => {
  console.log('250ms...');
},250);
console.log("***** END ******");

输入顺序会是怎样呢?

分析:
根据事件循环,

  1. 先执行同步方法console.log("***** START ******");
  2. 构造函数new Promise()是同步任务,所以执行 job1的console.log("job1 start...")
  3. 遇到setTimeout,移交给定时器线程,150ms后放入宏任务队列,到此job1结束
  4. 接下都是 Promise.then()的方法,是异步微任务,放入微任务队列
  5. 执行 console.log(p1, p2, p3, p);,这时,promise的状态都是pending
  6. 遇到setTimeout,移交给定时器线程,500ms后放入宏任务队列
  7. 遇到setTimeout,移交给定时器线程,0ms后放入宏任务队列(即使是0,但是仍然要按规矩)
  8. 遇到setTimeout,移交给定时器线程,250ms后放入宏任务队列
  9. 执行console.log("***** END ******"),到这里主线程执行完毕
  10. 开始执行任务队列,宏任务队列中根据时间顺序: [0ms, 200ms,250ms, 500ms]
    a. 执行console.log('0ms...');
    b. 执行console.log("job1 success");resolve(1);
    c. 执行console.log('250ms...');
    d. 执行console.log('500ms: the stack is now empty'');console.log(p1, p2, p3, p);
    但是这里注意一下,当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完 。b任务执行完的时候,p1.then(job2)会执行,即会执行console.log("job2 start..."),但是由于job2中也有setTimeout,根据时间放入宏任务队列

最后输出:

***** START ******
job1 start...
Promise { <pending> } Promise { <pending> } Promise { <pending> } Promise { <pending> }
***** END ******
0ms...
job1 success
job2 start...
job2 success
job3 start...
250ms...
job3 success
Fulfilled:  3
500ms: the stack is now empty
Promise { 1 } Promise { 2 } Promise { 3 } Promise { 'Completed!' }

最后所有promise都是fulfilled/rejected状态

Promise.all()的同步和异步

如果使用Promise.all()呢?

console.log("***** START ******");
let p1 = job1();
let p3 = job3();
let p2 = job2();
let p = Promise.all([p1, p2, p3]);
let ep = Promise.all([]);

console.log(p1, p2, p3);
console.log(ep, p);
setTimeout(() => {
  console.log('the stack is now empty');
  console.log(p1, p2, p3, p);
},500);
setTimeout(() => {
  console.log('0ms...');
},0);
console.log("***** END ******")

结果:

***** START ******
job1 start...
job3 start...
job2 start...
Promise { <pending> } Promise { <pending> } Promise { <pending> }
Promise { [] } Promise { <pending> }
***** END ******
job3 success
0ms...
job2 success
job1 success
the stack is now empty
Promise { 1 } Promise { 2 } Promise { 3 } Promise { [ 1, 2, 3 ] }

这里有一个注意点:

Promise.all当且仅当传入的可迭代对象为空时为同步

所以最开始的时候,console.log(ep, p);的输出一个是fulfilled,一个是pending

async/await

最后顺便看看 ES2017新增的 async/await

await关键字接收一个promise并奖其转换为一个返回值或抛出一个异常
async关键字意味着函数返回一个promise

任何使用await的代码都是异步的,只能在async关键字声明的函数内部使用await关键字

上面的例子,如果想要取出每一步的结果,可能会比较麻烦,可以改写成

async function run() {
  // 按顺序执行
  let r1 = await job1();
  let r2 = await job2();
  let r3 = await job3(); 
  console.log(r1,r2, r3);
}
// output: 1 2 3

或使用Promise.all

async function run() {
  // 不会按顺序执行
  let [r1,r2, r3] = await Promise.all([job1(), job2(), job3()]);
  console.log(r1,r2, r3);
}
// output: 1 2 3

参考文章:
HTML Standard
MDN上的说明
Promise+
讲JS运行机制,事件循环讲得很清晰

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):