Promise

    相信有一定前端或nodejs编程经验都会遇到回调函数嵌套的问题,就是大量的回调函数慢慢向右侧屏幕延伸的一种状态。解决此类问题一般有三种做法:1、用事件函数的订阅/通知机制, 把每一层嵌套拆分成多个事件监听。2、利用一些现成的异步函数库来解决这个问题,例如:async, step都属于这类函数库。3、拥抱promise。传送门: promise-spec。promise概念已经被提出很久了,前端工程师们大致都或多或少有听说过promise的概念吧,在Firefox和Chrome这样技术比较超前的浏览器上,不需要安装额外的插件就能使用Promise功能。 promise类库的实现基于标准的Promises/A+基本规范。常用的类库有:Q when等。

promise A+规范

通过 new Promise 实例化的对象有以下三个状态。
1. resolve, 成功状态, 此时会执行注入的onFulfilled函数
2. reject, 失败状态, 此时会执行注入的onRejected函数
3. pending, promise刚创建后的初始化状态

Promise_第1张图片

Promise常用的几种形态


解决多个异步回调问题

假设要读取两个文件,当两个文件都读取完毕之后合并文件数据,传递给调用者。
传统的回调形式:
var fs = require('fs');

function readFiles(file1, file2, onDone) {
    fs.readFile(file1, 'utf-8', function(error, data1) {
        fs.readFile(file2, 'utf-8', function(error, data2) {
            onDone(data1 + data2);
        });
    });
}

readFiles('./file1.txt', './file2.txt', function(data) {
    console.log(data);
});

这大概是最初级的解决方案,两个互不依赖的异步操作,大可不必串行。

来看看利用事件订阅/发布的方式应该怎么写:

var util = require('util');
var events = require('events');

function TestEventEmitter() {
    events.EventEmitter.call(this);
}

util.inherits(TestEventEmitter, events.EventEmitter);


TestEventEmitter.prototype.all = function() {

    if(arguments.length < 2) {
        throw new TypeError('all must a latest two params');
    }

    var params = Array.prototype.slice.call(arguments),
        handler = params.splice(-1, 1)[0],
        count = params.length,
        result = [],
        self = this;

    for(var i = 0; i < count; i++) {
        this.once(params[i], function(ret) {
            result.push(ret);
            if(result.length === count) {
                handler.apply(self, result);
            }
        });
    }
};

var ev = new TestEventEmitter();

ev.all('readFile1', 'readFile2', function(data1, data2) {
    console.log('result' + data1 + data2);
});

function readFile(fileName, encoding, onDone) {
    fs.readFile(fileName, encoding, onDone);
}

readFile('file1.txt', 'utf-8', function(err, data) {
    ev.emit('readFile1', data);
});

readFile('file2.txt', 'utf-8', function(err, data) {
    ev.emit('readFile2', data);
});

事件订阅发布的形式一个优点是代码比较直观,缺点是必须把每个操作都封装成一个事件发送的操作。如果要解决的问题不是太复杂可以考虑这种方式。


优化

考虑到要读取的两个问题没有相互依赖用promise可以写成如下形式:

var Promise = require('./promise-new');

function readFilePromise(fileName, encoding) {
    return new Promise(function(onResolve, onReject) {
        fs.readFile(fileName, encoding, function(err, data) {
            if(err) {
                onReject(err);
            }
            else {
                onResolve(data);
            }
        });
    });
}

Promise.all([readFilePromise('file1.txt', 'utf-8'), readFilePromise('file2.txt', 'utf-8')])
.then(function(data) {
    console.log('result:' + data);
})
.catch(function(err) {
    console.log('err' + err);
});

promise以相对优雅的方式解决了多个异步并行问题,同时有好的异常捕获机制使得我们再也不用担心异常抛出了,只需再末尾catch一个异常处理函数即可。


引入Deferred语法糖

对于不同的场景编写promise代码,我们必须把需要执行的具体操作封装为一个与promise结合的异步函数,如上面的readFilePromise。把该函数转换一下,改成Deferred的形式可以写成:

var Promise = require('./promise-new');

function Deferred() {
    var handler =  {};
    handler.promise = new Promise(function(resolve, reject) {
        handler.resolve = resolve;
        handler.reject = reject;
    });

    return handler;
}

function readFilePromise(fileName, encoding) {
    var defer = Deferred();
    fs.readFile(fileName, encoding, function(err, data) {
        if(err) {
            defer.reject(err)
        }
        else {
            defer.resolve(data);
        }
    });
    return defer.promise;
}

对于fs.readFile的回调函数处理形式也可以把它抽象出来。Q里面有一个专门解决这个问题的函数:

var Promise = require('./promise-new');

function Deferred() {
    if(!(this instanceof Deferred)) {
        return new Deferred();
    }
    var self = this;
    this.promise = new Promise(function(resolve, reject) {
        self.resolve = resolve;
        self.reject = reject;
    });
}

Deferred.prototype.makResolver = function() {
    var self = this;
    return function(err, data) {
        if(err) {
            self.reject(err);
        }
        else if(arguments.length > 2) {
            self.resolve([].slice.call(arguments, 1));
        }
        else {
            self.resolve(data);
        }
    };
}

function readFilePromise(fileName, encoding) {
    var defer = Deferred();
    fs.readFile(fileName, encoding, defer.makResolver());
    return defer.promise;
}

Promise.all([readFilePromise('file1.txt', 'utf-8'), readFilePromise('file2.txt', 'utf-8')])
.then(function(data) {
    console.log('result:' + data);
})
.catch(function(err) {
    console.log('err' + err);
});

Composing Promise(链式异步)

composing promises ,是 promises 的强大能力之一。每一个函数只会在前一个promise 被调用并且完成回调后调用,并且这个函数会被前一个 promise 的输出调用。

假设现在读取三个异步操作,而前一个输出结果被后一个所使用:


readFilePromise('file1.txt', 'utf-8')
.then(function(data1) {
    console.log('data1 ' + data1);
    return readFilePromise('file2.txt', 'utf-8');
})
.then(function(data2) {
    console.log('data2' + data2);
    return readFilePromise('file3.txt', 'utf-8');
})
.catch(function(err) {
    console.log('err' + err.toString());
})
.then(function(data3) {
    console.log('data3 ' + data3);
});

每一个then操作都会新建一个promise对象,如果想让后一个异步操作依赖于前一个的结果则必须把promise对象返回。

看一下composing promise的流程图:
Promise_第2张图片

如果file1不存在那么taskB永远不会被读取


参考资料:
1. https://github.com/promises-aplus/promises-spec
2. https://github.com/azu/promises-book/issues?state=open
3. http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html

你可能感兴趣的:(JavascriptDP,Nodejs)