前端知识库(lizh)
  • README
  • Bugs
    • 前端调试随笔
    • 浏览器常见问题概览
    • 浏览器兼容问题概览
    • HTML常见问题概览
    • CSS常见问题概览
    • JS常见问题概览
    • 移动端兼容性问题概览
    • 微信小程序开发
    • NodeJs常见问题概览
    • Mac常见问题概览
    • 微信开发遇坑指南
    • Npm包常见问题概览
    • 其他问题汇总
  • Css探索系列
    • CSS基础知识
    • CSS常见问答
    • CSS常见问答02
    • CSS应用示例
    • CSS应用示例02
    • 由Z Index引发的层叠上下文思考
    • 由浮动塌陷引发的块级格式上下文思考
    • CSS探索系列 Flex布局
    • CSS探索系列 Margin
    • CSS探索系列 Auto关键字
    • CSS探索系列 Gradient
    • CSS探索系列 Line Height
    • CSS探索系列 元素居中
    • CSS探索系列 动画
    • 为什么使用PostCSS处理CSS?
    • 重新认识伪类与伪元素
    • 自定义表单伪元素样式
    • 如何理解Css中的Display属性
    • 视口和软键盘对视口的影响
    • 关于Css
  • Frontend
    • 00 关于Web前端
    • 01 前端知识概览
    • 02 常用前端库概览
    • 基础 00 前端常见问题01
    • 基础 01 浏览器缓存
    • 基础 02 浏览器工作原理
    • 基础 03 谈谈前端跨源问题及解决方法
    • 进阶 01 Web性能优化
    • 进阶 02 搜索引擎优化(SEO)
    • 进阶 03 前端模块化编程
    • 进阶 04 规范代码:Linter、Prettier、EditorConfig
    • 进阶 11 前端自动化测试
    • 高级 01 前端安全
    • Vue2.X原理篇
    • Vue3初步了解及迁移指南
    • 重读Vue教程
    • React17.X原理篇
    • 你必须知道的React问题
    • 重读React教程
    • 聊一聊Cookie的一些问题
    • 如何理解HTTP响应的状态码
    • HTTP的历史演变及概述
    • Webpack4.X原理篇
    • Webpack基础入门篇
    • Webpack构建优化篇
    • TypeScript使用指南
    • 代码规范
      • 前端规范
      • HTML
      • CSS
      • JS
  • Html探索系列
    • HTML基础知识
    • HTML基础知识02
    • HTML常见问答
    • HTML经典实践用例
    • HTML元素的宽高及位置详解
    • Video元素的使用和常见问题总结
    • Html探索系列 Meta标签
    • DOCTYPE:文档类型与浏览器模式
    • DHTML(动态网页)简介
    • HTML标签详解
    • HTML布局的几种方式
    • HTML全局属性
    • 关于Html
  • Js探索系列
    • 基础知识
    • 常见问答
    • 应用示例
    • 趣味示例
    • 基础篇 05 AJAX
    • 基础篇 06 Window对象
    • 基础篇 07 Error、JSON、Math、Console对象
    • 基础篇 08 History、URL、Screen、Navigator、Location对象
    • 基础篇 09 文档对象模型(DOM)
    • 基础篇 10 Document对象
    • 基础篇 11 Element对象
    • 基础篇 12 Event对象
    • 基础篇 13 键盘、鼠标、触摸事件
    • 基础篇 15 CSS对象模型(CSSOM)
    • 进阶篇 01 Prototype对象和继承
    • 进阶篇 02 Promise对象
    • 进阶篇 07 迭代器(Iterator)
    • 进阶篇 08 Generator和Async函数
    • 进阶篇 09 JavaScript异步编程
    • Date对象和日期时间字符串格式
    • Canvas基础入门篇
    • Canvas进阶篇
    • SVG基础入门篇
    • 四种判断数据类型方法的优缺点
    • 深入理解JavaScript的浅拷贝和深拷贝
    • 谈谈JavaScript的作用域和上下文
    • 复制内容到剪贴板
    • 关于Javascript
  • NodeJs
    • 关于Node.Js
    • Node.Js:三种调试方法
    • Npm包管理器简介及一些机制
    • NPM:Package.Json详解(中文)
    • NPM:从零开始,开发一个软件包
    • NPM:常用命令
    • Node.Js:Fs(文件系统)
    • Node.Js:Global(全局变量)
    • Node.Js:HTTP
    • Node.Js:Module(模块)
    • Node.Js:Path(路径)
    • Node.Js:Readline(逐行读取)
  • Research
    • 极细边框(1px边框)实现方式
    • 如何监控前端异常?
    • H5页面跳转和刷新
    • Web主题切换和个性化定制方法总结
    • Vue SSR(服务端渲染)的简单实现
    • 基于Create React App打造代码规范化的React+Ts项目
    • H5可视化编辑
    • Web常用功能
    • Javascript加密混淆
    • Vue如何导入TypeScript
    • 移动端PDF预览
    • 纯CSS绘制箭头
    • 网站性能测量和优化方法
  • Tech
    • GOOGLE浏览器的搜索技巧
    • Curl的用法指南
    • Sublime3插件篇
    • Charles安装及使用
    • Nginx基础使用
    • 排序算法(Javascript)
    • 代码整洁之道(摘录笔记)
    • Java的24种设计模式与7大原则
    • 观察者和发布订阅模式
  • Tools
    • Git
      • Git基础教程
      • Git常见问题
    • Gitbook
      • Gitbook入门篇
      • Gitbook插件篇
      • Gitbook进阶篇
由 GitBook 提供支持
在本页
  • Generator函数
  • yield关键字
  • next()方法
  • throw()方法
  • return()方法
  • yield* 表达式
  • 异步编程的执行器
  • 其他应用场景
  • Async函数
  • async函数
  • await关键字
  • 相关问题
  • next()、throw()、return() 的有何异同?
  • Generator 与 Async 对比?
  • 顶层await
  • 参考资料

这有帮助吗?

  1. Js探索系列

进阶篇 08 Generator和Async函数

Generator 函数,也称生成器函数,是 ES2015 提供的异步解决方案;Async 函数是 ES2017 提供的异步编程的终极解决方案。

Async 函数是 Generator 函数的语法糖,其本质是 Generator 函数 + 执行器。

Generator函数

Generator 是 ES6 提供的一个新的数据类型,可以叫做 Generator 函数,其最大特点就是可以控制函数的执行(在执行时能暂停,后面又能从暂停处继续执行)。

语法上,Generator 函数是一个普通函数,但是有以下特征:

  • 函数声明中,function 关键字与函数名之间有一个星号(*);

  • 函数体内部使用 yield(英语里的意思就是“产出”) 关键字来暂停和恢复 Generator 函数;

  • Generator 函数调用后,并不立即执行,而是返回一个指向内部状态的指针对象,该对象是一个迭代器对象。

  • 调用返回的迭代器对象的 next() 方法,会移动内部指针,使得指针指向下一个状态。next() 方法会返回一个对象,该对象包含 value 和 done 属性,value 的值是 yield 后面表达式的值;done 属性表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

function* genFunc(x) {
    const y = 2 * (yield (x + 1))
    const z = yield (y / 3)
    return (x + y + z)
}
const it = genFunc(5)
console.log(it.next())   // {value: 6, done: false}
console.log(it.next(12)) // {value: 8, done: false}
console.log(it.next(13)) // {value: 42, done: true}

代码执行分析:

  • 调用 Generator 函数(可以传参),返回一个迭代器;

  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6;

  • 当执行第二次 next 时,传入的参数 12 就会被当作上一个 yield 表达式的返回值,此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8。注意: 如果 next 不传参,yield 永远返回 undefined。

  • 当执行第三次 next 时,传入的参数 13 就会被当作上一个 yield 表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42。

注意: Generator 函数不能当构造器使用。

yield关键字

yield 关键字用来暂停和恢复一个 Generator 函数。

yield [expression]

yield 关键字使 Generator 函数执行暂停,其后面的表达式的值返回给 Generator 函数的调用者。返回值是一个迭代器执行结果的对象,有两个属性,value 和 done。value 属性是对 yield 表达式求值的结果,而 done 是false,表示 Generator 函数尚未完全完成;如果 Generator 函数已完成,则 value 为 undefned,done 为 true。

Generator 函数的执行,一旦遇到 yield 表达式,将被暂停,直到 Generator 函数的 next() 方法再次调用。每次调用 next() 方法,Generator 函数都会恢复执行,直到达到以下某个值:

  • yield: Generator 函数再次暂停,返回结调用者的值,value 为 yield 表达式的值 并且 done 为 false。 下一次调用 next() 时,在 yield 之后的语句继续执行。

  • throw: 用于从 Generator 函数中抛出异常。这让 Generator 函数完全停止执行,并且调用者后面的代码也无法继续执行,正如通常情况下抛出异常一样。

  • 到达 Generator 函数的结尾:即,Generator 函数的执行结束,返回结调用者的值,value 为 undefined 并且 done 为 true。

  • 到达 return 语句:即,Generator 函数的执行结束,返回结调用者的值,value 值由 return 语句指定,并且done 为 true。

function* generatorFunc(x) {
    yield 1
    // throw '2'   // 抛出异常:Uncaught 2
    return 0
    console.log(3) // 永远不会执行
}
const it = generatorFunc()
console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 0, done: true}
console.log(it.next()) // {value: undefined, done: true}

注意: yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。

function* generatorFunc(x) {
    const result1 = 2 + (yield x + 5)
    }
const it = generatorFunc(1)
console.log(it.next()) // {value: 6, done: false}

注意: yield 只能在 Generator 函数中使用,在其他地方使用会报错。

注意: yield 后面的操作是同步的。也就是说,如果是 yield 后面是异步操作,调用 next() 方法时,执行 Generator 函数会立即往后执行,不会等待当前停留的 yield 表达式异步操作完成。

next()方法

返回一个包含 value 和 done 属性的对象。该方法也可以通过接受一个参数用以向 Generator 函数传值。

gen.next(value)

next() 方法中的参数将成为 Generator 函数上一个 yield 表达式的返回值。

function* generatorFunc(x) {
    const y = 2 * (yield (x + 1))
    const z = yield (y / 3)
    return z
}
const it = generatorFunc(5)
console.log(it.next(10)) // 传参将忽略
console.log(it.next())   // {value: NaN, done: false},相当于 const y = 2 * undefined
console.log(it.next(13)) // {value: 13, done: true},相当于 const z = 13

注意: 由于 next() 方法的参数是表示上一个 yield 表达式的返回值,所以在第一次使用 next() 方法时,传递参数是无效的。

next() 方法返回的对象包含两个属性:

  • value:迭代器返回的任意值,即 yield 表达的返回结果。当 done 的值为 true 时,可以返回指定值,也可以忽略该值。

  • done:布尔值。Generator 函数尚未完成,则值为 false;如果已完成,则返回 true。

throw()方法

用来向 Generator 函数抛出异常。

如果 throw() 抛出的异常能被 Generator 函数内的 try...catch 块捕获,则不会中断 Generator 函数执行,仍会执行到下一个 yield。也就是说,throw() 方法相当于执行了一次 next() 方法,只是跳到 catch 块中,并执行了其中的代码。

function* genFunc() {
    try {
        yield 1
    } catch (e) {
        console.log(e)
    }
    yield 2
    yield 3
}
const it = genFunc()
console.log(it.next())
console.log(it.throw('飘过,你们继续'))
console.log(it.next())

// {value: 1, done: false}
// 飘过,你们继续
// {value: 2, done: false}
// {value: 3, done: false}

如果 Generator 函数未定义 try...catch 块,但 throw() 抛出的异常能被外部 try...catch 块捕获,则 Generator 函数执行结束,程序继续执行。

function* genFunc() {
    yield 1
    yield 2
    yield 3
}
try {
    const it = genFunc()
    console.log(it.next())
    console.log(it.throw('你完了,但其他人还会好好的'))
    console.log(it.next())
} catch (e) {
    console.log(e)
}
console.log('是的,我们还很好')

// {value: 1, done: false}
// 你完了,但其他人还是好好的
// 是的,我们还很好

如果 throw() 抛出的异常,即未在 Generator 函数内被捕获,也没有被外部 try...catch 块捕获,则返回 throw() 抛出的异常,程序中断执行。

function* genFunc() {
    yield 1
    yield 2
    yield 3
}
const it = genFunc()
console.log(it.next())
console.log(it.throw('毁灭吧,我累了'))
console.log(it.next())

// {value: 1, done: false}
// Uncaught 毁灭吧,我累了

注意: throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next() 方法。

return()方法

用于结束 Generator 函数的执行,并返回给定的值。

function* genFunc(x) {
    yield 1
    yield 2
}
const it = genFunc()
console.log(it.next())
console.log(it.return('我睡了,你们继续'))
console.log(it.next())
console.log('接着奏乐接着舞')

// {value: 1, done: false}
// {value: '我睡了,你们继续', done: true}
// {value: undefined, done: true}
// 接着奏乐接着舞

yield* 表达式

yield* 表达式用于操作一个可迭代对象,并产生它返回的每个值。yield* 表达式本身的值是当迭代器关闭时返回的值(即 done 为 true 时)。

function* genFunc1(x) {
    yield 1
    yield 2
    yield 3
}
function* genFunc2(x) {
    yield 4
    yield genFunc1()
    yield 5
}
for (let v of genFunc2()) {
    console.log(v);
}
// 4
// genFunc1 {<suspended>}
// 5

yield* 表达式用于遍历一个可迭代对象。

将上述 yield genFunc1() 改成 yield * genFunc1(),得到结果:

function* genFunc2(x) {
    yield 4
    yield* genFunc1()
    yield 5
}
// 4
// 1
// 2
// 3
// 5

异步编程的执行器

使用 Generator 函数可以解决异步编程的问题:

const fs = require('fs')
function read(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) { reject(err) }
            console.log(data)
            resolve(data)
        })
    })
}
function * genRead(value) {
    const r1 = yield read(value)
    const r2 = yield read(r1)
    const r3 = yield read(r2)
}

const it = genRead('./1.txt');
it.next().value.then((r1) => {
    it.next(r1).value.then((r2) => {
        it.next(r2).value.then(() => {
            console.log('全部完成!')
        })
    })
})

该解决方案存在两个问题:

  • 需要手动调用每个步骤的执行。

  • 存在代码层级嵌套问题,不利于阅读和调试。

以下是一个 Generator 函数异步编程的自动执行器,实现的原理是,保证每个 yield 返回的都是函数,并且当前 yield 返回的函数将下一个 yield 返回的函数作为回调函数,即,回调函数嵌套。

const fs = require('fs')
// 将 yield 表达式转换为函数,作为迭代器返回的对象中 value 的值
function thunk(file) {
    return function (cb) {
        return fs.readFile(file, 'utf8', (err, data) => {
            if (err) { return }
            console.log(data)
            cb(data)
        })
    }
}
// 实现 Generator 函数异步编程的自动执行
function autoRunner(gen, value) {
    const it = gen(value);
    function next(data) {
        const result = it.next(data);
        if (result.done) {
            return;
        }
        result.value(next);
    }
    next();
}

const genFunc = function* (value){
    const f1 = yield thunk(value);
    const f2 = yield thunk(f1);
    const f3 = yield thunk(f2);
};
autoRunner(genFunc, './1.txt');

另外,也可以使用第三方包实现异步编程的自动执行,比如,co。

co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。

const fs = require('fs')
function read(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) { reject(err) }
            console.log(data)
            resolve(data)
        })
    })
}
function * geneRead(value) {
  const r1 = yield read(value)
  const r2 = yield read(r1)
  const r3 = yield read(r2)
}

const co = require('co')
co(geneRead('./1.txt')).then(() => {
    console.log('全部完成!')
})

其他应用场景

Iterator 生成函数

const myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
};
const it = myIterable[Symbol.iterator]();
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 2, done: false}
console.log(it.next()); // {value: undefined, done: true}

多维数组转一维数组

function* iterArr(arr) {
    if (Array.isArray(arr)) {
        for (let i = 0; i < arr.length; i++) {
            yield* iterArr(arr[i]);
        }
    } else {
        yield arr;
    }
}
const arr = [
    'a',
    ['b', 'c'],
    ['d', ['e', 'f']]
];
for (let v of iterArr(arr)) {
    console.log(v); // a b c d e f
}
// 或者
console.log([...iterArr(arr)]); // ['a', 'b', 'c', 'd', 'e', 'f']

Async函数

Async 函数是 ES2017 提供的一种异步编程解决方案,也是异步编程的终极解决方案。其最大的优点是代码清晰,让异步逻辑的代码看起来像同步一样。

本质上,Async 函数是 Generator 函数的语法糖,是 Generator 函数 + Promise 的组合。

其实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里,类似上述讲到的 Generator + co。

const fs = require('fs')
function read(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) { reject(err) }
            console.log(data)
            resolve(data)
        })
    })
}

async function promiseRead() {
    const r1 = await read('./1.txt')
    const r2 = await read(r1)
    const r3 = await read(r2)
}
promiseRead();

async函数

async 函数是使用 async 关键字声明的函数,函数体内允许使用 await 关键字。

async function promiseRead() {
    await read('./1.txt')
}

Async 函数一定会返回一个 Promise 对象,即使不是,也会被隐式地包装在一个 Promise 中。该 Promise 要么会通过一个由 async 函数返回的值被解决,要么会通过一个从 async 函数中抛出的(或其中没有被捕获到的)异常被拒绝。

await关键字

await 关键字用于等待一个 Promise 对象。它只能在 Async 函数中使用。

await 关键字会暂停当前 Async 函数的执行,并出让其控制权,并等待 Promise 处理完成,再继续执行。若 Promise 正常处理,其回调的 resolve 函数参数作为 await 表达式的值,继续执行 Async 函数;若 Promise 处理异常 ,await 表达式会把 Promise 的异常原因抛出,中断 Async 函数执行。

另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。

注意: await 关键字只在 Async 函数内有效。如果出现在非 Async 函数内,就会抛出异常。

注意: 为避免 Await 命令后面的 Promise 对象抛出的异常中断 Async 函数执行,最好把 await 命令放在 try...catch 代码块中。

相关问题

next()、throw()、return() 的有何异同?

next()、throw()、return() 都能让 Generator 函数恢复执行,并且执行到下一个 yield 表达式。

next() 将 yield 表达式的结果替换成一个指定的值,不会结束 Generator 函数的执行。

throw() 将 yield 表达式的结果替换成一个指定的值(建议是 Error 对象),会结束 Generator 函数或者程序的执行(如果 Generator 函数内部或者外部没有 try...catch 捕获异常)。

return() 将 yield 表达式的结果替换成一个指定的值,会结束 Generator 函数的执行。

Generator 与 Async 对比?

  • Generator 函数出现在 ES2015 中,Async 函数出现在 ES2017 中,Async 是 Generator 的语法糖。

  • Generator 函数需要手动逐步执行或者使用自动执行器(如,co);而 Async 函数自带执行器,执行方式与普通函数相同。

  • Generator 函数的 * 号和 yield 的语义没那么清晰;Async 函数的 async 表示异步,await 表示等待,语义更加清楚。

  • Generator 函数的 yield 后面只能跟 Thunk 函数或 Promise 对象;而 Async 函数的 await 后面可以是 Promise 对象或者原始类型的值(会自动转为立即 resovle 的 Promise 对象);

  • Generator 函数返回遍历器;Async 函数返回 Promise 对象。

顶层await

早期的语法规定是,await 命令只能出现在 async 函数内部,否则都会报错。

function genPromise() {
    return Promise.resolve(1)
}
const r1 = await genPromise()
console.log(r1)

报错:

Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules

从 ES2022 开始,允许在模块的顶层独立使用 await 命令,主要目的是为解决模块异步加载的问题。

参考资料

上一页进阶篇 07 迭代器(Iterator)下一页进阶篇 09 JavaScript异步编程

最后更新于1年前

这有帮助吗?

MDN - function*
MDN - async 函数
阮一峰 - Generator 函数的语法
阮一峰 - async 函数