木灵鱼儿
阅读:158
生成器 Generator
简介
Promise的出现,我们可以将回调进行反客为主,不在受回调调用者的限制(控制反转和信任问题),但是它并没有解决异步编程导致的代码顺序问题,有没有一种方式,可以让我们的代码虽然是异步的,但是书写顺序却是同步的,这样完全符合我们人类大脑理解方式?
console.log("同步的1");
axios({ ...
}).then(() => {
console.log("异步的回调,如果需要等待结果处理,代码只能写在这个回调里")
});
console.log("同步的2")
很明显,如果我们希望"同步的2"
在请求之后打印,只能写在then的回调函数中,但是这显然不是最佳的可被大脑阅读的代码,特别是在一些人根本不知道axios是什么的时候。
他很有可能会认为then中的log打印是在"同步的2"
之前输出的。
Generator 生成器
在我们的认知里,当一个函数运行时,它一定会完整运行完才会运行下一个函数。
一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插人其间
function a() {
//完整运行完后
}
function b() {
//才会到这里
}
a();
b();
这个特性也是因为我们的JavaScript是单线程语言。
在ES6的时候引入了一个新的函数类型:生成器(Generator);它并不符合我们上述的这种特性,它可以通过yield
实现暂停,但是它只是暂停函数内部的运行,并不会影响到函数外的代码,生成器运行后会生成一个迭代器,外部通过运行迭代器的next
方法,实现结束暂停继续运行,直到下一个yield
或者函数运行结束。
var x = 1;
function* foo() {
x++;
yield; // 暂停
console.log("x:", x);
}
可以看到foo
函数的前面是带有*
号的,当然不止这种写法,还有好几种写法,其实都是一个意思:
function* foo(x, y) {···}
function* foo(x, y) {···}
function* foo(x, y) {···}
function* foo(x, y) {···}
其中的yield
表示暂停,暂停执行后面的代码,这个后面指的是yield ...;
的后面,也就是说yield后面是可以接值的,这个值或者说表达式并不会被暂停,而是值后面的代码会被暂停。
yield 后面的值会作为迭代器对象的value属性的值返回出去。
function* foo() {
yield 1;
console.log(2);
yield 3;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.next(); //运行一次
console.log(res1.value); //1
const res2 = it.next(); //运行一次,log输出2
console.log(res2.value); //3
运行第一次next的时候,foo生成器中的代码才是真正的运行,而foo()
只是生成迭代器实例,并不会运行里面的代码。
第一次next运行到一个yield
处,此时后面跟着一个值1,这个值会被赋值给迭代器对象value属性,我们在外部可以获取到这个迭代器,从而获取到1
。
此时后面的代码是暂停的。
当我们运行第二次next的时候,log打印出2
;然后在第二个yield
处停止,yield 后面跟着一个3,3会被赋值给迭代器对象value属性,外面通过value获取到这个值。
但是需要注意,迭代器并没有停止,或者说迭代完毕,我们打印res3
得到:
{
value: 3,
done: false
}
done不等于true
表示还没有迭代完成,我们再运行一次next,此时才算迭代完成。
const res3 = it.next(); //运行一次
console.log(res3); //{ value: undefined, done: true }
此时没有yield,函数默认结尾是 return undefined
,于是这个值被作为迭代器value的值,此时done被改为true。
手动结束
迭代器是支持手动结束的,返回的迭代器对象it
除了next还有两个属性方法:
return()
:返回给定的值,并且终结遍历 Generator 函数throw()
:往 Generator 函数体内传入一个错误对象,throw new Error()这种
这两个方法,其中return是可以直接中介循环,而throw是类似于抛出错误的形式,从而结束代码,但是如果Generator 函数内部try...catch了,如果错误被try捕获了,那么就无法结束了。
function* foo() {
yield 1;
console.log(2);
yield 3;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.throw("错误"); //运行一次 Uncaught 错误
console.log(1111); //运行不到这了
const res2 = it.next(); //运行不到这了
console.log(res2); //运行不到这了
捕获了错误:
function* foo() {
try {
yield 1;
console.log(2);
yield 3;
} catch (error) {
console.log("捕获了错误", error);
}
yield 4;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.next(); //运行一次
console.log(res1);
const res2 = it.throw("错误"); //捕获了错误 错误
console.log(res2); //{ value: 4, done: false }
const res3 = it.next(); //运行一次
console.log(res3); //{ value: undefined, done: true }
由于捕获了错误,所以代码还是会继续运行,于是运行到了yield 4;
,所以打印res2时value是4。
return()则相对好理解一些。
function* foo() {
yield 1;
console.log(2);
yield 3;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.next(); //运行一次
console.log(res1.value); //1
const res2 = it.return("结束"); //运行一次
console.log(res2); // { value: "结束", done: true }
const res3 = it.next(); //运行一次
console.log(res3); //{ value: undefined, done: true }
next也可以传值
事实上生成器生成的迭代器在调用其next方法的时候,也是可以传值的,这个值可以看成是运行到的yield xxx;
代码的结果。但是第一次的next不管传什么都会被忽略,因为第一次是启动迭代并运行到yield处,第二次next时传的值才是第一个yield的结果。
function* foo() {
try {
const a = yield 1;
console.log(111, a); //111 undefined
console.log(2); // 2
yield 3;
} catch (error) {
console.log("捕获了错误", error);
}
yield 4;
}
const it = foo(); //运行生成器生成迭代器实例
const res1 = it.next("第一次"); //运行一次
console.log(res1); //{ value: 1, done: false }
const res2 = it.next(); //运行一次
console.log(res2); //{ value: 3, done: false }
const res3 = it.next(); //运行一次
console.log(res3); //{ value: undefined, done: true }
打印的顺序:
//{ value: 1, done: false }
//111 undefined
//2
//{ value: 3, done: false }
//{ value: 4, done: false }
第一个next的参数被忽略,无人使用它,传了也会被丢弃,因为规范就是这样。
异步
我们可以发现,只有调用了next方法,代码才会往下运行,这就带来一个非常大的改变,我们可以将异步的处理抽出来,然后在异步的回调里运行next方法,从而可以实现代码的同步书写顺序。
function foo(x, y) {
axios(`xxx?x=${x}&y=${y}`)
.then(function(res) {
it.next(data);
})
.catch(function(err) {
it.throw(err);
});
}
function* main() {
try {
var text = yield foo(11, 31);
console.log(text);
} catch (err) {
console.error(err);
}
}
var it = main();
// 启动
it.next();
这个时候你会发现,main里面的代码其实是同步运行的。
但是会有一个问题,我们需要手动next
触发,而且如果代码存在多个 yield
就得运行多个next方法,显然这并不是很方便。
运行器
如果能有一个工具,能够帮我们省略掉书写next运行代码就好了,事实上也是有的,但是由于这些库昙花一现,就不去找具体的链接了,我们可以看一个示例代码:
function run(gen) {
var args = [].slice.call(arguments, 1),
it;
it = gen.apply(this, args);
return Promise.resolve().then(function handleNext(value) {
var next = it.next(value);
return (function handleResult(next) {
if (next.done) {
return next.value;
} else {
return Promise.resolve(next.value).then(handleNext, function handleErr(err) {
return Promise.resolve(it.throw(err)).then(handleResult);
});
}
})(next);
});
}
function* main() {
const a = yield Promise.resolve(1);
console.log(a);
const b = yield Promise.resolve(2);
console.log(b);
}
run(main);
这段代码结合了promise,整体会稍微复杂一些,但是一定要读懂。
其实原理也非常简单,通过命名函数的方式实现递归调用,通过promise协议,在then之后将结果传给handleNext
函数,函数判断迭代器是否已经结束,如果结束直接return出结果。
如果没有结束,则继续handleNext
运行,在函数内部通过next方法得到迭代器,将迭代器的值封装到一个新的promise中,继续等待结果,以此往复。
async ? await?
此时你会发现,这种方式和现在我们常用的es7定义的async await非常相似,其实这种用法就是我们在es6时自己实现的用法,由于这是一种非常强大的方法,于是被纳入了标准中完善。
所以async和await底层其实就是Generator 生成器。
生成器委托
事实上我们除了yield一些异步处理函数,我们可能还会有yield 生成器()
的需求,这在一些稍微复杂的场景也是非常常见的,一个请求需要等待前面好几个异步函数的结果,为了方便会将一些异步函数封装成一个api异步函数(生成器)调用并做yield 等待其结果。
而这种方式可以利用生成器委托实现,用法就是在yield 时在生成器函数前面加*
号。
yield *foo();
此时foo的迭代器会委托给外部的生成器处理,不需要手动next或者再套一个run函数包起来。
ES6之前的生成器
其实就是如果在没有生成器的环境使用生成器。
从写法上我们无法使用更加简洁的方式,所以如果需要达到这种效果,需要写一堆套路代码。
//request会返回promise
function* foo(url) {
try {
console.log("requesting:", url);
var val = yield request(url);
console.log(val);
} catch (err) {
console.log("Oops:", err);
return false;
}
}
var it = foo("http://some.url.1");
转换后:
function foo(url) {
//管理器生成状态
var state;
//生成器变量范围声明
var val;
function process(v) {
switch (state) {
case 1:
console.log("requesting:", url);
return request(url);
case 2:
val = v;
console.log(val);
return;
case 3:
var err = v;
console.log("Oops:", err);
return false;
}
}
//构造并返回一个生成器
return {
next: function(v) {
//初始状态
if (!state) {
state = 1;
return {
done: false,
value: process(),
};
}
//yield
else if (state == 1) {
state = 2;
return {
done: true,
value: process(v),
};
}
//生成器已经完成
else {
return {
done: true,
value: undefined,
};
}
},
throw: function(e) {
//唯一的显示错误处理在状态1
if (state == 1) {
state = 3;
return {
done: true,
value: process(e),
};
}
//否则错误不会处理,直接抛出
else {
throw e;
}
},
};
}
将代码拆分成步骤,通过状态值管理进度,通过调用封装的回调函数,接受进度值来触发对应的具体逻辑代码。
对外返回一个迭代器,暴露next和throw方法。
你会发现这其实是一个套路,但是具体的业务代码也得写在里面,这就导致如果使用了async和await,对于低版本的支持,如果手动兼容的话,那还不如不用async呢!
所以后续大佬们提供了自动转换的工具:regenerator
由facebook的几位大佬开发的,其实大体原理都是差不多的,只是工具帮你自动做了,所以咱们现在使用async await的时候,都没啥后顾之忧了。
版权申明
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。
相关推荐
对象的遍历
for...in循环可以用来遍历对象的可枚举属性列表,但是如果遍历对象的值呢?以数组为例,我们最基础的方式就是for循环:var myArray = [1, 2, 3]; for (var i = 0; i < myArray.length; i++) { console.log(myArray[i]); } // 1 2 3事实上这种并不是真的遍历值,而是在遍历下标来获取到对应的值。ES5新增了一些数组的遍历方法,比如:forEach(..)、every(..) 和 some(..);每种方法都可以接受一个回调函数并将其应用到数组每个元素上,,唯一的区别就是它们对于回...
class 与 await结合
class As { constructor() {} then(resolve, reject) { setTimeout(() => { resolve(true); }, 3000) } } (async () => { const b = await new As(); console.log(b); })()
异步队列管理器
造这个轮子其实也是没得办法,搜不到合适的轮子用,就只能自己干了。使用场景我们有N个异步任务promise,他们没有顺序关系,谁先触发都无所谓,但是我们只关心一点,如果某一个任务出错,后续就不要运行了,只有全部都success完成,那么才运行成功后的处理方式then。当然,我们肯定不能使用Promise.all运行N个任务,这等于是同步触发了,如果我有2000个任务,难道你也一口气发2000个任务,那这就不现实了,所以这里我们要引入线程概念,一个进程可以有多个线程,那么进程就是管理器,线程就是我们一次可以发多少个请求。线程是可以配置的,我们可以指定触发多少个线程,当1个线程完成后,我们要填...
koa教程2 中间件和洋葱模型
koa的中间件通过new出Koa对象的use方法进行注册,每个注册的中间件其实就是一个函数,函数接收两个参数:ctx和next。ctx是上下文对象,是中间件共用的一个对象,也就是共享的对象,我在1中间件对这个ctx添加或者处理内容,再进过2中间件时,可以获取到1所做的所有处理后的结果。next是一个回调函数,他是运行下一个中间件的关键,如果没有next,那么运行就结束在当前中间件中。也正是这个next,才能达到koa所提倡的洋葱模型。const Koa = require("koa"); const app = new Koa(); app.use((ctx, ne...

git永久删除文件或文件夹并删除历史记录同步到远程仓库
删除文件需要注意一点,就是你当前的项目,没有任何文件在待提交上,或者暂存区,有的话如果不重要你可以取消掉,或者先提交一次。让git这两个区域空下来。如果没有清空使用删除,会提示如下代码:Cannot rewrite branches: You have unstaged changes.永久删除文件清空暂存和提交区,然后输入以下命令:git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch password.txt' --prune-empty --tag-name-filter cat -- -...
koa框架3 基础入门之 async、await
async和await可以理解为generator的升级,因为generator需要一个runner库来运行它,这样一来就很麻烦,所有就有了async,其用法差不多。await后面接异步操作或者同步操作,但是绝对不能为空,然后这个函数function开头接一个async用法如下:async function show() { let user = await $.ajax({ url: "xxx/api", dataType: "json" }); let items = null; ...
koa框架2 基础入门之generator、yield
koa框架2 基础入门之generator、yieldgenerator可以理解为一个可以分段执行内部代码的函数,通过yield分隔,yield接一个promise异步,这个异步完成,才能执行下一个代码片段。(异步的请求同步的写法)就好像是将一个异步的操作,转为同步了,但是,不是很方便,因为每一个片段运行结束后,需要执行next()才能进行下一个片段。创建generator和普通函数差不多,不过,他需要在函数名的前面加一个*星号,这个星号可以和function字母挨在一起,也可以和函数名挨在一起,但是,function和名字必须要有空格,不管星号挨谁。function* name() {...