ES6 —(async 函数)

1、含义

  ES2017 标准引入 async 函数,是的异步操作变得更加方便。async 是 Generator 函数的语法糖,是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await 仅此而已。 async 函数完全可以看做是多个异步操作,包装成一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。
  前文有一个 Generator 函数,依次读取两个文件。

var fs = require('fs');
function readFile(fileName){
    return new Promise(function(resolve, reject){
        fs.readFile(fileName, function(error, data){
            if(error) return reject(error);
            resolve(data);
        });
    });
}
var gen = function* (){
    var f1 = yield readFile('fileA');
    var f2 = yield readFile('fileB');
    console.log(f1.toString());
    console.log(f2.toString());
}

  写出 async 函数,就是下面这样

var asyncReadFile = async function(){
    var f1 = await readFile('fileA');
    var f2 = await readFile('fileB');
    console.log(f1.toString());
    console.log(f2.toString());
}

  async 函数对 Generator 函数的改进,体现在以下四点:
(1)内置执行器。
  Generator 函数的执行必须依靠执行器,所以才有了 co 模块,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。

asyncReadFile();

上面代码调用了 asyncReadFile 函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用 next 方法,或者使用 co 模块,才能真正执行,得到最后的结果。
(2)更好的语义
  async 和 await ,比起星号和 yield ,语义更清楚了。async 表示函数里有异步操作, await 表示后面的表达式需要等待结果。
(3)更广的适用性
  co 模块约定,yield 命令后面只能是 Thunk 函数或者Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串、布尔值,但这时等同于同步操作)。
(4)返回值是 Promise
  async 函数的返回值是 Promise 对象,这比 Generator 函数返回值是 Iterator 对象方便多了,你可以用 then 方法指定下一步的操作。

2、基本用法

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

// 函数声明
async function foo(){}
// 函数表达式
const foo = async function(){};
// 对象的方法
let obj = {async foo(){}};
obj.foo().then(...)
// Class 的方法
class Stroage{
    constructor(){
        this.cachePromise = caches.open('acatars');
    }
    async getAvatar(name){
        const cache = await this.cachePromise;
        return cache.match(`/avatars/${name}.jpg`);
    }
}
const stroage = new Stroage();
stroage.getAvatar('jake').then( ...);
// 箭头函数
const foo = async () => {};

3、语法

  async 函数的语法规则总体上比较简单,难点是错误处理机制。

3.1、返回 Promise 对象

  async 函数返回一个 Promise 对象。其中,return 语句返回的值,会成为 then 方法回调函数的参数。

async function f(){
    return 'hello world';
}
f().then(v => console.log(v)); // 输出 hello world

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

async function f(){
    throw new Error("出错了");
}
f().then(
    v => console.log(v),
    e => console.log(e));
或
f().then( v => console.log(v))
    .catch(e => console.log(e));
3.2、Promise 对象的状态变化

  async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 或抛出错误。也就是说,只有 async 函数内部的异步执行操作执行完,才会执行 then 方法指定的回调函数。

3.3、await 命令

  正常情况下,async 函数后面是一个 Promise 对象。如果不是,会转成一个立即 resolve 的 Promise 对象。

async function f(){
    return await 123;
}
f().then(console.log) // 123

  await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法或then方法的第二个回调函数接受到。

async function f(){
    await Promise.reject("出错了");
}
f().then(
    v => console.log(v),
    e => console.log(e));
或
f().then( v => console.log(v))
    .catch(e => console.log(e));

注意:
  (1)如果在 await 前面加上 return 效果是一样的。
  (2)只要有一个 await 语句后面的 Promise 变为 reject ,那么整个 async 函数都会中断执行。
  (3)如果想要 await 语句后的 Promise 发生错误后,不影响后面语句执行,则将会出错的 await 语句放置 try...catch 语句块中。

async function f(){
    try{
        await Promise.reject("出错了");
    } catch (e){
        console.log(`内部捕获${e}`);
    }
    return await Promise.resolve("hello world");
}
f().then(console.log).catch(e => console.log(`外部捕获${e}`));
// 输出结果
内部捕获出错了
hello world

或者, await 后面的 Promise 对象再跟一个 catch 方法,处理前面可能出现的错误。

async function f(){ 
    await Promise.reject("出错了")
            .catch ((e) => console.log(`内部捕获${e}`));
    return await Promise.resolve("hello world");
}
f().then(console.log).catch(e => console.log(`外部捕获${e}`));

  如果 await 后面的异步操作出错,那么等同于 async 函数返回的 Promise 对象被 reject。

33.4、使用注意点

  (1)await 命令后面的 Promise 对象,运行结果可能是 reject,所以最好把 await 放在 try...catch 代码块中,或者给后面的 Promise 对象加 catch 方法。
  (2)多个 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 都会同时触发,这样就会缩短程序执行时间。
  (3)await 命令只能在 async 函数之中,如果用在普通函数,就会报错。

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

    // 报错
    docs.forEach(function (doc){
        await db.post(doc);
    });
}

上面代码会报错,因为 await 位于 forEach 的回调函数中,但是该函数不是 async 函数,如果将 forEach 方法的参数改成 async 函数,也有问题。

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

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

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

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

async function dbFunc(db){
    let docs = [{},{},{}];
    let promises = docs.map((doc) => db.post(doc));
    let results = await Promises.all(promises);
    console.log(results);
}

async function dbFunc(db){
    let docs = [{},{},{}];
    let promises = docs.map((doc) => db.post(doc));
    let results = [];
    for(let promise of promises){
        result.push(await promise);
    }
    console.log(results)
}

  (4)await 表达式的返回值是后面 Promise 对象的 resolve 函数的参数。而 async 函数返回的 Promise 对象的 then 方法的参数是 async 函数内部 return 语句的返回值。

async function asyncPrint( ) {
  let value = await new Promise(function(resolve, reject){
    resolve("hello");
  });
  console.log(value);
  return "world";
}
asyncPrint().then(console.log); 
输出
hello
world

4、async 函数的实现原理

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

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

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

function spawn(genF){
    return Promise(function(resolve, reject){
        var gen = genF();
        fucntion step(nextF){
            try{
                var next = nextF();
            } catch (e){
                return reject(e);
            }
            if(next.done){
                return resolve(next.value);
            }
            Promise.resolve(
                (next.value).then(
                    function(v){
                        step(function(){ return gen.next(v)});
                    },
                    function(e){
                        step(function(){ return gen.throw(e)});
                    }
                );
            );
        }
        step(function(){ return gen.next(undefined);});
    });
}

5、与其他异步操作处理方法的比较

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

Promise 写法

function chainAnimationsPromise(elem,animations){
    // 变量 ret 用来保存上一个动画的返回值
    var ret = null;
    // 新建一个空的 Promise
    var p = Promise.resolve();
    // 使用 then 方法,添加所有动画
    for(var anim of animations){
        p = p.then(function(val){
            ret = val;
            return anim(elem);
        });
    }

    // 返回一个部署了错误捕捉机制的 Promise
    return p.catch(fucntion(e){  })
            .then(() => return ret);
}

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

Generator 写法

function chainAnimationsGenerator(elem, animations){
    return spawn(funciton* (){
            var ret = null;
            try{
                for(var anim of animations){
                    ret = yield anim(elem);
                }
            } catch(e){ ...}
            return ret;
    });
}

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

async 写法

async function chainAnimationsAsync(elem, animations){
    var ret = null;
    try{
        for(var anim of animations){
            ret = await anim(elem);
        }
    }catch(e){ ...}
    return ret;
}

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

阮一峰:ECMAScript 6入门

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值