javascript异步解决方案-Generator/async-await(三)

938 阅读7分钟

第二节中,已经了解了使用promise异步处理方法来改善callback-hell。但js的异步解决方法也不止步于此。Generator是否是个更优雅的解决异步的方法呢?

1. Generator简介

Generator函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同:

function* Hello() {
      yield 100
      yield (function () { return 200 })()
      return 300
}
var h = Hello()
console.log(h.next()) // {value: 100, done: false}
console.log(h.next()) // {value: 200, done: false}
console.log(h.next()) // {value: 300, done: true}
console.log(h.next()) // {value: undefined, done: true}

可以看到generator函数声明时在function和函数名之间有一个 * 号,函数体内部有 yield关键字。函数调用后不会立即执行,而是生成一个generator对象,通过generator对象的next方法执行内部代码,而next方法的返回值是{value: value,done: boolean} 的形式,直到遇到函数体内的return关键字,next的返回值变为{value: value,done: true},表示执行完毕。

同时也可以看到,generator函数相对于promise来讲,多了内部状态的控制。promise.then().then()这种链条反应是无法在内部暂停的。而generator函数本身就是暂停的,而执行要通过generator对象的next方法。

为何generator对象可以通过next方法来实现对流程的控制呢?很有必要了解下Iterator遍历器。

2. Iterator 遍历器

目前javascript表示集合的数据结构有四种: Array,Object, Set, Map。还可以组合使用他们定义更复杂的数据结构,比如Array数组的每个元素都是一个Object等。这样就需要一种统一的接口机制,来处理所有的数据结构。

var arr = [1,2,3,4,4]
for (var i = 0; i < arr.length; i++) {
    console.log(arr[i])  // 此方式来访问每项数据
}

var set = new Set(arr)
for (let item of set.values()) {
    console.log(item) // 此方式来访问每项数据
}

这个统一的机制就是Iterator。任何数据结构只要部署 Iterator 接口,就可以使用 for...of 的方式完成遍历操作。(for..of)是es6创建的一种新的遍历方式。

// array原生具有 Iterator接口 因此可以通过 for...of访问每个元素
var arr = [1,2,3,4]
for(let item of arr) {
    console.log('item:', item)
}

默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,可以进行如下访问:

let arr = [1,2,3]
console.log(arr[Symbol.iterator])  // ƒ values() { [native code] }
let gen = arr[Symbol.iterator]() // 生成遍历器对象
// 可以采用next方法进行访问
console.log(gen.next())  // {value: 1, done: false}
console.log(gen.next())  // {value: 2, done: false}
console.log(gen.next())  // {value: 3, done: false}
console.log(gen.next())  // {value: undefined, done: true}

let obj = {a: 1,b: 2}
console.log(obj[Symbol.iterator]) // undefined  对象原生没有部署Iterator接口

原生具备 Iterator 接口的数据结构如下: Array,Map,Set,String,TypedArray,arguments,NodeList对象。

到此可以简单对Iterator做个总结概括:

  1. 部署了Iterator的接口的数据结构,我们可以访问其Symbol.iterator属性找到iterator遍历器方法,调用这个方法可以生成对应的遍历器对象,通过遍历器对象的next方法可以逐一访问其数据结构的每个元素;也可以通过for...of对这个数据结构进行遍历;
  2. 没有部署Iterator接口的数据结构比如对象,我们可以手动进行部署,在此不做重点讨论。

到这里,对于generator函数生成的generator对象为何能next应该有了一个更加清晰的认识,Generator返回的也是Iterator对象。再看上面那个例子

function* Hello() {
      yield 100
      yield (function () { return 200 })()
      return 300
}
var hello = Hello()
for (let item of hello) {
    console.log(item)   // 100 200
}
** 注意next方法和for...of不要混合使用

3. Generator函数 next和yield参数传递

上个小结中,已经了解到调用Generator函数其实是生成了一个Iterator对象,所以可以通过next方法进行遍历。但是两者又有区别: 对于数据结构上部署的Iterator函数生成的Iterator对象调用next方法访问到的值是数据结构的元素值。对于Generator函数生成的Generator对象(本质是Iterator对象)调用next方法访问到的是 generator函数中 yield 关键字后面表达式的 值。

如下看参数传递:

function* G() {
    const a = yield 100
    console.log('a', a)  // a aaa
    const b = yield 200
    console.log('b', b)  // b bbb
    const c = yield 300
    console.log('c', c)  // c ccc
}
const g = G()
console.log(g.next())  // {value: 100, done: false}
console.log(g.next('aaa'))  // {value: 200, done: false}
console.log(g.next('bbb'))  // {value: 300, done: false}
console.log(g.next('ccc'))  // {value: undefined, done: true}

generator处理异步来讲比promise.then方式有两个优点: 1. 可控制的节点 2. 更优雅的写法(后面会提到)

generator函数内部代码的执行依赖于生成的generator对象对于next方法的调用。细细的品味就像一个内外交互的模式。把它想象成一个模型或许更容易理解:好比炖汤。promise的炖法就是把所有的原料都加进去,按下开始键,中途不会打开盖子观察锅内的情况,直至结束,至于炖出来的味道看天意! generator的炖法好比炖的过程中可以打开盖子,观察内部汤色及沸腾情况,据此来决定是否放新材料和调料,味道相对来说可控!

generator函数的参数传递因此也有两个方向:

  1. 从generator函数内部传到外部 next函数的返回值

以上案例中g.next()的值就是从函数内部传递到外部的值,传递的是内部 yield 关键字后表达式的值

  1. 通过next函数的参数传递到generator函数的内部

以上案例 g.next('aaa') 参数 'aaa'就是传递到generator函数内部的值,传给对应的当前next的 yiled 关键字 的上一个 yiled 关键字 ‘=‘ 前面的变量

4. generator函数的嵌套

generator函数不是一般的函数,甚至不能理解为函数。但是它在某些方面和函数有相同的表现形似和特点。比如声明方法都用function, 它和普通函数一样也是可以嵌套的。

function* G1 () {
    yield 'a'
    yield 'b'
}
function* G2 () {
    yield 'x'
    yield* G1()
    yield 'y'
}
for (let n of G2()) {
    console.log(n)   // x a b  y
}

可以看到,G1放到了G2内的一个yield后(注意此yield后有一个*)。从打印结果判断出在执行G2中遇到yiled* G1()时,进入到G1的yield。

5.thunk函数

以上内容对generator函数和Iterator对象做了个基本的了解,那么generator在实际项目中是如何处理异步操作呢? 毕竟它是需要next方法来进行下一步执行的,了解thunk后将解决你的疑惑!

// 1. 先写一个ajax请求的异步方法
const http = function (url,data,ck) {
    $.ajax({
        url: url,
        data: data,
        success: function () {
            ck && ck()
        }
    })
}
http(url,data,() => {
    console.log('数据请求成功')
})
// 2.对上面函数进行封装
const thunk = function (url,data) {
    return http
}
let fun = thunk(url,data)
fun(() => {
    console.log('数据请求成功') 
})

上面的代码封装了一个很简单的thunk函数。thunk函数和原来的方式相比最大的特点是将ck参数和其他(url,data)参数分离。fun函数只接受一个ck(回调函数)的参数。当然这类封装不要开发者在实际开发中进行封装,thunkify库帮你搞定。

const wait = function() {
    setTimeout((ck) => {
        ck && ck()
    },1000)
}
// 使用thunkify库进行封装
import thunkify from 'thunkify'
let thunk = thunkify(wait)
let fun = thunk()
fun(() => {
    console.log('执行完成')  // 1s后打印执行完成
})

在此离真相越来越近,还是接着模拟实际场景:要请求a,b,c三个api接口,a接口的返回值是b接口的请求参数,b接口的返回值是c接口的请求参数。在此为了保证程序能顺利执行,将请求接口的逻辑采用setTimeout代替。

const getA = function (ck) {
    setTimeout(() => {
        console.log('A接口请求完成')
        ck && ck()
    },1000)
}
const getB = function (ck) {
    setTimeout(() => {
        console.log('B接口请求完成')
        ck && ck()
    },1000)
}
const getC = function (ck) {
    setTimeout(() => {
        console.log('C接口请求完成')
        ck && ck()
    },1000)
}
const thunkA = thunkify(getA)
const thunkB = thunkify(getB)
const thunkC = thunkify(getC)

function* Test() {
    yield thunkA()
    yield thunkB()
    yield thunkC()
}
const test = Test()
test.next().value(() => {
    console.log('获取到a接口返回值')
    test.next().value(() => {
        console.log('获取到b接口返回值')
        test.next().value(() => {
            console.log('获取到c接口返回值')
        })
    })
})
// 控制台依次打印如下:
// A接口请求完成
// 取得a接口返回值
// B接口请求完成
// 取得b接口返回值
// C接口请求完成
// 取得c接口返回值

到这里,问题似乎越来越迷糊,promise和generator不就是为了解决callback-hell而来的,而代码最后的next调用又陷入了callback-hell。所以,有必要加上自驱动流程。

6.自驱动流程

// 自驱动流程管理函数
function run (generator) {
    const g = generator()
    function next(err,data) {
        const result = g.next()
        if (result.done) {
          return
        }
        result.value(next)
    }
    next()
}
// 上面的next回调地狱就可以简化为
run(Test)

就像封装thunk函数那样,流程管理的函数也有专门的库 co 来帮我们完成,co也是返回一个promise

cnpm install co --save

import co from 'co'
// 流程执行
co(Text).then(() => {
    console.log('流程执行完成')
})

到此,完整的generator处理异步的方法全部完成。最后感觉有必要再思考一下:Promise解决异步的方式的本质还是callback,只不过callback的方式通过某种方式实现了then()的链式调用,解决了callback-hell的问题。generator呢?脱离了callback没有?

7.Generator的本质

Generator虽然在解决异步问题的过程中使用了callback(thunk函数callback函数做为参数传入),但是其本质是 yiled 暂停关键字,“暂停执行” 和 “完成后进行callback回调” 还真需要时间去进行细细品味。

8.es7中的async-await

以上7节展示了generator是如何一步一步的实现解决异步问题的,虽然最终的结果相对来说较为简洁。但是过程的实现确实不太容易。就想es6 的class类的声明一样,generator也需要要语法糖。

const getData = function (api) {
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            console.log(`${api}接口请求完成`)
            resolve(api)
        },1000)
    })
}
const http = async function () {
    let a = await getData('a')
    console.log('a:',a)
    let b = await getData('b')
    console.log('b:',b)
    let c = await getData('c')
    console.log('c:',c)
}
http().then(() => {
    console.log('请求完成')
})
// 控制台依次打印
// a接口请求完成
// a: a
// b接口请求完成
// b: b
// c接口请求完成
// c: c
// 请求完成

注意下async-await使用和generator函数的使用区别:

  1. async-await在函数声明中使用async关键字; generator函数声明中使用*符号;
  2. async-await中使用 await关键字来暂停; generator函数中使用yield关键字来暂停;
  3. async-await中 await后面跟一个promise实例; generator函数中yield后可以跟promise实例也可以跟thunk函数;
  4. async-await中 await后跟的promise的实例中resolve(res)res的值会赋值给 await “=” 前的变量; generator函数中 yield “=” 前变量的值要在 generator对象的next()方法中传入;

到此,js的异步终极解决方案已出来了,async-await!!generator和promise的结合体。 后续有错误修正和新的解决方案再进行总结。