JavaScript 异步编程总结
同步 & 异步的概念
同步 (synchronization),简单来说,就是顺序执行,指的是同一时间只能做一件事情,只有目前正在执行的事情做完之后,才能做下一件事情。比如咱们去火车站买票,假设窗口只有 1 个,那么同一时间只能处理一个人的购票业务,其余的需要进行排队。这种 one by one 的动作就是同步。同步操作的优点在于做任何事情都是依次执行,井然有序,不会存在大家同时抢一个资源的问题。同步操作的缺点在于会阻塞后续代码的执行。如果当前执行的任务需要花费很长的时间,那么后面的程序就只能一直等待。从而影响效率,对应到前端页面的展示来说,有可能会造成页面渲染的阻塞,大大影响用户体验。
异步 (Asynchronization),指的是当前代码的执行不影响后面代码的执行。当程序运行到异步的代码时,会将该异步的代码作为任务放进任务队列,而不是推入主线程的调用栈。等主线程执行完之后,再去任务队列里执行对应的任务即可。因此,异步操作的优点就是:不会阻塞后续代码的执行。
JS 中异步的应用场景
- 定时任务:setTimeout、setInterval
- 网络请求:ajax 请求、动态创建 img 标签的加载
- 事件监听器:addEventListener
实现异步的四种方法
1、回调函数
在微任务队列出现之前,JS 实现异步的主要方式就是通过回调函数。以一个简易版的 ajax 请求为例,代码结构如下所示:
function ajax(obj) {
let default = {
url: '...',
type: 'GET',
async: true,
contentType: 'application/json',
success:function(){}
};
for (let key in obj) {
defaultParam[key] = obj[key];
}
let xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
xhr.open(defaultParam.type, defaultParam.url+'?'+dataStr, defaultParam.async);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4){
if(xhr.status === 200){
let result = JSON.parse(xhr.responseText);
// 在此处调用回调函数
defaultParam.success(result);
}
}
}
}
在业务代码里可以这样调用 ajax 请求:
ajax({
url:'#',
type:GET,
success:function(e){
// 回调函数里就是对请求结果的处理
}
})
ajax 的 success 方法就是一个回调函数,回调函数中执行的是我们请求成功之后要做的进一步操作。这样就初步实现了异步,但是回调函数有一个非常严重的缺点,那就是回调地狱的问题。如果嵌套的层级很深的话,代码结构可能就会非常难以阅读。
2、Promise
Promise 作为典型的微任务之一,它的出现可以使 JS 达到异步执行的效果。一个 Promise 函数的结构如下列代码如下:
const promise = new Promise((resolve, reject) => {
resolve('a');
});
promise
.then((arg) => { console.log(`执行resolve,参数是${arg}`) })
.catch((arg) => { console.log(`执行reject,参数是${arg}`) })
.finally(() => { console.log('结束promise') });
如果,我们需要嵌套执行异步代码,相比于回调函数来说,Promise 的执行方式如下列代码所示:
const promise = new Promise((resolve, reject) => {
resolve(1);
});
promise.then((value) => {
console.log(value);
return value * 2;
}).then((value) => {
console.log(value);
return value * 2;
}).then((value) => {
console.log(value);
}).catch((err) => {
console.log(err);
});
即通过 then 来实现多级嵌套 (链式调用)。
每个 Promise 都会经历的生命周期是:
- 进行中(pending) - 此时代码执行尚未结束,所以也叫未处理的(unsettled)
- 已处理(settled) - 异步代码已执行结束 已处理的代码会进入两种状态中的一种:
- 已完成(fulfilled) - 表明异步代码执行成功,由 resolve() 触发
- 已拒绝(rejected)- 遇到错误,异步代码执行失败 ,由 reject() 触发
因此,pending,fulfilled, rejected 就是 Promise 中的三种状态,在 Promise 中,要么包含 resolve() 来表示 Promise 的状态为 fulfilled, 要么包含 reject() 来表示 Promise 的状态为 rejected。不然 Promise 就会一直处于 pending 的状态,直至程序崩溃。
除此之外,Promise 不仅很好的解决了链式调用的问题,它还有很多神奇的操作:
- Promise.all (promises):接收一个包含多个 Promise 对象的数组,等待所有都完成时,返回存放它们结果的数组。如果任一被拒绝,则立即抛出错误,其他已完成的结果会被忽略
- Promise.allSettled(promises): 接收一个包含多个 Promise 对象的数组,等待所有都已完成或者已拒绝时,返回存放它们结果对象的数组。每个结果对象的结构为 {status:'fulfilled' // 或 'rejected', value // 或 reason}
- Promise.race(promises): 接收一个包含多个 Promise 对象的数组,等待第一个有结果(完成 / 拒绝)的 Promise,并把其 result/error 作为结果返回
function getPromises(){
return [
new Promise(((resolve, reject) => setTimeout(() => resolve(1), 1000))),
new Promise(((resolve, reject) => setTimeout(() => reject(new Error('2')), 2000))),
new Promise(((resolve, reject) => setTimeout(() => resolve(3), 3000))),
];
}
Promise.all(getPromises()).then(console.log);
Promise.allSettled(getPromises()).then(console.log);
Promise.race(getPromises()).then(console.log);
打印结果如下:

3、Generator
Generator 是 ES6 提出的一种异步编程的方案。因为手动创建一个 iterator 十分麻烦,因此 ES6 推出了 generator,用于更方便的创建 iterator。也就是说,Generator 就是一个返回值为 iterator 对象的函数。
iterator 是什么?
iterator 中文名叫迭代器。它为 js 中各种不同的数据结构 (Object、Array、Set、Map) 提供统一的访问机制。任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作。因此 iterator 也是一种对象,不过相比于普通对象来说,它有着专为迭代而设计的接口。
iterator 的作用:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- ES6 创造了一种新的遍历命令 for…of 循环,Iterator 接口主要供 for…of 消费
iterator 的结构: 它有 next 方法,该方法返回一个包含 value 和 done 两个属性的对象(我们假设叫 result)。value 是迭代的值,后者是表明迭代是否完成的标志。true 表示迭代完成,false 表示没有。iterator 内部有指向迭代位置的指针,每次调用 next,自动移动指针并返回相应的 result。
原生具备 iterator 接口的数据结构如下:
- Array
- Map
- Set
- String
- TypedArray
- 函数里的 arguments 对象
- NodeList 对象
这些数据结构都有一个 Symbol.iterator 属性,可以直接通过这个属性来直接创建一个迭代器。也就是说,Symbol.iterator 属性只是一个用来创建迭代器的接口,而不是一个迭代器,因为它不含遍历的部分。 使用 Symbol.iterator 接口生成 iterator 迭代器来遍历数组的过程为:
let arr = ['a','b','c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
for ... of 的循环内部实现机制其实就是 iterator,它首先调用被遍历集合对象的 Symbol.iterator 方法,该方法返回一个迭代器对象,迭代器对象是可以拥有.next() 方法的任何对象,然后在 for ... of 的每次循环中,都将调用该迭代器对象上的 .next() 方法。然后使用 for i of 打印出来的 i 也就是调用.next() 方法后得到的对象上的 value 属性。
对于原生不具备 iterator 接口的数据结构,比如 Object,我们可以采用自定义的方式来创建一个遍历器。
比如,我们可以自定义一个 iterator 来遍历对象:
let obj = {a: "hello", b: "world"};
// 自定义迭代器
function createIterator(items) {
let keyArr = Object.keys(items);
let i = 0;
return {
next: function () {
let done = (i >= keyArr.length);
let value = !done ? items[keyArr[i++]] : undefined;
return {
value: value,
done: done,
};
}
};
}
let iterator = createIterator(obj);
console.log(iterator.next()); // "{ value: 'hello', done: false }"
console.log(iterator.next()); // "{ value: 'world', done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
关于 Generator,通过一个例子来看看 Generator 的特征:
function* createIterator() {
yield 1;
yield 2;
yield 3;
}
// generators可以像正常函数一样被调用,不同的是会返回一个 iterator
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
Generator 函数是 ES6 提供的一种异步编程解决方案。形式上,Generator 函数是一个普通函数,但是有两个特征:
- function 关键字与函数名之间有一个星号
- 函数体内部使用 yield 语句,定义不同的内部状态
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。
打印 Generator 函数返回值的内容:

可以发现 generator 函数的返回值的原型链上确实有 iterator 对象该有的 next,这充分说明了 generator 的返回值是一个 iterator。除此之外还有函数该有的 return 方法和 throw 方法。
在普通函数中,我们想要一个函数最终的执行结果,一般都是 return 出来,或者以 return 作为结束函数的标准。运行函数时也不能被打断,期间也不能从外部再传入值到函数体内。但在 generator 中,就打破了这几点,所以 generator 和普通的函数完全不同。当以 function * 的方式声明了一个 Generator 生成器时,内部是可以有许多状态的,以 yield 进行断点间隔。期间我们执行调用这个生成的 Generator, 他会返回一个遍历器对象,用这个对象上的方法,实现获得一个 yield 后面输出的结果。
function* generator() {
yield 1
yield 2
};
let iterator = generator();
iterator.next() // {value: 1, done: false}
iterator.next() // {value: 2, done: false}
iterator.next() // {value: undefined, done: true}
yield 和 return 的区别:
- 都能返回紧跟在语句后面的那个表达式的值
- yield 相比于 return 来说,更像是一个断点。遇到 yield,函数暂停执行,下一次再从该位置继续向后执行,而 return 语句不具备位置记忆的功能。
- 一个函数里面,只能执行一个 return 语句,但是可以执行多次 yield 表达式。
- 正常函数只能返回一个值,因为只能执行一次 return;Generator 函数可以返回一系列的值,因为可以有任意多个 yield
语法注意点:
- yield 表达式只能用在 Generator 函数里面
- yield 表达式如果用在另一个表达式之中,必须放在圆括号里面
- yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
- 如果 return 语句后面还有 yield 表达式,那么后面的 yield 完全不生效
使用 Generator 的其余注意事项:
-
需要注意的是,yield 不能跨函数。并且 yield 需要和 * 配套使用,别处使用无效
function* createIterator(items) { items.forEach(function (item) { // 语法错误 yield item + 1; }); }
-
箭头函数不能用做 generator
-
因为 Generator 可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个 generator 就可以实现需要用面向对象才能实现的功能
-
Generator 还有另一个巨大的好处,就是把异步回调代码变成 “同步” 代码。这个在 ajax 请求中很有用,避免了回调地狱
4、async/await
async/await 是 ES7 提出的关于异步的终极解决方案。关于 async/await 是谁的语法糖这块有两个版本:
- async/await 是 Generator 的语法糖
- async/await 是 Promise 的语法糖
这两种说法都没有错。关于 async/await 是 Generator 的语法糖: 所谓 Generator 语法糖,表明的就是 aysnc/await 实现的就是 generator 实现的功能。但是 async/await 比 generator 要好用。因为 generator 执行 yield 设下的断点采用的方式就是不断的调用 iterator 方法,这是个手动调用的过程。针对 generator 的这个缺点,后面提出了 co 这个库函数来自动执行 next,相比于之前的方案,这种方式确实有了进步,但是仍然麻烦。而 async 配合 await 得到的就是断点执行后的结果。因此 async/await 比 generator 使用更普遍。
总结下来,async 函数对 Generator 函数的改进,主要体现在以下三点:
- 内置执行器:Generator 函数的执行必须靠执行器,因为不能一次性执行完成,所以之后才有了开源的 co 函数库。但是,async 函数和正常的函数一样执行,也不用 co 函数库,也不用使用 next 方法,而 async 函数自带执行器,会自动执行。
- 适用性更好:co 函数库有条件约束,yield 命令后面只能是 Thunk 函数或 Promise 对象,但是 async 函数的 await 关键词后面,可以不受约束。
- 可读性更好:async 和 await,比起使用 * 号和 yield,语义更清晰明了。
关于 async/await 是 Promise 的语法糖: 如果不使用 async/await 的话,Promise 就需要通过链式调用来依次执行 then 之后的代码:
function counter(n){
return new Promise((resolve, reject) => {
resolve(n + 1);
});
}
function adder(a, b){
return new Promise((resolve, reject) => {
resolve(a + b);
});
}
function delay(a){
return new Promise((resolve, reject) => {
setTimeout(() => resolve(a), 1000);
});
}
// 链式调用写法
function callAll(){
counter(1)
.then((val) => adder(val, 3))
.then((val) => delay(val))
.then(console.log);
}
callAll(); // 5
虽然相比于回调地狱来说,链式调用确实顺眼多了。但是其呈现仍然略繁琐了一些。而 async/await 的出现,就使得我们可以通过同步代码来达到异步的效果:
async function callAll(){
const count = await counter(1);
const sum = await adder(count + 3);
console.log(await delay(sum));
}
callAll(); // 5
由此可见,Promise 搭配 async/await 的使用才是正解。
总结
- Promise 让异步执行看起来更清晰明了,通过 then 让异步执行结果分离出来.
- async/await 其实是基于 Promise 的。async 函数其实是把 promise 包装了一下。使用 async 函数可以让代码简洁很多,不需要 promise 一样需要些 then,不需要写匿名函数处理 promise 的 resolve 值,也不需要定义多余的 data 变量,还避免了嵌套代码.
- async 函数是 Generator 函数的语法糖。async 函数的返回值是 promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。同时,我们还可以用 await 来替代 then 方法指定下一步的操作.
- Promise + async 的操作最为常见.
转载自:https://juejin.cn/post/7138750277020024839