JavaScript < ES5、ES6、ES7、… >ECMAScript 6

ES6(十九):Promise和异步编程

2019-02-14  本文已影响11人  CodeMT

前面的话


JS有很多强大的功能,其中一个是它可以轻松地搞定异步编程。作为一门为Web而生的语言,它从一开始就需要能够响应异步的用户交互,如点击和按键操作等。Node.js用回调函数代替了事件,使异步编程在JS领域更加流行。但当更多程序开始使用异步编程时,事件和回调函数却不能满足开发者想要做的所有事情,它们还不够强大,而Promise就是这些问题的解决方案

Promise可以实现其他语言中类似FutureDeferred一样的功能,是另一种异步编程的选择,它既可以像事件和回调函数一样指定稍后执行的代码,也可以明确指示代码是否成功执行。基于这些成功或失败的状态,为了让代码更容易理解和调试,可以链式地编写Promise。本文将详细介绍Promise和异步编程

引入

JS引擎是基于单线程(Single-threaded)事件循环的概念构建的,同一时刻只允许一个代码块在执行,与之相反的是像JavaC++一样的语言,它们允许多个不同的代码块同时执行。对于基于线程的软件而言,当多个代码块同时访问并改变状态时,程序很难维护并保证状态不会出错

JS引擎同一时刻只能执行一个代码块,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列(job queue)中,每当一段代码准备执行时,都会被添加到任务队列。每当JS引擎中的一段代码结束执行,事件循环(event toop)会执行队列中的下一个任务,它是JS引擎中的一段程序,负责监控代码执行并管理任务队列。队列中的任务会从第一个一直执行到最后一个

【事件模型】

用户点击按钮或按下键盘上的按键会触发类似onclick这样的事件,它会向任务队列添加一个新任务来响应用户的操作,这是JS中最基础的异步编程形式,直到事件触发时才执行事件处理程序,且执行时上下文与定义时的相同

let button = document.getElementById("my-btn");
button.onclick = function(event) {
  console.log("Clicked");
};

【回调模式】

Node.js通过普及回调函数来改进异步编程模型,回调模式与事件模型类似,异步代码都会在未来的某个时间点执行,二者的区别是回调模式中被调用的函数是作为参数传入的

readFile("example.txt", function(err, contents) {    
  if (err) {
    throw err;
  }
  console.log(contents);
});
console.log("Hi!");
readFile("example.txt", function(err, contents) {    
  if (err) {
    throw err;
  }
  writeFile("example.txt", function(err) {        
    if (err) {
      throw err;
    }
  console.log("File was written!");
  });
});

[图片上传中...(image-b48127-1550134626393-69)]

method1(function(err, result) {    
  if (err) {
    throw err;
  }
  method2(function(err, result) {        
    if (err) {
      throw err;
    }
    method3(function(err, result) {            
      if (err) {
        throw err;
      }
      method4(function(err, result) {                
        if (err) {
          throw err;
        }
        method5(result);
      });
    });
  });
});

基础

Promise相当于异步操作结果的占位符,它不会去订阅一个事件,也不会传递一个回调函数给目标函数,而是让函数返回一个Promise

// readFile 承诺会在将来某个时间点完成
let promise = readFile("example.txt");

【Promise的生命周期】

每个Promise都会经历一个短暂的生命周期:先是处于进行中(pending)的状态,此时操作尚未完成,所以它也是未处理(unsettled)的;一旦异步操作执行结束,Promise则变为已处理(settled)的状态

在之前的示例中,当readFile()函数返回promise时它变为pending状态,操作结束后,Promise可能会进入到以下两个状态中的其中一个

1、Fulfilled
Promise异步操作成功完成
2、Rejected
由于程序错误或一些其他原因,Promise异步操作未能成功

内部属性[[PromiseState]]被用来表示Promise的3种状态:"pending""fulfilled""rejected"。这个属性不暴露在Promise对象上,所以不能以编程的方式检测Promise的状态,只有当Promise的状态改变时,通过then()方法来采取特定的行动

所有Promise都有then()方法,它接受两个参数:第一个是当Promise的状态变为fulfilled时要调用的函数,与异步操作相关的附加数据都会传递给这个完成函数(fulfillment function);第二个是当Promise的状态变为rejected时要调用的函数,其与完成时调用的函数类似,所有与失败状态相关的附加数据都会传递给这个拒绝函数(rejection function)

[注意]如果一个对象实现了上述的then()方法,那这个对象我们称之为thenable对象。所有的Promise都是thenable对象,但并非所有thenable对象都是Promise

then()的两个参数都是可选的,所以可以按照任意组合的方式来监听Promise,执行完成或被拒绝都会被响应

let promise = readFile("example.txt");
promise.then(function(contents) {    
  // 完成 
  console.log(contents);
}, 
function(err) {    
  // 拒绝 
  console.error(err.message);
});
promise.then(function(contents) {    
  // 完成 
  console.log(contents);
});
promise.then(null, function(err) {    
  // 拒绝 
  console.error(err.message);
});
promise.catch(function(err) {    
  // 拒绝 console.error(err.message);
});
// 等同于:
promise.then(null, function(err) {    
  // 拒绝 
  console.error(err.message);
});

[图片上传中...(image-2a3d4c-1550134626393-63)]

let promise = readFile("example.txt");    // 原始的完成处理函数
promise.then(function(contents) {
  console.log(contents);        
  // 现在添加另一个
  promise.then(function(contents) {
    console.log(contents);
  });
});

[注意]每次调用then()方法或catch()方法都会创建一个新任务,当Promise被解决(resolved)时执行。这些任务最终会被加入到一个为Promise量身定制的独立队列中

【创建未完成的Promise】

Promise构造函数可以创建新的Promise,构造函数只接受一个参数:包含初始化Promise代码的执行器(executor)函数。执行器接受两个参数,分别是resolve()函数和reject()函数。执行器成功完成时调用resolve()函数,反之,失败时则调用reject()函数

// Node.js 范例
let fs = require("fs");
function readFile(filename) {
  return new Promise(function(resolve, reject) {        
    // 触发异步操作
    fs.readFile(filename, { encoding: "utf8" }, 
    function(err, contents) {            
      // 检查错误
      if (err) {
        reject(err);
        return;
      }            
      // 读取成功 
      resolve(contents);
      });
  });
}
let promise = readFile("example.txt");
// 同时监听完成与拒绝
promise.then(function(contents) {    
  // 完成 
  console.log(contents);
}, function(err) {    
  // 拒绝 
  console.error(err.message);
});
// 在 500 毫秒之后添加此函数到作业队列
setTimeout(function() {
  console.log("Timeout");
}, 500);
console.log("Hi!");
Hi!Timeout
let promise = new Promise(function(resolve, reject) {
  console.log("Promise");
  resolve();
});
console.log("Hi!");

这段代码的输出内容是

promise
Hi !
let promise = new Promise(function(resolve, reject) {
  console.log("Promise");
  resolve();
});
promise.then(function() {
  console.log("Resolved.");
});
console.log("Hi!");

这个示例的输出内容为

promise
Hi !Resolved

【创建已处理的Promise】

创建未处理Promise的最好方法是使用Promise的构造函数,这是由于Promise执行器具有动态性。但如果想用Promise来表示一个已知值,则编排一个只是简单地给resolve()函数传值的任务并无实际意义,反倒是可以用以下两种方法根据特定的值来创建己解决Promise

使用Promise.resolve()

Promise.resolve()方法只接受一个参数并返回一个完成态的Promise,也就是说不会有任务编排的过程,而且需要向Promise添加一至多个完成处理程序来获取值

let promise = Promise.resolve(42);
promise.then(function(value) {
  console.log(value); // 42
});

使用Promise.reject()

也可以通过Promise.reject()方法来创建已拒绝Promise,它与Promise.resolve()很像,唯一的区别是创建出来的是拒绝态的Promise

let promise = Promise.reject(42);
promise.catch(function(value) {
  console.log(value); // 42
});

[注意]如果向Promise.resolve()方法或Promise.reject()方法传入一个Promise,那么这个Promise会被直接返回

非Promise的Thenable对象

Promise.resolve()方法和Promise.reject()方法都可以接受非PromiseThenable对象作为参数。如果传入一个非PromiseThenable对象,则这些方法会创建一个新的Promise,并在then()函数中被调用

拥有then()方法并且接受resolvereject这两个参数的普通对象就是非PromiseThenable对象

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};
let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value); // 42
});
let thenable = {
  then: function(resolve, reject) {
    reject(42);
  }
};
let p1 = Promise.resolve(thenable);
p1.catch(function(value) {
  console.log(value); // 42
});

【执行器错误】

如果执行器内部抛出一个错误,则Promise的拒绝处理程序就会被调用

let promise = new Promise(function(resolve, reject) {    
  throw new Error("Explosion!");
});
promise.catch(function(error) {
  console.log(error.message); // "Explosion!"
});
let promise = new Promise(function(resolve, reject) {    
  try {throw new Error("Explosion!");
  } catch (ex) {
    reject(ex);
  }
});
promise.catch(function(error) {
  console.log(error.message); // "Explosion!"
});

拒绝处理

有关Promise的其中一个最具争议的问题是,如果在没有拒绝处理程序的情况下拒绝一个Promise,那么不会提示失败信息,这是JS语言中唯一一处没有强制报错的地方,一些人认为这是标准中最大的缺陷

Promise的特性决定了很难检测一个Promise是否被处理过

let rejected = Promise.reject(42);    // 在此刻 rejected 不会被处理
 // 一段时间后……
rejected.catch(function(value) {    
  // 现在 rejected 已经被处理了 
  console.log(value);
});

【Node.js环境的拒绝处理】

Node.js中,处理Promise拒绝时会触发process对象上的两个事件

1、unhandledRejection
在一个事件循环中,当Promise被拒绝,并且没有提供拒绝处理程序时被调用
2、rejectionHandled
在一个事件循环后,当Promise被拒绝,并且没有提供拒绝处理程序时被调用

设计这些事件是用来识别那些被拒绝却又没被处理过的Promise
拒绝原因(通常是一个错误对象)及被拒绝的Promise作为参数被传入unhandledRejection事件处理程序中

let rejected;
process.on("unhandledRejection", function(reason, promise) {
  console.log(reason.message); // "Explosion!"
  console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));
let rejected;
process.on("rejectionHandled", function(promise) {
  console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));
// 延迟添加拒绝处理函数
setTimeout(function() {
  rejected.catch(function(value) {
    console.log(value.message); // "Explosion!" 
  });
}, 1000);
let possiblyUnhandledRejections = new Map();
// 当一个拒绝未被处理,将其添加到 
mapprocess.on("unhandledRejection", function(reason, promise) {
  possiblyUnhandledRejections.set(promise, reason);
});
process.on("rejectionHandled", function(promise) {
  possiblyUnhandledRejections.delete(promise);
});
setInterval(function() {
  possiblyUnhandledRejections.forEach(function(reason, promise) {
    console.log(reason.message ? reason.message : reason);
    // 做点事来处理这些拒绝 
    handleRejection(promise, reason);
  });
  possiblyUnhandledRejections.clear();
}, 60000);

【浏览器环境的拒绝处理】

浏览器也是通过触发两个事件来识别未处理的拒绝的,虽然这些事件是在window对象上触发的,但实际上与Node.js中的完全等效

1、unhandledrejection
在一个事件循环中,当promise被拒绝,并且没有提供拒绝处理程序时被调用
2、rejectionhandled
在一个事件循环后,当promise被拒绝,并且没有提供拒绝处理程序时被调用

Node.js实现中,事件处理程序接受多个独立参数:而在浏览器中,事件处理程序接受一个有以下属性的事件对象作为参数

type 事件名称 ("unhandledrejection"或"rejectionhandled"), promise 被拒绝的promise对象, reason 来自promise的拒绝值

[图片上传中...(image-2ae8e3-1550134626392-41)]

let rejected;
window.onunhandledrejection = function(event) {
  console.log(event.type); // "unhandledrejection"
  console.log(event.reason.message); // "Explosion!"
  console.log(rejected === event.promise); // true
};
window.onrejectionhandled = function(event) {
  console.log(event.type); // "rejectionhandled"
  console.log(event.reason.message); // "Explosion!"
  console.log(rejected === event.promise); // true
};
rejected = Promise.reject(new Error("Explosion!"));
let possiblyUnhandledRejections = new Map();// 当一个拒绝未被处理,将其添加到 
mapwindow.onunhandledrejection = function(event) {
  possiblyUnhandledRejections.set(event.promise, event.reason);
};
window.onrejectionhandled = function(event) {
  possiblyUnhandledRejections.delete(event.promise);
};
setInterval(function() {
  possiblyUnhandledRejections.forEach(function(reason, promise) {
    console.log(reason.message ? reason.message : reason);
    // 做点事来处理这些拒绝 handleRejection(promise, reason);
  });
  possiblyUnhandledRejections.clear();
}, 60000);

串联

至此,看起来好像Promise只是将回调函数和setTimeout()函数结合起来,并在此基础上做了一些改进。但Promise所能实现的远超我们目之所及,尤其是很多将Promise串联起来实现更复杂的异步特性的方法

每次调用then()方法或catch()方法时实际上创建并返回了另一个Promise,只有当第一个Promise完成或被拒绝后,第二个才会被解决

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

这段代码输出以下内容

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

【捕获错误】

在之前的示例中,完成处理程序或拒绝处理程序中可能发生错误,而Promise链可以用来捕获这些错误

let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
p1.then(function(value) {    
  throw new Error("Boom!");
}).catch(function(error) {
  console.log(error.message); // "Boom!"
});
let p1 = new Promise(function(resolve, reject) {    
  throw new Error("Explosion!");
});
p1.catch(function(error) {
  console.log(error.message); // "Explosion!"
  throw new Error("Boom!");
}).catch(function(error) {
  console.log(error.message); // "Boom!"
});

[注意]务必在Promise链的末尾留有一个拒绝处理程序以确保能够正确处理所有可能发生的错误

【Promise链的返回值】

Promise链的另一个重要特性是可以给下游Promise传递数据,已经知道了从执行器resolve()处理程序到Promise完成处理程序的数据传递过程,如果在完成处理程序中指定一个返回值,则可以沿着这条链继续传递数据

let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
p1.then(function(value) {
  console.log(value); // "42"
  return value + 1;
}).then(function(value) {
  console.log(value); // "43"
});
let p1 = new Promise(function(resolve, reject) {
  reject(42);
});
p1.catch(function(value) {    
  // 第一个完成处理函数
  console.log(value); // "42"
  return value + 1;
}).then(function(value) {    
  // 第二个完成处理函数
  console.log(value); // "43"
});

【在Promise链中返回Promise】

Promise间可以通过完成和拒绝处理程序中返回的原始值来传递数据,但如果返回的是Promise对象,会通过一个额外的步骤来确定下一步怎么走

let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
  resolve(43);
});
p1.then(function(value) {    
  // 第一个完成处理函数
  console.log(value); // 42
  return p2;
}).then(function(value) {    
  // 第二个完成处理函数
  console.log(value); // 43
});
let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
  resolve(43);
});
let p3 = p1.then(function(value) {    
  // 第一个完成处理函数
  console.log(value); // 42
  return p2;
});
p3.then(function(value) {    
  // 第二个完成处理函数
  console.log(value); // 43
});
let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
  reject(43);
});
p1.then(function(value) {    
  // 第一个完成处理函数
  console.log(value); // 42
  return p2;
}).then(function(value) {    
  // 第二个完成处理函数
  console.log(value); // 永不被调用
});
let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
  reject(43);
});
p1.then(function(value) {    
  // 第一个完成处理函数
  console.log(value); // 42
  return p2;
}).catch(function(value) {    
  // 拒绝处理函数
  console.log(value); // 43
});
let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
p1.then(function(value) {
  console.log(value); // 42
  // 创建一个新的 promise
  let p2 = new Promise(function(resolve, reject) {
    resolve(43);
  });    
  return p2
}).then(function(value) {
  console.log(value); // 43
});

响应多个

如果想通过监听多个Promise来决定下一步的操作,可以使用ES6提供的Promise.all()Promise.race()这两个方法

【Promise.all()】

Promise.all()方法只接受一个参数并返回一个Promise,该参数是一个含有多个受监视Promise的可迭代对象(如一个数组),只有当可迭代对象中所有Promise都被解决后返回的Promise才会被解决,只有当可迭代对象中所有Promise都被完成后返回的Promise才会被完成

let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
  resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
  resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.then(function(value) {
  console.log(Array.isArray(value)); // true
  console.log(value[0]); // 42
  console.log(value[1]); // 43
  console.log(value[2]); // 44
});
let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
  reject(43);
});
let p3 = new Promise(function(resolve, reject) {
  resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.catch(function(value) {
  console.log(Array.isArray(value)) // false
  console.log(value); // 43
});

【Promise.race()】

Promise.race()方法监听多个Promise的方法稍有不同:它也接受含多个受监视Promise的可迭代对象作为唯一参数并返回一个Promise,但只要有一个Promise被解决返回的Promise就被解决,无须等到所有Promise都被完成。一旦数组中的某个Promise被完成,Promise.race()方法也会像Promise.all()方法一样返回一个特定的Promise

let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
  resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
  resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.then(function(value) {
  console.log(value); // 42
});
let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
let p2 = Promise.reject(43);
let p3 = new Promise(function(resolve, reject) {
  resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.catch(function(value) {
  console.log(value); // 43
});

继承

Promise与其他内建类型一样,也可以作为基类派生其他类,所以可以定义自己的Promise变量来扩展内建Promise的功能。例如,假设创建一个既支持then()方法和catch()方法又支持success()方法和failure()方法的Promise,则可以这样创建该Promise类型

class MyPromise extends Promise {    
  // 使用默认构造器 
  success(resolve, reject) {
    return this.then(resolve, reject);
  }
  failure(reject) {        
    return this.catch(reject);
  }
}
let promise = new MyPromise(function(resolve, reject) {
  resolve(42);
});
promise.success(function(value) {
  console.log(value); // 42
}).failure(function(value) {
  console.log(value);
});
let p1 = new Promise(function(resolve, reject) {
  resolve(42);
});
let p2 = MyPromise.resolve(p1);
p2.success(function(value) {
  console.log(value); // 42
});
console.log(p2 instanceof MyPromise); // true

异步

之前,介绍过生成器并展示了如何在异步任务执行中使用它

let fs = require("fs");
function run(taskDef) {
  // 创建迭代器,让它在别处可用
  let task = taskDef();
  // 开始任务
  let result = task.next();
  // 递归使用函数来保持对 next() 的调用
  function step() {
    // 如果还有更多要做的
    if (!result.done) {            
      if (typeof result.value === "function") {
        result.value(function(err, data) {                    
          if (err) {
            result = task.throw(err);                        
            return;
          }
          result = task.next(data);
          step();
         });
      } else {
        result = task.next(result.value);
        step();
      }
    }
   }
  // 开始处理过程 step();
 }
// 定义一个函数来配合任务运行器使用
function readFile(filename) {
  return function(callback) {
    fs.readFile(filename, callback);
  };
 }
// 运行一个任务run
(function*() {
  let contents = yield readFile("config.json");
  doSomethingWith(contents);
  console.log("Done");
});
let fs = require("fs");
function run(taskDef) {
  // 创建迭代器
  let task = taskDef();
  // 启动任务
  let result = task.next();
  // 递归使用函数来进行迭代
  (function step() {
    // 如果还有更多要做的
    if (!result.done) {            
      // 决议一个 Promise ,让任务处理变简单
      let promise = Promise.resolve(result.value);
      promise.then(function(value) {
        result = task.next(value);
        step();
      }).catch(function(error) {
        result = task.throw(error);
        step();
      });
    }
  }());
 }
// 定义一个函数来配合任务运行器使用
function readFile(filename) {
  return new Promise(function(resolve, reject) {
    fs.readFile(filename, function(err, contents) {            
      if (err) {
        reject(err);
      } else {
        resolve(contents);
      }    
    });
  });
}
// 运行一个任务run
(function*() {
  let contents = yield readFile("config.json");
  doSomethingWith(contents);
  console.log("Done");
});

【未来的异步任务执行】

JS正在引入一种用于执行异步任务的更简单的语法,例如,await语法致力于替代基于Promise的示例。其基本思想是用async标记的函数代替生成器,用await代替yield来调用函数

(async function() {
  let contents = await readFile("config.json");
  doSomethingWith(contents);
  console.log("Done");
});
上一篇 下一篇

猜你喜欢

热点阅读