2016年11月26日星期六

Thunk, Promise 和 Generator

一、回调函数

回调函数是异步编程最基本的方法
//一个异步函数,异步任务执行完成后调用回调函数
function asyncFn(text, callback){
    let time = 1000 * Math.random();

    //执行一些异步操作,然后调用回调函数
    setTimeout(()=>{
        callback(text, time);
    }, time);
}
然后这样调用
asyncFn("done", (text, time)=>{
    console.log(text, time);
});
如果要顺序执行一系列这样的任务的话,这样调用
asyncFn("first", (text, time)=>{
    asyncFn("second", (text, time)=>{
        asyncFn("third", (text, time)=>{
            console.log(text, time);
        });
        console.log(text, time);
    });
    console.log(text, time);
});
按照上面的写法,当依赖的前置任务比较多的时候,回调就会嵌套得越来越深,一般称其为 “callback hell”
所以要怎么才能写出功能一样,但是有不会变成”callback hell”的代码呢?

二、Thunk

Thunk函数的起源什么的这里就不说了,这里主要说JavaScript 里的 Thunk 函数,简单的来说,Thunk 函数的作用是将多参数函数(参数,回调)变为单参数函数(回调)。 
以一开始定义的异步函数来说明
这是一个多参数函数,接受一个text参数,和一个回调参数
//一个异步函数,异步任务执行完成后调用回调函数
function asyncFn(text, callback){
    let time = 1000 * Math.random();

    //执行一些异步操作,然后调用回调函数
    setTimeout(()=>{
        callback(text, time);
    }, time);
}
这是一个单参数函数,接受一个text参数,然后返回一个只接受回调参数的函数。
本质上就是把原来的异步函数进行了柯里化,把参数和回调函数分离了
//Thunk化的异步函数
function thunkifyAsyncFn(text){
    return function (callback){
        return asyncFn(text, callback);
    };
};

/** 因为只是简单的将参数和最后的回调函数进行分离,
    所以就可以很简单得写出一个适用于,最后一个参数为回调函数的异步函数的Thunk转换器
*/
function thunkify(fn) {
    return function(...args) {
        return function(callback) {
            args.push(callback);
            return fn(...args);
        };
    };
};

//将asyncFn 用Thunk转换器转换为Thunk化版本
let thunkifyAsyncFn = thunkify(asyncFn);
得到单参数版本的函数之后,就可以这样调用了
thunkifyAsyncFn("done")((text, time)=>{
    console.log(text, time);
});
如果要顺序执行一系列这样的任务的话,这样调用
thunkifyAsyncFn("first")((text, time)=>{
    thunkifyAsyncFn("second")((text, time)=>{
        thunkifyAsyncFn("third")((text, time)=>{
            console.log(text, time);
        });
        console.log(text, time);
    });
    console.log(text, time);
});
很显然,这样的写法跟之前的基本没有什么差别,所以 Generator 出现之前,Thunk函数基本没什么用,但是有了 Generator 之后,情况就变得不一样了。

三、Generator

Generator函数执行后会返回一个遍历器,遍历器的的每一个next对应一个yield
function* gen(){
    let result = yield "first";
    result = yield "second";
    result = yield "third";
}
然后这样来调用
let g = gen();
g.next();//{ value: "first", done: false }
g.next();//{ value: "second", done: false }
g.next();//{ value: "third", done: false }
g.next();//{ value: undefined, done: true }
因为是一个遍历器,也可以构造一个循环来自动调用,执行的结果为按顺序在控制台输出3个字符串

function run(gen){
    let g = gen();
    for( let value of g)
    {
        console.log(value);
    }
}
run(gen);
既然 next() 的返回值中的 value 可以是字符串,当然也可以是函数,那么,之前的Thunk函数就派上用场了
function* gen(){

    //将asyncFn 用Thunk转换器转换为Thunk化版本
    let thunkifyAsyncFn = thunkify(asyncFn);

    //thunkifyAsyncFn执行后返回一个只接受一个回调函数为参数的函数
    //即next()的返回值中的value为一个只接受一个回调函数为参数的函数
    let [text, time] = yield thunkifyAsyncFn("first");
    console.log(text, time);
    [text, time] = yield thunkifyAsyncFn("second");
    console.log(text, time);
    [text, time] = yield thunkifyAsyncFn("third");
    console.log(text, time);
}
因为是异步调用,所以之前的run函数中的循环就不适用了,所以要进行一下改造。run函数运行之后,可以看到控制台里的输出循序和之前的嵌套的写法是一致。
function run(gen) {
    let g = gen();
    function goNext(next) {
        if(next.done)
        {
            return;
        }
        else
        {
            //value为一个只接受一个回调函数为参数的函数
            next.value((...result)=>{

                //next()可以接受一个参数,这个参数就作为上一条 yield 语句的返回值
                goNext(g.next(result));
            });
        }
    }
    //next()不接受参数,因为为第一条 yield 语句
    goNext(g.next());
};

run(gen);
在 Generator 和 Thunk 组合之后,我们终于可以从回调嵌套中解放了出来,只要在需要进行异步调用的地方加上 yield 声明,就可以用同步的形式编写异步的调用了。
既然 Generator 可以和 Thunk 组合,用 Promise 当然也是可以的。

四、Promise

首先把原来的异步函数改成 Promise 的形式
function promiseAsyncFn(text){
    return new Promise((resolve, reject)=>{
        asyncFn(text, (text, time)=>{
            resolve([text, time]);
        })
    });   
}
然后改造一下run函数
function run(gen) {
    let g = gen();
    function goNext(next) {
        if(next.done)
        {
            return;
        }
        else
        {
            //value为一个promise
            next.value.then((result)=>{

                //next()可以接受一个参数,这个参数就作为上一条 yield 语句的返回值
                goNext(g.next(result));
            });
        }
    }
    //next()不接受参数,因为为第一条 yield 语句
    goNext(g.next());
}
Generator 函数
function* gen(){

    // promiseAsyncFn 执行后返回一个promise
    //即next()的返回值中的value为一个promise
    let [text, time] = yield promiseAsyncFn("first");
    console.log(text, time);
    [text, time] = yield promiseAsyncFn("second");
    console.log(text, time);
    [text, time] = yield promiseAsyncFn("third");
    console.log(text, time);
}
run(gen) 之后可以看到结果和Thunk化的版本是一样的。
End

没有评论 :

发表评论