资讯专栏INFORMATION COLUMN

Javascript异步编程:Callback、Promise、Generator

dadong / 1887人阅读

摘要:异步过程控制了解异步的意义之后,我们来对比目前主流几种异步过程控制方法,探讨一下异步编程的最佳实践。结语希望本文对大家有点帮助,能更深刻的理解异步编程,能写出更优雅更高效的代码。

同步和异步(Synchronous and Asynchronous)

了解javascript的同学想必对同步和异步的概念应该都很熟悉了,如果还有不熟悉的同学,我这里举个形象的例子,比如我们早上起床后要干三件事:烧水、洗脸、吃早饭,同步相当于我们先烧水,水烧开了再洗脸,洗完脸再吃早饭,三件事顺序执行,一件干完了再干下一件;而异步相当于我们在烧水的同时吃早饭(不洗脸就吃早饭不太卫生),吃完早饭再洗脸。显然异步比同步更加高效,省去了很多等待的时间,同步过程的执行时间取决于所有行为的总和,而异步过程的执行时间只取决于最长的那个行为,如下图所示:

由于Javascript是单线程的,同时只能处理一件事,在上面的例子中这个单线程就是“我”,比如我不能同时洗脸和吃早饭一样。所以为了让执行效率提高,我们要尽量让这个线程一直处于忙碌状态而不是闲置状态,就像我们不用干等烧水,可以同时去做其他事情,而烧水由系统的其他线程去处理(该线程不属于Javascript)。在计算机的世界中,很多I/O密集型的操作是需要等待的,比如网络请求、文件读写等,所以异步方法在处理这些操作会更加得心应手。

异步过程控制

了解异步的意义之后,我们来对比目前主流几种异步过程控制方法,探讨一下异步编程的最佳实践。

1. Callback 误区

首先callback和异步没有必然联系,callback本质就是类型为function的函数参数,对于该callback是同步还是异步执行则取决于函数本身。虽然callback常用于异步方法的回调,但其实有不少同步方法也可以传入callback,比如最常见的数组的forEach方法:

var arr = [1, 2, 3];
arr.forEach(function (val) {
  console.log(val);
});
console.log("finish");

// 打印结果:1,2,3,finish

类似的还有数组的map, filter, reduce等很多方法。

异步Callback

常见的异步callback如setTimeout中的回调:

setTimeout(function () {
  console.log("time"s up");
}, 1000);
console.log("finish");

// 打印结果:finish, time"s up

如果我们将延迟时间改为0,打印结果仍将是finish, time"s up,因为异步callback会等函数中的同步方法都执行完成后再执行。

Callback Hell

在实际项目中我们经常会遇到这样的问题:下一步操作依赖于上一步操作的结果,上一步操作又依赖于上上步操作,而每一步操作都是异步的。。这样递进的层级多了会形成很多层callback嵌套,导致代码可读性和可维护性变的很差,形成所谓的Callback Hell,类似这样:

step1(param, function (result1) {
  step2(result1, function (result2) {
    step3(result2, function (result3) {
      step4(result3, function (result4) {
        done(result4);
      })
    })
  })
})

当然在不放弃使用callback的前提下,上面的代码还是有优化空间的,我们可以将它重新组织一下:

step1(param, callbac1);

function callback1(result1){
  step2(result1, callback2);
}

function callback2(result2){
  step3(result2, callback3);
}

function callback3(result3){
  step4(result3, callback4);
}

function callback4(result4){
  done(result4);
}

相当于将Callback Hell的横向深度转化为代码的纵向高度,变得更接近于我们习惯的由上到下的同步调用, 复杂度没有变,只是看起来更清晰了,缺点就是要定义额外的函数、变量。将这一思想进一步延伸就有了下面的Promise。

2. Promise

Promise中文译为“承诺”,在Javascript中是一个抽象的概念,代表当前没有实现,但未来的某个时间点会(也可能不会)实现的一件事。举个实例化的例子:早上烧水,我给你一个承诺(Promise),十分钟后水能烧开,如果一切正常,10分钟之后水确实能烧开,代表这个promise兑现了(fullfilled),但是如果中途停电了,10分钟水没烧开,那这个promise兑现失败(rejected)。用代码可以表示为:

const boilWaterInTenMins = new Promise(function (resolve, reject) {
  boiler.work(function (timeSpent) {
    if (timeSpent <= 10) {
      resolve();
    } else {
      reject();
    }
  });
});
兼容性

如果想提高浏览器对Promise的兼容性可以使用babel或者第三方的实现(参考 github awesome promise)

Promise Chaining

我们再来看Promise对于异步过程控制有怎样的提升,还基于上面Callback Hell的例子,如果用Promise实现会如何呢?

首先我们需要将step1 ~ done 的函数用Promise实现(即返回一个Promise),然后进行一连串的链式调用就可以了:

stepOne(param)
  .then((result1) => { return step2(result1) })
  .then((result2) => { return step3(result2) })
  .then((result3) => { return step4(result3) })
  .then((result4) => { return done(result4) })
  .catch(err => handleError(err));

是不是简单很多!

Async/Await

如果你不太习惯Promise的调用方式,那我们可以用async/await将其转化成更接近同步调用的方式:

async function main() {
  try {
    var result1 = await step1(param);
    var result2 = await step2(result1);
    var result3 = await step3(result2);
    var result4 = await step4(result3);
    done(result4);
  } catch (err) {
    handleError(err);
  }
}

main();
3. Generator

Generator是一个更加抽象的概念,要弄懂什么是Generator首先要理解另外几个概念Iterable Protocol(可迭代协议),Iterator Protocol(迭代器协议)和 Iterator(迭代器)。

Iterable Protocol

Iterable Protocol 的特点可以概括为:

用于定义javascript对象的迭代行为

对象本身或者原型链上需要有一个名为Symbol.iterator的方法

该方法不接收任何参数,且返回一个Iterator

Iterable的对象可以使用for...of遍历

Javascript Array就实现了Iterable Protocol,除了常规的取值方式,我们也可以利用array的Symbol.iterator

var arr = [1, 2, 3];
var iterator = arr[Symbol.iterator]();
iterator.next(); // {value: 1, done: false}

我们也可以修改Array默认的迭代方式,比如返回两倍的值:

Array.prototype[Symbol.iterator] = function () {
  var nextIndex = 0;
  var self = this;
  return {
    next: function () {
      return nextIndex < self.length ?
        { value: self[nextIndex++] * 2, done: false } :
        { done: true }
    }
  };
}

for(let el of [1, 2, 3]){
  console.log(el);
}
// 输出:2,4,6
Iterator Protocol

Iterator Protocol 的特点可以概括为:

一种产生一个序列值(有限或无限)的标准方式

实现一个next方法

next方法返回的对象为 {value: any, done: boolean}

value为返回值,donetruevalue可以省略

donetrue表示迭代结束,此时value表示最终返回值

donefalse,则可以继续迭代,产生下一个值

Iterator

显然Iterator就是实现了Iterator Protocol的对象。

Generator

理解上面几个概念后,理解Generator就简单多了,generator的特点可概括为:

同时实现Iterable Protocol和Iterator Protocol,所以Genrator即是一个iterable的对象又是一个iterator

Generator由 generator function 生成

最简单的generator function比如:

function* gen() {
  var x = yield 5 + 6;
}

var myGen = gen(); // myGen 就是一个generator

我们可以调用next方法来获得yield表达式的值:

myGen.next(); // { value: 11, done: false }

但此时x并没有被赋值,可以想象成javascript执行完 yield 5 + 6 就停住了,为了继续执行赋值操作我们需要再次调用next,并将得到的值回传:

function* gen() {
  var x = yield 5 + 6;
  console.log(x); // 11
}

var myGen = gen();
console.log(myGen.next()); // { value: 11, done: false }
console.log(myGen.next(11)); // { value: undefined, done: true }

说了这么多,generator和异步到底有什么关系呢?我们来看Promise + Generator 实现的异步控制(step1 ~ done 返回Promise):

genWrap(function* () {
  var result1 = yield step1(param);
  var result2 = yield step2(result1);
  var result3 = yield step3(result2);
  var result4 = yield step4(result3);
  var result5 = yield done(result4);
});

function genWrap(genFunc) {
  var generator = genFunc();

  function handle(yielded) {
    if (!yielded.done) {
      yielded.value.then(function (result) {
        return handle(generator.next(result));
      });
    }
  }

  return handle(generator.next());
}

和async/await类似,这种实现也将异步方法转化成了同步的写法,实际上这就是 ES7中async/await的实现原理(将genWrap替换为async,将yield替换成await)。

结语

希望本文对大家有点帮助,能更深刻的理解javascript异步编程,能写出更优雅更高效的代码。有错误欢迎指正。新年快乐!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101542.html

相关文章

  • 我了解到的JavaScript异步编程

    摘要:接下来我们看下三类异步编程的实现。事件监听事件发布订阅事件监听是一种非常常见的异步编程模式,它是一种典型的逻辑分离方式,对代码解耦很有用处。 一、 一道面试题 前段时间面试,考察比较多的是js异步编程方面的相关知识点,如今,正好轮到自己分享技术,所以想把js异步编程学习下,做个总结。下面这个demo 概括了大多数面试过程中遇到的问题: for(var i = 0; i < 3; i++...

    RichardXG 评论0 收藏0
  • Node.js 异步异闻录

    摘要:的异步完成整个异步环节的有事件循环观察者请求对象以及线程池。执行回调组装好请求对象送入线程池等待执行,实际上是完成了异步的第一部分,回调通知是第二部分。异步编程是首个将异步大规模带到应用层面的平台。 showImg(https://segmentfault.com/img/remote/1460000011303472); 本文首发在个人博客:http://muyunyun.cn/po...

    zzbo 评论0 收藏0
  • Javascript中的异步编程

    摘要:接下来,我们一起来看看中的异步编程,具体有哪几种。实现异步编程的方法一回调函数上面不止一次提到了回调函数。它是异步编程中,最基本的方法。四对象接下来,我们聊聊与相关的异步编程方法,对象。 showImg(https://segmentfault.com/img/bVbneWy?w=1600&h=1200); 前言 最近,小伙伴S 问了我一段代码: const funB = (value...

    wemall 评论0 收藏0
  • 谈谈JavaScript异步代码优化

    摘要:异步问题回调地狱首先,我们来看下异步编程中最常见的一种问题,便是回调地狱。同时使用也是异步编程最基础和核心的一种解决思路。基于,目前也被广泛运用,其是异步编程的一种解决方案,比传统的回调函数解决方案更合理和强大。 关于 微信公众号:前端呼啦圈(Love-FED) 我的博客:劳卜的博客 知乎专栏:前端呼啦圈 前言 在实际编码中,我们经常会遇到Javascript代码异步执行的场景...

    chnmagnus 评论0 收藏0
  • 探索Javascript 异步编程

    摘要:因为浏览器环境里是单线程的,所以异步编程在前端领域尤为重要。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案函数体内外的数据交换和错误处理机制。 showImg(https://segmentfault.com/img/bVz9Cy); 在我们日常编码中,需要异步的场景很多,比如读取文件内容、获取远程数据、发送数据到服务端等。因为浏览器环境里Javascript是单线程的,...

    Salamander 评论0 收藏0

发表评论

0条评论

dadong

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<