JS异步编程的几种方式及区别

前言

众所周知Javascript是“单线程”语言,在实际开发中我们又不得不面临异步逻辑的处理,这时候异步编程就变得十分必要。所谓异步,就是指在执行一件任务,这件任务分A、B两个阶段,执行完A阶段后,需要去做另外一个任务得到结果后才能执行B阶段。异步编程有以下几种常用方式:callbackPromiseGeneratorasync

callback函数

callback函数是指通过函数传参传递到其他执行代码的,某一块可执行代码的引用,被主函数调用后又回到主函数,如下例:

function add(a, b, callback){
    var num = a + b;
    callback(num)
}
add(1, 2, function(num){
    console.log(num); # 3
    # ...
})
复制代码

如果是有个任务队列,里面包含多个任务的话,那就需要层层嵌套了

var readFile = require('fs-readfile-promise'); # 读取文件函数
readFile(fileA, function(data) {
    readFile(fileB, function(data) {
        # ...
    })
})
复制代码

如上如果我存在n个任务,那需要层层嵌套n层,这样代码显得非常冗余庞杂并且耦合度很高,修改其中某一个函数的话,会影响上下函数代码块的逻辑。这种情况被称为“回调地狱”(callback hell)

Promise

Promise是我们常用来解决异步回调问题的方法。允许将回调函数的嵌套,改为链式调用。以上多个任务的话,可以改造成如下例子:

function add(a, b){
    return new Promise((resolve, reject) => {
        var result = a+b;
        resolve(result);
    })
}
add(10, 20).then(res => {
    return add(res, 20) # res = 30
}).then(res => {
    return add(res, 20) # res = 50
}).then(res => {
    // ...
}).catch(err => {
    // 错误处理
})
复制代码

add函数执行后会返回一个Promise,它的结果会进入then方法中,第一个参数是Promiseresolve结果,第二个参数(可选)是Promisereject结果。我们可以把回调后的逻辑在then方法中写,这样的链式写法有效的将各个事件的回调处理分割开来,使得代码结构更加清晰。另外我们可以在catch中处理报错。

如果是我们的异步请求不是按照顺序A->B->C->D这种,而是[A,B,C]->D,先并行执行A、B、C完然后在执行D,我们可以用Promise.all();

# 生成一个Promise对象的数组
const promises = [2, 3, 5].map(function (id) {
  return getJSON('/post/' + id + ".json"); # getJSON 是返回被Promise包装的数据请求函数
});

Promise.all(promises).then(function (posts) {
  # promises里面装了三个Promise
  # posts返回的是一个数组,对应三个Promise的返回数据
  # 在这可以执行D任务
}).then(res => {
    //...
}).catch(function(reason){
    //...
});
复制代码

但是Promise的代码还是有些多余的代码,比如被Promise包装的函数有一堆new Promisethencatch

Generator函数

Generator函数是ES6提供的一种异步编程解决方案,由每执行一次函数返回的是一个遍历器对象,返回的对象可以依次遍历Generator里面的每个状态,我们需要用遍历器对象的next方法来执行函数。

先来个例子:

function* foo() {
    yield 'stepone';
    yield 'steptwo';
    return 'stepthree';
}
var _foo = foo();
_foo.next(); #{value: 'stepone', done: false}
_foo.next(); #{value: 'stepone', done: false}
_foo.next(); #{value: 'stepone', done: false}
复制代码

Generator有三个特征:函数命名时function后面需要加*;函数内部有yield;外部执行需要调用next方法。每个yield会将跟在她后面的值包裹成一个对象的返回,返回的对象中包括返回值和函数运行状态,直到return,返回donetrue

如果每次运行Generator函数我们都需要用next的话,你那就太麻烦了,我们需要一个可以自动执行器。co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。 运用co模块时,yield后面只能是 Thunk函数 或者Promise对象,co函数执行完成之后返回的是Promise。如下:

var co = require('co');
var gen = function* () {
  var img1 = yield getImage('/image01');
  var img2 = yield getImage('/image02');
  ...
};
co(gen).then(function (res){
  console.log(res);
}).catch(err){
    # 错误处理
};
复制代码

co模块的任务的并行处理,等多个任务并行执行完成之后再进行下一步操作:

# 数组的写法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).then(console.log).catch(onerror);

# 对象的写法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res);
}).then(console.log).catch(onerror);
复制代码

Generator函数虽然相比Promise在写法上更加精简且逻辑清晰,但是需要额外有个运行co函数去执行,为了解决优化这个问题,async函数出现了。

async函数

async函数Generator函数的语法糖。

var co = require('co');
var gen = function* () {
  var img1 = yield getImage('/image01');
  var img2 = yield getImage('/image02');
  ...
};
co(gen).then(function (res){
  console.log(res);
}).catch(err){
    # 错误处理
};
****
#以上Generator函数可以改为
var gen = async function () {
  var img1 = await getImage('/image01');
  var img2 = await getImage('/image02');
  return [img1, img2];
  ...
};
gen().then(res => {
    console.log(res) # [img1, img2]
});
复制代码

相比Generator函数,async函数在写法上的区别就是async替代了*await替代了yield,并且async自带执行器,只需gen()即可执行函数;拥有比较好的适应性,await后面可以是Promise也可以是原始类型的值;此外async函数返回的是Promise,便于我们更好的处理返回值。

async function gen() {
  return '111';
  # 等同于 return await '111';
};
gen().then(res => {
    console.log(res) # 111
});
复制代码

如果是直接return值,这个值会自动成为then方法回调函数中的值。

async function gen() {
  var a = await getA();
  var b = await getB();
  return a + b;
};
gen().then(res => {
    console.log(res)
});
复制代码

async函数返回的Promise,必须等到函数体内所有await后面的Promise对象都执行完毕后,或者return或者抛错之后才能改变状态;也就是只有async里面的异步操作全部操作完,才能回到主任务来,并且在then方法里面继续执行主任务。

# 错误处理1
async function gen() {
    await new Promise((resolve, reject) => {
        throw new Error('出错了');
    })
};
gen().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err) # 出错了
});
# 错误处理2:如下处理,一个await任务的错误不会影响到后面await任务的执行
async function gen() {
    try{
        await new Promise((resolve, reject) => {
            throw new Error('出错了');
        })
    }catch(e){
        console.log(e); # 出错了
    }
    return Promise.resolve(1);
};
gen().then(res => {
    console.log(res) # 1
});
复制代码

错误处理如上。

async function gen() {
    # 写法一
    let result = await Promise.all([getName(), getAddress()]);
    return result;
    # 写法二
    let namePromise = getName();
    let addressPromise = getAddress();
    let name = await namePromise;
    let address = await addressPromise;
    return [name, address];
};
gen().then(res => {
    console.log(res); # 一个数组,分别是getName和getAddress返回值
})
复制代码

多个异步任务互相没有依赖关系,需要并发时,可按照如上两种方法书写。

async与Promise、Generator函数之间的对比

function chainAnimationsPromise(elem, animations) {
  # 变量ret用来保存上一个动画的返回值
  let ret = null;
  # 新建一个空的Promise
  let p = Promise.resolve();
  # 使用then方法,添加所有动画
  for(let anim of animations) {
    p = p.then(function(val) {
      ret = val;
      return anim(elem);
    });
  }
  # 返回一个部署了错误捕捉机制的Promise
  return p.catch(function(e) {
    # 错误处理
  }).then(function() {
    return ret;
  });

}
复制代码

Promise虽然很好的解决了地狱回调的问题,但是代码中有很多与语义无关的thencatch等;

function chainAnimationsGenerator(elem, animations) {
  return co(function*() {
    let ret = null;
    try {
      for(let anim of animations) {
        ret = yield anim(elem);
      }
    } catch(e) {
      # 错误处理
    }
    return ret;
  });
}
复制代码

Generator函数需要自动执行器来执行函数,且yield后面只能是Promise对象或者Thunk函数。

async function chainAnimationsAsync(elem, animations) {
  let ret = null;
  try {
    for(let anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    # 错误处理
  }
  return ret;
}
复制代码

async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。与Generator相比不需要程序员再提供一个执行器,async本身自动执行,使用起来方便简洁。

参考:ECMAScript 6 阮一峰

转载于:https://juejin.im/post/5c95d9696fb9a070fa3769a9

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值