彻底弄清Promise、Generator以及Async/await

我们知道JavaScript是单线程语言,如果没有异步编程非得卡死。
以前,异步编程的方法有下面四种

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise对象

现在据说异步编程终极解决方案是——async/await

一、回调函数

所谓回调函数(callback),就是把任务分成两步完成,第二步单独写在一个函数里面,等到重新执行这个任务时,就直接调用这个函数。

例如Node.js中读取文件

fs.readFile('a,txt', (err,data) = >{
  if(err) throw err;
  console.log(data);
})

上面代码中readFile的第二个参数就是回调函数,等到读取完a.txt文件时,这个函数才会执行。

二、Promise

使用回调函数本身没有问题,但有“回调地狱”的问题。
假定我们有一个需求,读取完A文件之后读取B文件,再读取C文件,代码如下

fs.readFile(fileA,  (err, data) => {
  fs.readFile(fileB,  (err, data) => {
      fs.readFile(fileC, (err,data)=>{
        //do something
    })
  });
});

可见,三个回调函数代码看来就够呛了,有时在实际业务中还不止嵌套这几个,难以管理。

这时候Promise出现了!它不是新的功能,而是一种新的写法,用来解决“回调地狱”的问题。

我们再假定一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果,用setTimeout()来模拟异步操作

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function A(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return A(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
      return A(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
     return A(n);
}

上面代码中有4个函数,A()返回一个Promise对象,接收参数n,n秒后执行resolve(n+200)。step1、 step2、step3对应三个步骤

现在用Promise实现这三个步骤:

function doIt() {
    console.time('do it now')
    const time1 = 300;
    step1(time1)
          .then( time2 =>step2(time2))
          .then( time3 => step3(time3))
          .then( result => {
              console.log(`result is ${result}`)
           });
}

doIt();

输出结果如下

step1 with 300
step2 with 500
step3 with 700
result is 900

result是step3()的参数700+200 = 900。

可见,Promise的写法只是回调函数的改进,用then()方法免去了嵌套,更为直观。
但这样写绝不是最好的,代码变得十分冗余,一堆的then。
所以,最优秀的解决方案是什么呢?
开头暴露了,就是async/await
讲async前我们先讲讲协程Generator

三、协程

协程(coroutine),意思是多个线程相互协作,完成异步任务。

它的运行流程如下

  • 协程A开始执行
  • 协程A执行到一半,暂停执行,执行的权利转交给协程B。
  • 一段时间后B交还执行权
  • 协程A重得执行权,继续执行

上面的协程A就是一个异步任务,因为在执行过程中执行权被B抢了,被迫分成两步完成。

读取文件的协程代码如下:

function task() {
  // 其他代码
   var f = yield readFile('a.txt')
   // 其他代码
}

task()函数就是一个协程,函数内部有个新单词yield,yield中文意思为退让,
顾名思义,它表示执行到此处,task协程该交出它的执行权了。也可以把yield命令理解为异步两个阶段的分界线。

协程遇到yield命令就会暂停,把执行权交给其他协程,等到执行权返回继续往后执行。最大的优点就是代码写法和同步操作几乎没有差别,只是多了yield命令。

这也是异步编程追求的,让它更像同步编程

四、Generator函数

Generator是协程在ES6的实现,最大的特点就是可以交出函数的执行权,懂得退让。

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

上面代码中,函数多了*号,用来表示这是一个Generator函数,和普通函数不一样,不同之处在于执行它不会返回结果,

返回的是指针对象g,这个指针g有个next方法,调用它会执行异步任务的第一步。
对象中有两个值,value和done,value 属性是 yield 语句后面表达式的值,表示当前阶段的值,done表示是否Generator函数是否执行完毕。

下面看看Generator函数如何执行一个真实的异步任务

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then( data => return data.json)
                  .then (data => g.next(data))

上面代码中,首先执行Generator函数,得到对象g,调用next方法,此时
result ={ value: Promise { <pending> }, done: false }
因为fetch返回的是一个Promise对象,(即value是一个Promise对象)所以要用then才能调用下一个next方法。

虽然Generator将异步操作表示得很简洁,但是管理麻烦,何时执行第一阶段,又何时执行第二阶段?

是的,这时候到Async/await出现了!

五、Async/await

从回调函数,到Promise对象,再到Generator函数,JavaScript异步编程解决方案历程可谓辛酸,终于到了Async/await。很多人认为它是异步操作的最终解决方案(谢天谢地,这下不用再学新的解决方案了吧)

其实async函数就是Generator函数的语法糖,例如下面两个代码:

var gen = function* (){
  var f1 = yield readFile('./a.txt');
  var f2 = yield readFile('./b.txt');
  console.log(f1.toString());
  console.log(f2.toString());
};
var asyncReadFile = async function (){
  var f1 = await  readFile('./a.txt');
  var f2 = await  readFile('./b.txt');
  console.log(f1.toString());
  console.log(f2.toString());
};

上面的为Generator函数读取两个文件,下面为async/await读取,比较可发现,两个函数其实是一样的,async不过是把Generator函数的*号换成async,yield换成await。

1.async函数用法

上面说了async不过是Generator函数的语法糖,那为什么要取这个名字呢?自然是有理由的。
async是“异步”,而await是async wait的简写,即异步等待。所以应该很好理解async用于声明一个function是异步的,await用于等待一个异步方法执行完成

下面来看一个例子理解async命令的作用

async function test() {
  return "async 有什么用?";
}
const result = test();
console.log(result) 

输出:
Promise { 'async 有什么用?' }
可以看到,输出的是一个Promise对象!
所以,async函数返回的是一个Promise对象,如果直接return 一个直接量,async会把这个直接量通过PromIse.resolve()封装成Promise对象

注意点
一般来说,都认为await是在等待一个async函数完成,确切的说等待的是一个表示式,这个表达式的计算结果是Promise对象或者是其他值(没有限定是什么)

即await后面不仅可以接Promise,还可以接普通函数或者直接量。

同时,我们可以把async理解为一个运算符,用于组成表达式,表达式的结果取决于它等到的东西

  • 等到非Promise对象 表达式结果为它等到的东西
  • 等到Promise对象 await就会阻塞后面的代码,等待Promise对象resolve,取得resolve的值,作为表达式的结果

还是那个业务,分多个步骤完成,每个步骤依赖于上一个步骤的结果,用setTimeout模拟异步操作。

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

async实现方法

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

输出结果和上面用Promise实现是一样的,但这个代码结构看起来清晰得多,几乎跟同步写法一样。

2. async函数的优点

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

(2) 语义化更好
async 和 await,比起星号和 yield,语义更清楚了。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容