Async特性的语法糖下

本文现在这个时间点写貌似必要不大,因为内容比较基础,不过介于如果掌握不准确可能在一些bug面前束手无策,遂当作备忘录好了。

这里写图片描述

今天就讨论一个node里面很简单却又是很值得关注的话题--异步回调,相信很多小伙伴都熟知著名的回调地狱,社区这几年也是人才辈出,产出无数种优雅解决地狱的方案,最后ECMA一声号令,大家不用慌,爸爸出来镇场!这伙人效率颇高,一下子整出了个ES7标准,其中比较重要的特性就是async/await了,大家对这个比较熟悉,我就不多说什么,私以为,这是最潇洒的解决的地狱的方式了,用起来也是无比自然,以同步的思维来书写异步代码,然而,当时仔细一想,还是有个疑问,这玩意其实只是语法糖,本质还是ES6的Generator,然而生成器只是一个可迭代对象,退一步讲,跟一个数组类似,只是元素是函数的内部的一些片段,达到一种协程的效果,所以背后是谁在掌控着这一切?

比如我写一个异步函数

async () => {
    let res = await method_return_promise();
}

后面函数返回的promise会被解析,然后结果传入res中,这一切是怎么发生的呢,首先我们需要脱去它美丽的衣服~~

function* () {
    let res = yield method_return_promise();
}

ok,我们把它还原成一个生成器,然后考虑它将怎样被执行,yield运算符的优先级是非常低的,在右边的表达式中,该运算符右边的表达式先被执行,在这里这个函数被调用,返回一个promise对象,然后函数中断,保留堆栈,执行权回到我们的调度器,何为调度器,其实就是我们控制生成器的代码段。

这里写图片描述

一般来说生成器的执行只是简单的迭代

g.next();
g.next();
g.next();

我们需求其实就是等待promise解析,然后将其结果注入左边的结果变量,根据生成器也是传递结果的特性,可以这么干,当然它最后还是返回一个promise,它将解析函数返回的对象,如果返回的不是promise,那么就返回一个解析状态的promise

let ret = g.next();
//Generator yields, so it comes back to dispatcher
if (!ret.done) {
    ret.value.then((res) => {
        //now resume Generator, inject our result
        let ret = g.next(res);
        //then go on
        next(ret);
    });
}

这样,上面那个await表达式的神奇魔法就这样实现啦~
然而,我们要想做好执行器,还需要注意一个隐患--异常处理,上面的代码并没有任何的异常捕获,真实的环境IO,网络等环境很容易出现异常,我们岂能让这些异常来无影去无踪呢,所以我们需要加上try catch来完成最终的版本,

function runner(fn) {
    return new Promise((resolve, reject) => {
        let gen = fn();
        next(gen.next());
        function next(ret) {
            //it returns a promise in the end
            if (ret.done) return resolve(ret.value);
            return ret.value.then((res) => {
                let ret;
                try {
                    //inject result to left value
                    ret = gen.next(res);
                } catch (e) {
                    return reject(e);
                }
                next(ret);
            }, (err) => {
                try {
                    ret = gen.throw(err);
                } catch (e) {
                    return reject(e);
                }
                next(ret);
            });
        }
    });
}

然后我们写的生成器就可以正常运行啦,

runner(gen).then((res) => {
    console.log(res);
}, (err) => {
    console.error(err);
});

再套上语法糖,就是神奇的async/await,不过问题还稍微复杂一点,上面讨论的基本是返回一个promise对象,然而await运算符还给我们自带了一些promise转换功能,比如我们可以这样

await 6;
await {val: 6};

所以我们需要一个转换过程,进行promisify ,首先定义一些规则,数字,字符串以及undefined,null等类型,我们直接返回它本身,省的夜长梦多,至于对象,我们需要遍历它的属性,每个属性再进行递归的转换,

for (let i; i < keys.length; i++) {
    toPromise.call(this, obj[key]);
}

如果这个对象为普通对象,即任意一个属性都不是promise类型,好咯,原样返回,然而,只要有一个属性为promise类型,都需要等待它解析才能得到这个对象(在then方法参数得到),根据这个思路,很容易得到,

function objectToPromise(obj){
    let results = new obj.constructor();
    let keys = Object.keys(obj);
    let promises = [];
    for (let i = 0; i < keys.length; i++) {
        let key = keys[i];
        let promise = toPromise.call(this, obj[key]);
        if (promise && isPromise(promise)) defer(promise, key);
        else results[key] = obj[key];
    }
    //if all props of the object are not promise, return it originally
    //otherwise, we must wait until all promises are resolved
    return Promise.all(promises).then(() => {
        return results;
    });

    function defer(promise, key) {
        // predefine the key in the result
        results[key] = undefined;
        promises.push(promise.then(res => {
            results[key] = res;
        }));
    }
}

OK,对象解决了,还有一个很关键的问题,看看我们常用的node操作IO的API,似乎没几个返回promise对象的哦,这是个令人沮丧的事实,这些作者并不关心回调地狱,我们熟知的操作IO的API大概长这样,

read(path, (err, res) => {
    //do something
})

这怎么破,首先我们会想,不是可以把它装在一个promise里面吗?当然可以,

wrappedRead = () => {
    return new Promise((resolve, reject) => {
        read(path, (err, res) => {
            if (err) reject(err);
            else resolve(res);
        });
    });
};

好像很有道理的样子,但这样意味着我们每一个API都要进行这样的封装(我在这里就不寄希望什么bluebird之流了),这种麻烦的事不符合我的风格,所以需要优化,我需要设计一个通用的接口,输入一个任意接口函数,即可变形为一个promise对象!

首先,我们想为什么接口不能通用化,即设计接口先考虑什么是不确定的因素,在这里,很明显,参数!API的共同特征是带一个回调,以及前面的若干个参数,所以这里我可以利用函数curry(不明白的可以先学习下js函数式编程)对函数进行化简,我们的目标是一个thunk,这也是一个函数式概念,一个含有单参数,并且参数是函数表达式的函数就是一个thunk,对于它,我们所有的接口就只剩下共性了,即仅有一个回调参数,这下设计接口将非常简单,

function thunkToPromise(fn) {
    let ctx = this;
    return new Promise((resolve, reject) => {
        fn.call(ctx, function (err, res) {
            if (err) return reject(err);
            resolve(res);
        });
    });
}

很好,现在最后的问题就是怎样化简了,运用curry,过程也很简单,

let toThunk = (fn) => {
    return function () {
        let args = Array.prototype.slice.call(arguments);
        return (cb) => {
            args.push(cb);
            fn.apply(this, args);
        };
    };
};

巧妙运用闭包,就能这样将多参函数层层化简为thunk,就这么几行代码,就设计了一个通用的接口,

yield thunkToPromise(toThunk(anyAPI));

这样await表达式将支持任意的接口,当然这只是我对它背后的设想,目前它甚至连thunk都没发执行,这个有待日后更完善吧哈哈~

这里写图片描述

ok,想必到这里大家对这个神奇特性的背后,甚至一般的基于生成器异步执行核心算法都比较清楚了,日后我们在异步编程中应付这些类型的bug应该可以很快秒杀,羡煞旁人~~OK,本次的分享就到这,更多关于JS函数式的内容敬请期待~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值