Javascript异步编程之三Promise: 像堆积木同样组织你的异步流程

这篇有点长,不过干货挺多,既分析promise的原理,也包含一些最佳实践,亮点在最后:)javascript

还记得上一节讲回调函数的时候,第一件事就提到了异步函数不能用return返回值,其缘由就是在return语句执行的时候异步代码尚未执行完毕,因此return的值不是指望的运算结果。html

Promise却偏偏要回过头来从新利用这个return语句,只不过不是返回最终运算值,而是返回一个对象,promise对象,用它来帮你进行异步流程管理。java

先举个例子帮助理解。Promise对象能够想象成是工厂生产线上的一个工人,一条生产线由若干个工人组成,每一个工人分工明确,本身作完了把产品传递给下一个工人继续他的工做,以此类推到最后就完成一个成品。这条生产线的组织机制就至关于Promise的机制,每一个工人的工做至关于一个异步函数。后面会继续拿promise和这个例子进行类比。编程

 

 

Promise风格异步函数的基本写法:数组

若是用setTimeout来模拟你要进行的异步操做,如下是让异步函数返回promise的基本写法。调用Promise构造函数,生成一个promise对象,而后return它。把你的代码包裹在匿名函数function(resolve, reject){ … } 里面,做为参数传给Promise构造函数。resolve和reject是promise机制内部已经定义好的函数,传给你用来改变promise对象的状态。在你的异步代码结束的时候调用resolve来表示异步操做成功,而且把结果传给resolve做为参数,这样它能够传给下一个异步操做。promise

function asyncFn1() {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn1 is done');
            resolve('asyncFn1 value');
        }, 1000);
    });

   return promise;
}

 

在promise机制当中,resolve被调用后会把promise的状态变成’resolved’。 若是reject被调用,则会把promise的状态变成’rejected’,表示异步操做失败。因此在上面的例子中若是你有一些逻辑判断,能够在失败的时候调用reject:异步

//伪代码
function asyncFn1() {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn1 is done');
            if(success) {
                resolve('asyncFn1 value');
            } else {
                reject('error info');
            }
        }, 1000);
    });

    return promise;
}

 

then()方法:async

既然promise的用来作流程管理的,那确定是多个异步函数要按某种顺序执行,而每一个都要return promise对象。怎样把它们串起来呢?答案是调用promise对象最重要的方法promsie.then(),从它的字面意思就能够看出它的做用。并且then()方法也返回一个新的promise对象,注意是新的promise对象,而不是返回以前那个。函数

假若有三个异步函数:ui

function asyncFn1() {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn1 is done');
            resolve('asyncFn1 value');
        }, 1000);
    });
    return promise;
}

function asyncFn2(arg) {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn2 is done');
            resolve(arg + ' asyncFn2 value');
        }, 1000);
    });
    return promise;
}

function asyncFn3(arg) {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn3 is done');
            resolve(arg + ' asyncFn3 value');
        }, 1000);
    });
    return promise;
}

 

能够用then方法这样顺序来组织它们:

var p1 = asyncFn1(),
    p2 = p1.then(asyncFn2),
    p3 = p2.then(asyncFn3);

p3.then(function(arg) {
    console.log(arg);
});

 

这样组织起来后,就会按照顺序一个一个执行:asyncFn1执行完成后p1变成resolved状态并调用asyncFn2,asyncFn2运行完后p2变成resolved状态而且调用asyncFn3,asyncFn3执行完成后p3编程resolved状态并调用匿名函数打印输出结果。这个过程当中,若是任何一个promise被变成’rejected’,后续全部promise立刻跟着变成rejected,而不会继续执行他们所登记的异步函数。

上面代码能够更加简化成这样,看起来更清爽,用飘柔的感受有没有:

asyncFn1()
    .then(asyncFn2)
    .then(asyncFn3)
    .then(function(arg) {
        console.log(arg);
    });

 

怎么样,比上一节讲的回调嵌套代码漂亮太多啦,多苗条。

如今跟工厂生产线的例子进行类比一下加深理解。你猜上面这段飘柔代码在工厂生产线例子中至关于什么?你必定会说,你不是上面说了嘛,至关于一条顺序执行的生产线。错!!! 它至关于---------生产计划,或者生产图纸。怕了没?没错就是至关于生产计划,里面登记了每一个工人的任务和他们的工做顺序。若是把它当成生产线,就会误觉得asyncFn1()运行完了再调用then,当asyncFn2运行完了再调用下一个then,当asyncFn3运行完了再调用第三个then,这样会形成是由then来调用这些异步函数的错觉。实际上then的做用仅仅是登记当每一个promise变成resolved状态时要调用的下一个函数,仅仅是登记,而不是实际上调用它们,实际调用是发生在promise变成resolved的时候。(then能够用来登记生产计划的缘由是它实际上是个同步方法,因此这段飘柔代码噌得一下就执行完了,计划就出来了,而不是跟着那些asyncFn函数们一个等一个的执行)。搞清楚这个对于新手来讲很是重要,它可让你更好的来组织你的异步流程。后面会详细说。另外,工做计划产生后,生产也同时开始了,即asyncFn函数们也开始执行了,按登记的顺序。

 

catch()方法:

上面例子中then方法都是只接受一个异步函数做为参数,实际上then方法能够接受两个函数做为参数。第一个函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象 的状态变为Rejected时调用。其中,第二个函数是可选的,大部分状况下不须要提供。可是一种状况除外就是当你的异步流程结束的时候须要用第二个函数来捕获异常。即:

asyncFn1()
    .then(asyncFn2)
    .then(asyncFn3)
    .then(null, function(error) {
        console.log(error);
    });

 

最后一步的异常捕获一般会换一种写法:

asyncFn1()
    .then(asyncFn2)
    .then(asyncFn3)
    .catch(function(error) {
        console.log(error);
    });

 

catch()是then()用来捕获异常时的别名或语法糖。它能够捕获前面任何promise对象变成rejected的状态后,所传递下来的错误信息。若是不使用catch()方法,Promise对象抛出的错误就会石沉大海,让你没法调试。

 

嵌套promise

Promise机制自己是为了解决回调嵌套的,但有意思的是promise自己也能够嵌套,示例以下:

//伪代码
fn1()
    .then(fn2)
    .then(function(result) {
        return fn3(result)
                .then(fn31)
                .then(fn32)
                .then(fn33);
    })
    .then(fn4)
    .catch(function(err) {
        console.log(err);
    });

 

你怎么看?我我的观点,任何事情都没有绝对的对和错,好和很差,就是个度的问题。

 

Promise.all()方法:

上一节在回调风格的异步中,最后留了一个思考题,怎样在循环里面调用异步函数?如今揭晓答案。

var fs = require('fs');

function foo(dir, callback) {
    fs.readdir(dir, function(err, files) {
        var text = '',
        counter = files.length;
        for(var i=0, j=files.length; i<j; ++i) {
            void function(ii) {
                fs.readFile(files[ii], 'utf8', function(err, data) {
                    text += data;
                    --counter;
                    if(counter===0) {
                        callback(text);
                    }
                });
            } (i);
        }
    });
}

foo('./', function(data) {
    console.log(data);
});

 

上面代码foo函数读取当前目录下全部文件而后合并到一块儿,由callback把内容传出来。调用callback的时机也很清楚了,关键就是设个计数器(counter),必须当全部readFile回调都完成后再调用callback。顺便提一下循环调用异步的时候循环自己必须使用一个匿名函数包裹,为何?呵呵新手绕不过的坑,答案自行寻找。后面有时间再写文探讨一些javascript的坑坑吧。

怎样循环回调风格的异步函数如今清楚了,那么问题来了,怎样循环promise风格的函数呢?

var fs = require('fs');

//把fs.readdir()改造为promise风格
function readdirP(dir) {
    return newPromise(function(resolve, reject) {
        fs.readdir(dir, function(err, files) {
            if(err) {
                reject(err);
            } else {
                resolve(files);
            }
        });
    });
}

//把fs.readFile()改造为promise风格
function readFileP(file) {
    return new Promise(function(resolve, reject) {
        fs.readFile(file, 'utf8', function(err, data) {
            if(err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

function foo(dir) {
    return new Promise(function(resolve, reject) {
        var text = '';
        readdirP(dir).then(function(files) {
            return new Promise(function(resolve, reject) {
                var counter = files.length;
                console.log(counter);
                for(var i=0, j=files.length; i<j; ++i) {
                    void function(ii) {
                        readFileP(files[ii]).then(function(data) {
                            text += data;
                            --counter;
                            if(counter===0) {
                                resolve(text);
                            }
                        });
                    }(i);
                }
            });
        }).then(function(result) {
            resolve(result);
        });
    });
}

foo('./').then(function(data) {
    console.log(data);
});

 

我了个去,怎么看起来比回调风格的还复杂?没错的确是这样,由于你仍是在用回调思惟写promise风格的代码,是个四不像。正宗的写法应该是这样的:

function foo(dir) {
    var promise = readdirP(dir)

        .then(function(files) {
            var arr=[];
            for(var i=0, j=files.length; i<j; ++i) {
                arr.push(readFileP(files[i]));
            }
            return Promise.all(arr);
        })

        .then(function(datas) {
            return datas.join('');
        });

    return promise;
}

foo('./').then(function(data) {
    console.log(data);
});

 

这里关键就在于Promise.all()的使用。Promise.all(arr)接受一组promise为参数,即promise数组。当全部promise都变成resolved的时候就完成了,输出也是一个数组,即每一个promise所resolve的值。若是任何一个promise变成rejected,则整个失败,能够在后面用catch捕获。标准写法:

//伪代码
var arr = [promise1, promise2, promise3];
Promise.all(arr)
    .then(function(resultArr) {
        使用resultArr;
    })
    .catch(function(error) {
        console.log(error);
    });

 

Promise.race()方法:

稍提一下Promise.race(arr)方法,用法跟Promise.all(arr)相似,只不过arr中任何一个promise变resolved/rejected的时候就结束,输出这个resolve/reject的值。这个方法的功能从它的名字就能够看出来。

 

最佳实践:

Promise流程最后必定要加个catch()捕获可能发生的错误。

then(fn)方法只接受函数做为的参数,fn若是是异步的,则必需要return一个promise对象;若是是同步的,则能够直接return一个value

function foo(arg) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(arg + 1);
        }, 1000);
    });
}


foo(0)
    .then(foo)
    .then(foo)
    .then(function(arg) {
        return arg +1;
    })
    .then(foo)
    .then(function(arg) {
        console.log(arg);
    });

 

猜猜上述代码最后输出多少?foo被调用了4次,而且中间有一次同步arg+1的代码,因此最后输出5。这里的同步代码arg+1太简单只是为了演示,若是你的同步代码比较复杂并且中间可能抛出exception,那最好让同步代码也返回一个promise,这样就能够在最后catch里面捕获到,真是太爽了:

foo(0)
    .then(foo)
    .then(foo)
    .then(function(arg) {
        return Promise.resolve().then(function() {
            return arg +1;
        });
    })
    .then(foo)
    .catch(function(err) {
        console.log(err);
    });

 

即把同步代码用Promise.resolve().then(function() { … } 进行包裹。Promise.resolve()是生成promise对象的快捷方法,不过它生成的promise对象初始状态就是resolved的。Promise.resolve()方法还能够带参数,这里不进行详述,你们能够自行去了解一下。

用上述方法写出来的流程,出错概率会大大减小。

说了这么久,该说重点了:)

 

堆积木:

返本溯源,promise是为了解决什么问题来着?对了,解决回调地狱,本质上是为了更加清晰的组织异步代码。Promise的精髓用法就是把一个个异步函数像积木同样按照它们的顺序堆积自来,能够串行能够并行,这种堆积木方式的组织流程至关灵活,能够组织出任意你的业务中须要的流程。这样说比较抽象,仍是用例子吧:

(这是我实际项目中的一个真实例子)我有5个promise风格的异步函数fn1, fn2, fn3, fn4 和 fn5。fn3须要用到fn2的结果,fn4须要用到fn3的结果, fn5须要用到fn1, fn2, fn3和fn4的结果。是否是挺绕,应该怎么写?时间关系就不卖关子了。

var p1 = fn1(),
    p2 = fn2();
    p3 = p2.then(fn3);
    p4 = p3.then(fn4);

var arr = [p1, p2, p3, p4];

Promise.all(arr).then(fn5);

怎么样,是否是很神奇?发挥你的想象力,这些异步函数你能够随意组合,串行并行。

切记:组合的过程当中每一个异步函数一般只出现一次,除非你业务须要它使用不一样的数据运行屡次,不然若是出现屡次,极有可能你已经掉坑里了:

//错误代码
var p1 = fn1(),
    p2 = fn2();
    p3 = fn2().then(fn3);
    p4 = fn2().then(fn3).then(fn4);

var arr = [p1, p2, p3, p4];

Promise.all(arr).then(fn5);

看起来两组代码彷佛等价哦,呵呵,只不过错误代码中fn2会跑3次,fn3会跑2次。好好对比清楚:)

我在尚未领悟这种用法的时候是用这样直肠子的作法:

fn1()
   .then(fn2)
   .then(fn3)
   .then(fn4)
   .then(fn5);

哟?这不是更简单吗?错!由于fn1的输出在fn2, fn3和fn4中根本没用,可是仍是必须捎带在他们每个的输出结果里面; fn4根本不须要fn2的输出,但又要捎带在fn3里面以传给fn4最后给fn5。这样就形成这些函数深度耦合在一块儿,功能混乱。 因此记得promise不仅能串行,也能够并行,就像堆积木同样很是灵活的进行组合。不知谁这么聪明发明了这种方法:)

 

转载请注明出处: http://www.cnblogs.com/chrischjh/p/4692743.html 

『本集完』

相关文章
相关标签/搜索