回调函数

最早解决异步的方式就是使用回调函数,简单理解就是一个函数被作为参数传递给另一个函数

回调函数跟异步没有必然联系,只能说回调函数是解决异步的方法之一。

示例

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 这段代码展示了一个典型的回调地狱(callback hell)示例。
// 回调地狱指的是多个异步操作依次嵌套执行,代码层层嵌套,难以维护和理解。
// 在这个示例中,每个异步操作都有一个回调函数,且每个回调函数中又包含了下一个异步操作,导致了代码结构复杂,难以阅读和维护。

function getData(callback) {
// 模拟获取数据的异步操作,1秒后执行回调函数
setTimeout(() => {
console.log("获取数据");
// 调用回调函数,并将获取的数据作为参数传递给回调函数
callback("data");
}, 1000);
}

function processData(data, callback) {
// 模拟处理数据的异步操作,1秒后执行回调函数
setTimeout(() => {
console.log("处理数据" + data);
// 将处理后的数据作为参数传递给回调函数
callback(data.toUpperCase());
}, 1000);
}

function displayData(data, callback) {
// 模拟显示数据的异步操作,1秒后执行回调函数
setTimeout(() => {
console.log("显示数据" + data);
// 执行回调函数
callback();
}, 1000);
}

// 回调地狱
// 第一个异步操作:获取数据
getData(data => {
// 第二个异步操作:处理数据
processData(data, processedData => {
// 第三个异步操作:显示数据
displayData(processedData, () => {
// 最后的回调函数:操作完成
console.log("操作完成");
});
});
});

需要注意的是,回调虽简单,但当有多个异步操作需要串行执行或者有多个异步操作之间存在依赖关系时,嵌套使用回调函数会导致回调地狱,使得代码难以维护。

1
2
3
4
5
6
7
8
9
10
11
a(() => {
b(() => {
c(() => {
d(() => {
e(() => {
f(...)
})
})
})
})
})

总结:

  • 优点:简单粗暴
  • 缺点:不利于维护,引发回调地狱

Promise

Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

详细了解Promise可以看我之前的文章:

异步编程怎么搞,Promise 知多少? | 灰太羊的羊村 (huitaiyang.top)

示例

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 这段代码使用了 Promise,它可以避免回调地狱的问题,使代码结构更加清晰和易于理解。
// 每个函数返回一个 Promise 对象,表示一个异步操作。
// 每个异步操作都在一定的时间后执行 resolve 方法来表示操作成功完成。

// 获取数据的异步操作函数
function getData() {
return new Promise((resolve, reject) => {
// 模拟获取数据的异步操作,1秒后执行 resolve 方法
setTimeout(() => {
console.log("获取数据");
// 调用 resolve 方法,传递数据给后续的 then 方法
resolve("data");
}, 1000);
});
}

// 处理数据的异步操作函数
function processData(data) {
return new Promise((resolve, reject) => {
// 模拟处理数据的异步操作,1秒后执行 resolve 方法
setTimeout(() => {
console.log("处理数据" + data);
// 将处理后的数据传递给后续的 then 方法
resolve(data.toUpperCase());
}, 1000);
});
}

// 显示数据的异步操作函数
function displayData(data) {
return new Promise((resolve, reject) => {
// 模拟显示数据的异步操作,1秒后执行 resolve 方法
setTimeout(() => {
console.log("显示数据" + data);
// 调用 resolve 方法,表示操作完成
resolve();
}, 1000);
});
}

// 使用 Promise 链式调用异步操作
getData()
.then(data => processData(data)) // 获取数据后处理数据
.then(data => displayData(data)) // 处理数据后显示数据
.then(() => console.log("操作完成")); // 最后输出 "操作完成"

总结

  • 优点:解决了回调地狱;catch()方法处理错误;支持链式调用then()
  • 缺点:一旦创建无法取消,浪费资源;错误处理不够灵活

Generator函数

Generator 函数是 ES6 提供的一种异步编程解决方案。

Generator 函数与普通函数不同之处在于:

  • function与函数名之间有个*
  • Generator 函数调用得到一个生成器对象,具有可迭代属性调用next() 方法继续往后执行,碰到yield就暂停

详细了解Generator可以看我之前的文章:

听说你还没听过 ES6 的 Generator 函数? | 灰太羊的羊村 (huitaiyang.top)

示例

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 定义一个异步函数getData,模拟异步获取数据的过程
function getData(){
return new Promise((resolve, reject) => {
// 使用setTimeout模拟异步操作的延迟
setTimeout(() => {
// 1秒后,控制台输出"获取数据"
console.log("获取数据");
// 使用resolve函数来解决Promise,传递模拟获取到的数据"data"
resolve("data");
}, 1000);
});
}

// 定义一个异步函数processData,模拟异步处理数据的过程
function processData(data){
return new Promise((resolve, reject) => {
setTimeout(() => {
// 1秒后,控制台输出处理的数据,这里将获取到的数据转换为大写
console.log("处理数据" + data);
// 使用resolve函数来解决Promise,传递处理后的数据
resolve(data.toUpperCase());
}, 1000);
});
}

// 定义一个异步函数displayData,模拟异步显示数据的过程
function displayData(data){
return new Promise((resolve, reject) => {
setTimeout(() => {
// 1秒后,控制台输出显示的数据
console.log("显示数据" + data);
// 使用resolve函数来解决Promise,表示显示操作完成
resolve();
}, 1000);
});
}

// 定义一个生成器函数gen,它按顺序执行getData, processData, displayData
function* gen(){
// 使用yield关键字等待getData函数的Promise解决
const data = yield getData();
// 使用yield关键字等待processData函数的Promise解决
const processedData = yield processData(data);
// 使用yield关键字等待displayData函数的Promise解决
return displayData(processedData);
}

// 定义一个函数run,它接受一个生成器作为参数,并运行它
function run(generator){
// 创建生成器的一个实例
const g = generator();
// 定义next函数,它负责管理生成器的执行流程
function next(data){
// 使用g.next(data)来让生成器执行到下一个yield语句
const result = g.next(data);
// 如果生成器已经运行完成(即result.done为true),则结束
if(result.done) return;
// 如果yield返回的是一个Promise,则等待它解决
result.value.then(data => {
// 当Promise解决后,递归调用next函数,传入Promise解决的值
next(data);
});
}
// 启动生成器,不传递任何参数
next();
}

// 运行生成器函数
run(gen);

总结

  • 优点:可以分段执行,可以暂停;可以控制每个阶段的返回值;可以知道是否执行完毕;借助 co 模块处理异步
  • 缺点:语法复杂,学习成本高;使用迭代器执行,调试困难

async/await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。一句话,async 函数是 Generator 函数的语法糖。

async 用于声明一个 function 是异步的,await 用于等待一个异步方法执行完成

  • async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 PromIse.resolve() 封装成Promise对象返回
  • await 只能在 async 函数中使用
  • await 后面不是Promise对象,直接执行
  • await 后面是Promise对象会阻塞后面的代码,Promise对象 resolve,然后得到 ****resolve 的值,作为 await 表达式的运算结果

详细了解async/await可以看我之前的文章:

现代化的异步编程方式 ——async /await | 灰太羊的羊村 (huitaiyang.top)

示例

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 定义一个异步函数getData,模拟异步获取数据的过程
function getData(){
return new Promise((resolve, reject) => {
// 使用setTimeout模拟异步操作的延迟
setTimeout(() => {
// 1秒后,控制台输出"获取数据"
console.log("获取数据");
// 使用resolve函数来解决Promise,传递模拟获取到的数据"data"
resolve("data");
}, 1000);
});
}

// 定义一个异步函数processData,模拟异步处理数据的过程
function processData(data){
return new Promise((resolve, reject) => {
setTimeout(() => {
// 1秒后,控制台输出处理的数据,这里将获取到的数据转换为大写
console.log("处理数据" + data);
// 使用resolve函数来解决Promise,传递处理后的数据
resolve(data.toUpperCase());
}, 1000);
});
}

// 定义一个异步函数displayData,模拟异步显示数据的过程
function displayData(data){
return new Promise((resolve, reject) => {
setTimeout(() => {
// 1秒后,控制台输出显示的数据
console.log("显示数据" + data);
// 使用resolve函数来解决Promise,函数来解决Promise,表示显示操作完成
resolve();
}, 1000);
});
}

// 定义一个async函数main,使用async/await来顺序执行异步操作
async function main(){
try {
// 使用await等待getData的Promise解决,输出"获取数据",然后获取data
const data = await getData();
// 使用await等待processData的Promise解决,输出"处理数据data"
const processedData = await processData(data);
// 使用await等待displayData的Promise解决,输出"显示数据DATA"
await displayData(processedData);
// 所有异步操作完成后输出"操作完成"
console.log("操作完成");
} catch(error) {
// 如果有错误,捕获并输出错误信息
console.log(error);
}
}

// 在main函数调用之前输出,因为main是异步的,所以这部分代码会立即执行
console.log("我的位置在main之前");

// 第一次调用main函数,由于是async函数,返回一个Promise对象(Promise是同步的,async函数会返回一个Promise对象)
// 这个Promise对象会在所有await的异步操作完成后解决
main(); // 第一个main调用开始

// 由于main函数是异步的,JavaScript引擎会继续执行下面的代码
// 这意味着第二个main调用会在第一个main调用的异步操作开始之前就已经开始执行
console.log("我的位置在main之后");

// 第二次调用main函数,同样返回一个Promise对象
// 这个Promise对象也会在所有await的异步操作完成后解决
main(); // 第二个main调用开始

// 由于两个main函数调用都是异步的,它们内部的console.log语句会按照它们内部逻辑的顺序执行
// 这意味着无论main被调用了多少次,getData、processData和displayData内的console.log语句
// 都会按照它们被await的顺序执行,而不是按照main函数的调用顺序执行
// 也就是说,每个main函数调用内部的异步操作都是独立执行的,并且它们的执行顺序是固定的

// 预期输出结果,注意两个main函数调用的异步操作是并行执行的,但它们的输出顺序是固定的:
// 我的位置在main之前
// 我的位置在main之后
// 获取数据(第一个main调用)
// 获取数据(第二个main调用)
// 处理数据data(第一个main调用)
// 处理数据data(第二个main调用)
// 显示数据DATA(第一个main调用)
// 显示数据DATA(第二个main调用)
// 操作完成(第一个main调用)
// 操作完成(第二个main调用)

总结

  • 优点:代码结构清晰,十分优雅
  • 缺点:没有错误捕获机制,只能使用 try/catch;滥用await会导致性能问题(阻塞)