异步的JavaScript(终篇)

回顾

所谓的异步,就是程序的一部分现在进行,而另一部分则在将来运行。异步处理的重点就是如何处理将来运行的那一部分。

回调是 JavaScript 中最基本的异步模式,就是事先约定好将来要做的事然后回头调用。简单直接,但也存在不信任、调用嵌套过深等问题。对于编写代码、维护代码的我们而言,人类的大脑还是习惯于线性的处理方式。

基于回调的异步模式所存在的问题促使着我们寻求一种机制来保证回调的可信任,同时能更好的表达异步。这时候 Promise 出现了,Promise 的出现,并非要取代回调。而是把回调转交给了一个位于我们和其它工具之间的可信任的中介机制。Promise 链也提供(尽管并不完美)以顺序的方式表达异步流的一个更好的方法,这有助于我们的大脑更好地计划和维护异步 JavaScript 代码。

生成器

Promise 虽然有序、可靠地管理回调,但是我们还是希望如同步般表达异步。

我们已经知道生成器是作为生产迭代器的工厂函数,同时我们还要知道生成器也是一个消息传递系统。

为什么是生成器

在生成器出现之前,程序代码一旦执行,就没有停下来的时候,直到程序结束🔚。然而在生成器里代码是可以暂停的,而且还可以和生成器之外通信☎️,通信结束后又可以恢复执行。回想一下之前的异步流程控制,我们一直在想方设法使得异步任务能够同步表达。现在,我们可以借助生成器来实现这一想法💡。

了解了生成器的特性之后,我们就应该知道,当生成器在执行一个异步任务时,完全可以把异步任务放在生成器外部执行,待异步任务执行结束后再返回🔙生成器恢复执行。要知道,生成器暂停的只是内部的状态,程序的其余部分还是正常运行的。这样的话,生成器内部的所有代码看起来都是同步表达了。

同时我们也要注意到,生成器不过是一种新🆕的表达方式,和异步还是同步没有半毛钱💰关系。既然没有关系,那在异步模式选择上就更无所谓了。考虑到异步系列文章是渐进式的,所以我们就用 Promise + 生成器 模式来表达异步。

生成器与Promise的结合

在异步流程控制方面,生成器是由两部分组成的。一部分是生成器内部代码以同步的方式表达任务,另一部分是由生成器生成的迭代器处理异步。

const async = n => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(`第${n}个异步任务`);
        }, 0);
    })
};

const generator = function *generator(){
    const response_1 = yield async(1);
    const response_2 = yield async(2);
    const response_3 = yield async(3);
    console.log('response_1: %s;response_2: %s;response_3: %s;',response_1,response_2,response_3);
};

const gen = generator();
const gen_1 = generator();
console.log('gen_next_1: %s; gen_next_2: %s; gen_next_3: %s;', gen_1.next().value, gen_1.next().value, gen_1.next().value);
gen.next().value.then(yield_1 => {
    console.log('yield_1: %s;', yield_1);
    return gen.next(yield_1).value.then(yield_2 => {
        console.log('yield_2: %s;', yield_2);
        return gen.next(yield_2).value.then(yield_3 => {
            console.log('yield_3: %s', yield_3);
            return gen.next(yield_3);
        })
    })
});

// gen_next_1: [object Promise]; gen_next_2: [object Promise]; gen_next_3: [object Promise];
// yield_1: 第1个异步任务;
// yield_2: 第2个异步任务;
// yield_3: 第3个异步任务
// response_1: 第1个异步任务;response_2: 第2个异步任务;response_3: 第3个异步任务;

如果只看 generator 函数这块,函数内部的写法和同步无异。gengen_1 都是同一生成器的实例。

如前文所述,理解这块代码还是要从两方面入手 ———— 迭代和消息传递。迭代属性在此不再赘述,现在重点是消息传递的属性。在生成器中,生成器函数被调用后并未立即执行,而是构造了一个迭代器。而生成器正是靠着 yield/next 来完成生成器内外部的双向通信。

在生成器内部,yield 是用来暂停(完全保持其状态)和向外部传递数据的关键字/表达式(初始时函数也是处于未执行状态)。在生成器外部,next 具有恢复生成器和向生成器内部传递数据的能力。

混沌初始(gen 造出来了),盘古开天辟地(第一个 next() 执行),天地初成,继女娲造人后,一切欣欣向荣。共工和祝融两个调皮蛋撞坏了不周山,给女娲出了一个难题(yield),华夏史驻此不前。女娲向上天求助(yield async(1)),上天回应了并送来了五彩石(yield_1),女娲顺利补天,华夏史再次启程(next(yield_1))。

然而好景不长,华夏部落经常受到蚩尤部落骚扰侵犯,蚩尤的存在再次阻碍了华夏史的前行(yield)。黄帝无奈向其师求助(yield async(2)),九天玄女授其兵法(yield_2),黄帝顺利杀蚩尤,华夏史再次启程(next(yield_2))。

然而好景不长,中原地带洪水泛滥,华夏史再次受阻(yield)。夏禹无奈向太上老君求助(yield async(3)),太上老君赠其神铁(yield_3),夏禹顺利治水,华夏史再次启程(next (yield_3))。

实在编不下去了,还好结束了。😓 代码运行过程大抵如此。生成器内部生成一个数据,然后抛给迭代器消费,迭代器又把执行结果甩给了生成器。就是这么简单,别想的太复杂就行。

所谓的消息双向传递,指的不仅仅是正常情况下生成器内外部的数据。对于异常错误,生成器内外部也可以双向捕捉。因为生成器内部的暂停,是保留了其上下文的,所以 try...catch 又可以一展身手了。

生成器自执行 & async/await

Promise + 生成器 来表达异步算是实现了,然而我们也应该注意到在用迭代器控制生成器的那部分太过繁琐。
如果能够封装下就好了, 如下:

const generator_wrap = function (generator) {
    const args = [...arguments].slice(1);
    const gen = generator.apply(this, args);
    return new Promise((resolve, reject) => {
        const handleNext = function handleNext(yield){
            let next;
            try {
                next = gen.next(yield);
            } catch (error) {
                reject(error);
            }
            if (next.done) {
                resolve(next.value);
            } else {
                return Promise.resolve(next.value).then(yield => {
                    handleNext(yield);
                }, error => {
                    gen.throw(error);
                })
            }
        };
        handleNext();
    })
};
// ———————————— 手动分割线 ————————————
const generator = function *generator(){
    const response_1 = yield async(1);
    const response_2 = yield async(2);
    const response_3 = yield async(3);
    console.log('response_1: %s;response_2: %s;response_3: %s;',response_1,response_2,response_3);
};

generator_wrap(generator);
// response_1: 第1个异步任务;response_2: 第2个异步任务;response_3: 第3个异步任务;

不看 generator_wrap 函数,只看分割线以下的部分。至此,异步流程的表达越来越接近理想中的模样了。但 generator_wrap 函数还是需要自己手动封装,不过现在不用啦😄

ES2017 推出了 async/await ,我们不用再自己去管理生成器,简单、强大、方便的 async/await 为我们处理了一切。

const awati_async = async () => {
    const response_1 = await async(1);
    const response_2 = await async(2);
    const response_3 = await async(3);
    console.log('response_1: %s;response_2: %s;response_3: %s;', response_1, response_2, response_3);
};

awati_async();
// response_1: 第1个异步任务;response_2: 第2个异步任务;response_3: 第3个异步任务;

至此,关于 JavaScript 的异步表达暂时告一段落了👋。

异步的 JavaScript 系列:

异步的JavaScript(回调篇)

异步的JavaScript(Promise篇)

异步的JavaScript(终篇) 附(从迭代器模式到迭代协议

参考资料:

迭代器和生成器

你不知道的 JavaScript (中卷)

你不知道的 JavaScript (下卷)

Generator 函数的异步应用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章