资讯专栏INFORMATION COLUMN

那些年在异步代码上所做的努力

rubyshen / 242人阅读

摘要:当迭代器运行后,会返回第一次运行到或者时的返回值以格式进行返回。而现在了后面的方法必须是总结总结一下异步代码的发展过程回调函数最基本的解决方法,将异步结束函数以参数的方式传递到异步函数中,也就是使用回调函数的方式来实现异步逻辑。

介绍

写过JS代码的同学应该都知道,JS是单线程的,当出现异步逻辑时,就需要使用一些技巧来实现。最常见的方法就是使用回调方法。

回调方法

比如我们要实现一个功能:1s后运行逻辑,再过3s运行另外一段逻辑。使用回调方法可以这样写:

// 方法一 ,嵌套回调
// 模拟异步逻辑
function delay(time, callback) {
  setTimeout(function() {
    callback(time);
  }, time);
}

// 过1000ms后输出日志,再过3000ms后输出日志。
delay(1000, function(time1) {
  console.log("%s ms后运行", time1);
  delay(3000, function(time2) {
    console.log("%s ms后运行", time2);
  });
});

运行上面的代码,可以得到我们想要的结果:1s后输出了日志,再过3s又输出日志。但是如果逻辑复杂下去,会出现很深的回调方法嵌套问题,使得代码不可维护。为了使异步代码更清晰,就出现了Promise。

Promise

还是拿上面的例子来实现:

// 方法二 promise
function sleep(time) {
  return function() {
    return new Promise((resolve) => setTimeout(resolve, time));
  };
}

sleep(1000)().then(function() {
  console.log("1000ms后运行");
}).then(sleep(3000)).then(function() {
  console.log("3000ms后运行");
});

运行代码后,还是可以得到和使用回调方法实现时一样的效果。分析Promise实现的代码,可以发现,它对嵌套回调进行了改进,将原先横向发展的代码,改成了纵向发展,但是并没有解决本质问题。使用Promise的代码,异步逻辑变成了一堆then方法,语义还是不够清晰。那么有没有更好的写法呢?
在ES6中,提出了generator方法,可以做到使用同步代码来实现异步功能。下面我们来重点介绍下generator。

generator generator介绍

generator是ES6中新提出的函数类型,最大的特点就是函数的执行可以被控制。
举一个最简单的generator例子:

function *idMarker() {
  var i = 1;
  while(true) {
    yield i++;
  }
}

var ids = idMarker();
ids.next().value; // 1
ids.next().value; // 2
ids.next().value; // 3

上面的例子中,当运行了generator函数idMarker()后,函数体并没有开始运行,而是直接返回了一个迭代器ids。当迭代器运行next()后,会返回第一次运行到yield或者return时的返回值以格式{value:1,done:false}进行返回。其中value就是yield后面的值,done表示当前迭代器还没有结束迭代。从上面的代码可以看出,generator函数的运行过程可以在外边被控制,也就是说使用generator可以做到以下功能的实现:

运行A逻辑;
通过yield语句,暂停A,并开始异步逻辑B;
等B运行完成,再继续运行A;
generator实现异步逻辑

从上面的例子可以看出,使用generator可以解决Promise的问题,代码如下:

// 方法三 generator
function sleep(time) {
  return new Promise((resolve) => setTimeout(function() {
    resolve(time);
  }, time));
}

function run(gen) {
  return new Promise(function(resolve, reject) {
    gen = gen();
    onFulfilled();
    function onFulfilled(res) {
      var ret = gen.next(res);
      next(ret);
    }
    function next(ret) {
      if (ret.done) return resolve(ret.value);
      ret.value.then(onFulfilled);
    }
  });
}

function *syncFn() {
  var d1 = yield sleep(1000);
  console.log("%s ms后运行", d1);
  var d2 = yield sleep(3000);
  console.log("%s ms后运行", d2);
}

run(syncFn);

下面我们来分析下上面代码的逻辑:
首先运行run(syncFn);, 会运行到gen = gen();。这个时候gen变成了generator的迭代器。
通过运行onFulfilled中的gen.next(res),代码开始运行syncFn中的sleep(1000)。此时,gen.next(res)会返回syncFn中yield获得的值,即sleep方法返回的promise对象,并在next方法中对该promise设置了then(onFulfilled)。 也就是说,当sleep(1000)返回的promise运行结束后,会运行then中的onFulfilled。而onFulfilled会继续运行syncFn的迭代器。这样子,虽然syncFn中的异步逻辑,就会逐步执行了。

co

co是一个使用generator和yield来解决异步嵌套的工具库。它的实现类似于上面例子中的run方法。向co传入一个generator方法后,就会开始逐步执行其中的异步逻辑。
举个例子,我们需要读取三个文件并将文件内容依次打印出来。使用co的写法就是:

var fs = require("fs");
var co = require("co");

function readFile(path) {
  return function (cb) {
    fs.readFile(path, {encoding: "utf8"}, cb);
  };
}

co(function* () {
  var dataA = yield readFile("a.js");
  console.log(dataA);
  var dataB = yield readFile("b.js");
  console.log(dataB);
  var dataC = yield readFile("c.js");
  console.log(dataC);
}).catch(function (err) {
  console.log(err);
});
yield 与 yield*

yield语句还可以使用yield*,两则存在细微的差别。举个例子:

// 数组
function* GenFunc() {
  yield [1, 2];
  yield* [3, 4];
  yield "56";
  yield* "78";
}
var gen = GenFunc();
console.log(gen.next().value); // [1, 2]
console.log(gen.next().value); // 3
console.log(gen.next().value); // 4

// generator函数
function* Gen1() {
  yield 2;
  yield 3;
}
function* Gen2() {
  yield 1;
  yield* Gen1();
  yield 4;
}
var g2 = Gen2();
console.log(g2.next().value); // 1
console.log(g2.next().value); // 2
console.log(g2.next().value); // 3
console.log(g2.next().value); // 4

// 对象
function* GenFunc() {
  yield {a: "1", b: "2"};
  yield* {a: "1", b: "2"};
}
var gen = GenFunc();
console.log(gen.next()); // { value: { a: "1", b: "2" }, done: false }
console.log(gen.next()); // TypeError: undefined is not a function

从上面几个例子可以看出,yield 与 yield 的区别在于:yield 只是返回右侧对象的值,而 yield 则将函数委托(delegate)到另一个生成器( Generator)或可迭代的对象(如字符串、数组和类数组 arguments,以及 ES6 中的 Map、Set 等)。也就是说,yield*会逐个调用右侧可迭代对象的next方法。

async await

上面讲完了如何使用generator来解决异步代码的问题,可以看出,使用generator后,异步逻辑的代码基本和同步逻辑的代码差不多了。但是,generator的运行需要co来支持,所有最后又出现了asyncawaitasyncawait其实就是generator的语法糖。举个例子:

// generator
function *syncFn() {
  var d1 = yield sleep(1000);
  console.log("%s ms后运行", d1);
  var d2 = yield sleep(3000);
  console.log("%s ms后运行", d2);
}

上面的代码使用async改写就是:

// async
function async syncFn() {
  var d1 = await sleep(1000);
  console.log("%s ms后运行", d1);
  var d2 = await sleep(3000);
  console.log("%s ms后运行", d2);
}

可以看出,async的语法其实就是将generator中的*替换成async,将yield替换成await

async和await的优势

有了generator,为什么还要提出async呢?因为async有以下几点优势:

async的语义更清晰

async方法自带执行器,运行时和普通方法一样。而generator的运行依赖于co

await后面的方法可以是任意方法。而co现在了yield后面的方法必须是Promise

总结

总结一下异步代码的发展过程:

【回调函数】最基本的解决方法,将异步结束函数以参数的方式传递到异步函数中,也就是使用回调函数的方式来实现异步逻辑。

【Promise】为了解决回调函数的横向发展问题,定义了Promise。

【generator】Promise虽然解决了异步代码横向发展问题,可是使用Promise语义不够清晰,代码会呈现纵向发展趋势,所以,ES6中出现了generator函数来解决异步代码问题。

【async】generator函数基本上解决了异步代码问题,但是generator函数的运行却被外部控制着。最后提出了async,实现了generator + co的功能,而且语义更加清晰。

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

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

相关文章

  • 【译】确保网站性能的5个小贴士

    摘要:定期进行负载测试负载测试显示您的网站在一定数量的用户访问时的表现。如果负载测试显示的页面加载时间比预期的要长,那么网站设计的小改动就能带来所需的改进。 确保网站性能的5个小贴士 翻译:疯狂的技术宅作者:Jennifer Oksnevad英文标题:5 Tips to ensure website performance英文原文:https://www.catswhocode.com/b....

    ls0609 评论0 收藏0
  • 【译】确保网站性能的5个小贴士

    摘要:定期进行负载测试负载测试显示您的网站在一定数量的用户访问时的表现。如果负载测试显示的页面加载时间比预期的要长,那么网站设计的小改动就能带来所需的改进。 确保网站性能的5个小贴士 翻译:疯狂的技术宅作者:Jennifer Oksnevad英文标题:5 Tips to ensure website performance英文原文:https://www.catswhocode.com/b....

    singerye 评论0 收藏0
  • [译]JavaScript的调用栈、回调队列和事件循环

    摘要:在这个视频中,将的调用栈回调队列和事件循环的内容讲的很清晰。调用栈可以往里面放东西,可以在事件结束的时候把回调函数放进回调队列,然后是事件循环。为的时候这个过程看起来可能不明显,除非考虑到调用栈的执行环境和事件循环的情况。 译者按这篇文章可以看做是对Philip Roberts 2014年在JSConf演讲的《What the heck is the event loop anyway...

    YancyYe 评论0 收藏0
  • 云计算加速可穿戴设备落地,未来触手可及

    摘要:当出现疾病或意外伤害时,可穿戴设备可以及时向医院及周边人群或家人发出求助。可穿戴设备聚集地国内关注可穿戴设备的用户一定知道这个极客网站。随着芯片体积的不断减小,运算能力不断地提高,未来,芯片将成为所有可穿戴设备的核心力量。 相信每个人都梦想拥有科幻电影中的智能腕表、神奇头盔。随着科技的日新月异,这个愿望看起来并不那么遥远了。以智能手环、智能腕表、意念头箍为代表的智能可穿戴设备已经越来越...

    piapia 评论0 收藏0
  • Java 虚拟机对锁优化所做的努力

    摘要:自选锁锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会在做最后的努力自选锁。 showImg(https://segmentfault.com/img/remote/1460000016159660?w=500&h=333); 作为一款公用平台,JDK 本身也为并发程序的性能绞尽脑汁,在 JDK 内部也想尽一切办法提供并发时的系统吞吐量。这里,我将向大家简单介绍几种 J...

    ralap 评论0 收藏0

发表评论

0条评论

rubyshen

|高级讲师

TA的文章

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