引言
我们先引出一个非常常见的场景:对服务器端返回的数据进行操作
与服务器端交互的过程是一个异步操作
如果按照正常的代码编写的话,你可能会写出这样的代码
我也不知道打的什么,大概意思就是异步请求结果返回赋值给 data
然后输出,
虽然整个思路看起来没什么毛病,对吧。但是它就是不行的,获取数据是异步的,也就是说请求数据的时候,输出已经执行了,这时候必然是 undefined
那为什么它要这么做呢?
JavaScript 是一门单线程的语言,如果没有了异步执行,你想想会怎么样
就像逛街一样,你非要跟着前面的人走,它走了你才走,它停下了去买点东西,后面的人全部都停下来等它回来,那这会怎么办,很显然,路堵了!换到 JS 运行机制上来也是一样的,会阻塞代码运行。因此出现了“异步”的概念,接下来我们先了解一下异步的概念,以及传统方法是如何实现异步操作的
什么是同步、异步
同步:任务会按顺序依次执行,当遇到大量耗时任务,后面的任务就会被延迟,这种延迟称为阻塞,阻塞会造成页面卡顿
异步:不会等待耗时任务,遇到异步任务就开启后立即执行下一个任务,耗时任务的后续逻辑通常通过回调函数来定义执行,代码执行顺序混乱
实现异步编程
在 ES6 诞生之前,实现异步编程的方法有以下几种。
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
下面来先来回顾以下传统方法是如何实现异步编程的
Callback
回调函数可以理解为一件想要去做的事情,由调用者定义好函数,交给执行者在某个时机去执行,把需要执行的操作放在函数里,将函数传入给执行者执行
主要体现在,把任务的第二段写在一个函数里面,等到重新执行这个任务的时候,直接调用
那有人就会问了,第二段是指什么,我们再举一个例子,读取文件进行打印,这个操作肯定是异步的吧,那它怎么分两段呢?
按照逻辑来分,第一段是读取文件,第二段是打印文件,可以理解为第一段是请求数据,第二段是打印数据
阮老师的代码实例
fs.readFile('/etc/passwd', 'utf-8', function (err, data) { if (err) throw err; console.log(data); });
在第一阶段执行结束后,会将结果返回给后面的函数作为参数,传入第二段
回调函数的使用场景:
- 事件回调
- 定时器的回调
- Ajax 请求
Promise
采用回调函数的方法,本身是没有问题的,但是问题出现在多个回调函数的嵌套
想一想,我执行完执行你,你执行完执行他,他执行完又执行她…
是不是需要层层嵌套,那这样套娃式的操作显然不利于阅读
fs.readFile(fileA, 'utf-8', function (err, data) { fs.readFile(fileB, 'utf-8', function (err, data) { // ... }); });
同时你也可以这样去思考一下,如果有其中一个代码需要修改,那它的上层回调和下层回调都要修改,这也叫做强耦合
耦合,藕断丝连,关联性很强的意思
这种场景也叫做“回调地狱”
而 Promise 对象的诞生就是为了解决这个问题,它采用了以一种全新的写法,链式调用
Promise 可以用来表示一个异步任务执行的状态,有三种状态
- Pending:开始是等待状态
- Fulfilled:成功的状态,会触发 onFulfilled
- Rejected:失败的状态,会触发 onRejected
它的写法如下
const promise = new Promise(function(resolve, reject) { // 同步代码 // resolve执行表示异步任务成功 // reject执行表示异步任务失败 resolve(100) // reject(new Error('reject')) // 失败 }) promise.then(function() { // 成功的回调 }, function () { // 失败的回调 })
Promise 对象调用 then 方法后会返回一个新的 Promise 对象,这个新的 Promise 对象可以继续调用 then 实现链式调用
后面的 then 方法是为上一个 then 返回的 Promise 对象注册回调
前一个 then 方法中回调函数的返回值会作为后面 then 方法回调的参数
链式调用的目的是为了解决回调函数嵌套的问题
关于 Promise 的更多细节这里就不多说了,下一篇写吧
Promise 成功的解决了回调地狱的问题,它又不是异步编程的终极方案,那它又带来了什么问题呢?
- 无法取消 Promise
- 当处于 pending 状态时是,无法得知进展
- 错误不能被
catch
但是这些都不是 Promise 的最大问题,它最大的问题是代码冗余,当执行逻辑变得复杂时,代码的语义会变得很不清楚,全是 then
其实看过上一篇文章的读者们,看到这里应该对 Generator 实现异步编程有了一定的眉目,这里的 then 方法的作用,似乎 next 方法也能实现,启动,运行,传参,接下来我们来细说一下
Generator
Generator 函数可以暂停执行和恢复执行, 这是它能封装异步任务的根本原因。
除此之外,它还有两个特征,使它可以作为异步编程的完美解决方案。
- 函数体内外的数据传递
- 错误处理机制
数据传递
在学习它是如何实现异步编程的之前,我们先回顾一下 Generator 函数的执行方法
// 声明Generator函数 function* gen(x){ let y = yield x + 2 return y } // 遍历器对象 let g = gen() // 第一次调用next方法 g.next() // { value: 3, done: false } // 第二次调用 传递参数 g.next(2) // { value: 2, done: true }
首先执行 gen
函数,获得遍历器对象,此时函数并不会执行,当调用遍历器对象的 next
方法时,执行到第一个 yield
语句,以此类推
也就是说只有调用 next
方法,才会往下执行
同时在上面的代码中,我们可以通过 value
来获取返回的值,通过给 next
方法传递参数来实现数据交换
错误处理机制
Generator 函数内部可以部署错误处理代码,捕获函数体外抛出的错误
function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); g.next(); g.throw('出错了');
或许会有人不理解为什么内部的 catch
可以捕获外部的错误?
原因是我们通过 g.throw
来抛错误,其实是将错误抛入了生成器,毕竟我们是在 p
上来调用 throw
方法
实现异步编程
我们主意利用 yield
暂停生成器函数执行的特点,来使用生成器函数去实现异步编程,我们来看一个例子
Generator + Promise
function * main () { const user = yield ajax('/api/usrs.json') console.log(user) } const g = main() const result = g.next() result.value.then(data => { g.next(data) })
首先我们定义一个生成器函数 main ,然后在这个函数内部使用 yield 去返回一个 ajax 的调用,也就是返回了一个 Promise 对象。
然后去接收 yield 语句的返回值,也就是第二个 next 方法的参数。
我们可以在外界去调用生成器函数得到它的迭代器对象,然后调用这个对象的 next 方法,这样 main 函数就会执行到第一个 yield 的位置,也就是会执行到 ajax 的调用,这里 next 方法返回对象的 value 值就是 ajax 返回的 Promise 对象
因此我们可以通过 then 方法去指定这个 Promise 的回调,在这个 Promise 回调中我们就可以拿到这个 Promise 的执行结果 data,这时候我们就可以通过再调用一次 next 方法,把我们得到的 data 数据传递出去,这样 main 函数就可以继续执行了,而 data 就会被当作 yield 表达式的返回值赋值给 user 使用了
异步迭代生成器
如果上面的 generator + promise
能够理解的话,这个就更简单了,就是单纯的使用 generator
实现的异步编程
function foo(x, y) { ajax("1.2.34.2", function(err,data) { if(err) { it.throw(err) }else { it.next(data) } }) } function *main() { let text = yield foo(11, 31) console.log( text ) } const it = main() it.next()
在上面的代码中就是一个简单的例子,虽然看起来要比回调函数实现的方法要多很多,但是你会发现代码逻辑要好非常多
这里面最关键的代码
let text = yield foo(11,31) console.log( text )
这个在上一 part
我们已经解释过了
在 yield foo(11, 31)
中,首先调用 foo(11, 31)
没有返回值,发送请求获取数据,请求成功,调用 it.next(data)
,这样就将 data
作为上一个 yield
的返回值,这样就将异步代码同步化了
async await
在 Generator 中还有很多的内容,工具,并发,委托等等让生成器变得十分强大,但是这样也让手写一个执行器函数越来越麻烦,所以在 ES7 中又新增了 async await 这对关键字,它使用起来会更加的方便。
async 函数就是生成器函数的一个语法糖。
在语法上跟 Generator 函数非常类似,只要把生成器函数修改为 async 关键字修饰的函数,把 yield 修改为 await 就可以了。并且可以直接在外面调用这个函数,执行这个函数的话,内部这个执行过程会跟 Generator 函数会是完全一样的
相比于 Generator 函数 async 函数最大的好处就是不需要去配合一些工具去使用,类似于 Co
、runner
之类的
原因在于它是语言层面的标准异步编程,同时 async
函数可以返回一个 Promise 对象,这样也有利于控制代码。
需要注意的是,await
只能出现在 async
函数体中
//将生成器函数改为 async 修饰的函数 async function main() { try { // 将 yield 换成 await const a = await ajax('xxxx') console.log(a) const b = await ajax('xxx') console.log(b) const c = await ajax('xx') console.log(c) } catch (e) { console.log(e) } } // 返回一个Promise对象 const promise = main()
从上面的代码我们也可以知道,我们并不需要像 Generator
一样通过 next
来控制执行
async await
是 Generator 和 Promise 的组合,解决了先前方法留下的问题,这应该是目前处理异步的最优方案了
总结
本文写了异步编程的4个阶段,这是一个不断进步的过程,一步步的解决前面方法所带来的问题。
1.回调函数:导致了两个问题
1.缺乏顺序性:回调地狱,造成代码难以维护,阅读性差等问题
2.缺乏可信任性:控制反转,导致代码可能会执行错误
2.promise:解决了可信任性的问题,但是代码过于冗余
3.Generator:解决了顺序性的问题但是需要手动控制 next,同时搭配工具使用代码会十分的复杂
4.async await:结合了 generator + promise,无需手动调用,完美解决