异步编程,就是通过利用客户端环境的 Event-Loop 机制,去异步地执行某些代码
一些自己之前容易乱的关系
异步函数 = 异步操作,异步函数中包含回调函数做参数。回调函数只是一个普通函数。异步函数可以写成Promise形式
以前,异步编程的方法,大概有下面四种。
回调函数
事件监听
Promise 对象
回调函数
回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。英语名为 callback。
例如fs的读取文件
1 | fs.readFile('/etc/passwd', function (err, data) { |
readFile 函数的第二个参数,就是回调函数。第一个参数是错误对象err
Promise
当多个函数嵌套的时候,会出现“回调地狱”,代码难以理解。于是出现了Promise解决这个问题
Promise其实只是异步函数的一种新写法,允许将回调函数的横向加载,改成纵向加载
1 | const readFile = require('fs-readfile-promise') |
第一种方式是使用了 fs-readfile-promise 模块,它的作用就是返回一个 Promise 版本的 readFile 函数;第二种方法能把异步函数进行promisify处理赋值给一个变量。Promise 提供 then 方法加载回调函数,catch方法捕捉执行过程中抛出的错误
Promise对象
Promise对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态
Promise对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved
(已定型)
Promise实例
Promise实例的生成方式
- 用new生成
- promisify
- 直接require一些模块
监听结果的 Promise.prototype.then()
普通写法
1 | // 普通写法。先定义一个加载图片的函数 |
用Promise的写法
1 | function loadImg(src) { |
捕获异常的Promise.prototype.catch()
虽然then的第二个参数可以是reject函数,不过后来大家都默认then只接收一个参数,最后统一用catch捕获异常
1 | result.then(function (img) { |
执行顺序与串联
首先,Promise 新建后,里面的代码就会立即执行
执行顺序
1 | result.then(function (img) { |
也可以这么写
1 | console.log(1) |
PS:Promise实例中的代码如果不涉及费时的操作,它的.then()执行顺序是比setTimeout第二个参数的为0的代码要早的
串联
一般用于Ajax请求,这里图片做演示
多个Promise实例要串联的时候,需要在.then()中return 第二个实例
1 | var src = 'http://img1.qunarzz.com/piao/fusion/1804/ff/fdf170ee89594b02.png' |
Promise.all() & Promise.race()
这两个方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。都接收一个promise对象的数组。all()是待全部完成之后,统一执行success,race()是只要有一个完成,就执行success
1 | var src = 'http://img1.qunarzz.com/piao/fusion/1804/ff/fdf170ee89594b02.png' |
执行结果为:
Generator 函数
简介
Generator 函数是一个状态机,封装了多个内部状态。形式上,有两个特征,一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。一定要return,否则返回的对象的value属性值为undefined
调用 Generator 函数,会返回一个内部指针,而不是返回结果
用next方法,会移动内部指针(即执行异步任务的第一段),指向下一个遇到的 yield 语句
每次调用 next 方法,会返回一个对象,表示当前阶段的信息。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段
1 | function* gen(x){ |
当**next()**传入参数的时候,会被当作上个阶段异步任务的返回结果,此阶段的value就等于这个参数(一般用于yield用完了的时候)
【next()本来就有打印的功能?】
1 | function* gen(x){ |
yield表达式只能用在 Generator 函数里面,用在其他地方都会报错
在 Generator 函数里,yield表达式如果用在另一个表达式之中,必须放在圆括号里面
虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。所以有了以下两种方案:
Thunk 函数 + Generator方案
Thunk 函数是自动执行 Generator 函数的一种方法。是一个实现“传名调用”的临时函数,把这个临时函数作为参数传入真正的函数即可
JavaScript 语言是传值调用。JS中的Thunk替换的是多参数函数而不是表达式,将其替换成一个只接受回调函数作为参数的单参数函数。
单参数版本,就叫做 Thunk 函数
使用方法
Thunkify 模块
Thunk 函数现在可以用于 Generator 函数的自动流程管理
co 函数库 + Generator方案
co 模块用于 Generator 函数的自动执行。co函数返回一个Promise对象,因此可以用then方法添加回调函数。
async / await
与Generator比
异步编程的最高境界,就是根本不用关心它是不是异步。很多人认为Async就是异步操作的终极解决方案
如果认真看完两种自动化的 Generator 的方法 ,会有一种蛋疼的感觉(因为有这种感觉所以这两种方案没有仔细总结在博客里)。Generator 设计的初衷就是为了流程控制,而我们却给它加上了自动的流程机。还不如使用 promise.then()。Async 函数作为自带流程机的 Generator 语法糖,就完美得解决了这个矛盾
async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已
Generator版:
1 | var fs = require('fs'); |
async 函数版本
1 | var asyncReadFile = async function (){ |
与Promise比
相比Promise,.then()只是将callback拆分了,实际代码的语义化,执行顺序仍旧不清晰。async/await才是最直接的同步写法。解决JS单线程造成的编写的代码视觉顺序和实际执行顺序不同的问题
async / await用法总结
例如可以把Promise章节的代码改写为
1 | function loadImg(src) { |
async:
- 用于定义异步函数,函数可以返回一个 Promise 对象(所以函数执行的时候可以使用 then 方法添加回调函数)
- async函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句
- async函数中的forEach如果参数函数也是async函数的话,会报错,因为forEach内循环的async函数其实是并发的,而不是先后顺序的。正确的写法是采用 for 循环,再在里面写async函数。By the way,确实希望多个请求并发执行,可以使用 Promise.all 方法await:
1
2
3
4
5
6
7async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
} - 用于需要等待一段时间才能出结果的情况(eg. 将异步函数的执行结果赋值给变量),await后面必须跟一个Promise实例(或说异步函数)
- await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中(或更实用的写法,在后面直接加.catch()的写法)
1
2
3
4
5async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
});
} - await 命令只能用在 async 函数中,如果用在普通函数中,就会报错。PS: 其实await也能作为某些异步函数的运行前缀,例如用puppteer做爬虫的时候可以写
await page.focus('#kw')
,但自己写的async函数似乎不可以,还没明白
执行顺序
1 | const page = { |
参考资料
ECMAScript6标准入门
阮一峰博客-《深入掌握 ECMAScript 6 异步编程》系列文章
慕课网-揭秘一线互联网企业前端JavaScript高级面试