
大家好,我是小江同学,本文将会带你理解和感受 Generator 函数的异步应用
我们先引出一个非常常见的场景:对服务器端返回的数据进行操作
与服务器端交互的过程是一个异步操作
如果按照正常的代码编写的话,你可能会写出这样的代码
我也不知道打的什么,大概意思就是异步请求结果返回赋值给 data 然后输出,
let data = ajax("http://127.0.0.1",ab) //随便写的
console.log(data)虽然整个思路看起来没什么毛病,对吧。但是它就是不行的,获取数据是异步的,也就是说请求数据的时候,输出已经执行了,这时候必然是undefined
那为什么它要这么做呢?
JavaScript 是一门单线程的语言,如果没有了异步执行,你想想会怎么样
就像逛街一样,你非要跟着前面的人走,它走了你才走,它停下了去买点东西,后面的人全部都停下来等它回来,那这会怎么办,很显然,路堵了!换到 JS 运行机制上来也是一样的,会阻塞代码运行。因此出现了“异步”的概念,接下来我们先了解一下异步的概念,以及传统方法是如何实现异步操作的
同步:任务会按顺序依次执行,当遇到大量耗时任务,后面的任务就会被延迟,这种延迟称为阻塞,阻塞会造成页面卡顿
异步:不会等待耗时任务,遇到异步任务就开启后立即执行下一个任务,耗时任务的后续逻辑通常通过回调函数来定义执行,代码执行顺序混乱
在 ES6 诞生之前,实现异步编程的方法有以下几种。
下面来先来回顾以下传统方法是如何实现异步编程的
回调函数可以理解为一件想要去做的事情,由调用者定义好函数,交给执行者在某个时机去执行,把需要执行的操作放在函数里,将函数传入给执行者执行
主要体现在,把任务的第二段写在一个函数里面,等到重新执行这个任务的时候,直接调用
那有人就会问了,第二段是指什么,我们再举一个例子,读取文件进行打印,这个操作肯定是异步的吧,那它怎么分两段呢?
按照逻辑来分,第一段是读取文件,第二段是打印文件,可以理解为第一段是请求数据,第二段是打印数据
阮老师的代码实例
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});在第一阶段执行结束后,会将结果返回给后面的函数作为参数,传入第二段
回调函数的使用场景:
采用回调函数的方法,本身是没有问题的,但是问题出现在多个回调函数的嵌套
想一想,我执行完执行你,你执行完执行他,他执行完又执行她…
是不是需要层层嵌套,那这样套娃式的操作显然不利于阅读
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});同时你也可以这样去思考一下,如果有其中一个代码需要修改,那它的上层回调和下层回调都要修改,这也叫做强耦合
耦合,藕断丝连,关联性很强的意思
这种场景也叫做“回调地狱”
而 Promise 对象的诞生就是为了解决这个问题,它采用了以一种全新的写法,链式调用
Promise 可以用来表示一个异步任务执行的状态,有三种状态
它的写法如下
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 成功的解决了回调地狱的问题,它又不是异步编程的终极方案,那它又带来了什么问题呢?
catch但是这些都不是 Promise 的最大问题,它最大的问题是代码冗余,当执行逻辑变得复杂时,代码的语义会变得很不清楚,全是then
其实看过上一篇文章的读者们,看到这里应该对Generator实现异步编程有了一定的眉目,这里的then方法的作用,似乎next方法也能实现,启动,运行,传参,接下来我们来细说一下
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执行特点,可以先阅读一下
我们主意利用 yield 暂停生成器函数执行的特点,来使用生成器函数去实现异步编程,我们来看一个例子
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 的返回值,这样就将异步代码同步化了
在 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个阶段,这是一个不断进步的过程,一步步的解决前面方法所带来的问题。
next,同时搭配工具使用代码会十分的复杂generator + promise,无需手动调用,完美解决