异步编程方案:(很实用滴)
Generator 函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
表达式,定义不同的内部状态(yield
在英语里的意思就是“产出”)。
每次调用next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。换言之,Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行。
yield
表达式只能用在 Generator 函数里面,用在其他地方都会报错。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
});
};
for (var f of flat(arr)){
console.log(f);
}
因为是在forEach中(forEach 本身是一个函数,所以会报错)
yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() { console.log('Hello' + yield); // SyntaxError console.log('Hello' + yield 123); // SyntaxError console.log('Hello' + (yield)); // OK console.log('Hello' + (yield 123)); // OK }
yield
表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() { foo(yield 'a', yield 'b'); // OK let input = yield; // OK }
next方法参数:next方法可以传入一个参数,这个参数会被当做上一次yield执行时的初始值(如果不传参数,默认是undefined)
function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var a = foo(5); a.next() // Object{value:6, done:false} a.next() // Object{value:NaN, done:false} a.next() // Object{value:NaN, done:true} var b = foo(5); b.next() // { value:6, done:false } b.next(12) // { value:8, done:false } b.next(13) // { value:42, done:true }
主要看第一个实例(b),第一次调用next此次yield 后的值为6,第二次执行next时,此时设置yield为12,也就是上面我标红的地方,(yield (x + 1)) = 12, 所以y = 24,到第二行yield 时,此时yield 回传为8,当调用第三次next方法时,设置值为13,标红的地方 yield (y / 3) = 13, 所以z值为13,然后到达return ,此时 上面执行结果就是,x 为5,y为24,z为13,相加得到42。
注意如果yield 后面是一个表达式,那么初始值代表的是这个表达式的值,也就是yield紧跟着的。而上面的实例a因为在第二次调用next方法的时候没有传参,所以yield 的值默认为undefined,所以值为NaN
for...of : 因为Generator 运行时会生成iterator,所以可以直接遍历
function* foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5
遍历对象:
function* objectEntries(obj) { let propKeys = Reflect.ownKeys(obj);// 获取对象的keys for (let propKey of propKeys) {// 循环执行,返回key与value yield [propKey, obj[propKey]]; } } let jane = { first: 'Jane', last: 'Doe' }; for (let [key, value] of objectEntries(jane)) { console.log(`${key}: ${value}`); }
因为本身就会生成一个iterator 对象,所以可以直接赋值到对象的Symbol.iterator接口上
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
x.throw:抛出异常(记得用try catch捕获)
var g = function* () { try { yield; } catch (e) { console.log('内部捕获', e); } }; var i = g(); i.next(); try { i.throw('a'); i.throw('b'); } catch (e) { console.log('外部捕获', e); } // 内部捕获 a // 外部捕获 b
catch只会捕获一次,并且返回捕获后下一次的yield 值
如果yield 都在try catch中,那么不会返回。
没有返回2,因为yield 2 在try catch内(并且不会影响下一次的遍历上例中在错误后继续遍历,输出了4)。
Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的catch
捕获。
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next
方法,将返回一个value
属性等于undefined
、done
属性等于true
的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
return:终结遍历Generator
function* gen() { yield 1; yield 2; yield 3; } var g = gen(); g.next() // { value: 1, done: false } g.return('foo') // { value: "foo", done: true } g.next() // { value: undefined, done: true }
yield* 表达式:在 Generator 函数内部,调用另一个 Generator 函数。
对比:
function* bar() { yield 'x'; yield* foo(); yield 'y'; } // 等同于 function* bar() { yield 'x'; yield 'a'; yield 'b'; yield 'y'; } // 等同于 function* bar() { yield 'x'; for (let v of foo()) { yield v; } yield 'y'; } for (let v of bar()){ console.log(v); } // "x" // "a" // "b" // "y"
另外因为Generator 会生成一个遍历器对象,所以,会有下面这样的不同
function* inner() { yield 'hello!'; } function* outer1() { yield 'open'; yield inner(); yield 'close'; } var gen = outer1() gen.next().value // "open" gen.next().value // 返回一个遍历器对象 gen.next().value // "close" function* outer2() { yield 'open' yield* inner() yield 'close' } var gen = outer2() gen.next().value // "open" gen.next().value // "hello!" gen.next().value // "close"
out1 在执行到第二行的时候返回的是一个遍历器对象。
任何数据结构只要有 Iterator 接口,就可以被yield*
遍历。
yield*
后面的 Generator 函数(没有return
语句时),等同于在 Generator 函数内部,部署一个for...of
循环。(如果有return,那么return的值不会被for ...of 侦测到,但是可以作为想代理它的Generator 函数返回数据。)
function* concat(iter1, iter2) { yield* iter1; yield* iter2; } // 等同于 function* concat(iter1, iter2) { for (var value of iter1) { yield value; } for (var value of iter2) { yield value; } }
function* foo() { yield 2; yield 3; return "foo"; } function* bar() { yield 1; var v = yield* foo(); console.log("v: " + v); yield 4; } var it = bar(); it.next() // {value: 1, done: false} it.next() // {value: 2, done: false} it.next() // {value: 3, done: false} it.next(); // "v: foo" // {value: 4, done: false} it.next() // {value: undefined, done: true}
对象属性:
let obj = { * myGeneratorMethod() { ··· } };
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
协程:相比线程,由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield
表达式交换控制权。同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
(也就是说可以自己控制)。
异步操作同步化表达:Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加载UI loader.next() // 卸载UI loader.next()
不用回调函数一个一个套了,看着多清晰~~
控制流:
一般如果异步操作,我们以前会这样做,一直套
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); });
Promise写法:
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();
已经很清晰了。
function* longRunningTask(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 } }
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
更加趋于函数式编程。