这篇文章是讲 JS 异步原理和实现形式的第四篇文章,后面三篇是:
setTimeout 和 setImmediate 到底谁先执行,本文让你彻底了解 Event Loop
从公布订阅模式动手读懂 Node.js 的 EventEmitter 源码
手写一个 Promise/A+, 完满通过官网 872 个测试用例
本文次要会讲 Generator 的使用和实现原理,而后咱们会去读一下 co 模块的源码,最初还会提一下 async/await。
本文全副例子都在 GitHub 上:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/JavaScript/Generator
Generator
异步编程始终是 JS 的外围之一,业界也是始终在摸索不同的解决办法,从“回调天堂”到公布订阅模式,再到 Promise,都是在优化异步编程。只管 Promise 曾经很优良了,也不会陷入“回调天堂”,然而嵌套层数多了也会有一连串的then
,始终不能像同步代码那样间接往下写就行了。Generator 是 ES6 引入的进一步改善异步编程的计划,上面咱们先来看看根本用法。
根本用法
Generator 的中文翻译是“生成器”,其实他要干的事件也是一个生成器,一个函数如果加了*
,他就会变成一个生成器函数,他的运行后果会返回一个迭代器对象,比方上面的代码:
// gen 是一个生成器函数
function* gen() {
let a = yield 1;
let b = yield a + 2;
yield b + 3;
}
let itor = gen(); // 生成器函数运行后会返回一个迭代器对象,即 itor。
next
ES6 标准中规定迭代器必须有一个 next
办法,这个办法会返回一个对象,这个对象具备 done
和value
两个属性,done
示意以后迭代器内容是否曾经执行完,执行完为 true
,否则为false
,value
示意以后步骤返回的值。在 generator
具体使用中,每次遇到 yield
关键字都会暂停执行,当调用迭代器的 next
时,会将 yield
前面表达式的值作为返回对象的value
,比方下面生成器的执行后果如下:
咱们能够看到第一次调 next
返回的就是第一个 yeild
前面表达式的值,也就是 1。须要留神的是,整个迭代器目前暂停在了第一个 yield
这里,给变量 a
赋值都没执行,要调用下一个 next
的时候才会给变量 a
赋值,而后始终执行到第二个 yield
。那应该给a
赋什么值呢?从代码来看,a
的值应该是 yield
语句的返回值,然而 yield
自身是没有返回值的,或者说返回值是 undefined
,如果要给a
赋值须要下次调 next
的时候手动传进去,咱们这里传一个 4,4 就会作为上次 yield
的返回值赋给a
:
能够看到第二个 yield
前面的表达式 a + 2
的值是 6,这是因为咱们传进去的 4 被作为上一个 yield
的返回值了,而后计算 a + 2
天然就是 6 了。
咱们持续next
,把这个迭代器走完:
上图是接着后面运行的,图中第一个 next
返回的 value
是NaN
是因为咱们调 next
的时候没有传参数,也就是说 b
为undefined
,undefined + 3
就为 NaN
了。最初一个 next
其实是把函数体执行完了,这时候的 value
应该是这个函数 return
的值,然而因为咱们没有写 return
,默认就是return undefined
了,执行完后 done
会被置为true
。
throw
迭代器还有个办法是throw
,这个办法能够在函数体内部抛出谬误,而后在函数外面捕捉,还是下面那个例子:
function* gen() {
let a = yield 1;
let b = yield a + 2;
yield b + 3;
}
let itor = gen();
咱们这次不必 next
执行了,间接 throw
谬误进去:
这个谬误因为咱们没有捕捉,所以间接抛到最外层来了,咱们能够在函数体外面捕捉他,略微改下:
function* gen() {
try {
let a = yield 1;
let b = yield a + 2;
yield b + 3;
} catch (e) {console.log(e);
}
}
let itor = gen();
而后再来 throw
下:
这个图能够看进去,谬误在函数里外面捕捉了,走到了 catch
外面,这外面只有一个 console
同步代码,整个函数间接就运行完结了,所以 done
变成 true
了,当然 catch
外面能够持续写 yield
而后用 next
来执行。
return
迭代器还有个 return
办法,这个办法就很简略了,他会间接终止以后迭代器,将 done
置为true
,这个办法的参数就是迭代器的value
,还是下面的例子:
function* gen() {
let a = yield 1;
let b = yield a + 2;
yield b + 3;
}
let itor = gen();
这次咱们间接调用return
:
yield*
简略了解,yield*
就是在生成器外面调用另一个生成器,然而他并不会占用一个next
,而是间接进入被调用的生成器去运行。
function* gen() {
let a = yield 1;
let b = yield a + 2;
}
function* gen2() {
yield 10 + 5;
yield* gen();}
let itor = gen2();
下面代码咱们第一次调用 next
,值天然是10 + 5
,即 15,而后第二次调用next
,其实就走到了yield*
了,这其实就相当于调用了gen
,而后执行他的第一个yield
,值就是 1。
协程
其实 Generator 就是实现了协程,协程是一个比线程还小的概念。一个过程能够有多个线程,一个线程能够有多个协程,然而一个线程同时只能有一个协程在运行。这个意思就是说如果以后协程能够执行,比方同步代码,那就执行他,如果以后协程临时不能继续执行,比方他是一个异步读文件的操作,那就将它挂起,而后去执行其余协程,等这个协程后果回来了,能够持续了再来执行他。yield
其实就相当于将当前任务挂起了,下次调用再从这里开始。协程这个概念其实很多年前就曾经被提出来了,其余很多语言也有本人的实现。Generator 相当于 JS 实现的协程。
异步利用
后面讲了 Generator 的根本用法,咱们用它来解决一个异步事件看看。我还是应用后面文章用到过的例子,三个网络申请,申请 3 依赖申请 2 的后果,申请 2 依赖申请 1 的后果,如果应用回调是这样的:
const request = require("request");
request('https://www.baidu.com', function (error, response) {if (!error && response.statusCode == 200) {console.log('get times 1');
request('https://www.baidu.com', function(error, response) {if (!error && response.statusCode == 200) {console.log('get times 2');
request('https://www.baidu.com', function(error, response) {if (!error && response.statusCode == 200) {console.log('get times 3');
}
})
}
})
}
});
咱们这次应用 Generator 来解决“回调天堂”:
const request = require("request");
function* requestGen() {function sendRequest(url) {request(url, function (error, response) {if (!error && response.statusCode == 200) {console.log(response.body);
// 留神这里,援用了内部的迭代器 itor
itor.next(response.body);
}
})
}
const url = 'https://www.baidu.com';
// 应用 yield 发动三个申请,每个申请胜利后再持续调 next
const r1 = yield sendRequest(url);
console.log('r1', r1);
const r2 = yield sendRequest(url);
console.log('r2', r2);
const r3 = yield sendRequest(url);
console.log('r3', r3);
}
const itor = requestGen();
// 手动调第一个 next
itor.next();
这个例子中咱们在生成器外面写了一个申请办法,这个办法会去发动网络申请,每次网络申请胜利后又持续调用 next 执行前面的 yield
,最初是在外层手动调一个next
触发这个流程。这其实就相似一个尾调用,这样写能够达到成果,然而在 requestGen
外面援用了里面的迭代器itor
,耦合很高,而且不好复用。
thunk 函数
为了解决后面说的耦合高,不好复用的问题,就有了 thunk 函数。thunk 函数了解起来有点绕,我先把代码写进去,而后再一步一步来剖析它的执行程序:
function Thunk(fn) {return function(...args) {return function(callback) {return fn.call(this, ...args, callback)
}
}
}
function run(fn) {let gen = fn();
function next(err, data) {let result = gen.next(data);
if(result.done) return;
result.value(next);
}
next();}
// 应用 thunk 办法
const request = require("request");
const requestThunk = Thunk(request);
function* requestGen() {
const url = 'https://www.baidu.com';
let r1 = yield requestThunk(url);
console.log(r1.body);
let r2 = yield requestThunk(url);
console.log(r2.body);
let r3 = yield requestThunk(url);
console.log(r3.body);
}
// 启动运行
run(requestGen);
这段代码外面的 Thunk 函数返回了好几层函数,咱们从他的应用动手一层一层剥开看:
-
requestThunk
是 Thunk 运行的返回值,也就是第一层返回值,参数是request
,也就是:function(...args) {return function(callback) {return request.call(this, ...args, callback); // 留神这里调用的是 request } }
-
run
函数的参数是生成器,咱们看看他到底干了啥:- run 外面先调用生成器,拿到迭代器
gen
,而后自定义了一个next
办法,并调用这个next
办法,为了便于辨别,我这里称这个自定义的next
为部分next
-
部分
next
会调用生成器的next
,生成器的next
其实就是yield requestThunk(url)
,参数是咱们传进去的url
,这就调到咱们后面的那个办法,这个yield
返回的value
其实是:function(callback) {return request.call(this, url, callback); }
- 检测迭代器是否曾经迭代结束,如果没有,就持续调用第二步的这个函数,这个函数其实才真正的去
request
,这时候传进去的参数是部分next
,部分next
也作为了request
的回调函数。 - 这个回调函数在执行时又会调
gen.next
,这样生成器就能够持续往下执行了,同时gen.next
的参数是回调函数的data
,这样,生成器外面的r1
其实就拿到了申请的返回值。
- run 外面先调用生成器,拿到迭代器
Thunk 函数就是这样一种能够主动执行 Generator 的函数,因为 Thunk 函数的包装,咱们在 Generator 外面能够像同步代码那样间接拿到 yield
异步代码的返回值。
co 模块
co 模块是一个很受欢迎的模块,他也能够主动执行 Generator,他的 yield 前面反对 thunk 和 Promise,咱们先来看看他的根本应用,而后再去剖析下他的源码。
官网 GitHub:https://github.com/tj/co
根本应用
反对 thunk
后面咱们讲了 thunk 函数,咱们还是从 thunk 函数开始。代码还是用咱们后面写的 thunk 函数,然而因为 co 反对的 thunk 是只接管回调函数的函数模式,咱们应用时须要调整下:
// 还是之前的 thunk 函数
function Thunk(fn) {return function(...args) {return function(callback) {return fn.call(this, ...args, callback)
}
}
}
// 将咱们须要的 request 转换成 thunk
const request = require('request');
const requestThunk = Thunk(request);
// 转换后的 requestThunk 其实能够间接用了
// 用法就是 requestThunk(url)(callback)
// 然而咱们 co 接管的 thunk 是 fn(callback)模式
// 咱们转换一下
// 这时候的 baiduRequest 也是一个函数,url 曾经传好了,他只须要一个回调函数做参数就行
// 应用就是这样:baiduRequest(callback)
const baiduRequest = requestThunk('https://www.baidu.com');
// 引入 co 执行, co 的参数是一个 Generator
// co 的返回值是一个 Promise,咱们能够用 then 拿到他的后果
const co = require('co');
co(function* () {
const r1 = yield baiduRequest;
const r2 = yield baiduRequest;
const r3 = yield baiduRequest;
return {
r1,
r2,
r3,
}
}).then((res) => {// then 外面就能够间接拿到后面返回的{r1, r2, r3}
console.log(res);
});
反对 Promise
其实 co 官网是倡议 yield 前面跟 Promise 的,尽管反对 thunk,然而将来可能会移除。应用 Promise,咱们代码写起来其实更简略,间接用 fetch 就行,不必包装 Thunk。
const fetch = require('node-fetch');
const co = require('co');
co(function* () {
// 间接用 fetch,简略多了,fetch 返回的就是 Promise
const r1 = yield fetch('https://www.baidu.com');
const r2 = yield fetch('https://www.baidu.com');
const r3 = yield fetch('https://www.baidu.com');
return {
r1,
r2,
r3,
}
}).then((res) => {// 这里同样能够拿到{r1, r2, r3}
console.log(res);
});
源码剖析
本文的源码剖析基于 co 模块 4.6.0 版本,源码:https://github.com/tj/co/blob/master/index.js
认真看源码会发现他代码并不多,总共两百多行,一半都是在进行 yield 前面的参数检测和解决,检测他是不是 Promise,如果不是就转换为 Promise,所以即便你 yield 前面传的 thunk,他还是会转换成 Promise 解决。转换 Promise 的代码绝对比拟独立和简略,我这里不具体开展了,这里次要还是讲一讲外围办法co(gen)
。上面是我复制的去掉了正文的简化代码:
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
return new Promise(function(resolve, reject) {if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {ret = gen.next(res);
} catch (e) {return reject(e);
}
next(ret);
return null;
}
function onRejected(err) {
var ret;
try {ret = gen.throw(err);
} catch (e) {return reject(e);
}
next(ret);
}
function next(ret) {if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object,'
+ 'but the following object was passed:"' + String(ret.value) + '"'));
}
});
}
- 从整体构造看,co 的参数是一个 Generator,返回值是一个 Promise,简直所有逻辑代码都在这个 Promise 外面,这也是咱们应用时用 then 拿后果的起因。
- Promise 外面先把 Generator 拿进去执行,失去一个迭代器
gen
-
手动调用一次
onFulfilled
,开启迭代onFulfilled
接管一个参数res
,第一次调用是没有传这个参数,这个参数次要是用来接管前面的 then 返回的后果。- 而后调用
gen.next
,留神这个的返回值 ret 的模式是{value, done},而后将这个 ret 传给部分的 next
-
而后执行部分 next,他接管的参数是 yield 返回值{value, done}
- 这里先检测迭代是否实现,如果实现了,就间接将整个 promise resolve。
- 这里的 value 是 yield 前面表达式的值,可能是 thunk,也可能是 promise
- 将 value 转换成 promise
- 将转换后的 promise 拿进去执行,胜利的回调是后面的
onFulfilled
- 咱们再来看下
onFulfilled
,这是第二次执行onFulfilled
了。这次执行的时候传入的参数 res 是上次异步 promise 的执行后果,对应咱们的 fetch 就是拿回来的数据,这个数据传给第二个gen.next
,成果就是咱们代码外面的赋值给了第一个yield
后面的变量r1
。而后持续部分 next,这个 next 其实就是执行第二个异步 Promise 了。这个 promise 的胜利回调又持续调用gen.next
,这样就一直的执行上来,直到done
变成true
为止。 - 最初看一眼
onRejected
办法,这个办法其实作为了异步 promise 的谬误分支,这个函数外面间接调用了gen.throw
,这样咱们在 Generator 外面能够间接用try...catch...
拿到谬误。须要留神的是gen.throw
前面还持续调用了next(ret)
,这是因为在 Generator 的catch
分支外面还可能持续有yield
,比方谬误上报的网络申请,这时候的迭代器并不一定完结了。
async/await
最初提一下async/await
,先来看一下用法:
const fetch = require('node-fetch');
async function sendRequest () {const r1 = await fetch('https://www.baidu.com');
const r2 = await fetch('https://www.baidu.com');
const r3 = await fetch('https://www.baidu.com');
return {
r1,
r2,
r3,
}
}
// 留神 async 返回的也是一个 promise
sendRequest().then((res) => {console.log('res', res);
});
咋一看这个跟后面 promise 版的 co 是不是很像,返回值都是一个 promise,只是 Generator 换成了一个 async
函数,函数外面的 yield
换成了await
,而且外层不须要 co 来包裹也能够主动执行了。其实 async 函数就是 Generator 加主动执行器的语法糖,能够了解为从语言层面反对了 Generator 的主动执行。下面这段代码跟 co 版的 promise 其实就是等价的。
总结
- Generator 是一种更古代的异步解决方案,在 JS 语言层面反对了协程
- Generator 的返回值是一个迭代器
- 这个迭代器须要手动调
next
能力一条一条执行yield
next
的返回值是 {value, done},value
是 yield 前面表达式的值yield
语句自身并没有返回值,下次调next
的参数会作为上一个yield
语句的返回值- Generator 本人不能主动执行,要主动执行须要引入其余计划,后面讲
thunk
的时候提供了一种计划,co
模块也是一个很受欢迎的主动执行计划 - 这两个计划的思路有点相似,都是先写一个部分的办法,这个办法会去调用
gen.next
,同时这个办法自身又会传到回调函数或者 promise 的胜利分支外面,异步完结后又持续调用这个部分办法,这个部分办法又调用gen.next
,这样始终迭代,直到迭代器执行结束。 async/await
其实是 Generator 和主动执行器的语法糖,写法和实现原理都相似 co 模块的 promise 模式。
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。
作者博文 GitHub 我的项目地址:https://github.com/dennis-jiang/Front-End-Knowledges