Javascript 异步处理
Zhongjun Qiu 元婴开发者

JavaScript 异步处理方法梳理,内容涵盖回调函数的基本用法与错误处理、Promise 的创建与链式调用、常用 API 如 all、race、allSettled、any 等使用方式,以及 async/await 的基本语法与典型示例。

回调

1
2
3
4
5
6
7
8
9
10
11
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`酷,脚本 ${script.src} 加载完成`);
alert( _ ); // _ 是所加载的脚本中声明的一个函数
});

这被称为“基于回调”的异步编程风格。异步执行某项功能的函数应该提供一个 callback 参数用于在相应事件完成时调用。(译注:上面这个例子中的相应事件是指脚本加载)

处理 Error

在上述示例中,我们并没有考虑出现 error 的情况。如果脚本加载失败怎么办?我们的回调应该能够对此作出反应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));

document.head.append(script);
}

loadScript('/my/script.js', function(error, script) {
if (error) {
// 处理 error
} else {
// 脚本加载成功
}
});

加载成功时,它会调用 callback(null, script),否则调用 callback(error)

Promise

Promise 对象的构造器(constructor)语法如下:

1
2
3
let promise = new Promise(function(resolve, reject) {
// executor
});

传递给 new Promise 的函数被称为 executor。当 new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。

它的参数 resolvereject 是由 JavaScript 自身提供的回调。我们的代码仅在 executor 的内部。

当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:

  • resolve(value) —— 如果任务成功完成并带有结果 value
  • reject(error) —— 如果出现了 error,error 即为 error 对象。

new Promise 构造器返回的 promise 对象具有以下内部属性:

  • state —— 最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"
  • result —— 最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error
1
2
3
4
5
6
7
8
9
10
let promise = new Promise(function(resolve, reject) {
// 当 promise 被构造完成时,自动执行此函数
// 1 秒后发出工作已经被完成的信号,并带有结果 "done"
setTimeout(() => resolve("done"), 1000);
});

let promise = new Promise(function(resolve, reject) {
// 1 秒后发出工作已经被完成的信号,并带有 error
setTimeout(() => reject(new Error("Whoops!")), 1000);
});

只能有一个结果或一个error:第一次调用resolve或reject后,函数就执行结束了。

stateresult 都是内部的:无法直接访问。

then

1
2
3
4
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);

.then 的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。

.then 的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});

// resolve 运行 .then 中的第一个函数
promise.then(
result => alert(result), // 1 秒后显示 "done!"
error => alert(error) // 不运行
);
// ------------------------------------------------------
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// reject 运行 .then 中的第二个函数
promise.then(
result => alert(result), // 不运行
error => alert(error) // 1 秒后显示 "Error: Whoops!"
);

then中第一个函数必须是对successful处理,第二个是对error处理,不能调换。

catch

如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。或者我们也可以使用 .catch(errorHandlingFunction),其实是一样的:

1
2
3
4
5
6
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) 与 promise.then(null, f) 一样
promise.catch(alert); // 1 秒后显示 "Error: Whoops!"

finally

1
2
3
4
5
6
7
new Promise((resolve, reject) => {
/* 做一些需要时间的事,之后调用可能会 resolve 也可能会 reject */
})
// 在 promise 为 settled 时运行,无论成功与否
.finally(() => stop loading indicator)
// 所以,加载指示器(loading indicator)始终会在我们继续之前停止
.then(result => show result, err => show error)

finally不关心Promise结果

Promise 链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
new Promise(function(resolve, reject) {

setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

alert(result); // 1
return result * 2;

}).then(function(result) { // (***)

alert(result); // 2
return result * 2;

}).then(function(result) {

alert(result); // 4
return result * 2;

});

它的想法是通过 .then 处理程序(handler)链进行传递 result。

下面的不是链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
alert(result); // 1
return result * 2;
});

promise.then(function(result) {
alert(result); // 1
return result * 2;
});

promise.then(function(result) {
alert(result); // 1
return result * 2;
});

我们在这里所做的只是一个 promise 的几个处理程序。它们不会相互传递 result;相反,它们之间彼此独立运行处理任务。

一个完整的例子

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
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}

function loadGithubUser(name) {
return loadJson(`https://api.github.com/users/${name}`);
}

function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}

// 使用它们:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));

promise 进行错误处理

promise 链在错误(error)处理中十分强大。当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));

通常情况下,这样的 .catch 根本不会被触发。但是如果上述任意一个 promise rejected(网络问题或者无效的 json 或其他),.catch 就会捕获它。

未处理的 rejection

在浏览器中,我们可以使用 unhandledrejection 事件来捕获这类 error:

1
2
3
4
5
6
7
8
9
window.addEventListener('unhandledrejection', function(event) {
// 这个事件对象有两个特殊的属性:
alert(event.promise); // [object Promise] —— 生成该全局 error 的 promise
alert(event.reason); // Error: Whoops! —— 未处理的 error 对象
});

new Promise(function() {
throw new Error("Whoops!");
}); // 没有用来处理 error 的 catch

Promise API 示例

Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
.then(responses => {
// 所有响应都被成功 resolved
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // 对应每个 url 都显示 200
}

return responses;
})
// 将响应数组映射(map)到 response.json() 数组中以读取它们的内容
.then(responses => Promise.all(responses.map(r => r.json())))
// 所有 JSON 结果都被解析:"users" 是它们的数组
.then(users => users.forEach(user => alert(user.name)));

如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。

Promise 类有 6 种静态方法:

  1. Promise.all(promises) —— 等待所有 promise 都 resolve 时,返回存放它们结果的数组。如果给定的任意一个 promise 为 reject,那么它就会变成 Promise.all 的 error,所有其他 promise 的结果都会被忽略。
  2. Promise.allSettled(promises)(ES2020 新增方法)—— 等待所有 promise 都 settle 时,并以包含以下内容的对象数组的形式返回它们的结果:
    • status: "fulfilled""rejected"
    • value(如果 fulfilled)或 reason(如果 rejected)。
  3. Promise.race(promises) —— 等待第一个 settle 的 promise,并将其 result/error 作为结果返回。
  4. Promise.any(promises)(ES2021 新增方法)—— 等待第一个 fulfilled 的 promise,并将其结果作为结果返回。如果所有 promise 都 rejected,Promise.any 则会抛出 AggregateError 错误类型的 error 实例。
  5. Promise.resolve(value) —— 使用给定 value 创建一个 resolved 的 promise。
  6. Promise.reject(error) —— 使用给定 error 创建一个 rejected 的 promise。

async/await

async function

该函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中

1
2
3
4
5
6
7
async function f() {
return 1;
// 等价于
// return Promise.resolve(1);
}

f().then(alert); // 1

await

等待特定的promise完成

1
2
3
4
5
6
7
8
9
10
11
12
async function f() {

let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});

let result = await promise; // 等待,直到 promise resolve (*)

alert(result); // "done!"
}

f();

非 async 函数中调用 async 函数

如果我们尝试在非 async 函数中使用 await,则会报语法错误:

1
2
3
4
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}

只需要把 async 调用当作 promise 对待,并在它的后面加上 .then 即可:

1
2
3
4
5
6
7
8
9
10
11
12
async function wait() {
await new Promise(resolve => setTimeout(resolve, 1000));

return 10;
}

function f() {
// 1 秒后显示 10
wait().then(result => alert(result));
}

f();
 REWARD AUTHOR
 Comments
Comment plugin failed to load
Loading comment plugin