8.ES6-ES6异步操作和async函数

ES6异步操作和async函数

一.基本概念

(1)异步
  1. 异步简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,在回过头执行第二段.这种不连续的执行,就叫做异步.
  2. 相对的,连续的执行就叫做同步.由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着.
(2)回调函数
  1. javaScript语言对异步编程的实现,就是回调函数. 所谓回调函数, 就是把任务的第二段单独写在一个函数里面, 等到重新执行这个任务的时候, 就直接调用这个函数. 它的英文名字callback,直译就是"重新调用".

  2. 读取文件进行处理

    fs.readFile('/etc/passwd', function(err, data){
        if (err) throw err;
        console.log(data);
    });
    
    • readFile函数的第二个参数,就是回调函数,也就是任务的第二段. 等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行.
    • node.js约定,回调函数的第一个参数, 必须是错误对象err(如果没有错误, 改参数就是null). 执行分成两段,在这两段之间抛出的错误, 程序无法捕捉, 只能当作参数,传入第二段.
(3)Promise
  1. 回调函数本身没有问题,它的问题出现在多个回调函数嵌套. 如果一次读取多个文件,就会出现多重嵌套.代码不是纵向发展而是横向发展,很快就会乱成一团, 无法管理. 这种情况就被称为"回调函数地狱" (callback hell)

    fs.readFile(fileA, function(err, data){
        fs.readFile(fileB, function(err,data){
            //...
        });
    });
    
  2. Promise有一种新的写法,允许将回调函数的嵌套, 改成链式调用(.then). catch方法捕捉执行过程中抛出的错误.

  3. Promise的最大问题是代码冗余, 原来的任务被Promise包装了一下, 不管什么操作, 一眼看上去都是一堆then, 原来的语义变的很不清晰.

(4)Generator函数
  1. Generator函数是协程在ES6的实现, 最大特点就是可以交出函数的执行权(即暂停执行).

  2. 整个Generator函数就是封装的异步任务, 或者说是异步任务的容器. 异步操作需要暂停的地方,都用yield语句注明. Generator函数的执行方法如下:

    function* gen(x){
        var y = yield x + 2;
        return y;
    }
    var g = gen(1);
    g.next() //{ value: 3, done: false}
    g.next() //{ value: undefined, done: true}
    
    • 上边的代码中,调用Generator函数, 会返回一个内部指针(即遍历器) g. 这是Generator函数不同于普通函数的另一个地方, 即执行它不会返回结果, 返回的是指针对象. 调用只合作呢g的next方法, 会移动内部指针(即执行异步任务的第一段), 指向第一个遇到的yield语句, 上例是执行到x + 2为止.
    • 换而言之,next方法的作用是分阶段执行Generator函数. 每次调用next方法, 会返回一个对象,表示当前阶段的信息(value属性和done属性). value属性是yield语句后面表达式的值, 表示当前阶段的值; done属性是一个布尔值, 表示Generator函数是否执行完毕,即是否还有下一个阶段.
(5)Generator函数的数据交换和错误处理
  1. Generator函数可以暂停执行和恢复执行, 这是它能封装异步任务的根本原因.除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制.

  2. next方法返回值的value属性,是Generator函数向外输出数据;next方法还可以接受参数,这是向Generator函数体内输入数据.

    function* gen(x){
        var y = yield x + 2;
        return y;
    }
    var g = gen(1);
    g.next();// {value: 3, done: false }
    g.next(2);// {value: 2, done: true}
    

    上边代码中,第一个next方法的value属性,返回表达式x+2的值(3).第二个next方法带有参数2,这个参数可以传入Generator函数,作为上一个阶段异步任务的返回结果,被函数体内的变量y接受.因此,这一步的value属性,返回的就是2(变量y的值).

  3. Generator函数内部还可以部署错误处理代码, 捕获函数体外抛出的错误.

    function* gen(x){
        try {
            var y = yield x + 2;
        } catch (e){
            console.log(e);
        }
        return y;
    }
    var g = gen(1);
    g.next();
    g.throw('出错了');
    //出错了
    

    上边代码的最后一行,Generator函数体外,使用指针对象的throw方法抛出的错误, 可以被函数体内的try …catch代码块捕获. 这意味着, 出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的.

一.async函数

(一)含义
  1. ES7提供了async函数,使得异步操作变得更加方便.async函数就是Generator函数的语法糖

依次读取两个文件操作

var fs = require('fs');
var readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function(error, data){
            if(error) reject(error);
            resolve(data);
        });
    });
};

var gen = function* (){
    var f1 = yield readFile('/etc/fstab');
    var f2 = yield readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
};

写成async函数

var asyncReadFile = async function(){
    var f1 = await readFile('/etc/fstab');
    var f2 = await readFile('/etc/shells');
    console.log(f1.toString());
    console.log(f2.toString());
};

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

async函数对Generator函数的改进:

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

    var result = asyncReadFile();
    

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

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

  3. 更广的使用性. co模块 约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

  4. 返回值是Promise. async函数的返回值是Promise对象, 这比Generator函数的返回值是Iterator(迭代程序)对象方便多了. 可以用then方法指定下一步的操作.

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

(二)语法

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

  1. async函数返回一个Promise对象.

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

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

函数f内部return命令返回的值, 会被then 方法回调函数接收到.

  1. async函数内部抛出错误, 会导致返回的Promise对象变成reject状态. 抛出的错误对象会被catch方法回调函数接受到.

    async function f() {
        throw new Error('出错了');//抛出一个错误
    }
    f().then(
    	v => console.log(v),
        e => console.log(e)
    )
    //Error: 出错了
    
  2. async函数返回的Promise对象, 必须等到nebula所有await命令的Promise对象执行完,才会发生状态改变. 也就是说, 只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数.

  3. 正常情况下,await命令后边是一个Pormise对象. 如果不是, 会被转成一个立即resolve的Promise对象.

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

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

  4. 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,效果是一样的。

  1. 只要一个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命令,可以统一放在try...catch结构中.

    async function main() {
      try {
        var val1 = await firstStep();
        var val2 = await secondStep(val1);
        var val3 = await thirdStep(val1, val2);
    
        console.log('Final: ', val3);
      }
      catch (err) {
        console.error(err);
      }
    }
    
  2. 如果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方法的回调函数被调用,它的参数就是抛出的错误对象。

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

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

(三)async函数的用法

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

指定多少毫秒之后输出一个值

function timeout(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve,ms);
    });
}
async function asyncPrint(value, ms) {
    await timeout(ms);
    console.log(value);
}

asyncPrint('hello world', 50);

代码指定50毫秒之后,输出"hello world";

用两行代码实现,封装一个函数B实现让一个函数中间暂停1s继续执行

function b(timer){
    return new Promise((resolve,reject) => {
        setTimeout(resolve,timer*1000);
    });
}
async function A(){
    console.log('程序开始执行');
    await b(1);
    console.log('程序结束执行');
}
A();

(四)注意点
  1. await命令后边的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中.
async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一种写法

async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  };
}

  1. 多个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;

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

  2. 如果确实希望多个请求并发执行,可以使用Promise.all方法。

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与Promise、Generator的比较

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

Promise的写法

function a(elem, animationis){
    //变量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(function(e){
        //忽略错误,继续执行
    }).then(function(){
    	return ret;
    });
    
    
}

接着是Generator函数的写法。

function a(elem, animations){
    return spawn(function* (){
        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;
}

参考文档

ES6官方文档

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值