导航
导航
文章目录
  1. 1)Promise 概述
    1. 1.1 概念
    2. 1.2 基本用法
      1. 1.2.1 创建 promise 实例及 executor 函数
      2. 1.2.2 then() 方法
      3. 1.2.3 catch() 方法
  2. 2)Promise 状态及内部属性
    1. 2.1 Promise 状态
    2. 2.2 Promise 内部属性
    3. 2.3 关于 rejected 状态
    4. 2.4 来一道习题
  3. 3)使用 Promise
    1. 3.1 用 promise 封装 ajax
    2. 3.2 链式调用 Promise
      1. 3.2.1 没显式的 return 语句
      2. 3.2.2 显式 return 非 promise
      3. 3.2.3 显式 return 未完成的 promise
      4. 3.2.4 一个真实的例子
    3. 3.3 Promise 其他用法
  4. 4)promise 的实现
  5. 5)其他说明
  6. 附录
    1. 2.4 答案及分析分析
  7. 参考资料

异步之二:Promise

解决回调地狱的异步操作,Async 函数 是终极办法,但了解生成器和 Promise 有助于理解 Async 函数原理。由于内容较多,分三部分进行,这是第二部分,介绍 Promise 相关。第一部分介绍 Generator,第三部分介绍 Async 函数。

1)Promise 概述

1.1 概念

Promise是 ES6 引入的一个内建对象,通过它可以创建 Promise 实例。

Promise 实例可以想像成一个占位符,当获取到异步数据后,更新 promise。

这里需要注意的是,异步数据不是直接赋值给 promise 实例,而是赋值给 promise 实例的内部属性[[PromiseValue]],通过 promise 的then()或者catch()方法可以获取到这个值。

这也是 “promise” 名称的由来:承诺将来会给一个值,这个值可能是成功的数据也可能是错误信息,总之,会给一个值。

1.2 基本用法

基本用法包含 promise 创建、获取 promise 完成后的值以及错误捕获,他们三者结合使用才算完成了一次 promise 的完整使用过程。

1.2.1 创建 promise 实例及 executor 函数

通过内建对象Promise(promise 的构造函数)创建 promise 实例,其中,Promise接收一个函数作为参数,这个函数称之为 executor 函数。

Executor 函数本身接收两个函数作为参数,通常命名为resolvereject。它们接收一个值作为参数,作为 promise 完成之后的值传给 promise 实例的内部属性[[PromiseValue]]。它们当中只能有一个执行,执行之后表示当前 promise 已完成(当然,分别表示两个不同的已完成状态,兑现或拒绝)。

实际开发中我们会把期待得到的数据传给resolve函数,把错误信息传给reject函数。

另外,Executor 函数在传入Promise时会立即执行。见下面例子:

// executor 函数在传入 Promise 后会立即执行
var executor = function(resolve, reject) {
console.log(111);

// resolve 的执行结果需配合 then() 提取,这里给 resolve 传入了一个立即执行函数作为参数
resolve(
(() => {
console.log(222);
return 333;
})()
);

console.log(444);
};
var p = new Promise(executor);

console.log(555);

上面输出分别为:111,222,444,555

分析:

1)executor函数立即执行,所以111先打印出来;
2)resolve此时也立即执行,为了体现这一点,给其传入了立即执行函数,直接打印出了222。实际上此时 promise 已经完成,但我们还无法访问完成后的 promise,需要配合下文讨论的then()访问;
3)紧接着是后面代码执行,打印444
5)最后打印555

虽然一直在讨论resolvereject执行时过程同resolve,不单独讨论。

到目前为止,我们看到的都是同步代码,那 promise 和异步有什么关系呢?

实际使用中,resolve会将 promise 完成后的数据传给then()方法,而then()中的代码是放在微任务中异步执行的,这是其一;
第二点是,resolve往往不是立即执行,比如请求服务器上的数据,数据请求完成后再执行resolve,这个时候,resolve本身也是异步执行的。

1.2.2 then() 方法

promise 实例拥有then()方法,它设计的目的是 异步获取 promise 完成后 的值。

then()方法接受两个函数作为参数(这两个参数都不是必须),当 promise 完成后会执行它们:

  • 第一个参数为称为 success 回调函数,在 promise 执行resolve函数后执行,同时可以接收resovle的入参作为自己的入参;
  • 第二个参数称为 failure 回调函数,在 promise 执行reject函数或者程序发生异常时执行,可以接收reject的入参或者错误信息作为参数。见下面例子:

resolve或者reject传给then()的回调函数的参数,实际就是 promise 的内部属性[[PromiseValue]]的值,[[PromiseValue]]作为 promise 实例内部属性没法直接访问,必须借助then()或者下文将要提到的catch()方法获取。
另外,这个属性很重要,因为 promise 成功兑现后,它上面 保存的通常就是我们需要的异步数据

这里要注意,函数完成之后才执行then(),并且then()里面的函数是异步执行的,最快在当前事件循环的微任务开始执行时执行,这里有两点要注意:

1)虽然是异步,但仍然是在当前事件循环中执行(微任务在当前事件循环中执行,只是在宏任务之后),所以,会比setTimeout()中的代码先执行(setTimeout()中代码会在下一个事件循环中执行);
2)之所以称之为异步,是因为then()里面的代码会移到当前事件循环的微任务中执行,如果then()之后还有宏任务代码需要执行,微任务要等宏任务中的代码执行完之后才执行。

关于 JS 运行机制更详细的讨论可以参考这篇文章 这一次,彻底弄懂 JavaScript 执行机制

看下面代码:

var p = new Promise((resolve, reject) => {
// 直接 resolve,所以 then 执行 success 函数,同时传入 111 作为参数
resolve(111);
});

var success = function(resolvedValue) {
console.log("resolvedValue:", resolvedValue);
};

var failure = function(errorInfo) {
console.log("errorInfo:", errorInfo);
};

// 下一个事件循环中执行
setTimeout(() => console.log(222));

// success 和 failure 在当前事件循环的微任务中执行
p.then(success, failure);

// 当前事件循环宏任务中最后一行代码
console.log(333);

上面代码中,我们在 executor 函数中直接执行了resolve,所以then()立即执行,并且把111传给success函数,然后把success函数放到了 当前事件循环的微任务队列中

上述代码分别输出:333resolvedValue: 111222

分析:setTimeout()中代码在下一次事件循环中执行,所以最后输出222then()中的代码在当前事件循环的微任务中执行,需要等到当前事件循环宏任务中的代码执行完,即console.log(333)执行完再执行,所以先输出333,再输出resolvedValue: 111

为了分离错误处理的代码,promise 还提供了 catch 方法,见下面用法。

1.2.3 catch() 方法

promise 被拒绝后除了可以在then()中传入第二个参数处理,还可以通过catch()方法处理。

catch()可通过在then()后进行链式调用(加多一个.),作用等同在then()中传入的第二个参数,见下代码:

var p = new Promise((resolve, reject) => {
reject(111);
});

p.then(resolvedValue => console.log(resolvedValue)).catch(errorInfo =>
console.log(errorInfo)
);

执行上述输出:errorInfo: 111

如果只处理一个 promise,两种方式差不多,至于选择哪一种,看个人喜好。但通过链式调用处理多个 promise 时,用catch更加方便。

但不管怎样,都要选择一种错误处理方式,不然当出现错误时引擎会报错:Uncaught (in promise)

2)Promise 状态及内部属性

Promise 作为异步数据的占位符,在整个生命周期中会有不同状态,这些状态在其内部属性[[PromiseStatus]]中有记录。

2.1 Promise 状态

Promise 整个生命周期状态可分为两个部分:未完成状态和已完成状态,其中,未完成状态只有 pending 一种情况,而已完成状态又分为 fullfilled(已兑现,resolve函数执行)和 rejected(被拒绝,reject函数执行或者 promise 处理时发生异常)两种情况,见下图:

promise state

图拍引自 Secrets of the JavaScript Ninja(2nd)

英文已完成状态称为 “resolved states”,后面将要介绍的[[PromiseStatus]]也有resolved状态值。从生命周期的角度看,如果用英文描述,则 resolved states 包括resolvedrejected两种情况。

另外,promise 一般完成,状态就不能改变

2.2 Promise 内部属性

Promise 实例创建以后,我们需要关注两个内部属性:

[[PromiseStatus]]:保存当前 promise 的状态,可取值pending,resolved,rejected
[[PromiseValue]]:当 promise 在未完成状态时,其值为undefined,当 promise 完成后,其值为异步数据,或者错误信息。

见下面例子:

1)pending 状态

var p = new Promise((r, j) => {});

console.log(p);

此时,p 处于pending状态,内部属性值如下:

// 实际代码中这样写无效,这里只是延时,但可以在浏览器 console 中看到
p {
[[PromiseStatus]]: 'pending',
[[PromiseValue]]: undefined
}

2)resolved 状态

var p = new Promise((r, j) => {
r(111);
});

console.log(p);

此时,p 处于resolved状态,内部属性值如下:

p {
[[PromiseStatus]]: 'resolved',
[[PromiseValue]]: 111
}

3)rejected 状态

var p = new Promise((r, j) => {
j(111);
});

console.log(p);

此时,p 处于rejected状态,内部属性值如下:

p {
[[PromiseStatus]]: 'rejected',
[[PromiseValue]]: 111
}

这里没有捕获错误,所以会报错Uncaught (in promise) 111,不过,此处不是我们关注的重点

2.3 关于 rejected 状态

两种情况可能导致 promise 进入 rejected 状态:

  • 显式拒绝:在 promise 的 executor 函数中执行reject();
  • 隐式拒绝:处理 promise 的时候程序发生了异常。

显式拒绝的情况我们在上文已看,下面我们看个隐式拒绝的例子:

// promise 隐式拒绝
var p = new Promise((r, j) => {
a++;
});

p.then().catch(e => console.log("出错了:", e));

 因为a 没有定义,所以a++执行时报错了,promise 被拒绝,然后我们在catch()方法中捕捉到了错误,此时p内部属性描述如下:

p {
[[PromiseStatus]]: 'rejected',
[[PromiseValue]]: 'ReferenceError: aaa is not defined ...',
}

2.4 来一道习题

关于这个习题的分析看附录部分。

console.log("1");

setTimeout(function() {
console.log("2");
new Promise(function(resolve) {
console.log("3");
resolve();
}).then(function() {
console.log("4");
});
}, 400);

new Promise((resolve, reject) => {
console.log("5");
setTimeout(() => {
resolve("6");
}, 500);
setTimeout(() => {
reject("7");
}, 1000);
})
.catch(res => {
console.log("8,", res);
})
.then(res => {
console.log("9,", res);
});

new Promise(function(resolve) {
console.log("10");
resolve();
}).then(function() {
console.log("11");
});

console.log("12");

3)使用 Promise

3.1 用 promise 封装 ajax

这部分将用 promise 封装 ajax 的 GET 请求,为接下来的链式调用 promise 做准备。

最简单的 ajax 请求,没有考虑兼容性,请求错误处理等,主要说明 promise 应用

// 使用 promise 封装 ajax
function promiseAjax(url) {
// 返回 promise 对象
// 这样就可以后续通过 then 或者 catch 处理异步数据
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();

xhr.onload = () => {
if (xhr.status === 200) {
// 传递出成功的数据
resolve(xhr.responseText);
} else {
// 传递出错误信息
reject("出错了:", xhr.status);
}
};

xhr.open("GET", url);
xhr.send();
});
}

使用通过 promise 封装过的 ajax 就简单了:

promiseAjax("someURL")
.then(data => console.log(data))
.catch(e => console.log(e));

promise 在请求大量异步数据时优势会体现的更明显。

3.2 链式调用 Promise

链式调用之所以能够实现是因为then()catch()返回一个新的 promise 对象

链式调用有几个特点:

  • 当前面的 promise 完成后,才会执行后面的 promise;
  • 如果then()catch()没有显式 return promise,则当前返回的 promise 默认是兑现的;
  • 如果then()catch()显式 return promise,则当前返回的 promise 是兑现还是拒绝取决于then()catch()显式 return promise 的状态。

基于上,以then()为例,可能出现几种写法:

3.2.1 没显式的 return 语句

then()里面没有显式返回任何代码,如下:

var p = new Promise((r, j) => {
r(111);
});

p.then(v => console.log(v)).then(v => console.log(v));

没有显式的return语句,默认返回已完成且[[PromiseValue]]值为undefined的 promise

上面代码等同于:

var p = new Promise((r, j) => {
r(111);
});

var p1 = p.then(v => {
console.log(v);
return Promise.resolve();
});

// p1 已兑现,且 [[PromiseValue]] 值为 undefined
p1.then(v => console.log(v));

Promise.resolve() 返回一个完成状态的 promise,并且p1.then()也返回一个已完成的 promise,但没有使用

此时 p、p1 值如下:

p {
[[PromiseStatus]]: "resolved",
[[PromiseValue]]: 111,
}

// p1 已完成,但值是 undefined
p1 {
[[PromiseStatus]]: "resolved",
[[PromiseValue]]: undefined,
}

3.2.2 显式 return 非 promise

如果在then()中显式return非 promise,返回的数据就是当前 promise 兑现后的[[PromiseValue]]的值:

var p = new Promise((r, j) => {
r(111);
});

p.then(v => 222)
.then(v => console.log(v));

以上代码相当于:

var p = new Promise((r, j) => {
r(111);
});

var p1 = p.then(v => {
return Promise.resolve(222);
});

// p1 已兑现,且 [[PromiseValue]] 值为 222
p1.then(v => console.log(v));

此时 p、p1 的值如下:

p {
[[PromiseStatus]]: "resolved",
[[PromiseValue]]: 111,
}

// p1 已完成,但值是 undefined
p1 {
[[PromiseStatus]]: "resolved",
[[PromiseValue]]: 222,
}

NOTICE

返回错误对象也是相当于返回兑现的 promise,如下:

var p = new Promise((r, j) => {
r(111);
});

p.then(() => new Error("出错了"))
.then(v => console.log("then:", v))
.catch(e => console.log("error:", e));

此时打印的是then:出错了,因为相当于() => Promise.resolve(new Error('出错了')),此时是兑现的 promise,只不过显式传递了一个“出错了”的数据给当前兑现后的 promise 的[[PromiseValue]]

3.2.3 显式 return 未完成的 promise

但在then()中显式返回未完成的 promise 时,当前then()返回的 promise 是兑现还是拒绝取决于显式返回的未完成的 promise 的状态。

如果显式返回未完成的 promise 最终兑现了,当前 promise 是兑现状态,否则为拒绝状态。这是实际工作中常见的场景,比如链式调用相互依赖的异步数据, 3.2.4 中我们将看到这个例子,现在看下基本用法。

最终兑现的状态和前面的显式返回Promise.resolve()相同,我们看个最终被拒绝的例子:

var p = new Promise((r, j) => {
r(111);
});

p.then(v => new Promise((r, j) => j()))
.then(v => console.log("兑现"))
.catch(e => console.log("拒绝"));

此时会打印出 拒绝,因为显式返回未完成的 promise 最终被拒绝了。相当于:

var p = new Promise((r, j) => {
r(111);
});

p.then(v => {
return Promise.reject();
})
.then(v => console.log("兑现"))
.catch(e => console.log("拒绝"));

NOTICE

如果当前返回的是被拒绝的 promise,则后面的所有then()都不执行,直到遇到第一个catch(),执行catch()里面的代码,并且当前catch()也会返回一个 promise。

catch()返回的 pormise 一般不用,只在链式调用的最后放个catch()捕捉错误就好了,如下:

var p = new Promise((r, j) => {
r(111);
});

p.then(v => new Promise((r, j) => j()))
.then(v => console.log("兑现1")) // 不执行
.then(v => console.log("兑现2")) // 也不执行
.catch(e => console.log("拒绝"))
.then(v => console.log("兑现3")); // 执行,但一般不再 catch 后继续链式调用了

上面返回 拒绝兑现3

3.2.4 一个真实的例子

如果几个异步数据间存在依赖关系,可以使用 promise 的链式调用方式,比如:

需求:请求 URL1 得到 data1;请求 URL2 得到 data2,但 URL2 = data1[0].url2;请求 URL3 得到 data3,但 URL3 = data2[0].url3

使用 promise 链式调用可以这样写代码:

// promiseAjax 在 3.1 中有定义
promiseAjax("URL1")
.then(data1 => {
// 异步获取 data1 做相关处理
console.log(data1);

// 请求新数据依赖 data1 的数据,并将获取到的数据传递出去
// 且要记得显式 return
return promiseAjax(data1[0].url2);
})
// 如果不需要对 data2 做处理,可以使用箭头函数简化代码
.then(data2 => promiseAjax(data2[0].url3);)
.then(console.log(data3))
// 任何一个出现错误,都会捕捉到
.catch(e => console.log(e));

3.3 Promise 其他用法

Promise 还有两个常用功能Promise.all()Promise.race,它们都用于一次处理多个 promise,不同点是:

  • Promise.all()可以一次处理多个 promise,我们 不需要关心哪个先完成,全部兑现后后统一返回,但任何一个 promise 被拒绝都会导致整个 promise 被拒绝
  • 使Promise.race()时我们也 不需要关心执行顺序,但任何一个 promise 完成就会立即返回这个完成的 promise

它们的不同点主要体现在 promise 兑现后传递给then()的数据:Promise.all()返回的是所有 promise 兑现后组成的数组数据,而Promise.race()返回的是 最先完成的那一个 promise 返回的数据。见下面的代码例子:

1)Promise.all()

Promise.all([
promiseAjax("URL1"),
promiseAjax("URL2"),
promiseAjax("URL3"),
]).then(data => {
// data[1] 对应请求 URL1 后得到的数据,其他蕾丝
console.log(data[1], data[2], data[3]);
}).catch(e => console.log(e));

2)promise.rase()

Promise.race([
promiseAjax("URL1"),
promiseAjax("URL2"),
promiseAjax("URL3"),
]).then(data => {
// data 表示先完成的那个 promise 请求的 URL 数据
console.log(data);
}).catch(e => console.log(e));

4)promise 的实现

限于篇幅,将在另一篇文章中专门写 promise 的原理及实现。

网上有很多类似文章了,自己写一遍加深印象。

5)其他说明

相比使用回调函数,promise 已经使代码得到了很大改善, 但相比同步代码还是看着有些复杂,比如,链式调用那部分,代码看上去还是有些混乱。

那能不能用写同步代码的方式请求异步数据呢?可以的,这就是第三部分将要介绍的 Async 函数。

那是不是说 promise 就没用了呢?并不是,Async 函数实际是 生成器 + promise 的语法糖,只有理解了生成器和 promise 的原理,才能更好的理解 Async 函数。并且,现在很多 Web API 甚至库(比如 axios)都是经过 promise 封装的,熟悉 promise 的使用及原理有助于理解和 promise 相关的其他代码。

附录

2.4 答案及分析分析

2.4 的习题分析,见注释:

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

setTimeout(function() {
/**
* setTimeout 下一个事件循环执行
* 如果多个 setTimeout 执行顺序 先进先出
*/
console.log("2"); // nextLoop(400): 2
new Promise(function(resolve) {
/**
* new Promise 接受一个函数做参数
* 此函数体中的代码立即执行
*/
console.log("3"); // nextLoop(400): 2, 3
resolve();
}).then(function() {
/**
* then 中的代码在当前事件循环的微任务中执行
* 此时没有其他宏任务
*/
console.log("4"); // nextLoop(400): 2, 3, 4
});
}, 400);

new Promise((resolve, reject) => {
console.log("5"); // 1,5
setTimeout(() => {
/**
* promise 一旦完成(兑现或拒绝)状态就不能改变
* 所以后面的 reject 不执行
* 且这个promise在下一个时间循环中完成
*/
resolve("6");
}, 500);
setTimeout(() => {
reject("7"); // 前面已兑现,此处不执行
}, 1000);
})
.catch(res => {
/**
* 这个promise已兑现,不执行catch
*/
console.log("8,", res); // 不执行
})
.then(res => {
/**
* 加入下一个事件循环的微任务
*/
console.log("9,", res); // nextLoop(500): 9, 6
});

new Promise(function(resolve) {
/**
* 理解执行
*/
console.log("10"); // 1,5,10

/**
* 立即解决,但 then 中代码还是要放在当前循环的微任务中
*/
resolve();
}).then(function() {
/**
* 放入微任务等待执行
*/
console.log("11"); // 等待2(11)
});

console.log("12"); // 1,5,10,12,等待2

/**
* 当前循环中的等待后输出: 1,5,10,12,11
*/

/**
* 下一个事件循环(nextLoop)后输出: 1,5,10,12,11,nextLoop(400), nextLoop(500)
* 即:1,5,10,12,11,2, 3, 4, 5, 9, 6
*/

参考资料

【1】[美]JOHN RESIG,BEAR BIBEAULT and JOSIP MARAS 著(2016),Secrets of the JavaScript Ninja (Second Edition),第 6 章 promise 部分,Manning Publications Co.
【2】[美]Nicholas C. Zakas 著,刘振涛 译(2017),深入理解 ES6,p86~p97,电子工业出版社
【3】Promise 必知必会(十道题)