前言
JavaScript 是一门单线程语言,即同一时间只能做一件事。但实际业务中,存在很多耗时的任务,如果按照同步任务的方式处理这些耗时操作,那么程序将在此中断,直到等待耗时任务完成才会继续向下执行,这样的体验太差了。
JavaScript 引擎对于这些耗时操作是通过异步来处理。但如何优雅组织异步代码,却是个困扰开发者多时的难题,本文主要是总结下学习异步编程的过程。
进化过程
一、嵌套回调
如果想要实现先请求 A 资源,再请求 B 资源,即后面的资源请求是依赖于前面的请求结果(串行),就需要下面的写法。
// 模拟 ajax 请求
function ajax(fn) {
setTimeout(fn, 20);
}
function log(text) {
console.log(text);
}
// 嵌套回调
ajax(() => {
log("获取A资源");
ajax(() => {
log("获取B资源");
})
})
可以发现,如果还需要请求 C、D、E 甚至更多相互依赖的请求,那么将形成回调地狱(callback hell)了,这是不利于开发者阅读和维护的。
二、promise
2.1 promise 是什么?
A promise represents the eventual result of an asynchronous operation。 -- Promise/A+
promise 表示一个异步操作的最终结果。
一个 Promise 是对一个异步操作的封装,promise 就像一个状态机,内部有 3 种状态:
- pending: 挂起,正在执行
- fulfilled(resolved):异步操作已完成,并且成功
- rejected:异步操作失败,原因可能是发生了错误或其他理由。
当异步操作执行完成之后,promise 的状态由 pending 变成 resolved 或者 rejected,并且这种状态变化是不可逆的。
2.2 then 方法
通过 then 方法获取异步操作的结果。 then 方法接收两个匿名函数作为参数,分别代表 onResolved 和 onRejected 函数。
then 方法默认返回一个新的 promise 对象,所以可以多次调用 then 方法。
2.3 promise 链返回值
链中的 promise 能够向下一个 promise 传递数据:
- 如果then中的回调函数返回一个值,那么then返回的Promise将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
- 如果then中的回调函数没有返回值,那么then返回的Promise将会成为接受状态,并且该接受状态的回调函数的参数值为 undefined。
- 如果then中的回调函数抛出一个错误,那么then返回的Promise将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
- 如果then中的回调函数返回一个已经是接受状态的Promise,那么then返回的Promise也会成为接受状态,并且将那个Promise的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
- 如果then中的回调函数返回一个已经是拒绝状态的Promise,那么then返回的Promise也会成为拒绝状态,并且将那个Promise的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
- 如果then中的回调函数返回一个未定状态(pending)的Promise,那么then返回Promise的状态也是未定的,并且它的终态与那个Promise的终态相同;同时,它变为终态时调用的回调函数参数与那个Promise变为终态时的回调函数的参数是相同的。
通过 promise 的链式操作完成串行请求:
// 模拟 ajax 请求
function ajax() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 20);
})
}
function log(text) {
console.log(text);
}
// 串行请求
ajax().then(() => {
log("获取A资源");
return ajax()
}).then(() => {
log("获取B资源");
}).catch((err) => {
console.log(err);
})
三、generator
迭代器
为什么有迭代器的出现?
迭代器的出现是为了解决 for 循环的问题:嵌套增加复杂度,追踪多个变量。
迭代器是什么?
迭代器是带有特殊接口的对象。
迭代器带有 next() 方法,并返回一个包含两个属性的结果对象。
- value: 代表下一个位置的值
- done:代表是否有更多迭代,没有更多迭代时为 true。
生成器
Generator 本质上是一个函数,它的最大特点就是可以被中断,然后恢复执行。
和普通函数的区别:
- 在 function 关键字和方法名中间有个星号 (*)
- 方法体中使用 "yield" 关键字
执行顺序:
- 调用
say()
函数后,该函数并未立即执行,函数的返回值是一个迭代器。 - 依次调用迭代器的 next() 方法
3.1 关键字说明
*
表示该函数是个生成器。
yield
关键字指定迭代器调用 next() 时按顺序返回的值。
yield 关键字本身不产生返回值。即:next() 返回的是 yield 关键字后面的表达式的值。
next 可以接受参数代表可以从外部传一个值到 Generator 函数内部。
3.2 可迭代类型
可迭代类型指那些包含 Symbol.iterator
属性的对象。
在 ES6 中,所有集合对象 (数组、set、map) 与字符串都是可迭代类型。
3.3 用 Generator 组织异步方法
Generator 函数处理异步操作的核心思想:先将函数暂停在某处,然后拿到异步操作的结果,再把这个结果传到方法体内。
// 模拟 ajax 请求
function ajax(val) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`请求${val}资源`);
resolve(val);
}, 20);
})
}
function *Generator() {
let first = yield ajax('A');
let two = yield ajax(first);
}
const gen = Generator();
const gen1 = gen.next();
console.log('第1次执行next()',gen1);
gen1.value.then((res1) => {
const gen2 = gen.next('B');
console.log('第2次执行next()',gen2);
gen2.value.then(res2 => {
console.log('第3次执行next()',gen.next());
})
})
在上面的例子中,通过手工一步一步地调用 next() 方法,完成了两次资源请求,下面将介绍如何自动执行 next() 。
3.4 自动执行器/任务运行器
简单的任务运行器:
function run(gen) {
let task = gen();
let result = task.next();
function step() {
if (!result.done) {
result = task.next();
step();
}
}
step();
}
基于 Promise 的执行器:(yield 后面返回的是 Promise)
function run(gen) {
let task = gen();
let result = task.next();
function step() {
if (!result.done) {
result.value.then((data) => {
result = task.next();
step(data);
})
} else {
return result.value;
}
}
step();
}
运行测试:
// 模拟 ajax 请求
function ajax(val) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`请求${val}资源`);
resolve(val);
}, 20);
})
}
function *Generator() {
let first = yield ajax('A');
let two = yield ajax('B');
}
run(Generator);
3.5 生成器的 return()
方法
return() 方法后面的 next 不会执行
四、async/await
async 函数返回一个 Promise 对象,如果 return 关键字后面不是一个 Promise,那么默认调用 promise.resolve 方法进行转换。
async 函数的执行过程:
- 在 async 函数开始执行时,会自动生成一个 Promise 对象。
- 当方法体开始执行后,如果遇到 return 关键字或者 throw 关键字,执行会立刻退出,如果遇到 await 关键字则会暂停执行,异步操作结束后,恢复执行。
- 执行完毕,返回一个 Promise
参考
developer.mozilla Promise.then()
2.6. 专栏: 每次调用then都会返回一个新创建的promise对象
Promise 与异步编程
迭代器与生成器