Generator函数
基本概念
Generator函数时ES6提供的一种异步编程解决方案;
一、语法上:
- 可以理解为一个状态机,封装了多个内部状态
- 执行Generator函数会返回一个遍历器对象
二、形式上的特点:
- function命令和函数名之间有一个星号*(星号位置没有明确规定,我们一般紧跟function后)
- 函数内部使用yield定义不同的状态
function* genetatorTest() {
yield "start";
yield "running";
return "finished";
}
三、调用与执行-分段
调用也是使用小括号,但是调用Generator函数后,该函数并不执行,返回的也不是函数运行的结果,而是一个遍历器对象 Iterator Object
Generator函数分段执行,yield语句是暂停执行的标记,而next 方法可以恢复执行
Generator函数返回的遍历器对象每次调用 next() 方法,内部指针就从函数头部或上一次停下来的方法开始执行,直到遇到下一条 yield 或 return 语句为止
next方法返回一个对象 { value: 状态值, done: 遍历是否完成boolean}
function* genetatorTest() {
console.log(1);
yield "start";
console.log(2);
yield "running";
return "finished";
}
const obj = genetatorTest();
console.log(obj);
const re1 = obj.next(); // {value: 'start', done: false}
console.log(re1);
const re2 = obj.next(); // {value: 'running', done: false}
console.log(re2);
const re3 = obj.next(); // {value: 'finished', done: true}
console.log(re3);
console.log(obj.next()); // {value: undefined, done: true}
调用Generator函数的时候,函数并没有执行,只是返回了一个遍历器对象,因此并没有打印1
- 第一次调用next(),函数开始执行,打印出1,直到遇到yield停下; 返回一个对象包含value(当前内部状态值,yield 或 return 后面的值)和done(布尔值,遍历是否结束)属性
- 第二次调用next(), 函数接着执行,遇到yield停下
- 第三次调用next(), 函数接着执行,遇到return,函数执行完成;如果没有return语句,就执行到函数结束
- 第四次调用,函数已经执行完成了,返回对象属性为undefined,done为true,以后调用都是这个值
yield表达式
一、暂停的标志
只有调用了next 方法才会执行 yield 语句后面的表达式,因此等于为 JS 提供了惰性求值的功能
二、yield和return的比较:
相似点:都可以返回紧跟后面的表达式的值
区别:每次遇到yield就会暂停执行,下一次会从该位置继续往下执行;而return没有位置记忆的功能,遇到return整个函数终止执行没有多个返回值页不能暂停
三、Generator函数可以不使用yield,此时就是一个单纯的暂缓执行函数
function* generator() {
console.log("命运的此轮停止旋转");
}
const iObj = generator();
iObj.next();
四、yield表达式只能用在 Generator 函数里面,用在其他地方会报错
function fn() {
console.log('这样会报错');
yield 'test' // Uncaught SyntaxError: Unexpected string
}
fn()
五、yield表达式如果用在另一个表达式中,必须放在小括号中
function* demo(x) {
console.log(1 + (yield));
console.log(2 + (yield 1 + x));
}
六、yield表达式用作函数的参数或放在复制表达式的右边,可以不加小括号
function* demo(x) {
const yy = yield 11;
generator(yield x);
}
next()方法
next方法的运行逻辑
- 遇到yield语句就暂停执行后面的操作,并将紧跟在yield后的表达式的值作为返回对象 value 的属性值
- 下一次调用 next 方法继续往下执行,直到遇到下一条 yield 语句
- 如果没有遇到新的 yield 语句,就一直运行到函数结束,直到遇到 return 语句为止,并将 return 语句后的表达式的值作为返回对象 value 的属性值
- 如果函数没有 return语 句,则返回对象 value 的属性值为 undefined
next方法的参数
yield语句本身没有返回值,或者说总是返回undefined,而next可以带一个参数,这个参数会被当作上一条yield的返回值
因此通过next方法的参数可以实现在Generator函数运行的不同阶段从外部注入不同值
next不传入参数:
function* fn(x) {
const y = yield x + 2;
const z = 2 * (yield x);
return x + y + z;
}
const fnObj = fn(2);
fnObj.next();
fnObj.next();
const result = fnObj.next(); // {value: NaN, done: true}
分析:第一次调用next启动遍历器对象,遇到yield暂停,返回一个对象{value:4,done:false};第二次调用next,由于yield返回值为undefined,所以 y=undefined,遇到yield暂停,返回 {value:2,done:false};第三次调用next,z为2与undefined相乘,所以为NaN,最后返回对象 {value:NaN,done:true}
next传入参数:
function* fn(x) {
const y = yield x + 2;
const z = 2 * (yield x);
return x + y + z;
}
const fnObj = fn(2);
fnObj.next();
fnObj.next(4);
fnObj.next(2);
console.log(result);
分析:第一次调用next启动遍历器对象,遇到yield暂停,返回一个对象{value:4,done:false}; 第二次调用next,传入的参数4作为上一条yield表达式的返回值,所以 y=4,遇到yield暂停,返回 {value:2,done:false};第三次调用next,传入参数2,所以z=2*2=4;最后返回对象 {value:10,done:true}
第一次使用next方法传参是无效的,只是用来启动遍历器对象
与Iterator接口的关系
Symbol.iterator方法等于该对象的遍历器对象生成函数,调用该函数会返回该对象的一个遍历器对象
由于Generator函数就是遍历器生成函数,因此可以把Generator函数赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield "一";
yield "one";
};
console.log(...myIterable); // 1 "一" "one"
Generator函数执行后,返回一个遍历器对象,改对象本身也具有 [Synbol.iterator] 属性,执行后返回自身
function* gen(params) {}
const genObj = gen();
genObj[Symbol.iterator]() === genObj; // true
for of循环
for…of 循环可以自动遍历 Generator 函数生成的 Iterator 对象,并且此时不再需要调用next方法
一旦next方法返回的对象done属性为true for…of 就会终止循环,并且不包含该返回对象;所以下面代码return语句返回的值没有打印
function* genFn() {
yield 1;
yield 2;
yield "hello";
yield "everybody";
return "have a nice day";
}
for (let k of genFn()) {
console.log(k); // 1 2 "hello" "everybody"
}
原生的 JavaScript 对象没有遍历接口,无法使用 for… of 循环,通过Generator函数为它加上这个接口就可以用了
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let key of propKeys) {
yield [key, obj[key]];
}
}
const tempObj = { name: "如花", age: 28, gender: "未知" };
const arr = [];
for (let k of objectEntries(tempObj)) { // for...of遍历遍历器对象
arr.push(k);
console.log(k);
}
console.log(arr);
// console.log(Object.entries(tempObj));
Generator.prototype.throw
Generator函数返回的遍历器对象都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获
function* genFunc(params) {
try {
yield;
} catch (err) {
console.log("函数内部捕获异常", err);
}
}
const g = genFunc();
g.next();
try {
g.throw("错误信息msg1");
g.throw("错误信息msg2");
} catch (err) {
console.log("函数外部捕获异常", err);
}
上面代码遍历器对象连续抛出两个异常,第一个被Generator函数内部捕获,第二个异常由于Generator函数内部的catch已经执行过了,不会再捕获这个错误了,所以这个错误抛出了Generator函数体,被外部的catch捕获
注意:这里都是使用遍历器对象的throw方法抛出异常,而不是全局的throw(),如果上面是使用全局的throw抛出异常,只有函数外的catch能捕获
如果于Generator函数内部没有 try… catch ,异常会被函数外的 catch 捕获
function* genFunc(params) {
yield;
}
const g = genFunc();
g.next();
try {
g.throw("错误信息msg1");
} catch (err) {
console.log("函数外部捕获异常", err);
}
如果于Generator函数内部有 try… catch ,那么遍历器的throw方法抛出的错误不影响下一次遍历,否则遍历直接终止
function* genFunc(params) {
try {
yield 1;
yield 2;
yield 3;
} catch (err) {
console.log("内部捕获", err);
}
}
const g = genFunc();
g.next();
console.log(g.throw()); // {value: undefined, done: true}
console.log(g.next()); // {value: undefined, done: true}
这里捕获到错误,try中的代码是不会执行了的,如果generator中,错误之后还有代码会执行;这里后面没有了,所以执行了throw后整个函数就执行完了
抛出错误没捕获和throw一样,程序终止执行了
function* genFunc(params) {
yield 1;
yield 2;
}
const g = genFunc();
g.next();
g.throw(); // 报错:Uncaught undefined
g.throw被捕获后会执行catch中代码,执行完后继续执行,直到遇到yield暂停;throw也会触发一次next的启动,让函数往下走
function* genFunc(params) {
try {
yield 1;
} catch (err) {
console.log("内部捕获", err);
}
yield 2;
yield 3;
}
const g = genFunc();
g.next();
console.log(g.throw()); // {value: 2, done: false}
console.log(g.next()); // {value: 3, done: false}
Generator.prototype.return
返回给定的值,并中介Generator函数的遍历
使用return方法状态直接变为done,后面再调用next,就是 {value: undefined, done: true}
function* genFunc(params) {
yield 1;
yield 2;
yield 3;
}
const g = genFunc();
g.next();
console.log(g.return("终止了")); // {value: '终止了', done: true}
console.log(g.next()); // {value: undefined, done: true}
当Generator函数内部有try…finally ,调用return方法就开始执行finally代码,finally中代码执行完再执行return方法
function* generatorFn() {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
}
const iteratorObj = generatorFn();
const obj11 = iteratorObj.next(); //{value: 1, done: false}
const obj22 = iteratorObj.next(); //{value: 2, done: false}
const obj33 = iteratorObj.return(66); // {value: 4, done: false}
const obj44 = iteratorObj.next(); // {value: 5, done: false}
const obj55 = iteratorObj.next(); // {value: 66, done: true}
const obj66 = iteratorObj.next(); // {value: undefined, done: true}
yield*表达式
yield* 语句可以Generator函数执行另一个Generator函数
如果直接调用gen1()是没有效果的;如果没有加*, 直接使用 yield 调用,就是返回一个遍历器对象
function* gen1(params) {
yield 1;
yield 2;
}
function* gen2(params) {
yield 3;
yield* gen1();
yield 4;
}
等同于
function* gen2(params) {
yield 3;
yield 1;
yield 2;
yield 4;
}
for (let k of gen2()) {
console.log(k); // 3 1 2 4
}
Generator函数this
Generator 函数返回的总是遍历器对象,不是this对象
问题:
- Generator 函数在this对象上面添加了一个属性,但是该函数的实例对象拿不到这个属性
- Generator 函数不能跟
new
命令一起用,会报错
解决:首先,使用call方法将 Generator 函数的this绑定成自身的protoytype,再将 Generator 函数改成构造函数,就可以对它执行new命令了, Generator 函数中this通过call指向了gen的原型
function* gen() {
this.a = 1; // this === gen.prototype
yield (this.b = 2);
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: undefined, done: true}
console.log(f.a); // 1
console.log(f.b); // 2
Generator函数与状态机制
generator是实现状态机的最佳解构
调用一次函数就修改一次bool的状态值
var bool = true;
var changeStatus = function () {
console.log(bool);
if (bool) {
console.log("1");
} else {
console.log("2");
}
bool = !bool;
};
等价与generator的写法
function* statusGen(params) {
while (true) {
console.log("1");
yield;
console.log("2");
yield;
}
}
这种写法不需要保存状态的外部遍历,更加简洁,安全(状态不会被非法篡改)
应用
异步操作的同步化表达
function* loadFn() {
const dom = document.querySelector(".box");
dom.innerText = "加载中";
const re = yield asyncTask();
dom.innerText = re;
}
function asyncTask() {
setTimeout(() => {
loadingObj.next("result:请求到的数据");
}, 3000);
}
const loadingObj = loadFn();
loadingObj.next();
控制流管理
step1(function (value1) {
step2(value1, function (value2) {
step3(value2, function (value3) {});
});
});
promise改写:
Promise.resolve(step1)
.then(step2)
.then(step3)
.catch((e) => {});
generator写法:
function* runTask(params) {
try {
yield step1(value1);
yield step1(value1);
yield step1(value1);
} catch (e) {}
}
部署Iterator接口
利用Generator函数可以在任意对象上部署Iterator接口
Generator函数的异步应用
异步
异步:可以理解为一个任务不是连续完成的,任务完成了一部分之后,过了会才执行剩余部分
ES6之前,异步编程的方案大概有以下几种方案:回调函数、事件监听、订阅发布、promise
回调函数
function fn2(err) {
if (err) throw err;
console.log("执行第二部分");
}
function fn(callback = fn2) {
try {
console.log(ff);
console.log("执行第一部分");
callback();
} catch (err) {
callback(err);
}
}
fn();
将任务分段处理,通过调用回调函数,执行其他部分
问题:多个函数的嵌套,多层函数嵌套形成回调地狱,不好阅读、维护
promise
promise就是为了解决回调地狱而被提出来的
它将回调函数的嵌套改成了链式调用(.then和.catch)
问题:代码冗余,多个then的堆积
generator与协程
协程是程序运行的一种方式,是一种多任务的解决方案,可以理解为 “协作的线程” 或 “协作的函数”
子例程与协程的区别:
子例程采用堆栈式先进后出的执行方式,只有当调用的子函数完全执行完毕,才会结束执行夫函数;多个线程(或函数)可以并行执行,但是只有一个处于正在运行状态,其它处于暂停状态,线程(或函数)之间可以交换执行权
在内存中,子例程只是用一个栈,而协程使用多个栈,但是只有一个栈处于正在运行状态,协程是以多占用内存为代价实现多任务的并行运行
由于JS是单线程语言,只能保持一个调用栈。引入协程后,每个任务可以保持自己的调用栈。好处就是抛出错误的时候可以找到原始的调用栈
协程有点像函数,又有点像线程,它的大概执行流程:
- 协程A开始执行
- 协程A执行到一半,进入暂停状态,执行权转移到携程B中
- 一段时间后,协程B交换执行权
- 协程A恢复执行
Generator函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因;另外函数体内外的数据交互和错误处理机制也使得它能够更好处理成异步编程
generator对于异步任务的封装
const fetch = require("node-fetch");
function* gen() {
const url = "https://api.github.com/users/github";
const result = yield fetch(url);
return result;
}
const genObj = gen();
genObj
.next()
.value.then((res) => {
return data.json();
})
.then((res) => {
genObj.next(res);
});
Thunk函数
Thunk函数是自动执行Generator函数的一种方法
Thunk函数介绍
const x = 1;
function fn(params) {
return params + 1;
}
fn(x + 2);
参数求值策略:什么时候计算x+2
1》传值调用
在进入函数体之前就计算参数的值
2》传名调用
直接将表达式传入函数体,只在用到它的时候求值
Thunk函数是“传名调用”的一种实现方式;就是将函数参数放到一个临时函数中,再将这个临时函数传入函数体,这个临时函数就是Thunk函数
const x = 1;
function fn(params) {
return params + 1;
}
fn(x + 2);
等同于
const thunk = function () {
return x + 2;
};
function fn() {
return thunk() + 1;
}
JS中的Thunk函数
JS语言是传值调用,它的Thunk函数含义有所不同
它是将多参函数替换成一个只接受回调函数作为参数的单参函数
// 正常的多参函数
fs.readFile(fileName, callback);
// thunk版本
const Thunk = function (fileName) {
return function (callback) {
fs.readFile(fileName, callback);
};
};
const readThunk = Thunk(fileName);
readThunk(callback);
流程管理:
function* gen(params) {
// ……
}
const g = gen(0);
const res = g.next();
while (!res.done) {
g.next();
}
前面有看过这段代码:Generator函数gen回自动执行完所有步骤
但是不适合异步操作,这时Thunk函数就派上用处了
基于回调的自动执行
基于Promise的自动执行
function run(gen) {
const genObj = gen();
function next(data) {
const result = genObj.next(data);
if (result.done) return result.value;
result.value.then((data) => {
next(data);
});
}
next();
}
function* gen(params) {
re1 = yield asncPromise(11);
console.log(1);
re2 = yield asncPromise(22);
console.log(2);
re3 = yield asncPromise(33);
console.log(3);
re4 = yield asncPromise(44);
console.log(4);
return re4;
}
function asncPromise(num) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(num);
resolve(12);
}, Math.random() * 1000);
});
}
run(gen);
async、await与generator
Generator函数:
function asyncOperate(num) {
return new Promise((resolve) => {
const time = Math.random() * 2000;
setTimeout(() => {
console.log(num, time);
resolve(num * 2);
}, time);
});
}
function* gen() {
const re1 = yield asyncOperate(1);
const re2 = yield asyncOperate(2);
console.log(re1, re2);
}
function run(gen) {
const genObj = gen();
function next(data) {
const result = genObj.next(data);
if (result.done) return result.value;
result.value.then((data) => {
next(data);
});
}
next();
}
run(gen);
async await:
async function fn() {
const re1 = await asyncOperate(1);
const re2 = await asyncOperate(2);
console.log(re1, re2);
}
function asyncOperate(num) {
return new Promise((resolve) => {
const time = Math.random() * 2000;
setTimeout(() => {
console.log(num, time);
resolve(num * 2);
}, time);
});
}
fn();
async函数实际上就是将Generator函数的星号 * 替换成 async ; 将 yield 替换成 await
async函数对Generator函数的改进
1》自带执行器,而Generator函数的执行依靠执行器,co模块就是用于Generator函数的自动执行
2》语义更加清晰,async说明函数中有异步操作,await是等待异步执行
3》返回值是promise,更加方便;Generator返回的是Iterator对象
4》适用性更广,co模块规定yield后面跟异步或promise,保证执行顺序,await后面可以是promise,也可以是原始类型值
await后面一般是跟一个Promise,如果不是,会被转为一个resolved状态的Promise