背景

由于浏览器的渲染主线程只有一个,JS 采用了异步的方式避免任务阻塞。从原理上,JS 的异步通过 event loop 实现;而从使用上,JS 的异步主要通过 Promise 实现

对于同步代码,先后执行顺序是固定的,能够得知确切的输入与输出。而在异步代码中,执行完成的之间无法确定,不能保证后续代码执行时已经得到结果,比如 DOM 操作:

1
2
3
4
5
6
7
8
function addContent(src) {
let content = document.createElement('content');
content.src = src;
document.head.append(content);
}

addContent('./a.js');
init(); // 执行 init 初始化函数时,addContent 并没有完成

直接按照同步代码的方式编写异步任务时,逻辑会有错误。因此,以往对于异步任务,会给函数传递一个回调函数,在回调函数中执行后续代码。

这种方式在异步任务数量较多时,代码在结构上会层层嵌套,导致代码可读性变得非常差(回调地狱)

作用

Promise 正是为了解决异步编程的问题而提出的一种规范

在此规范中,promise 是一种有状态的对象(pending、fulfilled/rejected),可以通过构造器创建:let proms = new Promise((resolve, reject) => {})

1
2
// 有状态有值的对象,格式:
// Promise {<fulfilled>: value}

传递给构造器的参数为一个函数,称为执行器 executor,执行器函数中的代码会在创建 promise 对象时立即同步执行

构造器的两个参数 resolve 和 reject 均为回调函数,分别为执行成功的回调和执行错误的回调

实例方法(ainstance.xxx())

  • Promise.prototype.then()

    对于 Promise 对象本身所代表的异步任务执行完成后,可以通过 .then() 执行后续代码

    .then() 中接收两个函数参数:.then(res => {}, err => {}),分别对应 Promise 执行器函数体中的 resolve()rejected() 语句,表示异步任务执行成功/失败后的回调。后者可以省略 .then(res => {}),而没有前者时,需要设置第一个参数为 null .then(null, err => {})

  • Promise.prototype.catch()

    Promise 本身代表的异步任务执行失败时,可以用 .catch() 捕获并处理:.catch(err => {}),实际上与 .then(null, err => {}) 等效

  • Promise.prototype.finally()

    用于 promise 状态变化后,做一些清理工作。其中的回调函数没有参数:.finally(() => {}),所以内部无法获取当前 promise 的状态

Promise 能够链式调用,是因为 Promise 实例调用实例方法后,仍然返回一个 Promise 对象,即 let A = xxx.then()let B = xxx.catch()let C = xxx.finally() 均为 Promise 对象

静态方法(Promise.xxx())

  • Promise.resolve()

    直接创建初始状态为 fulfilled 的 promise 实例:let A = Promise.resolve(3),等效于 let A = new Promise(resolve => resolve(3))

  • Promise.reject()

    同上,直接创建初始状态为 rejected 的 promise 实例,let A = Promise.reject(),等效于 let A = new Promise((resolve, reject) => reject())

  • Promise.all()

    将多个 promise 实例合并为一个新实例,参数通常为数组:let B = Promise.all([promiseA, promiseB, promiseC]),返回值 B 为一个新 promise 实例

    • 如果其中所有 promise 实例均为成功执行 fulfilled 状态,则返回的 promise 实例 B 也为 fulfilled 状态。此时,返回的 promise 实例 B 的值也为同样大小的数组,对应各个 promise 实例的值
    • 只要其中 promise 实例中有等待执行 pending 状态,则返回的 promise 实例 B 也为 pending 状态。此时,返回的 promise 实例 B 没有值
    • 只要其中 promise 实例中出现执行错误 rejected 状态,则返回的 promise 实例 B 也为 rejected 状态。此时,返回的 promise 实例 B 的值为其中第一个执行错误的 promise 实例的值
  • Promise.race()

    参数和效果与 promise.all() 相同,但仅返回其中第一个状态变化的 promise 实例

注意,由于 JS 的单线程,实际执行时,Promise.all() 或 Promise.race() 中数组参数内的 promise 实例还是按照先后顺序开始执行的。但 Promise.all() 返回数组的顺序和实例执行结束的先后无关,而是对应输入数组的顺序;而 Promise.race() 返回的实例就是第一个状态变化的实例,哪怕是第一个开始执行的,不考虑开始执行的先后时差

应用示例

异步加载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();

image.onload = function() {
resolve(image);
};

image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};

image.src = url;
});
}

使用 Promise 处理 AJAX 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();

});

return promise;
};

getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function fetchData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response);
} else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.send();
});
}

fetchData('https://api.example.com/data')
.then((data) => {
console.log('获取数据成功:', data);
})
.catch((error) => {
console.error('获取数据失败:', error);
});

使用 Promise 链式调用处理多个异步任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function getUser(userId) {
return fetch(`/users/${userId}`);
}

function getPosts(userId) {
return fetch(`/users/${userId}/posts`);
}

getUser(123)
.then((user) => {
console.log('获取用户信息:', user);
return getPosts(user.id);
})
.then((posts) => {
console.log('获取用户帖子:', posts);
})
.catch((error) => {
console.error('操作失败:', error);
});

改进

Promise 的链式调用格式在代码结构上更清晰了,但还不够直观,因此出现了 async/await 语法糖,这种语法糖基于 Promise,但书写格式上使得异步代码看起来类似于同步代码,可读性更强

具体来说,使用 let A = async function xxx() {} 表示一个异步函数,返回值 A 为一个 Promise 对象

在异步函数 async 中,使用 await 表达式表示其中的异步任务,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

async function showMessage() {
console.log('开始'); // 直接执行
await delay(1000); // 暂停执行,等待异步任务完成
console.log('1秒后'); // 上一步延时任务完成后再执行
await delay(1000); // 暂停执行,等待异步任务完成
console.log('又1秒后'); // 上一步延时任务完成后执行
}

showMessage();

① async 函数中的异步任务必须使用 await 表达式

② 如果 async 函数中没有 await 表达式,那么 async 是没有必要的

② await 表达式必须在 async 函数中,不能单独使用(例外:ESM 标准中,通过顶层 await 动态导入模块)

实际使用 await 时,尽量将其放在 try {} catch {} 代码块中进行错误捕获。否则 await 的异步任务执行失败时,会直接中断整个 async 函数

如果 async 函数中的多个 await 异步任务之间不存在先后顺序,尽量将这些异步任务并发执行(使用 Promise.all()),提高性能

应用示例

使用 async/await 语法糖处理多个异步任务

1
2
3
4
5
6
7
8
9
async function fetchData() {
try {
const user = await getUser(123);
const posts = await getPosts(user.id);
console.log('用户帖子:', posts);
} catch (error) {
console.error('获取数据失败:', error);
}
}

并行执行多个异步操作

1
2
3
4
5
6
7
8
9
10
11
async function fetchMultipleData() {
const [userData, productData] = await Promise.all([
fetch('/api/user'),
fetch('/api/products')
]);

const user = await userData.json();
const products = await productData.json();

return { user, products };
}

对比

读取一组 URL,按照读取顺序输出结果

  • Promise 写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function logInOrder(urls) {
    // 远程读取所有URL
    const textPromises = urls.map(url => {
    return fetch(url).then(response => response.text());
    });

    // 按次序输出
    textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise)
    .then(text => console.log(text));
    }, Promise.resolve());
    }
  • Async/await 写法

    1
    2
    3
    4
    5
    6
    async function logInOrder(urls) {
    for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
    }
    }