es6之generator函数的应用
es6之generator函数的应用
最近在学习到redux-saga
用法的时候,发现其中大量用到了es6
中新增的Generator
函数,在学习Generator
函数的同时,整理了一些关于Generator
函数的基本应用,这篇文章不涉及到Generator函数的基本用法,关于基本语法知识大家可以参考这里。
Generator 与状态机
如果我们想实现一个功能,函数每次执行一次就改变一次状态,要怎么实现?如果你使用es5
实现你可能会这样做:
//ES5
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
这么写可以实现,但是会存在一个问题,我们需要定义一个全局变量来保存内部状态,有没有更好的实现方式呢?当然!!下面看下es6
的实现方式:
//ES6
const clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
const gen = clock();
gen.next(); // Tick!
gen.next(); // Tock!
gen.next(); // Tick!
gen.next(); // Tock!
这里定义了一个Generator
函数clock
,当执行第一个next()
方法时,打印出'Tick!'
,这个时候遇到yield
关键字,函数暂时停止执行并跳出;执行第二个next()
方法,函数接着第一个yield
语句后面继续执行,打印出'Tock!'
,遇到yield
关键字,函数停止执行并跳出;这个时候Generator
函数里面的两个yield
语句就执行完毕了,那么我们如何实现有周期的改变状态呢?实现的方法就是借助于Generator
函数体里面的while(){...}
语句。当我们继续执行第三个next()语句,此时函数会重复执行while循环里面的第一个console
方法,打印出'Tick!'
,接着打印出'Tock!'
,如此循环往复。
Generator 异步操作的同步化表达
下面要介绍的部分是Generator
函数的应用中应用比较广泛的。Generator
函数可以利用yield
关键字暂停函数执行,利用这一特性,我们可以将耗时的异步操作放在yield
关键字之后,这样等到调用next()
方法的时候再继续往后执行,这样实际上就省略了回调函数。看下面的例子:
function apiInterface(url) {
ajaxCall(url, function(response){
it.next(response);
});
}
function* getUserInfo() {
var result = yield apiInterface("http://test/getUserList");
var resp = JSON.parse(result);
console.log(resp.value);
}
var it = getUserInfo();
it.next();
这里定义了一个Generator
函数getUserInfo
,yield
关键字后面跟着一个调用ajax
方法获取信息的方法,调用it.next()
开始执行apiInerface
方法,apiInerface
方法里面又执行了一次next()
,并同时将ajax
请求的返回值response
作为参数传入next()
,注意,这个response
参数一定要传,否则result
值为undefined
,这里要记住一句话:next
方法参数的作用,是覆盖掉上一个yield
语句的值。
再看一个例子:
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()
// 卸载UI
loader.next()
上面代码中,第一次调用loadUI
函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next
方法,则会显示Loading
界面,并且异步加载数据。等到数据加载完成,再一次使用next
方法,则会隐藏Loading
界面。可以看到,这种写法的好处是所有Loading
界面的逻辑,都被封装在一个函数,按部就班非常清晰。
在看一个复杂点的例子:
如果现在有三个比较耗时的操作step1
,step2
,step3
,现在想先执行step1
,然后将执行step1
之后得到的值value1
作为参数传入step2
,再将执行step2
之后得到的结果value2
作为参数执行step3
,如果使用回调的方式可以这样编写代码:
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
// do something with value3
});
});
});
如果将上面的需求用Promise方法改写可以这么写:
step1().then(function(value1) {
return step2(value1);
}).then(function(value2){
return step3(value2);
}) .then(function(value3) {
// do something with value3
});
用Promise
的写法可以很好的避免出现回调函数的情况,注意,这里我们假设step1
,step2
,step3
是三个Promise
对象。
最后我们看下使用Generator
函数如何改造上述需求:
function* someTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
然后定义一个调度函数一次执行三个任务:
scheduler(someTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
通过Generator
函数将回调方式改成了线性方式,但是上述方式只适合同步操作,因为这里的代码在得到上一步的结果之后就会立即执行,没有判断异步操作合适结束。