JavaScript异步编程解决方案

Generator 函数

学习指导:阮一峰ES6 -- Generator函数

基本概念

  • 语法上:Generator 函数是一个状态机,封装了多个内部状态;执行 Generator 函数会返回一个遍历器对象。也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

  • 形式上:Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号*;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

    function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator();

    // 定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world) // 即该函数有三个状态:hello,world 和 return 语句(结束执行)。

  • 调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

    // 上述函数下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。 // 即每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行, // 直到遇到下一个yield表达式(或return语句)为止。 // 换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

    hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true }

    /* 解释说明: 上面代码一共调用了四次next方法。 第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。 next方法返回一个对象,它的value属性就是当前yield表达式的值hello, done属性的值false,表示遍历还没有结束。

    第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。 next方法返回的对象的value属性就是当前yield表达式的值world, done属性的值false,表示遍历还没有结束。

    第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句 (如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性, 就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined), done属性的值true,表示遍历已经结束。

    第四次调用,此时 Generator 函数已经运行完毕, next方法返回对象的value属性为undefined,done属性为true。 以后再调用next方法,返回的都是这个值。 */

yield 表达式说明

  • Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

  • 遍历器对象的next方法的运行逻辑如下。

    • 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
    • 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
    • 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
    • 如果该函数没有return语句,则返回的对象的 value 属性值为 undefined。
  • 需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

    function* gen(){ yield 123 + 456; }

    // yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值.

yield 表达式语法

  • yield表达式只能用在 Generator 函数里面,用在其他地方都会报错

    (function (){ yield 1; })() // SyntaxError: Unexpected number

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

    function* demo() { console.log('Hello' + yield); // SyntaxError console.log('Hello' + yield 123); // SyntaxError

    console.log('Hello' + (yield)); // OK console.log('Hello' + (yield 123)); // OK }

  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号

    function* demo() { foo(yield 'a', yield 'b'); // OK let input = yield; // OK }

用法实例讲解

1var arr = [1, [[2, 3], 4], [5, 6]]; 2 3var flat = function* (a) { 4 a.forEach(function (item) { 5 if (typeof item !== 'number') { 6 yield* flat(item); 7 } else { 8 yield item; 9 } 10 }); 11}; // 错误用法 12// forEach方法的参数是一个普通函数,但是在里面使用了yield表达式 13 14// 一种修改方法是改用for循环。 15var flat = function* (a) { 16 var length = a.length; 17 for (var i = 0; i < length; i++) { 18 var item = a[i]; 19 if (typeof item !== 'number') { 20 yield* flat(item); 21 } else { 22 yield item; 23 } 24 } 25}; 26 27for (var f of flat(arr)) { 28 console.log(f); 29} 30// 1, 2, 3, 4, 5, 6

yield表达式与return语句

  • yield 表达式与return 语句既有相似之处,也有区别。
  • 相似之处在于,都能返回紧跟在语句后面的那个表达式的值。
  • 区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。
  • 一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式
  • 正常函数只能返回一个值,因为只能执行一次return;
  • Generator 函数可以返回一系列的值,因为可以有任意多个yield
  • 从另一个角度看,也可以说 Generator 生成了一系列的值。

next 方法的参数

  • yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

    function* f() { for(var i = 0; true; i++) { var reset = yield i; if(reset) { i = -1; } } } var g = f();

    g.next() // { value: 0, done: false } g.next() // { value: 1, done: false } g.next(true) // { value: 0, done: false }

    /* 上面代码先定义了一个可以无限运行的 Generator 函数f, 如果next方法没有参数,每次运行到yield表达式,变量reset的值总是undefined。

    当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1, 下一轮循环就会从-1开始递增。 */

  • 这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。

  • 通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

    function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); }

    var a = foo(5); a.next() // Object{value:6, done:false} a.next() // Object{value:NaN, done:false} a.next() // Object{value:NaN, done:true}

    var b = foo(5); b.next() // { value:6, done:false } b.next(12) // { value:8, done:false } b.next(13) // { value:42, done:true }

    /* 上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN), 除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。 第三次运行Next方法的时候不带参数,所以z等于undefined, 返回对象的value属性等于5 + NaN + undefined,即NaN。

    如果向next方法提供参数,返回结果就完全不一样了。 上面代码第一次调用b的next方法时,返回x+1的值6; 第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8; 第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13, 这时x等于5,y等于24,所以return语句的值等于42。 */

  • 注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

for...of 循环

    1. for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法。

    function* foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5

    // 上面代码使用for...of循环,依次显示 5 个yield表达式的值。 // 这里需要注意,一旦next方法的返回对象的done属性为true, // for...of循环就会中止,且不包含该返回对象, // 所以上面代码的return语句返回的6,不包括在for...of循环之中。

  • 2)利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。

    function* objectEntries(obj) { let propKeys = Reflect.ownKeys(obj);

    for (let propKey of propKeys) { yield [propKey, obj[propKey]]; } }

    let jane = { first: 'Jane', last: 'Doe' };

    for (let [key, value] of objectEntries(jane)) { console.log(${key}: ${value}); } // first: Jane // last: Doe

  • 3)加上遍历器接口的另一种写法是,将 Generator 函数加到对象的Symbol.iterator属性上面

    function* objectEntries() { let propKeys = Object.keys(this);

    for (let propKey of propKeys) { yield [propKey, this[propKey]]; } }

    let jane = { first: 'Jane', last: 'Doe' };

    jane[Symbol.iterator] = objectEntries;

    for (let [key, value] of jane) { console.log(${key}: ${value}); } // first: Jane // last: Doe

  • 4)除了for...of循环以外,扩展运算符(...)解构赋值Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。

    function* numbers () { yield 1 yield 2 return 3 yield 4 }

    // 扩展运算符 [...numbers()] // [1, 2]

    // Array.from 方法 Array.from(numbers()) // [1, 2]

    // 解构赋值 let [x, y] = numbers(); x // 1 y // 2

    // for...of 循环 for (let n of numbers()) { console.log(n) } // 1 // 2

Generator函数的异步操作

1function* asyncJob() { 2 // ...其他代码 3 var f = yield readFile(fileA); 4 // ...其他代码 5}

函数asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。 也就是说,yield命令是异步两个阶段的分界线。 协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。

异步任务的封装

1var fetch = require('node-fetch'); 2 3function* gen(){ 4 var url = 'https://api.github.com/users/github'; 5 var result = yield fetch(url); 6 console.log(result.bio); 7} 8 9// Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。 10// 就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。 11 12// 执行上述代码 13var g = gen(); 14var result = g.next(); 15 16result.value.then(function(data){ 17 return data.json(); 18}).then(function(data){ 19 g.next(data); 20}); 21 22// 首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。 23// 由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

async 函数

学习指导:阮一峰ES6 -- async函数

含义

  • async 函数是什么?一句话,它就是 Generator 函数的语法糖

    // generator 函数依次读取两个文件 const fs = require('fs'); const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) return reject(error); resolve(data); }); }); }; const gen = function* () { const f1 = yield readFile('/etc/fstab'); const f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };

    // async函数,就是下面这样 const asyncReadFile = async function () { const f1 = await readFile('/etc/fstab'); const f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };

    // async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

  • async函数对 Generator 函数的改进,体现在以下四点:

    • 内置执行器 —— Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
    • 更好的语义 —— async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
    • 更广的适用性 —— co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
    • 返回值是 Promise —— async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

  • 引入demo

    /* async 表示异步调用 返回一个Promise对象 / async function timeout(ms) { await new Promise((resolve) => { setTimeout(resolve, ms); }); } / 与下边等价*/

    // function timeout(ms) { // return new Promise((resolve) => { // setTimeout(resolve, ms); // }); // }

    async function asyncPrint(value, ms) { await timeout(ms); console.log(value); }

    asyncPrint('hello world', 3000) // 3000毫秒以后,输出hello world

  • async的表达方式

    // 函数声明 async function foo() {}

    // 函数表达式 const foo = async function () {};

    // 对象的方法 let obj = { async foo() {} }; obj.foo().then(...)

    // Class 的方法 class Storage { constructor() { this.cachePromise = caches.open('avatars'); }

    async getAvatar(name) { const cache = await this.cachePromise; return cache.match(/avatars/${name}.jpg); } }

    const storage = new Storage(); storage.getAvatar('jake').then(…);

    // 箭头函数 const foo = async () => {};

async 的语法

  • 返回Promise 对象

    • async函数返回一个 Promise 对象。

    • async函数内部return语句返回的值,会成为then方法回调函数的参数。

      async function f() { return 'hello world'; }

      f().then(v => console.log(v)) // "hello world" // 函数 f 内部return命令返回的值,会被then方法回调函数接收到。

  • async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

    async function f() { throw new Error('出错了'); }

    f().then( v => console.log(v), e => console.log(e) ) // Error: 出错了

  • Promise 对象的状态变化

    • async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。

    • 也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

      async function getTitle(url) { let response = await fetch(url); let html = await response.text(); return html.match(/<title>([\s\S]+)</title>/i)[1]; } getTitle('https://tc39.github.io/ecma262/').then(console.log) // "ECMAScript 2017 Language Specification"

      // 函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。 // 只有这三个操作全部完成,才会执行then方法里面的 console.log。

  • await 命令

    • 正常情况下,await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。

      async function f() { return await 123; }

      f().then(v => console.log(v)) // 123

      // await命令的参数是数值123,它被转成 Promise 对象,并立即resolve

    • await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

      async function f() { await Promise.reject('出错了'); }

      f() .then(v => console.log(v)) .catch(e => console.log(e)) // 出错了

      // await 语句前面没有 return,但是 reject 方法的参数依然传入了 catch 方法的回调函数。 // 这里如果在await前面加上return,效果是一样的。

    • 只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行

      async function f() { await Promise.reject('出错了'); await Promise.resolve('hello world'); // 不会执行 }

    • 可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

      async function f() { try { await Promise.reject('出错了'); } catch(e) { } return await Promise.resolve('hello world'); }

      f() .then(v => console.log(v)) // hello world

    • 另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误

      async function f() { await Promise.reject('出错了') .catch(e => console.log(e)); return await Promise.resolve('hello world'); }

      f() .then(v => console.log(v)) // 出错了 // hello world

使用注意点

  • 第一点,前面已经说过,await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

  • 第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

    let foo = await getFoo(); let bar = await getBar(); // 上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。 // 这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

    // 写法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 写法二 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise; // getFoo和getBar都是同时触发,这样就会缩短程序的执行时间

  • 第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。

    async function dbFuc(db) { let docs = [{}, {}, {}];

    // 报错 docs.forEach(function (doc) { await db.post(doc); }); } // 上面代码会报错,因为await用在普通函数之中了

    // 采用for循环 async function dbFuc(db) { let docs = [{}, {}, {}];

    for (let doc of docs) { await db.post(doc); } }

    // 如果确实希望多个请求并发执行,可以使用Promise.all方法。 // 当三个请求都会resolved时,下面两种写法效果相同。 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc));

    let results = await Promise.all(promises); console.log(results); }

    // 或者使用下面的写法 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc));

    let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); }

async函数实现原理

  • async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里

    async function fn(args) { // ... }

    // 等同于 function fn(args) { return spawn(function* () { // ... }); }

    // 所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

场景再现

  • 假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

    async function chainAnimationsAsync(elem, animations) { let ret = null; // 变量ret用来保存上一个动画的返回值 try { for(let anim of animations) { ret = await anim(elem); } } catch(e) { /* 忽略错误,继续执行 */ } return ret; }

  • 依次远程读取一组 URL,然后按照读取的顺序输出结果。

    // 远程操作继发.只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间 async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } }

    // 并发发出远程请求 async function logInOrder(urls) { // 并发读取远程URL const textPromises = urls.map(async url => { const response = await fetch(url); return response.text(); });

    // 按次序输出 for (const textPromise of textPromises) { console.log(await textPromise); } }

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

swap空间的增减方法

(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap

JavaScript异步编程解决方案 - HelloWorld