首页你好哇,世界马蹄疾
Async专题:状态机
马蹄疾
2019-05-05 19:25:21

Generator简单讲就是一个状态机。但它和Promise不一样,它可以维持无限个状态,并且提出它的初衷并不是为了解决异步编程的某些问题。

一个线程一次只能做一件任务,并且任务与任务之间不能间断。而Generator开了挂,它可以暂停手头的任务,先干别的,然后在恰当的时机手动切换回来。

这是一种纤程或者协程的概念,相比线程切换更加轻量化的切换方式。

Iterator

在讲Generator之前,我们要先和Iterator遍历器打个照面。

Iterator对象是一个指针对象,它是一种类似于单向链表的数据结构。JavaScript通过Iterator对象来统一数组和类数组的遍历方式。

const arr = [1, 2, 3]; const iteratorConstructor = arr[Symbol.iterator]; console.log(iteratorConstructor); // ƒ values() { [native code] }
const obj = { a: 1, b: 2, c: 3 }; const iteratorConstructor = obj[Symbol.iterator]; console.log(iteratorConstructor); // undefined
const set = new Set([1, 2, 3]); const iteratorConstructor = set[Symbol.iterator]; console.log(iteratorConstructor); // ƒ values() { [native code] }

我们已经见到了Iterator对象的构造器,它藏在Symbol.iterator下面。接下来我们生成一个Iterator对象来了解它的工作方式吧。

const arr = [1, 2, 3]; const it = arr[Symbol.iterator](); console.log(it.next()); // { value: 1, done: false } console.log(it.next()); // { value: 2, done: false } console.log(it.next()); // { value: 3, done: false } console.log(it.next()); // { value: undefined, done: true } console.log(it.next()); // { value: undefined, done: true }

既然它是一个指针对象,调用next()的意思就是把指针往后挪一位。挪到最后一位,再往后挪,它就会一直重复我已经到头了,只能给你一个空值

Generator

Generator是一个生成器,它生成的到底是什么呢?

对咯,他生成的就是一个Iterator对象。

function *gen() { yield 1; yield 2; return 3; } const it = gen(); console.log(it.next()); // { value: 1, done: false } console.log(it.next()); // { value: 2, done: false } console.log(it.next()); // { value: 3, done: false } console.log(it.next()); // { value: undefined, done: true } console.log(it.next()); // { value: undefined, done: true }

Generator有什么意义呢?普通函数的执行会形成一个调用栈,入栈和出栈是一口气完成的。而Generator必须得手动调用next()才能往下执行,相当于把执行的控制权从引擎交给了开发者。

所以Generator解决的是流程控制的问题。

它可以在执行过程暂时中断,先执行别的程序,但是它的执行上下文并没有销毁,仍然可以在需要的时候切换回来,继续往下执行。

最重要的优势在于,它看起来是同步的语法,但是却可以异步执行。

yield

对于一个Generator函数来说,什么时候该暂停呢?就是在碰到yield关键字的时候。

function *gen() { console.log('a'); yield 13 * 15; console.log('b'); yield 15 - 13; console.log('c'); return 3; } const it = gen();

看上面的例子,第一次调用it.next()的时候,碰到了第一个yield关键字,然后开始计算yield后面表达式的值,然后这个值就成了it.next()返回值中value的值,然后停在这。这一步会打印a,但不会打印b

以此类推。return的值作为最后一个状态传递出去,然后返回值的done属性就变成true,一旦它变成true,之后继续执行的返回值都是没有意义的。

这里面有一个状态传递的过程。yield把它暂停之前获得的状态传递给执行器。

那么有没有可能执行器传递状态给状态机内部呢?

function *gen() { const a = yield 1; console.log(a); const b = yield 2; console.log(b); return 3; } const it = gen();

当然是可以的。

默认情况下,第二次执行的时候变量a的打印结果是undefined,因为yield关键字就没有返回值。

但是如果给next()传递参数,这个参数就会作为上一个yield的返回值。

it.next('biu');

别急,第一次执行没有所谓的上一个yield,所以这个参数是没有意义的。

it.next('piu'); // 打印 piu。这个 piu 是 console.log(a) 打印出来的。

第二次执行就不同了。a变量接收到了next()传递进去的参数。

这有什么用?如果能在执行过程中给状态机传值,我们就可以改变状态机的执行条件。你可以发现,Generator是可以实现值的双向传递的。

为什么要作为上一个yield的返回值?你想啊,作为上一个yield的返回值,才能改变当前代码的执行条件,这样才有价值不是嘛。这地方有点绕,仔细想一想。

自动执行

好吧,既然引擎把Generator的控制权交给了开发者,那我们就要探索出一种方法,让Generator的遍历器对象可以自动执行。

function* gen() { yield 1; yield 2; return 3; } function run(gen) { const it = gen(); let state = { done: false }; while (!state.done) { state = it.next(); console.log(state); } } run(gen);

不错,竟然这么简单。

但想想我们是来干什么的,我们是来探讨JavaScript异步的呀。这个简陋的run函数能够执行异步操作吗?

function fetchByName(name) { const url = `https://api.github.com/users/${name}/repos`; fetch(url).then(res => res.json()).then(res => console.log(res)); } function *gen() { yield fetchByName('veedrin'); yield fetchByName('tj'); } function run(gen) { const it = gen(); let state = { done: false }; while (!state.done) { state = it.next(); } } run(gen);

事实证明,Generator会把fetchByName当做一个同步函数来执行,没等请求触发回调,它已经将指针指向了下一个yield。我们的目的是让上一个异步任务完成以后才开始下一个异步任务,显然这种方式做不到。

我们已经让Generator自动化了,但是在面对异步任务的时候,交还控制权的时机依然不对。

什么才是正确的时机呢?

在回调中交还控制权

哪个时间点表明某个异步任务已经完成?当然是在回调中咯。

我们来拆解一下思路。

  • 首先我们要把异步任务的其他参数和回调参数拆分开来,因为我们需要单独在回调中扣一下扳机。
  • 然后yield asyncTask()的返回值得是一个函数,它接受异步任务的回调作为参数。因为Generator只有yield的返回值是暴露在外面的,方便我们控制。
  • 最后在回调中移动指针。
function thunkify(fn) { return (...args) => { return (done) => { args.push(done); fn(...args); } } }

这就是把异步任务的其他参数和回调参数拆分开来的法宝。是不是很简单?它通过两层闭包将原过程变成三次函数调用,第一次传入原函数,第二次传入回调之前的参数,第三次传入回调,并在最里一层闭包中又把参数整合起来传入原函数。

是的,这就是大名鼎鼎的thunkify

以下是暖男版。

function thunkify(fn) { return (...args) => { return (done) => { let called = false; args.push((...innerArgs) => { if (called) return; called = true; done(...innerArgs); }); try { fn(...args); } catch (err) { done(err); } } } }

宝刀已经有了,咱们去屠龙吧。

const fs = require('fs'); const thunkify = require('./thunkify'); const readFileThunk = thunkify(fs.readFile); function *gen() { const valueA = yield readFileThunk('/Users/veedrin/a.md'); console.log('a.md 的内容是:\n', valueA.toString()); const valueB = yield readFileThunk('/Users/veedrin/b.md'); console.log('b.md 的内容是:\n', valueB.toString()); } function run(gen) { const it = gen(); const state1 = it.next(); state1.value((err, data) => { if (err) throw err; const state2 = it.next(data); state2.value((err, data) => { if (err) throw err; it.next(data); }); }); } run(gen);

卧槽,老夫宝刀都提起来了,你让我切豆腐?

这他妈不就是把回调嵌套提到外面来了么!我为啥还要用Generator,感觉默认的回调嵌套挺好的呀,有一种黑洞般的简洁和性感...

别急,这只是Thunk解决方案的PPT版本,接下来咱们真的要造车并开车了哟,此处@贾跃亭。

const fs = require('fs'); const thunkify = require('./thunkify'); const readFileThunk = thunkify(fs.readFile); function *gen() { const valueA = yield readFileThunk('/Users/veedrin/a.md'); console.log('a.md 的内容是:\n', valueA.toString()); const valueB = yield readFileThunk('/Users/veedrin/b.md'); console.log('b.md 的内容是:\n', valueB.toString()); } function run(gen) { const it = gen(); function next(err, data) { const state = it.next(data); if (state.done) return; state.value(next); } next(); } run(gen);

我们完全可以把回调函数抽象出来,每移动一次指针就递归一次,然后在回调函数内部加一个停止递归的逻辑,一个通用版的run函数就写好啦。上例中的next()其实就是callback()呢。

在Promise中交还控制权

处理异步操作除了回调之外,我们还有异步容器Promise。

和在回调中交还控制权差不多,于Promise中,我们在then函数的函数参数中扣动扳机。

我们来看看威震海内的co

function co(gen) { const it = gen(); const state = it.next(); function next(state) { if (state.done) return; state.value.then(res => { const state = it.next(res); next(state); }); } next(state); }

其实也不复杂,就是在then函数的回调中(其实也是回调啦)移动Generator的指针,然后递归调用,继续移动指针。当然,需要有一个停止递归的逻辑。

以下是暖男版。

function isObject(value) { return Object === value.constructor; } function isGenerator(obj) { return typeof obj.next === 'function' && typeof obj.throw === 'function'; } function isGeneratorFunction(obj) { const constructor = obj.constructor; if (!constructor) return false; if (constructor.name === GeneratorFunction || constructor.displayName === 'GeneratorFunction') return true; return isGenerator(constructor.prototype); } function isPromise(obj) { return typeof obj.then === 'function'; } function toPromise(obj) { if (!obj) return obj; if (isPromise(obj)) return obj; if (isGenerator(obj) || isGeneratorFunction(obj)) { return co.call(this, obj); } if (typeof obj === 'function') { return thunkToPromise.call(this, obj); } if (Array.isArray(obj)) { return arrayToPromise.call(this, obj); } if (isObject(obj)) { return objectToPromise.call(this, obj); } return obj; } function typeError(value) { return new TypeError(`You may only yield a function, promise, generator, array, or object, but the following object was passed: "${String(value)}"`); } function co(gen) { const ctx = this; return new Promise((resolve, reject) => { let it; if (typeof gen === 'function') { it = gen.call(ctx); } if (!it || typeof it.next !== 'function') { return resolve(it); } onFulfilled(); function onFulfilled(res) { let ret; try { ret = it.next(res); } catch (err) { return reject(err); } next(ret); } function onRejected(res) { let ret; try { ret = it.throw(res); } catch (err) { return reject(err); } next(ret); } function next(ret) { if (ret.done) { return resolve(ret.value); } const value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) { return value.then(onFulfilled, onRejected); } return onRejected(typeError(ret.value)); } }); }

co是一个真正的异步解决方案,因为它暴露的接口足够简单。

import co from './co'; function fetchByName(name) { const url = `https://api.github.com/users/${name}/repos`; return fetch(url).then(res => res.json()); } function *gen() { const value1 = yield fetchByName('veedrin'); console.log(value1); const value2 = yield fetchByName('tj'); console.log(value2); } co(gen);

直接把Generator函数传入co函数即可,太优雅了。

#JavaScript
@Async
0comments withand markdown
输入"@+用户名+空格"可以AT某人,AT某人时可以按TAB键补全。
输入"登记过的邮箱"会自动补全剩余信息,可以登记原账号,也可以修改用户名再登记原账号。
先登记后评论,乖
性别: