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

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

还记得上一节讲回调函数的时候,第一件事就提到了异步函数不能用return返回值,其原因就是在return语句执行的时候异步代码还没有执行完毕,所以return的值不是期望的运算结果。

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

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

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

如果用setTimeout来模拟你要进行的异步操作,以下是让异步函数返回promise的基本写法。调用Promise构造函数,生成一个promise对象,然后return它。把你的代码包裹在匿名函数function(resolve, reject){ … } 里面,作为参数传给Promise构造函数。resolve和reject是两个由javascript引擎已经定义好的全局函数。在你的异步代码结束的时候调用resolve来表示异步操作成功,并且把结果传给resolve作为参数,这样它可以传给下一个异步操作。

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()方法:

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

假如有三个异步函数:

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

『本集完』

时间: 2024-07-30 20:26:09

Javascript异步编程之三Promise: 像堆积木一样组织你的异步流程的相关文章

探索Javascript 异步编程

在我们日常编码中,需要异步的场景很多,比如读取文件内容.获取远程数据.发送数据到服务端等.因为浏览器环境里Javascript是单线程的,所以异步编程在前端领域尤为重要. 异步的概念 所谓异步,是指当一个过程调用发出后,调用者不能立刻得到结果.实际处理这个调用的过程在完成后,通过状态.通知或者回调来通知调用者. 比如我们写这篇文字时点击发布按钮,我们并不能马上得到文章发布成功或者失败.等待服务器处理,这段时间我们可以做其他的事情,当服务器处理完成后,通知我们是否发布成功. 所谓同步,是指当一个过

javascript 异步编程-setTimeout

javascript的执行引擎是单线程的,正常情况下是同步编程的模式,即是程序按照代码的顺序从上到下依次顺序执行.只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),那么在执行期间任何UI更新都会被阻塞,界面事件处理也会停止响应.导致整个页面卡在这个地方,其他任务无法执行. 特别是在for循环语句里,如果for循环的处理逻辑比较复杂,并且循环次数过多,超过1000次时,javasc

JavaScript学习--Item27 异步编程异常解决方案

1.JavaScript异步编程的两个核心难点 异步I/O.事件驱动使得单线程的JavaScript得以在不阻塞UI的情况下执行网络.文件访问功能,且使之在后端实现了较高的性能.然而异步风格也引来了一些麻烦,其中比较核心的问题是: 1.函数嵌套过深 JavaScript的异步调用基于回调函数,当多个异步事务多级依赖时,回调函数会形成多级的嵌套,代码变成 金字塔型结构.这不仅使得代码变难看难懂,更使得调试.重构的过程充满风险. 2.异常处理 回调嵌套不仅仅是使代码变得杂乱,也使得错误处理更复杂.这

Promise和异步编程

前面的话 JS有很多强大的功能,其中一个是它可以轻松地搞定异步编程.作为一门为Web而生的语言,它从一开始就需要能够响应异步的用户交互,如点击和按键操作等.Node.js用回调函数代替了事件,使异步编程在JS领域更加流行.但当更多程序开始使用异步编程时,事件和回调函数却不能满足开发者想要做的所有事情,它们还不够强大,而Promise就是这些问题的解决方案 Promise可以实现其他语言中类似Future和Deferred一样的功能,是另一种异步编程的选择,它既可以像事件和回调函数一样指定稍后执行

JavaScript异步编程的方法

异步编程: 在浏览器端,异步编程非常重要,耗时很长的操作都应该异步执行,避免浏览器失去响应.最常见的例子就是通过AJAX向服务器发送异步请求. 异步编程有很多种方法 1.回调函数 比如有两个函数f1();f2();//f2依赖于f1的执行状态如果f1耗时很长,它会阻塞后面程序的运行我们利用setTimeout来改写f1,因为setTimeout是异步的 function f1(callback){ setTimeout(function(){ //f1的代码,耗时很长,这里是又开启了一个线程,

深入浅出NodeJS——异步编程

函数式编程 Javascript中函数作为一等公民,函数看成普通对象,可以作为参数或者返回值. 高阶函数:函数作为参数或者将函数作为返回值的函数 异步编程优势 基于事件驱动的非阻塞I/O模型 异步编程难点 (1) 异常处理,通常try/catch不一定适用,因为callback并不是在当前Tick中执行. Node在异常处理中约定将异常作为回调函数的第一个实参传回. (2)  函数嵌套过深 (3) 代码阻塞:没有sleep函数,通过setInterval()和setTimeout()模拟 (4)

谈谈异步编程

目前需求中涉及到大量的异步操作,实际的页面越来越倾向于单页面应用.以后可以会使用backbone.angular.knockout等框架,但是关于异步编程的问题是首先需要面对的问题.随着node的兴起,异步编程成为一个非常热的话题.经过一段时间的学习和实践,对异步编程的一些细节进行总结. 1.异步编程的分类 解决异步问题方法大致包括:直接回调.pub/sub模式(事件模式).异步库控制库(例如async.when).promise.Generator等. 1.1 回调函数 回调函数是常用的解决异

JS异步编程 (1)

JS异步编程 (1) 1.1 什么叫异步 异步(async)是相对于同步(sync)而言的,很好理解. 同步就是一件事一件事的执行.只有前一个任务执行完毕,才能执行后一个任务.而异步比如: setTimeout(function cbFn(){ console.log('learnInPro'); }, 1000); console.log('sync things'); setTimeout就是一个异步任务,当JS引擎顺序执行到setTimeout的时候发现他是个异步任务,则会把这个任务挂起,

异步编程系列第05章 Await究竟做了什么?

p { display: block; margin: 3px 0 0 0; } --> 写在前面 在学异步,有位园友推荐了<async in C#5.0>,没找到中文版,恰巧也想提高下英文,用我拙劣的英文翻译一些重要的部分,纯属娱乐,简单分享,保持学习,谨记谦虚. 如果你觉得这件事儿没意义翻译的又差,尽情的踩吧.如果你觉得值得鼓励,感谢留下你的赞,愿爱技术的园友们在今后每一次应该猛烈突破的时候,不选择知难而退.在每一次应该独立思考的时候,不选择随波逐流,应该全力以赴的时候,不选择尽力而