简介

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的时候,都没啥后顾之忧了。

分类: 你不知道的JavaScript 标签: generatorasyncawait同步异步迭代器生成器

评论

暂无评论数据

暂无评论数据

目录