你不知道的JavaScript-----生成器

目录

生成器、迭代器、iterable

迭代器

生成器 

Iterable 

初探生成器

输入和输出

参数

返回值

迭代消息传递

两个问题的故事

多个迭代器

异步迭代生成器(解决回调地狱问题-回调中带有回调) 

生成器+Promise

编写支持Generator的Promise Runner

async和await

找回并行执行的能力

隐藏Promise

生成器委托 

ES6之前的生成器


生成器、迭代器、iterable

迭代器

迭代器其实它和其他语言中的迭代器并无差别,就是一个对象,它包含一个next方法用来遍历这个对象中的某种序列。

//创建迭代器
let iter=arr[Symbol.iterator]();
//value 就是值,done 表示跌代结束了没有
console.log(iter.next());  //{value: 10, done: false}
console.log(iter.next());  //{value: 20, done: false}
console.log(iter.next());  //{value: 30, done: false}
console.log(iter.next());  //{value: undefined, done: true}

假定你要产生一系列值,其中每个值都与前面一个有特定的关系。要实现这一点,需要一个有状态的生产者能够记住其生成的最后一个值

const gimmeSomething = (function () {
    let nextVal = 0;
    return function () {
        nextVal += 2;
        return nextVal;
    }
}());

console.log(gimmeSomething()); // 2
console.log(gimmeSomething()); // 4
console.log(gimmeSomething()); // 6
console.log(gimmeSomething()); // 8

 迭代器来解决方案:

for of自动调用迭代器返回对象中的value, 等价于 this.next().value, this指向当前对象

// 迭代器解决方案
const something = (function () {
    let nextVal = 0;
    return {
        // for..of循环需要
        [Symbol.iterator]: function () { return this; },
        // 标准迭代器接口方法
        next: function () {
            nextVal += 2;
            return { done: false, value: nextVal };
        }
    }
}());

// 因为迭代器 something 总是返回 done:false
for (const iterator of something) {
    console.log(iterator); // 2 4 6 8 返回对象中的value, 等价于 this.next().value, this指向当前对象
    if(iterator >= 8) break;
}

console.log(something.next()); // {done: false, value: 10}
console.log(something.next()); // {done: false, value: 12}
console.log(something.next()); // {done: false, value: 14}
console.log(something.next()); // {done: false, value: 16}

生成器 

生成器把一系列通过yield让出cpu,暂时暂停运行的过程看作一个序列,也就是说,JS为了让我们方便的使用生成器的功能,它选择了让生成器返回一个迭代器来帮助我们,让我们使用简单的迭代器API来操作生成器。


function* genFunc(){
    console.log('执行了genFunc-1');
    yield 1;  //genFunc函数里面的代码遇到yield就会停下来并跳出这个函数,返回yield后面的值(加工成一个对象)给调用.next()的地方
    console.log('执行了genFunc-2');
    yield 2;
    console.log('执行了genFunc-3');
    yield 3;
}
//  生成器函数的执行机制:生成器函数并不是执行函数里面的代码,而是创建一个迭代器
let iters=genFunc() //创建一个迭代器
console.log(iters.next());// 执行了genFunc-1 {value: 1, done: false}

console.log(iters.next());  //  执行了genFunc-2 {value: 2, done: false}
console.log(iters.next());  //  执行了genFunc-3  {value: 3, done: false}
console.log(iters.next()); //  {value: undefined, done: true}

无限生成器 

function* foo() {
    let nextVal = 0;
    while (true) {
        nextVal += 1;
        yield nextVal;
    }
}
let it = foo();
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 2, done: false}
console.log(it.next()); // {value: 3, done: false}

Iterable 

下面我们介绍一下Iterable,我们可以把它翻译成“可迭代的”,一个对象,如果有一个名为[Symbol.iterator]的方法,这个方法返回一个迭代器,就认为它是Iterable的。

注意它和迭代器之间的关系,迭代器只用于提供一套公有的API来遍历某种序列,而Iterable则代表了一个对象是具有迭代器的。

遍历Array可以采用下标循环,遍历MapSet就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,ArrayMapSet都属于iterable类型。

Iterable可以和for-of循环交互,。

var a = ['A', 'B', 'C'];
var s = new Set(['A', 'B', 'C']);
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
for (var x of a) { // 遍历Array
    console.log(x);
}
for (var x of s) { // 遍历Set
    console.log(x);
}
for (var x of m) { // 遍历Map
    console.log(x[0] + '=' + x[1]);
}

初探生成器

一个任务就是一个函数,一个任务一旦执行,没有人能够打破这个执行,这也保证了JS中不会发生类似Java等抢占式多线程编程语言的竞态问题。

可能看起来似乎有点奇怪,不过ES6 引入了一个新的函数类型,它并不符合这种运行到结束的特性。这类新的函数被称为生成器。

var x = 0;

function *foo(){
    yield;
    console.log(x);
}

function bar(){
    x++;
}


var it = foo();
it.next();
bar();
it.next(); // 1
  1. 生成器函数通过在函数名前面加*来定义
  2. 调用生成器函数并没有直接执行这个函数,而是返回一个对象,这个对象有next方法
  3. 调用next方法启动生成器函数
  4. 生成器函数执行过程中遇到yield会主动让出控制权。
  5. 同第4点,yield后调用next方法会从暂停位置启动生成器函数,恢复函数内部之前的状态

输入和输出

参数

生成器函数虽不是不同函数,但仍是个函数,它依然可以有参数和返回值。

var x = 0;

function *foo(prefix){
    yield;
    console.log(prefix+": "+x);
}

function bar(){
    x++;
}


var it = foo("x is");
it.next();
bar();
it.next(); // x is: 1

返回值

通过刚刚的例子,我们发现调用生成器方法返回了一个对象,这个对象叫迭代器,我们后面会介绍,那么生成器方法的返回值会被返回到哪里呢?

前面的例子中也能发现,调用next才会实际启动生成器,实际上生成器函数中的return语句返回给最后一个it.next,如果没有返回语句也会有一个隐式的return undefined

    let x = 0;
    function* foo(prefix) {
        yield;
        console.log(prefix + ": " + x);
        return 'OK';
    }
    function bar() {
        x++;
    }


    let it = foo("x is");
    it.next();
    bar();
    let status = it.next();
    console.log(status);
//----------
x is: 1
{ value: 'OK', done: true }

可以看到生成器的next返回的不是一个单独的值,而是一个对象,我们可以通过value来获取这个值,done属性代表的是生成器是否已经执行完成。

迭代消息传递

function* foo(x) {
    var y = x * (yield);
    return y;
}
var it = foo(6);
// 启动foo(..)
it.next();
var res = it.next(7);
res.value; // 42
  • 传入6 作为参数x。然后调用it.next(),这会启动foo(..)
  • foo(..) 内部,开始执行语句var y = x ..,但随后就遇到了一个yield 表达式。它就会在这一点上暂停*foo(..)(在赋值语句中间!),并在本质上要求调用代码为yield表达式提供一个结果值。
  • 接下来,调用it.next( 7 ),这一句把值7 传回作为被暂停的yield 表达式的结果。

两个问题的故事

消息是双向传递的——yield.. 作为一个表达式可以发出消息响应next(..) 调用,next(..) 也可以向暂停的yield 表达式发送值。

function* foo(x) {
    var y = x * (yield "Hello"); // <-- yield一个值!
    return y;
}
var it = foo(6);
var res = it.next(); // 第一个next(),并不传入任何东西
res.value; // "Hello"
res = it.next(7); // 向等待的yield传入7
res.value; // 42

yield .. 和next(..) 这一对组合起来,在生成器的执行过程中构成了一个双向消息传递系统

我们并没有向第一个next() 调用发送值,这是有意为之。只有暂停的yield才能接受这样一个通过next(..) 传递的值,而在生成器的起始处我们调用第一个next() 时,还没有暂停的yield 来接受这样一个值。规范和所有兼容浏览器都会默默丢弃传递给第一个next() 的任何东西。传值过去仍然不是一个好思路,因为你创建了沉默的无效代码,这会让人迷惑。因此,启动生成器时一定要用不带参数的next()。

多个迭代器

迭代器控制生成器的时候,实际上是在控制一个生成器的实例,所以同一个生成器的多个实例可以独立运行,甚至可以彼此交互(把迭代器A的next()取到的值传到迭代器B的next方法里当参数)。
      另外,多个生成器在共享作用域上并发运行根据调用顺序的不同可有多种复杂的运行结果(就像多线程语言的并发)。

    var z = 1;

    function* foo() {
        var x = yield 2;
        z++;
        var y = yield (x * z);
        console.log(x, y, z);
    }

    var it1 = foo();
    var it2 = foo();
    var { value: val1 } = it1.next(); 
    var { value: val2 } = it2.next();
    val1 = it1.next(val1 * 10).value;
    val2 = it2.next(val1 * 5).value; 
    it1.next(val2 / 2);  
    it2.next(val1 / 4);    

var { value: val1 } = it1.next(); // 2

  • 执行到 var x = yield 2; 暂停,此时还没有执行到 z++,所以 z = 1, x = 2, val1 = 2;

var { value: val2 } = it2.next(); // 2            

  • 执行到 var x = yield 2; 暂停,此时还没有执行到 z++,所以 z = 1, x = 2, val2 = 2;

val1 = it1.next(val1 * 10).value; // 40    

  •  从 var x = yield 2; 执行到 var y = yield (x * z); 暂停,
  • 此时 x = val1 * 10 = 2 * 10 = 20,   x:20
  • z = z++ = 2,  z:2
  • y = x * z = 20 * 2 = 40, val1 = 40; y:40 val1 :40

val2 = it2.next(val1 * 5).value;  //600

  • 从 var x = yield 2; 执行到 var y = yield (x * z); 暂停,
  • 此时 x = val1 * 5 = 40 * 5 = 200, x:200
  • z = z++ = 3, z:3
  • y = x * z = 200 * 3 = 600, val2 = 600; y:600 val2:600

    it1.next(val2 / 2);    // 20   300  3

  • 从 var y = yield (x * z); 执行到 console.log(x, y, z),
  • 这时 var y = yield (x * z); 被看作整体 var y = yield ...,
  • 此时 x = 20, z = 3, y = val2 / 2 = 600 / 2 = 300;

it2.next(val1 / 4);    / / 200  10   3

  •  从 var y = yield (x * z); 执行到 console.log(x, y, z),
  • 这时 var y = yield (x * z); 被看作整体 var y = yield ...,
  • 此时 x = 200, z = 3, y = val1 / 4 = 40 / 4 = 10;

异步迭代生成器(解决回调地狱问题-回调中带有回调) 

回调:

// 回调方式
function request(params, callback) {
    $.ajax({
        method: 'GET',
        url: './response.json', // { "code": 200, "msg": "ok", "data": {} }
        data: params,
        success(res, status) {
            let data = { ...res, data: params };
            callback(data, status);
        },
        error(err, status) {
            callback(err, status);
        }
    })
}

request({ name: 'Lee' }, (data1, status1) => {
    
    // {"code":200,"msg":"ok","data":{"name":"Lee"}} success
    console.log(JSON.stringify(data1), status1);
    
    request({name: 'Tom'}, (data2, status2) => {
        
        // {"code":200,"msg":"ok","data":{"name":"Tom"}} success
        console.log(JSON.stringify(data2), status2);
    
    });

});

如果想要通过生成器来表达同样的回调任务流程控制,怎么实现?

let it = null;

function request(params) {
    $.ajax({
        method: 'GET',
        url: './response.json', // { "code": 200, "msg": "ok", "data": {} }
        data: params,
        success(res, status) {
            let data = { ...res, data: params };
            it.next(data);
        },
        error(err, status) {
            it.throw(err);
        }
    })
}

// 等价于回调
function* main() {
    try {
        let data1 = yield request({name: 'Lee'});
        console.log(data1); // {"code":200,"msg":"ok","data":{"name":"Lee"}}
        
        let data2 = yield request({name: 'Tom'});
        console.log(data2); // {"code":200,"msg":"ok","data":{"name":"Tom"}}
        
        let data3 = yield request({name: '张三'});
        console.log(data3); // {"code":200,"msg":"ok","data":{"name":"张三"}}
    
    } catch (error) {
        console.log(error);
    }
}

it = main();
it.next();
  • it = main(); 定义一个迭代器
  • it.next(); 启动了生成器 *main(),并运行到了第一个 yield (let data1 = yield request({name: 'Lee'});)
  • 这时 it 已经存在值了,接下来调用了 request 函数,并将成功或失败结果作为参数传给了下次调用生成器(it.next(data);)
  • 接下来程序走到了第二个yield(let data2 = yield request({name: 'Tom'});)
  • 以此类推···

以上代码还有一个特点:异步请求的方法,变成了同步请求

function request(params) {
    return new Promise((resolve, reject) => {
        $.ajax({
            method: 'GET',
            url: './response.json', // { "code": 200, "msg": "ok", "data": {} }
            data: params,
            success(res, status) {
                let data = { ...res, data: params };
                resolve(data);
            },
            error(err, status) {
                reject(err);
            }
        })
    })
}

async function main() {
    let data1 = await request({ name: 'Lee' });
    console.log(data1); // {"code":200,"msg":"ok","data":{"name":"Lee"}}

    let data2 = await request({ name: 'Tom' });
    console.log(data2); // {"code":200,"msg":"ok","data":{"name":"Tom"}}

    let data3 = await request({ name: '张三' });
    console.log(data3); // {"code":200,"msg":"ok","data":{"name":"张三"}}
}

main();

生成器+Promise

function request(params) {
    return new Promise((resolve, reject) => {
        $.ajax({
            method: 'GET',
            url: './response.json', // { "code": 200, "msg": "ok", "data": {} }
            data: params,
            success(res, status) {
                let data = { ...res, data: params };
                resolve(data);
            },
            error(err, status) {
                reject(err);
            }
        })
    })
}

function* main() {
    let [pageNum, pageSize] = [1, 10];
    while (true) {
        yield request({ pageNum, pageSize });
        pageNum++;
    }
}

let it = main();

// 第一页
it.next().value // Promise
    .then(res => {
        // { code: 200, data: {pageNum: 1, pageSize: 10}, msg: "ok" }
        console.log(res);
    })
    .catch(err => {
        console.log(err);
    })

// 第二页
it.next().value // Promise
    .then(res => {
        // { code: 200, data: {pageNum: 2, pageSize: 10}, msg: "ok" }
        console.log(res);
    })
    .catch(err => {
        console.log(err);
    })

编写支持Generator的Promise Runner

我们很需要一个公共的库函数来帮我们做上面接收Promise决议结果,调用生成器步骤的事,毕竟这种代码不应该是我们在每个项目中都重新写一次的。

下面提供一个公共的方法,它允许你直接像上面一样组合生成器和Promise而不用自己做那些糟心事。

// Generator Runner
function run(gen) {
    var args = [].slice.call(arguments, 1);
    var it = gen.apply(this, args);
    return Promise.resolve().then(function handleNext(val) {
        var next = it.next(val);

        return (function handleResult(next) {
            if (next.done) {
                return next.value;
            } else {                
                return Promise.resolve(next.value).then(handleNext, function handleError(err) {
                    return Promise.resolve(it.throw(err)).then(handleResult);
                });
            }
        })(next);
    });
}

我们解释一下其中的代码,首先,下面两行不用多说,只是调用生成器函数,拿到一个迭代器。

var args = [].slice.call(arguments, 1);
var it = gen.apply(this, args);

然后下面,返回了一个总的Promise,外部可以通过这个Promise来获取生成器函数执行的最终结果。剩下的代码,和我们之前写的大同小异,就是递归调用,不断进行处理,直到生成器执行完成。

return Promise.resolve().then(function handleNext(val) {
    // 下一步
    var next = it.next(val);

    return (function handleResult(next) {
        // 如果没有下一步了
        if (next.done) {
            return next.value; // 直接返回生成器最终的返回值
        } else {                
            // 如果还有下一步,通过`Promise.resolve`来转换响应`yield`语句的返回值,即使它所调用的只是同步函数,也会被转换成Promise,保证整个调用链的一致性。然后就是递归调用handleNext
            return Promise.resolve(next.value).then(handleNext, function handleError(err) {
                // 如果出错,通过it.throw上报,并且重新调用handleResult,去返回生成器最终的返回值
                return Promise.resolve(it.throw(err)).then(handleResult);
            });
        }
    })(next);
});

async和await

在后期的ES6和未来的ES7中,async和await已经成了一种使用同步方式进行异步编程的规范,它的原理其实就和上面差不多,使用async和await来编写

使用async和await,就可以告别生成器语法,并且它可以自动支持Promise。虽然大部分语言都使用生成器作为一个异步编程的方案,但是生成器的本意并不是进行异步编程,所以语义上它不是很清晰。async和await解决了这个问题。

async function main(){
    try{
        var rndX = await delayRandom(100,1000);
        var rndY = await delayRandom(10,200);
        console.log(rndX,rndY,rndX + rndY);
        var rndZ = await delayRandom(10,1000);
        console.log(rndZ);
    }catch(e){
        console.log("found an error: ",e);
    }
}

main();

找回并行执行的能力

使用yield确实可以让我们以同步的顺序组织异步代码了,但是现在它们只能串行了,这会损失效率,很多时候其中的一些任务是完全可以并行执行的。

比如如下的代码,这里和之前有些改动,注意。

这段代码的作用是取两个随机数,一个会在1000毫秒后返回,一个则是2000毫秒。然后将它们的得数相加,然后取第三个随机数,它在1000毫秒后返回,最后把第一个和第二个相加的结果乘以第三个随机数,作为最终结果。

它的运行时间大概是:1000+2000+1000=4000ms

function *main(){
    try{
        var rndX = yield delayRandom(100,1000);
        var rndY = yield delayRandom(10,2000);
        var xAndY = rndX + rndY;
        var rndZ = yield delayRandom(200,1000) * xAndY;
        console.log(rndZ);
    }catch(e){
        console.log("found an error: ",e);
    }
}

问题在于,第一个任务和第二个任务之间完全没有依赖关系,它们可以并行执行,如果并行执行,任务的运行时间将会缩短到3000ms左右。

如下是一个办法,它先不让步,先让两个异步方法执行起来,然后再进行阻塞,这样两个异步方法是并行执行的。得到xAndY的时间总是这两个异步任务中耗时时间最长的那个任务的时间。

function *main(){
    try{
        var pX = delayRandom(100,1000);
        var pY = delayRandom(10,2000);
        var xAndY = (yield pX) + (yield pY);
        var rndZ = (yield delayRandom(200,1000)) * xAndY;
        console.log(rndZ);
    }catch(e){
        console.log("found an error: ",e);
    }
}

如下又是一个办法,使用Promise.all

function *main(){
    try{
        var results = yield Promise.all([delayRandom(100,1000),delayRandom(10,2000)]);
        var xAndY = results[0] + results[1];
        var rndZ = (yield delayRandom(200,1000)) * xAndY;
        console.log(rndZ);
    }catch(e){
        console.log("found an error: ",e);
    }
}

隐藏Promise

我们使用生成器进行异步开发的目的就是尽量隐藏异步代码的细节,让它看起来更像同步,但是现在我们却在里面写了Promise。多明显的异步痕迹啊。

就是说,我们的生成器方法中不应该有任何异步风格的代码。我个人的想法就是,一个项目,或者一个模块,或者一个函数,要么你用纯异步的风格来写,要么你就用纯同步的风格来写,不要两种风格混合。

所以我们应该隐藏生成器方法中的Promise。

function fetchXY(){
    return Promise.all([delayRandom(100,1000),delayRandom(10,2000)]);
}

function *main(){
    // ...
    var results = yield fetchXY();
    var xAndY = results[0] + results[1];
    // ...
}

同时,如果你在开发库函数,应该尽量开发出方便调用者隐藏调用细节的并发处理函数。 

生成器委托 

  • yield 委托的主要目的是代码组织,以达到与普通函数调用的对称
  • 生成器委托,就像普通函数之间的互相调用,通过互相调用,函数才能灵活组合。但是生成器是一断一断的,需要迭代器控制它往下走,所以不能用普通函数的调用方式,所以就有了生成器委托

  •  tips:yield委托不仅可以委托生成器迭代器,普通迭代器也能委托。

因为生成器也是函数,所以没有理由不赋予它可以在任意位置被复用的能力,生成器委托可以让我们实现生成器中逻辑的抽取和复用。

function request(params) {
    return new Promise((resolve, reject) => {
        $.ajax({
            method: 'GET',
            url: './response.json', // { "code": 200, "msg": "ok", "data": {} }
            data: params,
            success(res, status) {
                let data = { ...res, data: params };
                resolve(data);
            },
            error(err, status) {
                reject(err);
            }
        })
    })
}


function* foo() {

    var r2 = yield request({ name: 'Lee' });
    
    var r3 = yield request({ name: 'Tom' });

}

function* bar() {
    
    var r1 = yield request({ name: '张三' });

    // 通过 yeild* "委托"给*foo()
    yield* foo();

}

let it = bar();

/**
 * // {value: Promise, done: false}
 * it.next().value.then(res => console.log(res.data.name)); // 张三
 * 
 * // {value: Promise, done: false}
 * it.next().value.then(res => console.log(res.data.name)); // Tom
 * 
 * // {value: Promise, done: false}
 * it.next().value.then(res => console.log(res.data.name)); // Lee
 * 
 * // {value: undefined, done: true}
 * it.next();
 * 
 */
for (const iterator of it) {
    iterator.then(res => console.log(res.data.name)); // 张三 Tom Lee
}

ES6之前的生成器

首先考虑“成品”应有的“外观”:有一个函数,它是我们的“手工生成器”。执行手工生成器,应该得到一个“迭代器”,也就是一个对象,它有next和throw方法,调用后能得到{value: 值, done: 布尔值}这样的对象。
      我想象了一下这个手工生成器的样子,大概是这样:

function myGenerator(入参) {
  return {
    next: function() {
      第一段代码
      this.next = function() {
        第二段代码
        this.next = function() { 第三段代码…… }
        return {
          value: 第二段代码结束后要yield出去的值,
          done: false
        }
      }
      return {
        value: 第一段代码结束后要yield出去的值,
        done: false
      }
    }
  }
}

但是后面往下看,发现我忘了考虑throw的情况。“外面“不仅可能调it.next(),还可能调it.throw()。
     

function* A() {
  // 点1
  xxx;
  try {
    yield y;
    // 点2
    zzz;
  } catch {
    // 点3
    vvv;
  }
  www;
}

  第一次调用it.next(),是从点1开始执行。然后如果再调一次it.next(),是从点2继续执行,反之如果调的是it.throw(),则从点3开始执行。
 

function myGenerator(入参) {
  let status = null
  // 一些变量声明,给下面的代码片段用
  function process() {
    switch (status) {
      case 1:
        xxx;
        return { value: y, done: false }
      case 2:
        zzz;
        www;
        return { value: undefined, done: true }
      case 3:
        vvv;
        www;
        return { value: undefined, done: true }
    }
  }
  return {
    next: function (v) {
      switch (status) {
        case null:
          status = 1;
          return process(v)
        case 1:
          status = 2;
          return process(v)
      }
    },
    throw: function (err) {
      switch (status) {
        case 1:
          status = 3
          return process(err)
        default:
          throw err;
        // 我没考虑到如果it不在我期望的地方调throw该咋办,回顾的时候发现作者的答案里有这部分逻辑,所以补上。
        // default表示it不在能被捕获到错误的地方抛出错误,所以错误不会被生成器处理,会被抛回到外面
      }
    }
  }
}


总结

  •      生成器是一个新的函数类型,可以通过yield语句暂停自身,它的暂停和恢复是合作式的而不是抢占式的。
  •       yeild和next()除了可以控制流程的暂停和结束,还能进行双向消息传递。
  •       在异步流程控制方面,生成器的优点在于可以以同步的、顺序的方式表达异步逻辑,提高了代码可读性。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值