前言 💬
Generator 是 ES6 的最新语法,译为生成器 ,是一种异步的解决方案,一种特殊写法的函数,体内有 yield
关键字,译为产出 ,可以把 yield
理解成一个指针,分段执行 Generator 函数内部的状态
js
function* gen() {
yield 'Hello';
yield 'World';
}
调用生成器 (gen
) 会返回一个 迭代器 对象(g
),调用 g.next()
方法可以移动 生成器 内部的指针(yield
),并返回一个包含 { value: yield后的值, done: false/true }
属性的对象,done
属性提示状态(yield
)是否都结束了
js
const g = gen();
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'World', done: false }
g.next(); // { value: undefined, done: true }
当然你也可以手动 return
结束掉
js
function* gen() {
yield 'Hello';
return 'bye';
yield 'World';
}
const g = gen();
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'bye', done: true }
g.next(); // { value: undefined, done: true }
Genarator 与 Iterator的关系 👬
我们知道所有部署了[Symbol.iterator]
属性的数据类型,都可以被for of
遍历
js
const obj = {}
obj[Symbol.iterator] = function* (){
yield 1;
yield 2;
yield 3;
}
console.log([...obj])
// [ 1, 2, 3 ]
for (let i of obj) {
console.log(i)
}
// 1 2 3
yield
返回值 🤷
yield
表达式本身没有返回值,或者总是返回 undefined
js
function* gen() {
const x = yield 1;
console.log('x:', x)
}
const g = gen();
console.log('out:', g.next()) // out: { value: 1, done: false }
console.log('out2:', g.next())
// x: undefined
// out2: { value: undefined, done: true }
可以看到,当打印out2:
的时候,x:
为 undefined
迭代器 向 ➡️ 生成器 通信
还是上面的代码,只在第二次 g.next(2)
中传了一个 2
js
function* gen() {
const x = yield 1;
console.log('x:', x)
}
const g = gen();
console.log('out:', g.next())
console.log('out2:', g.next(2)) //<---
// out: { value: 1, done: false }
// x: 2
// out2: { value: undefined, done: true }
可以发现 迭代器 通过 .next()
方法成功的把值,传递进了 生成器 内部上一阶段(yield
)的返回值
回顾异步的历史
回调函数♻️
js
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
这种持续向右➡️ 延伸的恐惧感就是回调地狱
Promise👌
js
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
Promise 用链式调用
把 向右的方向 优化成了向下的直线形式 ,但是代码冗余,一眼看上去全是 then
Generator 异步操作同步化表达 🙋
这种干净清晰的书写方式,像极了顺序执行的同步代码
js
function* longTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
借助一个方法让它自动运行
js
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
scheduler(longTask(initialValue));
longTask()
在传入scheduler
的时候已经调用并生成了task
迭代器initialValue
会当作value1
传递给 step1(value1
)- 外部
var taskObj
接收了step1(value1)
任务的返回值 - 用
taskObj.done
属性判断任务还没有结束(继续) - 把第 1 次任务的结果 (
task.value
),进入调度函数scheduler
, 赋值给var value2
,同时把var value2
当作第 2 次任务的参数传入step2(value2)
- 循环往复,直到所有任务完结
task.done:true
以上只适合同步操作
这种做法只适合同步操作,即所有的任务步骤都是同步的,没有异步操作。这是因为调度器函数 scheduler
在执行任务步骤时,没有考虑异步操作的完成时间。
在同步操作中,每个步骤的执行是顺序的,每个步骤的结果都会立即返回,然后传递给下一个步骤。调度器函数 scheduler
在每个步骤完成后,通过调用 task.next()
继续执行下一个步骤。
然而,在存在异步操作的情况下,调度器函数 scheduler
的简单递归调用方式就无法正确处理异步操作的完成时间。异步操作的完成时间是不确定的,可能需要一段时间才能得到结果。而简单的递归调用方式会立即执行下一个步骤,而不等待异步操作完成。
Generator 函数的异步应用
Js 是单线程的,大部分任务都是采取异步的方式,如果 Generator 只能玩同步,那... 好像没啥意思
回顾 - 读取文件的异步操作
1. 回调函数
先看下 node 中 fs
模块异步读取文件的操作
- 参数1: 你要读取的文件在哪里(
文件路径
) - 参数2: 读取的回调结果(
是成功还是失败
)
js
fs.readFile('/etc/passwd', function (err, data) {
if (err) throw err;
console.log(data);
});
🙅:回调地狱
2. Promise
js
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
🙅♂️: 代码冗余,一眼看去都是一堆then
Thunk
Thunk 函数 是自动执行 Generator 函数的一种方法,可以理解成一种 高阶函数
Thunk 函数 将 多参数函数 ,替换成一个只接受回调函数作为参数的单参数函数 ( 是不是嗅到了一丝 柯里化
的味道~ )
Thunk 函数 可以用于 Generator 函数的自动流程管理
js
function a (b, c) {
return b + c;
}
function thunk (b) {
return function(c) {
return b + c;
}
}
thunk(b)(c);
逐渐演化到应用场景
js
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
Thunkify 模块 📦
推荐一个包 thunkify
,使用方式如下
js
var thunkify = require('thunkify');
var fs = require('fs');
var readFileThunk = thunkify(fs.readFile);
readFileThunk('package.json')(function(err, str){
// ...
});
Thunk 函数的自动流程管理
Thunk 函数真正的威力是,可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器
js
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
function* g() {
var f1 = yield readFileThunk('fileA');
var f2 = yield readFileThunk('fileB');
// ...
var fn = yield readFileThunk('fileN');
}
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
run(g);
请细细的品这部分代码,result.value(next)
中的 next()
本身就是异步操作的 回调函数
加之,thunkify(fs.readFile)
中 fs.readFile
被 Thunk
化,
所以每次 readFileThunk('fileN')
执行后的返回值,是一个函数,就是 result.value
, 准备 接收第2个参数 , 那个回调函数