zoukankan      html  css  js  c++  java
  • ES6学习笔记(十六)async函数

    1.含义

    ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

    async 函数是什么?一句话,它就是 Generator 函数的语法糖,号称异步的终极解决方案。

    前文有一个 Generator 函数,依次读取两个文件。

     1 const fs = require('fs');
     2 
     3 const readFile = function (fileName) {
     4   return new Promise(function (resolve, reject) {
     5     fs.readFile(fileName, function(error, data) {
     6       if (error) return reject(error);
     7       resolve(data);
     8     });
     9   });
    10 };
    11 
    12 const gen = function* () {
    13   const f1 = yield readFile('/etc/fstab');
    14   const f2 = yield readFile('/etc/shells');
    15   console.log(f1.toString());
    16   console.log(f2.toString());
    17 };

    上面代码的函数gen可以写成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 函数的改进,体现在以下四点。

    (1)内置执行器。

    Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

    asyncReadFile();

    上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

    (2)更好的语义。

    asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

    (3)更广的适用性。

    co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

    (4)返回值是 Promise。

    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

    进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

    2.基本用法

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

    async function getStockPriceByName(name) {
      const symbol = await getStockSymbol(name);
      const stockPrice = await getStockPrice(symbol);
      return stockPrice;
    }
    
    getStockPriceByName('goog').then(function (result) {
      console.log(result);
    });

    上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。

     1 function timeout(ms) {
     2   return new Promise((resolve) => {
     3     setTimeout(resolve, ms);
     4   });
     5 }
     6 
     7 async function asyncPrint(value, ms) {
     8   await timeout(ms);
     9   console.log(value);
    10 }
    11 
    12 asyncPrint('hello world', 50);

    上面代码指定 50 毫秒以后,输出hello world

    由于async函数返回的是 Promise 对象,可以作为await命令的参数。所以,上面的例子也可以写成下面的形式。

     1 async function timeout(ms) {
     2   await new Promise((resolve) => {
     3     setTimeout(resolve, ms);
     4   });
     5 }
     6 
     7 async function asyncPrint(value, ms) {
     8   await timeout(ms);
     9   console.log(value);
    10 }
    11 
    12 asyncPrint('hello world', 50);

    async 函数有多种使用形式。

     1 // 函数声明
     2 async function foo() {}
     3 
     4 // 函数表达式
     5 const foo = async function () {};
     6 
     7 // 对象的方法
     8 let obj = { async foo() {} };
     9 obj.foo().then(...)
    10 
    11 // Class 的方法
    12 class Storage {
    13   constructor() {
    14     this.cachePromise = caches.open('avatars');
    15   }
    16 
    17   async getAvatar(name) {
    18     const cache = await this.cachePromise;
    19     return cache.match(`/avatars/${name}.jpg`);
    20   }
    21 }
    22 
    23 const storage = new Storage();
    24 storage.getAvatar('jake').then(…);
    25 
    26 // 箭头函数
    27 const foo = async () => {};

    3.语法

    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>([sS]+)</title>/i)[1];
    }
    getTitle('https://tc39.github.io/ecma262/').then(console.log)
    // "ECMAScript 2017 Language Specification"

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

    await 命令

    正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

    async function f() {
      // 等同于
      // return 123;
      return await 123;
    }
    
    f().then(v => console.log(v))
    // 123

    上面代码中,await命令的参数是数值123,这时等同于return 123

    另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。

     1 class Sleep {
     2   constructor(timeout) {
     3     this.timeout = timeout;
     4   }
     5   then(resolve, reject) {
     6     const startTime = Date.now();
     7     setTimeout(
     8       () => resolve(Date.now() - startTime),
     9       this.timeout
    10     );
    11   }
    12 }
    13 
    14 (async () => {
    15   const actualTime = await new Sleep(1000);
    16   console.log(actualTime);
    17 })();

    上面代码中,await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。

    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语句是不会执行的,因为第一个await语句状态变成了reject

    有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个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后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

    async function f() {
      await new Promise(function (resolve, reject) {
        throw new Error('出错了');
      });
    }
    
    f()
    .then(v => console.log(v))
    .catch(e => console.log(e))
    // Error:出错了

    上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。具体的执行机制,可以参考后文的“async 函数的实现原理”。

    防止出错的方法,也是将其放在try...catch代码块之中。

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

    如果有多个await命令,可以统一放在try...catch结构中。

     1 async function main() {
     2   try {
     3     const val1 = await firstStep();
     4     const val2 = await secondStep(val1);
     5     const val3 = await thirdStep(val1, val2);
     6 
     7     console.log('Final: ', val3);
     8   }
     9   catch (err) {
    10     console.error(err);
    11   }
    12 }

    下面的例子使用try...catch结构,实现多次重复尝试。

     1 const superagent = require('superagent');
     2 const NUM_RETRIES = 3;
     3 
     4 async function test() {
     5   let i;
     6   for (i = 0; i < NUM_RETRIES; ++i) {
     7     try {
     8       await superagent.get('http://google.com/this-throws-an-error');
     9       break;
    10     } catch(err) {}
    11   }
    12   console.log(i); // 3
    13 }
    14 
    15 test();

    上面代码中,如果await操作成功,就会使用break语句退出循环;如果失败,会被catch语句捕捉,然后进入下一轮循环。这个操作很神奇啊。

    使用注意点 

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

     1 async function myFunction() {
     2   try {
     3     await somethingThatReturnsAPromise();
     4   } catch (err) {
     5     console.log(err);
     6   }
     7 }
     8 
     9 // 另一种写法
    10 
    11 async function myFunction() {
    12   await somethingThatReturnsAPromise()
    13   .catch(function (err) {
    14     console.log(err);
    15   });
    16 }

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

    let foo = await getFoo();
    let bar = await getBar();

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

    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);
    
    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;

    上面两种写法,getFoogetBar都是同时触发,这样就会缩短程序的执行时间。

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

    async function dbFuc(db) {
      let docs = [{}, {}, {}];
    
      // 报错
      docs.forEach(function (doc) {
        await db.post(doc);
      });
    }

    上面代码会报错,因为await用在普通函数之中了。但是,如果将forEach方法的参数改成async函数,也有问题。

    function dbFuc(db) { //这里不需要 async
      let docs = [{}, {}, {}];
    
      // 可能得到错误结果
      docs.forEach(async function (doc) {
        await db.post(doc);
      });
    }

    上面代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。

    async function dbFuc(db) {
      let docs = [{}, {}, {}];
    
      for (let doc of docs) {
        await db.post(doc);
      }
    }

    如果确实希望多个请求并发执行,可以使用Promise.all方法。当三个请求都会resolved时,下面两种写法效果相同。

     1 async function dbFuc(db) {
     2   let docs = [{}, {}, {}];
     3   let promises = docs.map((doc) => db.post(doc));
     4 
     5   let results = await Promise.all(promises);
     6   console.log(results);
     7 }
     8 
     9 // 或者使用下面的写法
    10 
    11 async function dbFuc(db) {
    12   let docs = [{}, {}, {}];
    13   let promises = docs.map((doc) => db.post(doc));
    14 
    15   let results = [];
    16   for (let promise of promises) {
    17     results.push(await promise);
    18   }
    19   console.log(results);
    20 }

    目前,esm模块加载器支持顶层await,即await命令可以不放在 async 函数里面,直接使用。

    // async 函数的写法
    const start = async () => {
      const res = await fetch('google.com');
      return res.text();
    };
    
    start().then(console.log);
    
    // 顶层 await 的写法
    const res = await fetch('google.com');
    console.log(await res.text());

    上面代码中,第二种写法的脚本必须使用esm加载器,才会生效。

    第四点,async 函数可以保留运行堆栈。

    const a = () => {
      b().then(() => c());
    };

    上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()c()报错,错误堆栈将不包括a()

    现在将这个例子改成async函数。

    const a = async () => {
      await b();
      c();
    };

    上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()c()报错,错误堆栈将包括a()

    4.async 函数的实现原理

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

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

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

    下面给出spawn函数的实现,基本就是前文自动执行器的翻版。

     1 function spawn(genF) {
     2   return new Promise(function(resolve, reject) {
     3     const gen = genF();
     4     function step(nextF) {
     5       let next;
     6       try {
     7         next = nextF();
     8       } catch(e) {
     9         return reject(e);
    10       }
    11       if(next.done) {
    12         return resolve(next.value);
    13       }
    14       Promise.resolve(next.value).then(function(v) {
    15         step(function() { return gen.next(v); });
    16       }, function(e) {
    17         step(function() { return gen.throw(e); });
    18       });
    19     }
    20     step(function() { return gen.next(undefined); });
    21   });
    22 }

    5.与其他异步处理方法的比较

    我们通过一个例子,来看 async 函数与 PromiseGenerator 函数的比较

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

    首先是 Promise 的写法。

     1 function chainAnimationsPromise(elem, animations) {
     2 
     3   // 变量ret用来保存上一个动画的返回值
     4   let ret = null;
     5 
     6   // 新建一个空的Promise
     7   let p = Promise.resolve();
     8 
     9   // 使用then方法,添加所有动画
    10   for(let anim of animations) {
    11     p = p.then(function(val) {
    12       ret = val;
    13       return anim(elem);
    14     });
    15   }
    16 
    17   // 返回一个部署了错误捕捉机制的Promise
    18   return p.catch(function(e) {
    19     /* 忽略错误,继续执行 */
    20   }).then(function() {
    21     return ret;
    22   });
    23 
    24 }

    虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(thencatch等等),操作本身的语义反而不容易看出来。

    接着是 Generator 函数的写法。

     1 function chainAnimationsGenerator(elem, animations) {
     2 
     3   return spawn(function*() {
     4     let ret = null;
     5     try {
     6       for(let anim of animations) {
     7         ret = yield anim(elem);
     8       }
     9     } catch(e) {
    10       /* 忽略错误,继续执行 */
    11     }
    12     return ret;
    13   });
    14 
    15 }

    上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn函数就是自动执行器,它返回一个 Promise 对象,而且必须保证yield语句后面的表达式,必须返回一个 Promise。

    最后是 async 函数的写法。

     1 async function chainAnimationsAsync(elem, animations) {
     2   let ret = null;
     3   try {
     4     for(let anim of animations) {
     5       ret = await anim(elem);
     6     }
     7   } catch(e) {
     8     /* 忽略错误,继续执行 */
     9   }
    10   return ret;
    11 }

    可以看到 Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用 Generator 写法,自动执行器需要用户自己提供。

    6.实例:按顺序完成异步操作

    实际开发中,经常遇到一组异步操作,需要按照顺序完成。比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。

    Promise 的写法如下。

     1 function logInOrder(urls) {
     2   // 远程读取所有URL
     3   const textPromises = urls.map(url => {
     4     return fetch(url).then(response => response.text());
     5   });
     6 
     7   // 按次序输出
     8   textPromises.reduce((chain, textPromise) => {
     9     return chain.then(() => textPromise)
    10       .then(text => console.log(text));
    11   }, Promise.resolve());
    12 }

    上面代码使用fetch方法,同时远程读取一组 URL。每个fetch操作都返回一个 Promise 对象,放入textPromises数组。然后,reduce方法依次处理每个 Promise 对象,然后使用then,将所有 Promise 对象连起来,因此就可以依次输出结果。

    这种写法不太直观,可读性比较差。下面是 async 函数实现。

    async function logInOrder(urls) {
      for (const url of urls) {
        const response = await fetch(url);
        console.log(await response.text());
      }
    }

    上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求

     1 async function logInOrder(urls) {
     2   // 并发读取远程URL
     3   const textPromises = urls.map(async url => {
     4     const response = await fetch(url);
     5     return response.text();
     6   });
     7 
     8   // 按次序输出
     9   for (const textPromise of textPromises) {
    10     console.log(await textPromise);
    11   }
    12 }

    上面代码中,虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for..of循环内部使用了await,因此实现了按顺序输出。

    7.异步遍历器

    《遍历器》一章说过,Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的next方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。next方法返回的对象的结构是{value, done},其中value表示当前的数据的值,done是一个布尔值,表示遍历是否结束。

    这里隐含着一个规定,next方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行next方法,就必须同步地得到valuedone这两个属性。如果遍历指针正好指向同步操作,当然没有问题,但对于异步操作,就不太合适了。目前的解决方法是,Generator 函数里面的异步操作,返回一个 Thunk 函数或者 Promise 对象,即value属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而done属性则还是同步产生的。

    ES2018 引入了“异步遍历器”(Async Iterator),为异步操作提供原生的遍历器接口,即valuedone这两个属性都是异步产生。

    异步遍历的接口

    异步遍历器的最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象。

    asyncIterator
      .next()
      .then(
        ({ value, done }) => /* ... */
      );

    上面代码中,asyncIterator是一个异步遍历器,调用next方法以后,返回一个 Promise 对象。因此,可以使用then方法指定,这个 Promise 对象的状态变为resolve以后的回调函数。回调函数的参数,则是一个具有valuedone两个属性的对象,这个跟同步遍历器是一样的。

    我们知道,一个对象的同步遍历器的接口,部署在Symbol.iterator属性上面。同样地,对象的异步遍历器接口,部署在Symbol.asyncIterator属性上面。不管是什么样的对象,只要它的Symbol.asyncIterator属性有值,就表示应该对它进行异步遍历。

    下面是一个异步遍历器的例子。

     1 const asyncIterable = createAsyncIterable(['a', 'b']);
     2 const asyncIterator = asyncIterable[Symbol.asyncIterator]();
     3 
     4 asyncIterator
     5 .next()
     6 .then(iterResult1 => {
     7   console.log(iterResult1); // { value: 'a', done: false }
     8   return asyncIterator.next();
     9 })
    10 .then(iterResult2 => {
    11   console.log(iterResult2); // { value: 'b', done: false }
    12   return asyncIterator.next();
    13 })
    14 .then(iterResult3 => {
    15   console.log(iterResult3); // { value: undefined, done: true }
    16 });

    了解一下,异步遍历器不再深究。

    for await...of

    前面介绍过,for...of循环用于遍历同步的 Iterator 接口。新引入的for await...of循环,则是用于遍历异步的 Iterator 接口。

    async function f() {
      for await (const x of createAsyncIterable(['a', 'b'])) {
        console.log(x);
      }
    }
    // a
    // b

    异步 Generator 函数

     就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。

    yield* 语句

    yield*语句也可以跟一个异步遍历器。

    async function* gen1() {
      yield 'a';
      yield 'b';
      return 2;
    }
    
    async function* gen2() {
      // result 最终会等于 2
      const result = yield* gen1();
    }

    上面代码中,gen2函数里面的result变量,最后的值是2

    与同步 Generator 函数一样,for await...of循环会展开yield*

    没有最好的方法,只有最适合的方法。

  • 相关阅读:
    帝国 标签模板 使用程序代码 去除html标记 并 截取字符串
    iis6 伪静态 iis配置方法 【图解】
    您来自的链接不存在 帝国CMS
    帝国cms Warning: Cannot modify header information headers already sent by...错误【解决方法】
    .fr域名注册 51元注册.fr域名
    帝国网站管理系统 恢复栏目目录 建立目录不成功!请检查目录权限 Godaddy Windows 主机
    星外虚拟主机管理平台 开通数据库 出现Microsoft OLE DB Provider for SQL Server 错误 '8004' 从字符串向 datetime 转换失败
    ASP.NET 自定义控件学习研究
    CSS层叠样式表之CSS解析机制的优先级
    ASP.NET程序员工作面试网络收藏夹
  • 原文地址:https://www.cnblogs.com/jixiaohua/p/10674686.html
Copyright © 2011-2022 走看看