前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从Generator到Async function

从Generator到Async function

作者头像
ayqy贾杰
发布2019-06-12 15:00:28
4880
发布2019-06-12 15:00:28
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

写在前面

说到异步函数,不由地想起Wind.js,以及老赵的远见:

Wind.js在JavaScript异步编程领域绝对是一个创新,可谓前无来者。有朋友就评价说“在看到Wind.js之前,真以为这是不可能实现的”,因为Wind.js事实上是用类库的形式“修补”了JavaScript语言,也正是这个原因,才能让JavaScript异步编程体验获得质的飞跃。 ——2012年7月 ES2017的async&await从promise,generator一路辗转走来,而Wind早在6年前就看到了这一天,并提前实现了愿景

一.yield与await

为什么说Async function是从Promise,Generator一路走来的?

因为异步函数与Generator特性有着千丝万缕的关系,比如,语义上都有暂停的意思:

  • yield:让步,歇会儿喘口气
  • await:桥多麻袋

先对比一个最简单的场景:

代码语言:javascript
复制
// generator
function* gen() {
 console.log('Do step 1');
 yield 'Until step1 completed';
 console.log('Do step 2');
}
let iter = gen();
iter.next();
iter.next();// async function
async function f() {
 console.log('Do step 1');
 await 'Until step1 completed';
 console.log('Do step 2');
}
f();

二者代码结构相似,并且输出也类似(作为两个例子分开执行):

代码语言:javascript
复制
// generator
Do step 1
Do step 2
{value: undefined, done: true}// async function
Do step 1
Do step 2
Promise {<resolved>: undefined}

二.暂停呢?

生成器能让执行流“喘口气”,能让停不下来的东西暂停,能用来重构循环,能驾驭无限序列,能包装迭代器。。。好处多多

(摘自generator(生成器)_ES6笔记2)

但上例中好像并没有看到暂停的效果,我们加点log,让一切更明显一些:

代码语言:javascript
复制
// generator
function* gen() {
 console.log('Do step 1');
 yield 'Until step1 completed';
 console.log('Do step 2');
}
let iter = gen();
iter.next();
console.log('generator抽根儿烟');
iter.next();// async function
async function f() {
 console.log('Do step 1');
 await 'Until step1 completed';
 console.log('Do step 2');
}
f();
console.log('async function抽根儿烟');

这次不关注各自的返回值(上面已经看过了),连在一起执行,输出结果如下:

代码语言:javascript
复制
Do step 1
generator抽根儿烟
Do step 2
Do step 1
async function抽根儿烟
Do step 2

输出没什么差异,但log('xxx抽根儿烟')所在的位置有差异

实际区别在于,上例中Generator的执行过程是纯同步的,而async function的执行过程含有异步的部分,用Generator来描述的话,相当于:

代码语言:javascript
复制
// generator假装async function
function* gen() {
 console.log('Do step 1');
 yield Promise.resolve('Until step1 completed');
 console.log('Do step 2');
}
let iter = gen();
let step1 = iter.next();
step1.value.then(iter.next.bind(iter));
console.log('generator假装async function抽根儿烟');// 输出结果
Do step 1
generator假装async function抽根儿烟
Do step 2

三.近一点,更近一点

更进一步地,很容易用Generator去实现Async function特性

代码语言:javascript
复制
function asyncFunction(gen, ...args) {
 return new Promise((resolve, reject) => {
   resolve(safeNext(gen(...args)));
 });
}function safeNext(iter, last) {
 let step;
 try {
   step = iter.next(last);
 } catch(ex) {
   step = iter.throw(ex);
 } return Promise.resolve(step.value)
   .catch(ex => iter.throw(ex).value)
   .then(result => step.done ? result : safeNext(iter, result))
}

P.S.Github repo地址ayqy/asyncFunction

试玩一下:

代码语言:javascript
复制
asyncFunction(function* (){
 console.log('Do step 1');
 // Wait 100ms
 let x = yield new Promise((resolve, reject) => {
   setTimeout(resolve.bind(null, 1), 100);
 });
 // 100ms later
 console.log(`Step1 completed, got ${x}`);
 try {
   throw ++x;
 } catch(ex) {
   x = -1;
 }
 console.log(`x = ${x}`);
 x = yield x * 2;
 console.log(`All steps passed, got ${x}`);
 return x;
}).then(result => {
 console.log(`Final result ${result}`);
});
let intervalId = setInterval(console.log.bind(console, 'tick'), 10);
setTimeout(() => {
 clearInterval(intervalId);
}, 100);

会得到类似输出:

代码语言:javascript
复制
Do step 1
3
⑨tick
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
tick

其中第二行的3setTimeout返回值(因此asyncFunction中只有第一段是同步执行的),第三行输出9次'tick'表示过了90多ms,此时Wait 100ms结束了,接着执行剩余部分直到结束

另外,还有一个难以察觉的细节是,本例中剩余部分的执行不会被interval回调打断(即便间隔极短),例如:

代码语言:javascript
复制
asyncFunction(function* (){
 setTimeout(console.log.bind(console, '#0'), 0)
 console.log('Do step 1');
 // Wait 100ms
 let x = yield new Promise((resolve, reject) => {
   setTimeout(resolve.bind(null, 1), 100);
 });
 setTimeout(console.log.bind(console, '#1'), 0)
 // 100ms later
 console.log(`Step1 completed, got ${x}`);
 setTimeout(console.log.bind(console, '#2'), 0)
 try {
   throw ++x;
 } catch(ex) {
   x = -1;
 }
 setTimeout(console.log.bind(console, '#3'), 0)
 console.log(`x = ${x}`);
 x = yield x * 2;
 setTimeout(console.log.bind(console, '#4'), 0)
 console.log(`All steps passed, got ${x}`);
 return x;
}).then(result => {
 console.log(`Final result ${result}`);
});

输出结果是:

代码语言:javascript
复制
Do step 1
Promise {<pending>}
#0
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
#1
#2
#3
#4

#1, 2, 3, 4最后输出,这与任务类型有关,具体见macrotask与microtask

对比正版async function:

代码语言:javascript
复制
(async function(){
 setTimeout(console.log.bind(console, '#0'), 0)
 console.log('Do step 1');
 // Wait 100ms
 let x = await new Promise((resolve, reject) => {
   setTimeout(resolve.bind(null, 1), 100);
 });
 setTimeout(console.log.bind(console, '#1'), 0)
 // 100ms later
 console.log(`Step1 completed, got ${x}`);
 setTimeout(console.log.bind(console, '#2'), 0)
 try {
   throw ++x;
 } catch(ex) {
   x = -1;
 }
 setTimeout(console.log.bind(console, '#3'), 0)
 console.log(`x = ${x}`);
 x = await x * 2;
 setTimeout(console.log.bind(console, '#4'), 0)
 console.log(`All steps passed, got ${x}`);
 return x;
})().then(result => {
 console.log(`Final result ${result}`);
});

输出完全一致:

代码语言:javascript
复制
Do step 1
Promise {<pending>}
#0
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
#1
#2
#3
#4

四.语法糖?

基本语法形式如下:

代码语言:javascript
复制
async function name([param[, param[, ... param]]]) {
 statements
}

需要知道2点:

  • await关键字只能出现在Async function里,否则报错
  • Async function的返回值是Promise

实际上,async function共有4种形式:

  • 函数声明:async function foo() {}
  • 函数表达式:const foo = async function () {};
  • 方法定义:let obj = { async foo() {} }
  • 箭头函数:const foo = async () => {};

例如:

代码语言:javascript
复制
async function fetchJson(url) {
 try {
   console.log('Starting fetch');
   let request = await fetch(url);
   let text = await request.text();
   return JSON.parse(text);
 } catch(error) {
   console.error(error);
 }
}// test
fetchJson('https://unpkg.com/emoutils/package.json')
 .then(json => console.log(json));
console.log('Fetching...');

输出:

代码语言:javascript
复制
Starting fetch
Fetching...
undefined
{name: "emoutils", …}

咦,异步函数貌似并不“异步”,Async function函数体的第一段(第一个await之前的部分)是同步执行的,类似于:

代码语言:javascript
复制
new Promise(resolve => {
 console.log('Starting fetch');
 setTimeout(resolve.bind(null, 'data'), 100);
}).then(data => console.log(data));
console.log('Fetching...');

同样,很容易把这个东西换成我们的盗版:

代码语言:javascript
复制
asyncFunction(function* fetchJson(url) {
 try {
   console.log('Starting fetch');
   let request = yield fetch(url);
   let text = yield request.text();
   return JSON.parse(text);
 } catch(error) {
   console.error(error);
 }
}, 'https://unpkg.com/emoutils/package.json')
 .then(json => console.log(json));// test
console.log('Fetching...');

事实上我们做了3件事:

  • 把函数体用Generator包起来,await都换成yield
  • 去掉asyncfunction之间的空格并驼峰命名
  • 把参数挪到Generator后面去

如果把这3件事通过编译转换屏蔽掉的话(甚至简单匹配替换就能做到):

代码语言:javascript
复制
function afunction(templateData) {
 const source = templateData;
 // ...一顿操作把上面字符串内容转换成
 let params = ['url'];
 let transformed = `function* fetchJson(url) {
   try {
     console.log('Starting fetch');
     let request = yield fetch(url);
     let text = yield request.text();
     return JSON.parse(text);
   } catch(error) {
     console.error(error);
   }
 }`; return function(...args) {
   return asyncFunction(new Function(...params, `return ${transformed}`)(), ...args);
 };
}

async function特性就被盗版方案完全取代了,语法形式也可以变得更相近:

代码语言:javascript
复制
afunction`(url) => {
 try {
   console.log('Starting fetch');
   let request = await fetch(url);
   let text = await request.text();
   return JSON.parse(text);
 } catch(error) {
   console.error(error);
 }
}`('https://unpkg.com/emoutils/package.json')
 .then(json => console.log(json));

P.S.这里应用了ES2015标签模板(tagged templates)特性,具体见模板字符串_ES6笔记3

那么,Async function是语法糖吗?

可以认为是。因为有了Generator特性后,Async function也就呼之欲出了(从yieldawait,本质上只是进一步提升了异步编程体验,算是微改进):

Internally, async functions work much like generators, but they are not translated to generator functions.

但语言层面的特性支持要比类似编译转换的替代方案更具优势,体现在性能、错误追踪(干净的调用栈)、与其它特性无缝贴合(如箭头函数、方法定义)等方面

异步编程体验

从编程体验上来看,Async function特性带来的提升在于:

  • 以同步形式编写异步代码,异步、回调等概念被淡化了
  • try-catch能够捕获到异步操作中的异常

能让含有异步操作的代码块仍然顺序执行,这无疑是最好的异步编程体验了:

代码语言:javascript
复制
// callback reqXXX(参数, 成功回调, 失败回调)
reqLogin(password, reqOrderNo, notFound);
 reqOrderNo(uid, reqOrderDetail, notFound);
   reqOrderDetail(orderNo, render, boom);
     render(data);// promise
promisifiedReqLogin(password)
 .then(({ uid }) => promisifiedReqOrderNo(uid), notFound)
 .then(({ orderNo }) => promisifiedReqOrderDetail(orderNo), notFound)
 .then(({ data }) => render(data))
 .catch(boom)// async function
async function renderPage(password) {
 let uid, orderNo;
 try {
   uid = await promisifiedReqLogin(password);
   orderNo = await promisifiedReqOrderNo(uid);
 } catch(ex) {
   notFound(ex);
 } let data = await promisifiedReqOrderDetail(orderNo);
 return render(data);
}
renderPage().catch(boom);

data = await fetchData(),仅此而已。回调的概念不复存在,减轻了大脑跟着异步操作入栈出栈的负担,毕竟

代码是写给人看的,附带可以在机器上运行

(摘自写好JavaScript)

五.渊源

至此,我们已经用Generator和Promise特性实现了盗版Async function,甚至没费多少工夫(仅18行代码)

现在回想一下我们是如何把这两个特性组合起来的?或者说,依靠这两个特性的哪些机制,让盗版得以轻松实现?

首先,要实现Async function的话,最关键的特性是Generator,通过yield让顺序执行流停下来,才有“等待”一说:

代码语言:javascript
复制
function* infSeq() {
 let i = 0;
 // 不会发生死循环哟,yield让while true“停”下来了
 while (true) {
   console.log(i);
   yield i++;
 }
}// test
let iter = infSeq();
// 输出0, 1, 2...
iter.next();
iter.next();
iter.next();

能“等待”了,那么等谁呢?直接等异步操作吗?如何区分异步操作?

没错,该Promise上场了:

代码语言:javascript
复制
// generator假装async function
function* gen() {
 console.log('Do step 1');
 yield Promise.resolve('Until step1 completed');
 console.log('Do step 2');
}
let iter = gen();
let step1 = iter.next();
step1.value.then(iter.next.bind(iter));
console.log('generator假装async function抽根儿烟');

只要next().value是Promise,那就肯定是异步操作,等它完成了再next(),这样就实现了等待一个异步操作做完再继续下面的事情,即Async function特性

上层概念上来看,三者关系如下:

代码语言:javascript
复制
Async function = 调度机(Generator) + 异步任务(Promise)

其中,Generator这个调度机的作用在于:

  • 分片(拆不开怎么等):将函数体顺序代码块拆分成几段
  • 调度(拆开了怎么执行):从外部控制这些片段的执行流,如next()throw()

Promise作为异步任务模型,主要特点如下:

  • 状态丢弃:一次性的Promise对象,用完即扔(then()等都返回新Promise)
  • 任务组合:可以通过类似resolve(promise)的方式形成任务链,结合all()race()等控制其顺序
  • 错误上抛:类似于冒泡的异常处理机制,沿任务链向上抛出异常,简化了异步任务的异常捕获

Generator并不直接调度Promise(调度的对象是被拆开的片段),但它关注每一段的执行结果,如果结果是pending Promise,就等到不pending了,再控制下一段执行

所以,Promise只是配角儿,可以替换成任意的异步任务模型,其主要作用在于告知Generator这里有个异步操作得等一下:

代码语言:javascript
复制
调度机:(把一段代码戳在纸带上,塞进计算机,取出执行结果)咦,这是个啥?
异步任务:Hey,我是个异步任务啊,还没完事儿,完了我告诉你
调度机:好,我抽根儿烟(头像变灰)
异步任务:完事了完事了,结果是xxx
调度机:(立即上线,拿起下一段代码和xxx,都戳在纸带上,塞进计算机,取出执行结果)咦,这……尼玛,咋还出错了捏?
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-11-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 一.yield与await
  • 二.暂停呢?
  • 三.近一点,更近一点
  • 四.语法糖?
    • 异步编程体验
    • 五.渊源
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档