【10bet】js之yeild

来源:http://www.chinese-glasses.com 作者:Web前端 人气:126 发布时间:2020-04-29
摘要:时间: 2019-09-14阅读: 91标签: 并发背景 1、万恶的回调 对前端工程师来说,异步回调是再熟悉不过了,浏览器中的各种交互逻辑都是通过事件回调实现的,前端逻辑越来越复杂,导致回调

时间: 2019-09-14阅读: 91标签: 并发背景

1、万恶的回调

对前端工程师来说,异步回调是再熟悉不过了,浏览器中的各种交互逻辑都是通过事件回调实现的,前端逻辑越来越复杂,导致回调函数越来越多,同时 nodejs 的流行也让 javascript 在后端的复杂场景中得到应用,在 nodejs 代码中更是经常看到层层嵌套。异步操作的回调一旦嵌套很多,不仅代码会变的臃肿,还很容易出错。

以下是一个典型的异步场景:先通过异步请求获取页面数据,然后根据页面数据请求用户信息,最后根据用户信息请求用户的产品列表。过多的回调函数嵌套,使得程序难以维护,发展成万恶的回调

$.get('/api/data', function(data) {
    console.log(data);
    $.get('/api/user', function(user) {
        console.log(user);
        $.get('/api/products', function(products) {
            console.log(products)
        });
    });
});

异步是 js 一个非常重要的特性,但很多时候,我们不仅仅想让一系列任务并行执行,还想要控制同时执行的并发数,尤其是在针对操作有限资源的异步任务,比如文件句柄,网络端口等等。

2、异步流程控制

  • 最原始异步流程的写法,就是类似上面例子里的回调函数嵌套法,用过的人都知道,那叫一个酸爽。

  • 后来出现了 Promise ,它极大提高了代码的可维护性,消除了万恶的回调嵌套问题,并且现在已经成为 ES6 标准的一部分。

$.get('/api/data')
.then(function(data) {
    console.log(data);
    return $.get('/api/user');
})
.then(function(user) {
    console.log(user);
    return $.get('/api/products');
})
.then(function(products) {
    console.log(products);
});
  • 之后在 nodejs 圈出现了 co 模块,它基于 ES6 的 generator 和 yield ,让我们能用同步的形式编写异步代码
co(function *() {
    var data = yield $.get('/api/data');
    console.log(data);
    var user = yield $.get('/api/user');
    console.log(user);
    var products = yield $.get('/api/products');
    console.log(products);
});
  • 以上的 Promise 和 generator 最初创造它的本意都不是为了解决异步流程控制。其中 Promise 是一种编程思想,用于“当xx数据准备完毕,then执行xx动作”这样的场景,不只是异步,同步代码也可以用 Promise。而 generator 在 ES6 中是迭代器生成器,被 TJ 创造性的拿来做异步流程控制了。真正的异步解决方案请大家期待 ES7 的 async 吧!本文以下主要介绍 co 模块。

看一个例子。

3、co 模块

上文已经简单介绍了co 模块是能让我们以同步的形式编写异步代码的 nodejs 模块,主要得益于 ES6 的 generator。nodejs >= 0.11 版本可以加 --harmony 参数来体验 ES6 的 generator 特性,iojs 则已经默认开启了 generator 的支持。

要了解 co ,就不得不先简单了解下 ES6 的 generator 和 iterator。

  • ### Iterator

Iterator 迭代器是一个对象,知道如何从一个集合一次取出一项,而跟踪它的当前序列所在的位置,

它的接口很简单,一般拥有以下三个方法就可以了。

hasNext() //集合中是否还有下一个元素
next() //迭代到下一个元素
reset()//重置,我见到的代码一般是抛出异常,即一般不支持多次迭代

它提供了一个next()方法返回序列中的下一个项目。

var lang = { name: 'JavaScript', birthYear: 1995 };
var it = Iterator(lang);
var pair = it.next(); 
console.log(pair); // ["name", "JavaScript"]
pair = it.next(); 
console.log(pair); // ["birthYear", 1995]
pair = it.next(); // A StopIteration exception is thrown
  • 可以没有真正的集合(像Array),只要有相应的生成规则就行。这种情况下,没有内存的限制,因此可以表示无限序列

  • 不调用next(),迭代器不进行迭代的,因此有延迟加载的特性。

  • 迭代器,本质上是一个状态机,每个状态下,进行一些操作,然后进入下一个状态或维持状态不变。

乍一看好像没什么奇特的,不就是一步步的取对象中的 key 和 value 吗,for ... in也能做到,但是把它跟 generator 结合起来就大有用途了。

  • ### Generator

迭代器模式是很常用的设计模式,但是实现起来,很多东西是程序化的;当迭代规则比较复杂时,维护迭代器内的状态,是比较麻烦的。 于是有了generator。Generator 生成器允许你通过写一个可以保存自己状态的的简单函数来定义一个迭代算法。 Generator 是一种可以停止并在之后重新进入的函数。生成器的环境(绑定的变量)会在每次执行后被保存,下次进入时可继续使用。generator 字面上是“生成器”的意思,在 ES6 里是迭代器生成器,用于生成一个迭代器对象,最大特点就是可以交出函数的执行权(即暂停执行)。只有在调用了 next() 函数之后,函数才会开始执行。

function *gen() {
    yield 'hello';
    yield 'world';
    return true;
}

以上代码定义了一个简单的 generator,看起来就像一个普通的函数,区别是function关键字后面有个*号,函数体内可以使用yield语句进行流程控制

var iter = gen();

var a = iter.next();

console.log(a); // {value:'hello', done:false}

var b = iter.next();

console.log(b); // {value:'world', done:false}

var c = iter.next();

console.log(c); // {value:true, done:true}

当执行gen()的时候,并不执行 generator 函数体,而是返回一个迭代器。迭代器具有next()10bet,方法,每次调用 next() 方法,函数就执行到yield语句的地方。next() 方法返回一个对象,其中value属性表示 yield 关键词后面表达式的值,done 属性表示是否遍历结束。generator 生成器通过nextyield的配合实现流程控制,上面的代码执行了三次 next() ,generator 函数体才执行完毕。

  • ### co 模块思路

从上面的例子可以看出,generator 函数体可以停在 yield 语句处,直到下一次执行 next()。co 模块的思路就是利用 generator 的这个特性,将异步操作跟在 yield 后面,当异步操作完成并返回结果后,再触发下一次 next() 。当然,跟在 yield 后面的异步操作需要遵循一定的规范 thunks 和 promises。

function sleep(ms) { return new Promise(resolve = setTimeout(resolve, ms));}// simulate an async work that takes 1s to finishasync function execute(id) { console.log(`start work ${id}`); await sleep(1000); console.log(`work ${id} done`);}Promise.all([1, 2, 3, 4, 5, 6, 7, 8, 9].map(execute));

4、7行代码

再看看文章开头的7行代码:

function co(gen) {
    var it = gen();
    var ret = it.next();
    ret.value.then(function(res) {
        it.next(res);
    });
}

首先生成一个迭代器,然后执行一遍 next(),得到的 value 是一个 Promise 对象,Promise.then() 里面再执行 next()。当然这只是一个原理性的演示,很多错误处理和循环调用 next() 的逻辑都没有写出来。

下面做个简单对比: 传统方式,sayhello是一个异步函数,执行helloworld会先输出"world"再输出"hello"

function sayhello() {
    return Promise.resolve('hello').then(function(hello) {
        console.log(hello);
    });
}
function helloworld() {
    sayhello();
    console.log('world');
}
helloworld();

输出

> "world"
> "hello"

co 的方式,会先输出"hello"再输出"world"

function co(gen) {
    var it = gen();
    var ret = it.next();
    ret.value.then(function(res) {
        it.next(res);
    });
}
function sayhello() {
    return Promise.resolve('hello').then(function(hello) {
        console.log(hello);
    });
}
co(function *helloworld() {
    yield sayhello();
    console.log('world');
});

输出

> "hello"
> "world"

输出结果:

5、消除回调金字塔

假设sayhello/sayworld/saybye是三个异步函数,用真正的 co 模块就可以这么写:

var co = require('co');
co(function *() {
    yield sayhello();
    yield sayworld();
    yield saybye();
});

输出

> "hello"
> "world"
> "bye

 通过上面的分析,yield之后,实际上本次调用就结束了,控制权实际上已经转到了外部调用了generator的next方法的函数,调用的过程中伴随着状态的改变。那么如果外部函数不继续调用next方法,那么yield所在函数就相当于停在yield那里了。所以把异步的东西做完,要函数继续执行,只要在合适的地方再次调用generator 的next就行,就好像函数在暂停后,继续执行。

  • ### yield与return的区别:

(1)yield仅代表本次迭代完成,并且还必有下一次迭代;
(2) return则代表生成器函数完成;

利用生成器函数可以进行惰性求值,但无法获取到第一次next函数传入的值,而且只要执行了yield的返回操作,那么构造函数一定没有执行完成,除非遇到了显式的return语句。

"start work 1""start work 2""start work 3""start work 4""start work 5""start work 6""start work 7""start work 8""start work 9""work 1 done""work 2 done""work 3 done""work 4 done""work 5 done""work 6 done""work 7 done""work 8 done""work 9 done"

6、代理yeild

yield* 后面接受一个 iterable object 作为参数,然后去迭代(iterate)这个迭代器(iterable object),同时 yield* 本身这个表达式的值就是迭代器迭代完成时(done: true)的返回值。调用 generator function 会返回一个 generator object,这个对象本身也是一种 iterable object,所以,我们可以使用 yield* generator_function() 这种写法。

➜ qiantou cat yield05.js

function* outer(){

yield 'begin';

var ret = yield* inner();

console.log(ret); yield 'end';

}

function * inner(){

yield 'inner'; r

return 'return from inner';

}

var it = outer(),v;

v = it.next().value;

console.log(v);

v = it.next().value;

console.log(v);

v = it.next().value;

console.log(v)输出:

➜ qiantou node yield05.js

begin

inner

r``eturn from inner

end

`yield 后面接受一个 iterable object 作为参数,然后去迭代(iterate)这个迭代器(iterable object),同时 yield 本身这个表达式的值就是迭代器迭代完成时(done: true)的返回值。调用 generator function 会返回一个 generator object,这个对象本身也是一种 iterable object,所以,我们可以使用 yield* generator_function() 这种写法。```

  • ### yield* 的作用

(1)用原生语法,抛弃 co 的黑魔法,换取一点点点点性能提升

(2)明确表明自己的意图,避免混淆

(3)调用时保证正确的 this 指向

可以看到,所有的 work 都同时开始执行了。

7、总结

对于ES6的生成器函数总结有四点:

(1) yield必须放置在*函数中;
(2) 每次执行到yield时都会暂停函数中剩余代码的执行;

(3) *函数必须通过函数调用的方式(new方式会报错)才能产生自身的实例,并且每个实例都互相独立;

(4)一个生成器函数一旦迭代完成,则再也无法还原,一直停留在最后一个位置;

尤其是第二点,是非常强大的功能,暂停代码执行,以前只有在浏览器环境中,alert、comfirm等系统内置函数才具有类似的能力,所以如果熟悉多线程的语言,你会找到类似的感觉,于是也有人说,有了yield,NodeJS就有协程的能力,完全可以处理多个需要协作的任务。

 

现在,如果我们想让这些 work 每次只执行 2 个,2 个完成之后再继续后面的 2 个,即并发数为 2 应该怎么做呢?

解决方案控制Promise的生成是关键

我们知道,Promise.all并不会触发Promise的执行,真正触发执行的是创建Promise本身,换句话说,Promise在生成的一瞬间就已经开始执行了!因此,如果要控制Promise的并发,我们就要控制Promise的生成。

通过Iterator控制并发数

常见的解决方案是通过一个函数接收并发任务数组,并发函数,并发数3 个参数,根据并发数,监控Promise的完成状态,批量创建新的Promise,从而达到控制Promise生成的目的。

现在,我们来尝试另外一个思路,通过Iterator来控制并发数。

同时遍历同一个Iterator会发生什么?

本文由10bet发布于Web前端,转载请注明出处:【10bet】js之yeild

关键词:

上一篇:程序员的幽默

下一篇:Js执行上下文

最火资讯