ES6学习系列之一:Promise

210 阅读9分钟

0 关于

Promise是ES6的核心概念之一,多种前端库中的API中都会见到Promise身影,因此掌握Promise概念异常重要。然而:

(1) 网上的关于Promise资料杂乱繁多,而且多数是一堆API的罗列,甚至不知所云,导致学习起来耗时耗力。

(2) 官网规范逻辑严谨,但是由于原版为英文,多数人限时时间和精力,无暇研究

本文尝试重新梳理Promise概念,结合示例用例,力求以更明、简洁、直观的方式去说明Promise概念、原理、简单应用等。

基于以上原因,本文有如下特点:

(1) API介绍力求通俗,可能导致部分措辞不够严谨,如有错误请及时指正

(2) 配合实际业务场景进行说明,力求形象

(3) 示例较多,导致文章内容略显过多,但肯定不难,请细致阅读

因此,请耐心读完本文:)

郑重声明:文章所有示例、语言均为原创,如果转载,请著名出处!所有的参考资料,在文档末的"参考资料"单独说明。

1 引例

本文以一个简单的业务场景开始:小明是单位的普通员工,有事需要请假3天,需要向上级请假。

这里的上级包括项目经理、人事经理、老板。流程如下:

(1) 向主管的项目经理提交请假申请。项目经理进行批准或拒绝

(2) 项目经理申请通过后,向人事经理提交申请,请求审批通过

(3) 人事经理审批通过后,向公司老板提交申请,请求审批通过

其中任一环节失败后,都会导致请假失败。

JavaScript中通过代码描述请假过程,包括如下几个函数。(为便于理解,函数相当简单且雷同。请仔细看代码和注释)

向项目经理提交请假申请

// 向项目经理提交请求。下面的2个方法类似。
function applyToProjectManager(callback) {
  // 通过setTimeout异步操作模拟ajax请求。下同。
  // 异步过程完全可以用异步Ajax替换
  setTimeout(function() {
    // 返回随机的0或1,模拟审批通过或拒绝。
    // 可直接设置1:let sucecess = 1;
    let success = parseInt(Math.random() * 2); 
    // 根据结果,执行操作
    if (!success) {
      throw new Error("projectManager审批失败!");
    }
    console.log("projectManager审批通过!");
    callback()
  }, 1000); 
  // 项目经理过了1天进行了审批,此处用1秒(1000毫秒)表示。
  // 如果程序中真的用1天,这个例子就类似无限等待啦:)
} 

向人事经理提交请假申请

function applyToHrManager(callback) {
  setTimeout(function() {
    let success = parseInt(Math.random() * 2); 
    if (!success) {
      throw new Error("HrtManager审批失败!");
    }
    console.log("HrManager审批通过!");
    callback()
  }, 2000); // 人事经理用了2天,进行行政审批
}

向老板提交请假申请

function applyToBoss(callback) {
  setTimeout(function() {
    let success = parseInt(Math.random() * 2); 
    if (!success) {
      throw new Error("boss审批失败!");
    }
    console.log("boss审批通过!");
    callback()
  }, 3000); // 老板太忙,等了3天才出结果,悲剧啊:(
}

传统的调用过程形如:

applyToProjectManager(function() {
  applyToHrManager(function() {
    applyToBoss(function() {
      // 层层审批成功了,想去干点啥吧...
      console.log("请假成功了,太好了,搓一顿去!");
    });
  })
})
console.log("小明说:等着审批就行了,等几天吧。我先去搬砖了...");

传统调用存在多种缺点:

  1. 回调多,导致回调地狱。场景中,如果小明请假成功后,去"搓一顿"时饭店关门,会多另一层回调;如果饭店开门,吃完后付款失败。。。导致无限级别的回调。My God!
  2. 不易阅读,调试不方便。

2 Promise

Promise解决以上的嵌套调用问题。

2.1 构造函数

Promise对象是一个构造函数,用来生成Promise实例。如下所示。

// 构造函数接收单个参数,类型为Function,规范中称为executor
// function需要传递2个参数:resolve、reject,均为函数类型
const promise = new Promise(function(resolve, reject) { 
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

看到构造函数看着有点复杂,其实不难。说明如下:

  • 构造函数接收单个参数,类型为Function,在规范中被称为***executor***,包含了初始化Promise的代码块

  • 单个参数的Function又包含2个参数:resolve、reject,同样为Function类型

  • 一般来说,在构造函数内执行异步操作(如Ajax),执行完毕后,如果成功则调用resolve函数;如果失败,调用reject函数。

  • resolve和reject函数在executor中调用时,只能传递1个业务参数:代表相应的业务数据,一般为对象类型。

这几点说明有些抽象,如果结合本文中的业务场景会更好理解。第一步,根据上面的构造函数定义,小明向项目经理请假过程可以描述为

const promiseOfProjectMananger = new Promise(function(resolve, reject) {
  console.log("executor开始执行...");
  // 小明向项目经理请假,审批过程为异步。
  setTimeout(function() {
    let success = parseInt(Math.random() * 2); 
    console.log("过了一段时间,审批结果出来啦...")
    if (!success) {
      // 审批未通过
      reject("projectManager审批失败!");
    }
    // 审批通过
    resolve("projectManager审批通过!")
  }, 1000); 
})
console.log("等待项目经理审批吧...");

注意:new Promise构造函数一开始被调用时,executor立即执行。上面代码在Chrome控制台打印先后顺序如下:

这也意味着:异步操作代码立即执行(尽管执行结果会异步)。

浏览器控制台输入变量promiseOfProjectMananger并回车后,成功或失败情况截图如下。

2.2 生命周期

上述代码被调用后,返回了一个Promise对象实例(通过promiseOfProjectMananger变量描述),此时处于pending状态。

Promise对象在生命周期内,有如下3个状态:

  • pending: 初始状态,既不是fulfilled状态,也不是rejected状态。
  • fulfilled: 意味着异步操作已经成功完成。(规范上是fulfilled,一些资料和浏览器使用resolved描述,属于一个含义)
  • rejected: 意味着异步操作失败

Promise对象不受外界影响,初始状态为pending,可能装换为fulfilled和reject状态,即:

pending -> fulfilledpending -> reject

上面创建的对象promiseOfProjectMananger,虽然操作结果还不确定,但有预期:不是成功,就是失败。

如何处理成功或失败呢?答案是:使用then或catch方法。

2.3 对象方法

任何一个Promise类型的对象实例,都有then、catch等方法(此外,还有其它的方法,此处略)

(1) then方法

then方法对异步操作的结果(成功或失败)进行处理。调用示例如下。

// then方法的调用
promise.then(function(value) {
  // 成功后调用,也叫fulfillment handler
}, function(error) {
  // 失败时调用,也叫failure handler
});

其中,2个handler的中的单个参数(value参数),数据是在构造函数的异步操作完毕后传入过来的。如下。

// ...2.1小节中的构造函数部分代码
if (!success) {
  // 这里,传递了失败后的业务数据。相当于then中的第2个函数。
  reject("projectManager审批失败!"); 
}
// 传递了成功后的业务数据。相当于then中的第1个函数。
resolve("projectManager审批通过!")   

注意:then方法中的2个Function参数是可选的,见如下的then方法调用方式:

let promise = doSomthingAsync();

// 监听了fulfillment handler和failure handler
promise.then(function(contents) {
  // fulfillment
  console.log(contents);
}, function(err) {
  // rejection
  console.error(err.message);
});

// 仅监听了fulfillment handler
promise.then(function(contents) {
  // fulfillment
  console.log(contents);
});

// 仅监听了failure handler
promise.then(null, function(err) {
  // rejection
  console.error(err.message);
});

结合本文中的业务场景,对初始化的promiseOfProjectMananger对象,进行then操作

promiseOfProjectMananger.then(function(result) {
  // 如果成功,控制台打印:projectManager审批成功!
  console.log(result); 
}, function(error) {
  // 如果失败,控制台打印:projectManager审批失败!  
  console.log(error);  
})

浏览器运行截图如下:

(2) catch方法

catch方法仅监听了rejected状态的Promise对象,进行相应的处理,等效于then方法仅有rejection handler参数的情况。如下。

// catch调用
promise.catch(function(err) {
  // rejection
  console.error(err.message);
});

// 等同于:
promise.then(null, function(err) {
  // rejection
  console.error(err.message);
});  
2.4 Promise链

Promise的优势在于:将多个Promise进行链式调用,完成复杂的异步操作。

事实上,每次调用then和catch方法时,创建并返回了另外一个Promise对象。查看如下代码

let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
p1.then(function(value) {
  console.log(value);
}).then(function() {
  console.log("Finished");
})

p1.then()的调用后,返回了第二个Promise对象,该对象的then方法继续被调用。

只有在第一个Promise被执行求解后(产生了fullfilled和rejected状态后),第二个then()方法的fulfillment handler才被调用。

上述代码进行分解后,如下。

let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
let p2 = p1.then(function(value) {
  console.log(value);
})
p2.then(function() {
  console.log("Finished");
});

常见的链式调用方式为

new Promise(function(resolve, reject) {
  // 异步操作,调用resolve和reject
})
.then(function(data) {
  // 异步方式操作后返回Promise对象实例 
})
.then(function(data) {
  // 异步方式操作后返回Promise对象实例 
})
.then(function(data) {
   // 异步方式操作后返回Promise对象实例 
});

3 Promise使用示例

有了链式调用为基础,引例中的回调地狱问题可以Promise解决了。直接上代码。

// 为了后面的链式调用更直观,这里提取公用操作为单独函数
function apply(roleName, time) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      // 为了输出方便,直接给了"审批通过"
      let success = 1; 
      if (!success) {
        reject(roleName + "审批失败!");
      }
      resolve(roleName + "审批通过!")
    }, time); 
  })
}

小明开始申请逐级审批,业务代码如下。

// 链式调用
// 同步书写方式,实现异步编程
// 避免了嵌套回调!
apply("projectManager", 1000)
.then(function(result) {
  console.log(result);
  return apply("HrManager", 2000);
})
.then(function(result) {
  console.log(result);
  return apply("Boss", 3000);
})
.then(function(result) {
  console.log(result)
  console.log("请假成功,搓一顿去!")
});
console.log("向项目经理已经提交了申请,搬砖去!等待结果...");

浏览器控制台,输出结果如下。

4 结论

本文篇幅稍长了一点,说明了Promise的一般概念,以及典型应用场景。解决了传统回调地狱问题。

内容仅为说明Promise概念、原理等:一些严谨的术语如job queuejob schedulingcallback hell 未提及或详细说明。

实际上,后续的规范ES7、ES8等,已经提及了利用await实现异步编程的规范。相比而言,比Promise更直观了一下。这种语法形如

async function getAysncResult() {
  // 通过关键字async、await进行异步编程,更像同步编程的方式
  let contents = await doSomethingAsync();
  
  doSomethingWith(contents);
  console.log("Done");
};

后续系列会进行探讨。

5 参考文献