Generator函数与async函数介绍

前言

javascript 中经常会用到异步编程,在 ES6 之后我们使用的 Generator函数、async函数、promise都是我们异步编程的一大助力,这里我们主要讲解 Generator、async 函数,并且简介他们之间的一些联系

本篇文章会带着一些简易案例,方便大家理解使用(注:Generator函数大多数人平时基本用不到,有些场景也许能用上,对于我们来说就是如虎添翼了)

需要注意的是 JavaScript 中我们编写的代码基本上都是单线程的,因此这里的异步函数,也只是通过某种手段,通过给任务分配不同执行优先级(顺序)而出现的异步现象,并不是 ios、android 中的多线程的那种异步并发

Generator函数

Generator 函数看起来像指针函数,实际上跟我们的普通函数差不多,有两个特征

1、function关键字与函数名之间有一个星号

2、函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”),这里有暂停、等待执行的意思

Generator函数 运行后跟普通函数一样,只不过碰到 yield 关键字会暂停,需要等待调用next 之后才会继续往后执行,直到遇到下一个 yieldreturn

yield 作为表达式一项时,需要使用 () 括起来,否则会报错,例如: 2 * (yield x)

next 标识着 generator 函数开始往后执行语句,默认执行到 下一个 yield 语句,并返回 yield 后的内容value 中,没有遇到 return或者函数结束done参数为false,否则done参数为 true

return 标识着函数的结束,这里也不例外,只不过结束时返回的内容也可以被 next 返回; 另外,不写 return 则与return undefined类似

可以参考下面案例理解

function* generator() {
    yield 1
    yield 2
    yield 3
    yield 4
    return 5  // { value: 5, done: true },再往后 next 就和不写一样了
    // 不写return,则与return undefined 一样 返回 { value: undefined, done: true }
}

//获取 generator 遍历器
let g = generator()
let next = null
do {
    next = g.next() //开始往后执行到下一个 yield 语句,并返回
    console.log(next)
}while(next?.done === false) //执行到 return 或 函数结束 则停止打印

next = g.next()
console.log(next)

// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: false }
// 有return 5 打印 { value: 5, done: true }, 没有则打印 { value: undefined, done: true }

如果在 class 中怎么表示呢,里面是构造函数的包装,写法不一样, 只需要在前面加上一个* 即可

class A {
    //前面加上 * 即可
    * generator() {
        yield 1
        yield 2
    }
}

let a = new A()
let g = a.generator()
let next = g.next()
console.log(next)
next = g.next()
console.log(next)

generator 与 Iterator遍历器

从上面打印就可以看出,generator函数,实际上返回的是一个 Iterator遍历器,因此我们亦可以通过遍历器的手段来执行 generator遍历器

下面使用 for ... of 来遍历一下我们上面的 generator遍历器

//从上面就可以看出,generator函数 返回了一个 Iterator遍历器对象
//我们遍历一次试试
let g = generator()
for (let item of g) {
    console.log(item)
}
//结果打印 1 2 3 4

看其结果发现没有打印最后的return,了解遍历器就会知道,遍历器遇到 donetrue时会直接结束(作为遍历器使用的话,不要在 return 语句 放遍历内容)

我们再对比一下看看我们的 generator遍历器是否真的是 Iteraotr遍历器,发现一模一样

//g[Symbol.iterator]() === g //遍历器属性一致,返回为 true,不信打印遍历器属性比较试试

next

前面介绍了,generator遍历器调用 next(),会继续执行到下一个 yield语句

此外,next 可以传参,我们传递的参数会作为 上一个 yield 返回的结果,没有传参,默认返回都是 undefined

注意yield 返回的结果不是该表达式后面的内容,且 yield 作为表达式一项时,需要使用 () 括起来,否则会报错

下面看一下案例就知道结果了(过程已经标出)

//包含表达式时,yield 语句需要用括号括起来,否则会报错
function* generator() {
    let num1 = yield 1
    console.log('num1', num1)
    let num2 = num1 * (yield 2)
    console.log('num2', num2)
    yield 3
    yield 4
}

//next传入的值,作为上一个 yield 的返回值使用
let g = generator()
let next = g.next() //此时执行到 yield 1,并包装返回 yield 1 执行的结果
console.log(next)  //{ value: 1, done: false }

//传入 2, 当做上一个语句的 yield 1 的返回值(不传接收到的值均为undefined)
//即:返回 2 赋值给 num,然后执行到 (yield 2),并包装返回 (yield 2) 的结果
next = g.next(2) //执行完毕后,打印:num1 2
console.log(next) //{ value: 2, done: false }

//传入 3. 当做上一个语句的 (yield 2) 的返回值(不传接收到的值均为undefined)
//即:num2 = num1 * 3, 然后执行到 yield 3,并包装返回 yield 3 的结果
next = g.next(3)  //执行完毕后,打印:num2 6
console.log(next) //{ value: 3, done: false }

由上述可以可以看到 next 传参 挺好用的,除了上述,甚至可以使用传递的参数,用于跳出死循环……

throw

这里的throw为通过 generator遍历器抛出的异常,其异常会抛出到 generator 函数里面,因此需要注意抛出异常的位置,才能更好地配合try...catch使用

//throw()
// 相当于在 generator 里面抛出了一个异常
function* generator() {
    try {
        yield 1
    }catch(err) {
        console.log(err)
    }
    yield 2
    yield 3
    yield 4
}

let g = generator()
let next = g.next()
console.log(next)

//执行完毕 yield 1后,随后在 yield 1 ~ yield 2 之间抛出异常,因此,try...catch 包裹主 yield 1 才行
let t = g.throw('啦啦啦') // 抛出异常并执行到下一个 yield,如果没处理错误,程序异常
console.log(t)

next = g.next()
console.log(next)

return

指定 return 会理立即结束 generator 函数,并返回 { value: undefined, done: true },上面有介绍

function* generator() {
    yield 1
    yield 2
    yield 3
    yield 4
}
let g = generator()
next = g.next()
next = g.return() //直接返回 { value: undefined, done: true },如果传参则返回return传递的参数
console.log(next) //会直接结束

另外,当存在 try...finally 代码块时,并且执行到 try 里面时return ,会仍然执行finally 里面的语句,且会执行到finally里面yield语句,finally执行完毕,才是真正结束,并且return带入的参数,会应用到 finally语句的末尾

function* generator() {
    yield 1
    try {
        //执行到这里,外面 return 的话,也需要走完 finally 才会结束
        yield 2
        yield 3
    }finally {
        yield 7
        yield 8
    }
    yield 4
}

let g1 = generator()
next = g1.next() // { value: 1, done: false }
next = g1.next() // { value: 2, done: false }
next = g1.return(100) // { value: 7, done: false },return 后返回下一个,为finally中的 yield 7,这里顺道返回一个值测试一下最后的返回值
next = g1.next() // { value: 8, done: false }
next = g1.next() // { value: 100, done: true }
next = g1.next() // { value: undefined, done: true }

yield*

yield*看着像个指针,其指向一个 generator遍历器,会自动展开,不多说

//yield* 会展开 generator 函数
function* gen1() {
    yield 1
    yield 2
}

function* gen2() {
    yield 0
    yield* gen1()
}

//相当于下面展开的语句,可以看出yield语句总是一个一个执行的,即使嵌套也不会一次执行一堆
function* gen3() {
    yield 0
    yield 1
    yield 2
}

generator应用

generator应用很多,这里简单介绍两种,算是小试牛刀

正常切换开关,需要状态,这里通过 generator 遍历器,避免了新增开关状态(不适用于多方控制,例如:需要与后台远程同步状态)

//切换状态
function* toggleSwitch() {
    while(true) {
        console.log('开灯')
        yield 1
        console.log('关灯')
        yield 0
    }
}
//是不是切换很简单了
let t = toggleSwitch()
let next = t.next() //开灯
console.log(next)
next = t.next() //关灯
console.log(next)

流程化管理,外部不用关心内部(例如:加工软件,不同工种对于自己的操作工序,完成后只需要点击一下 next 即可)

function* order() {
    yield '收到订单'
    yield '加工'
    yield '送检'
    yield '质检'
    yield '发货'
    yield '完成订单'
}
let o = order()
let next = o.next() //收到订单
next = o.next() //加工
next = o.next() //送检
next = o.next() //质检
next = o.next() //发货
next = o.next() //完成订单

async、await函数

async、await,我们平时用的比较多,这里简单介绍一下

async、await 就是根据 generator函数改造而成,其改进了 generator函数作为普通函数痛点,其加入了执行器,能像普通函数一样直接执行,更加方便,且语义更加清晰,结果返回一个 Promise(可以看出使用Promise包装而成)

简而言之async 声明一个异步函数,await 等待执行,当await语句执行完毕后,才会执行后面语句(出现错误直接抛出错误结束,不往后执行),并且如果没有 await 的存在,async函数,跟一个普通的同步函数执行顺序没有什么区别

ps: 关于 promise 前面有介绍,await执行的过程与其一样,实际上会马上执行任务队列下一个任务,前面的任务执行完毕后,才会执行到 await 后面的语句

简单做一个使用案例,

async function a() {
    await promise1...
    await promise2...
    // 如果不return自己内容,返回的则是 undefind
    return 1
}

function cc() {
    a().then(res => {
        console.log(res)
    }).catch(err => {
        console.log(err)
    })
}

你可能会想,如果第一个 promise 出错了怎么办,第二个会执行么?

答案:不会,第一个出错后,会抛出一个异常,此时会结束解释器的执行,直接反馈错误到外部

下面改进上一个案例

function a1() {
    return Promise.reject('出错了 ')
}

function a2() {
    return Promise.resolve('成功了 ')
}

async function a() {
    await a1()
    console.log(11111111111)
    await a2()
    // 如果不return自己内容,返回的则是 undefind
    return 1
}

function cc() {
    a().then(res => {
        console.log(res)
    }).catch(err => {
        console.log(err)
    })
}

//结果是没有执行打印 11111111111,且执行到了 .catch 中

下面我们模拟传递多个图片的案例,可以看出 async、await 应用多么方便

function uploadAImage(fileUrl) {
    return new Promise(function(resolve, reject) {
        setTimeout(() => {
            let res = fileUrl + '成功了'
            console.log(res)
            resolve(res)
        }, 500);
    })
}

async function uploadImages() {
    //准备多个图片url
    let fileUrl = ['url1', 'url2', 'url3']
    for (url of fileUrl) {
        try {
            await uploadAImage(url)
        }catch(err) {
            console.log(err)
            //实际上更复杂,成功的下次就不需要在上传了
            return Promise.reject('存在上传失败的内容') 
        }
    }
    return '都成功了'
}

uploadImages().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

async、await 函数使用简单就先介绍到这里了,后面会对 generator 进行对比,是怎么改造了的

generator 模仿 async 自动执行

前面说了 async函数 是通过 generator函数 改造而来的,里面添加了执行器,我们自己尝试一下做一个简易的generator执行器

这里是一个async 的默认执行案例

//使用 generator + 执行器,简单翻译一下 async、await
async function asyncDefaultFunction() {
    let res = await new Promise((resolve) => {
        setTimeout(() => {
            resolve(1)
        }, 1000);
        // setTimeout(() => {
        //     reject('啦啦啦')
        // }, 1000);
    })
    return res
}

asyncDefaultFunction().then(res => {
    console.log(res)
})

我们使用 generator 改造成 async 自动执行的模式

//使用 async 之后,实际上我们将返回函数包装一下即可,并且将 yield 就是 await
function asyncFunction() {
    return co(function* () {
        //async 函数里面的内容,将 yield 与 await 替换即可
        let res = yield new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 1000);
            // setTimeout(() => {
            //     reject('啦啦啦')
            // }, 1000);
        })
        return res
    })
}
//编写一个自动执行 generator 的 co 函数
function co(genFunc) {
    return new Promise(function (resolve, reject) {
        const g = genFunc() //获取generator,准备执行
        function nextFunc(value) {
            let next;
            try {
                next = g.next(value)
            }catch(err) {
                return reject(err)
            }
            if (next.done) {
                return resolve(next.value)
            }
            //为什么要promise包装,语句可能为同步函数,可能为异步函数,木事保证正确执行
            //如果为 promise 直接原封不动返回,如果为普通对象则包装,具体见promise
            Promise.resolve(next.value).then(function(res) {
                nextFunc(res)
            }, function(err) {
                reject(err)
            })
        }
        nextFunc(undefined)
    })
}

asyncFunction().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

这样就实现了一个简易的 generator 自动执行器

同时我们也发现了,async 与我们普通函数相比,加剧了负担,为了优化性能,我们平时如果没有用到异步函数(或者连 await 都用不到的),将多余的 async 去掉吧,这样某种程度上能够避免性能浪费,尤其是到了循环语句

最后

看到了这里,相信我们也能学习到不少东西,可以想一下,其他语言是不是也是类似这样呢,遇到了相似的情况我们是不是也豁然开朗了呢,很多语言是互通的,学习就是积累的过程,希望我们此次能有所收获!

原文链接:https://juejin.cn/post/7241538641569497148 作者:剪刀石头布啊

(0)
上一篇 2023年6月7日 上午10:52
下一篇 2023年6月7日 上午11:02

相关推荐

发表回复

登录后才能评论