Skip to content

1、异步编程

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。

异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。

异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。

Promise 究竟是什么

理解:首先Promise本身是一个对象,通过resolve()与reject()这两个函数给promise赋值(改变状态)。

有时我们会难以理解Promise是一个什么东西,如果了解他的核心机制,我们就可以更好的理解Promise。

我们来看看如何创建一个PromisePromise()使用一个简单的构造器界面来让用户方便地创建promise对象。

其中,executor()是用户定义的执行器函数。当JavaScript引擎通过new运算来创建promise对象时,它事实上会在调用executor()之前就创建好一个新的promise对象的实例,并且得到关联给该实例的两个置值器:resolve()与reject()函数。接下来,它会调用executor(),并将resolve()与reject()作为入口参数传入,而executor()函数会被执行直到退出。但是executor()函数并不通过退出时所返回的值来对系统产生影响—该返回值将会被忽略(无论是return显式返回的结果值,还是默认的返回值undefined)。executor()中的用户代码可以利用上述的两个置值器,来向promise对象“所代理的那个数据”置值。亦即是说,为promise对象绑定(binding)值的过程是由用户代码触发的。这个过程看起来像是“让用户代码回调JavaScript引擎”。例如

需要清楚的事实:没有延时

在整个构建promise对象的过程中,有一个事实是需要读者清晰理解的,那就是所谓的“没有延时”。在传统的并发思路上理解Promise机制时,最容易犯的错误就是搞不清“promise什么时候执行”。

在ECMAScript中没有约定任何与调度时间相关的运行期(Runtime library)机制,亦即是说,没有进程、线程,也没有单线程/多线程这样的调度模型。因此仅使用ECMAScript约定的标准库,事实上是无法“写出一个并行过程”的。这也是几乎所有展示Promise特性的示例代码都要使用setTimeout的原因—这样才能创建一个并行任务。但setTimeout并不是ECMAScript规范下的,而是由宿主提供的应用层接口。setTimeout将隐含地受到许多宿主限制条件的影响,例如采用何种时间片调度,或者时钟管理机制,又或者是否是在多核的、多CPU的环境下等。

Promise机制中并没有延时,也没有被延时的行为,更没有对“时间”这个维度的控制。因此在JavaScript中创建一个promise时,创建过程是立即完成的;使用原型方法promise.XXX来得到一个新的promise(即pr+omise2)时也是立即完成的。同样类似于此的,所有promise对象都是在你需要时立即就生成的,只不过—重要的是—这些promise所代理的那个值/数据还没有“就绪(Ready)”。这个就绪过程要推迟到“未知的将来”才会发生。而一旦数据就绪,promise.then(foo)中的foo就会被触发了。

async await的实现原理

2、期约

ECMAScript 6新增的引用类型Promise,它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。Promise简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

基本用法:可以通过new操作符来实例化。创建新期约时需要传入执行器(executor)函数作为参数。

plain
const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

2.1  期约基础

状态机

期约是一个有状态的对象,可能处于如下3种状态之一:

  • 待定(pending
  • 兑现(fulfilled,有时候也称为“解决”,resolved
  • 拒绝(rejected

待定(pending)是期约的最初始状态。在待定状态下,期约可以落定为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态

解决值、拒绝理由及期约用例

每个期约只要状态切换为兑现,就会有一个私有的内部值(value)。类似地,每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)。在期约到达某个落定状态时执行的异步代码始终会收到这个值或理由。

通过执行函数控制期约状态

期约的状态是私有的,内部操作在期约的执行器函数中完成。

  • 控制期约状态的转换是通过调用它的两个函数参数实现的(resolve()reject()
  • 期约状态转换只能发生一次,不可撤销

Promise.resolve()

调用Promise.resolve()静态方法,可以实例化一个解决的期约。

javascript
// 这两种写法是等价的
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();

给Promise.resolve()的第一个参数对应着解决的期约,使用这个静态方法,可以把任何值都转换为一个期约

javascript
setTimeout(console.log, 0, Promise.resolve());
// Promise <resolved>: undefined
setTimeout(console.log, 0, Promise.resolve(3));
// Promise <resolved>: 3
// 多余的参数会忽略
setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
// Promise <resolved>: 4

PS:传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法

Promise.reject()

Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获)。

2.2 期约的实例方法

Promise.prototype.then()

这个then()方法接收最多两个参数:onResolved处理程序和onRejected处理程序。

这两个处理程序参数都是可选的。而且,传给then()的任何非函数类型的参数都会被静默忽略。如果想只提供onRejected参数,那就要在onResolved参数的位置上传入undefined。

javascript
function onResolved(id) {
 setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
 setTimeout(console.log, 0, id, 'rejected');
}

let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));

p1.then(() => onResolved('p1'),
 		() => onRejected('p1'));
p2.then(() => onResolved('p2'),
 	    () => onRejected('p2'));

//(3秒后)
// p1 resolved
// p2 rejected

// 非函数处理程序会被静默忽略,不推荐
p1.then('gobbeltygook');
// 不传onResolved处理程序的规范写法
p2.then(null, () => onRejected('p2'));

Promise.prototype.then()方法返回一个新的期约实例。这个新期约实例基于onResovled处理程序的返回值构建。

Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。

这个方法只接收一个参数:onRejected处理程序

事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null, onRejected)。

Promise.prototype.finally()

Promise.prototype.finally()方法用于给期约添加onFinally处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。

但onFinally处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码

非重入期约方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。这个特性被称为非重入特性。

非重入适用于onResolved/onRejected处理程序、catch()处理程序和finally()处理程序。

传递解决值和拒绝理由

在执行函数中**,解决的值和拒绝的理由是分别作为resolve()和reject()的第一个参数往后传的**。然后,这些值又会传给它们各自的处理程序,作为onResolved或onRejected处理程序的唯一参数

javascript
let p1 = new Promise((resolve, reject) => resolve('foo'));
p1.then((value) => console.log(value)); // foo
let p2 = new Promise((resolve, reject) => reject('bar'));
p2.catch((reason) => console.log(reason)); // bar

拒绝期约与拒绝错误处理

拒绝期约类似于throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。

2.3 期约连锁与期约合成

期约连锁

把期约逐个地串联起来是一种非常有用的编程模式。

因为每个期约实例的方法(then()、catch()和finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的**“期约连锁”**。

javascript
let p1 = new Promise((resolve, reject) => {
 console.log('p1 executor');
 setTimeout(resolve, 1000);
});
p1.then(() => new Promise((resolve, reject) => {
 console.log('p2 executor');
 setTimeout(resolve, 1000);
 }))
 .then(() => new Promise((resolve, reject) => {
 console.log('p3 executor');
 setTimeout(resolve, 1000);
 }))
 .then(() => new Promise((resolve, reject) => {
 console.log('p4 executor');
 setTimeout(resolve, 1000);
 }));
// p1 executor(1秒后)
// p2 executor(2秒后)
// p3 executor(3秒后)
// p4 executor(4秒后)

Promise.all()和Promise.race()

_Promise类_提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()Promise.race()

Promise.all()

Promise.all()静态方法_创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象_(一般用期约对象),返回一个新期约。

如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序:

javascript
// promise 语法
let p = Promise.all([
 Promise.resolve(3),
 Promise.resolve(),
 Promise.resolve(4)
]);
// 合成期约的解决值就是所有包含期约解决值的数组
p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]

Promise.all()静态方法,如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的期约也会拒绝:

javascript
// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>

// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
 Promise.resolve(),
 Promise.reject(),
 Promise.resolve()
]);
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught (in promise) undefined

Promise.race()

Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像

Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约:

javascript
// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
 Promise.resolve(3),
 new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
setTimeout(console.log, 0, p1); // Promise <resolved>: 3

3、异步函数

异步函数,也称为“async/await”(语法关键字),是ES6期约模式在ECMAScript函数中的应用。async/await旨在解决_利用异步结构组织代码_的问题。

3.1 async

async关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。

async 可以让函数具有异步特征,但总体上其代码仍然是同步求值的。

异步函数如果使用return关键字返回了值(如果没有return则会返回_undefined_),这个值会被_Promise.resolve()_包装成一个期约对象。

javascript
async function foo() {
 return 1;
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log); 
//1

在异步函数中抛出错误会返回拒绝的期约:(拒绝期约的错误不会被异步函数捕获)

javascript
async function foo() {
 console.log(1);
 throw 3;
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3

3.2 await

await关键字会暂停执行异步函数后面的代码,让出JavaScript运行时的执行线程。

await关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。

await关键字可以暂停异步函数代码的执行,等待期约解决:

javascript
async function foo() {
 let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
 console.log(await p);
}
foo();
// 3

await关键字必须在异步函数中使用,不能在顶级上下文如