格式约定:
1. 绑定在对象上的静态方法使用 Object.method 表示,如 Promise.resolve
2. 对象实例的方法使用 instance#method 表示,如 Promise#then

本文所出现的 concat 示例代码可以在 Github Gist 上找到

文中代码可能引起强烈不适,非战斗人员请撤离

Callback vs Promise

熟悉 JavaScript 的程序员一定都了解其事件模型和回调函数,如:

img.addEventListener('load', function imgLoaded() {  
    // do something after image loaded
});

上面这段代码表示在一个图片元素加载完成时,触发回调函数 imgLoaded()

假设我们现在有这么个需求:利用 node.js 实现简单的文件合并功能,通过传入一个包含资源路径和输出路径的配置对象,合并资源路径下的所有 *.js 文件并输出到指定路径。

利用传统的异步回调思路,我们可能会写出这样的代码:

var fs = require('fs');

function getFilesInDir(dir, cb) {  
    fs.readdir(dir, function (err, files) {
        if (err) {
            cb(err);
            return;
        }

        var jsFiles = files.filter(function (filename) {
            return filename.match(/\.js$/);
        }).map(function (filename) {
            return dir + '/' + filename;
        });

        cb(null, jsFiles);
    });
};

function readFiles(files, cb) { ... };

function output(contentList, cb) { ... };

function concatJS(options, cb) {  
    if (!options) {
        return cb(new Error('concat options is required'));
    }

    var writeFile = output.bind(null, options);

    getFilesInDir(options.path, function(err, files) {
        if (err) {
            return cb(err);
        }

        readFiles(files, function (err, contentList) {
            if (err) {
                return cb(err);
            }

            writeFile(contentList, function (err) {
                if (err) {
                    return cb(err);
                }
            });
        });
    });
};

module.exports = concatJS;  

看到最后一段层层嵌套的回调函数,我想大多数人都会感觉到不适吧!每个回调中的异常处理尤为让人不畅快,难道异步操作只能用回调吗?让我们来看看 Promise 的解决方案:

function getFilesInDir(dir) {  
    return new Promise(function (resolve, reject) {
        fs.readdir(dir, function (err, files) {
            if (err) {
                reject(err);
                return;
            }

            var jsFiles = files.filter(function (filename) {
                return filename.match(/\.js$/);
            }).map(function (filename) {
                return dir + '/' + filename;
            });

            resolve(jsFiles);
        });
    });
};

function readFiles(files) { ... };

function output(contentList) { ... };

function concatJS(options) {  
    if (!options) {
        return Promise.reject(new Error('concat options is required'));
    }

    var writeFile = output.bind(null, options);

    return getFilesInDir(options.path)
        .then(readFiles)
        .then(writeFile)
        .catch(function (err) {
            throw new Error(err);
        });
};

简洁明了的同步链式写法、一个错误捕获即可捕获(在此之前)所有 Promise 链上的错误,Promise 好赞!那么,Promise 到底是什么样的一个东西呢?

什么是 Promise

Promise 对象是对一个异步事件最终状态的封装,其最基本的用法是通过 then 方法注册一个回调函数,来接收该异步事件的结果[1.1]

一个 Promise 可以处于三种状态[1.2]

  1. pending: 异步事件正在进行中
  2. fulfilled: 异步事件成功完成
  3. rejected: 异步事件执行过程中发生错误

其中,一旦该 Promise 已进入 fulfilledrejected 状态,就不能转变为其它状态。也就是说,一个 Promise 的最终状态有且只有一个

Promise 核心实现

首先让我们来看看如何生成一个 Promise 对象的实例:

var promise = new Promise(function (resolve, reject) {  
    // do some async
    if (error) {
        reject(errorMessage);
        return;
    }

    resolve(result);
});

你可能会感到纳闷:这里的 resolve, reject 分别是什么?resolve, reject 分别设定了该 Promise 对象在成功/失败时调用的回调函数,通过 Promise#then 调用[2.1]

语言描述太过抽象,我们来找个第三方 Polyfill 看看 Promise 的核心实现。以 Github · then/promise为例:

function Promise(fn) {  
    var state = null
    var value = null
    var deferreds = []
    var self = this

    this.then = function (onFulfilled, onRejected) { ... }

    function handle(deferred) { ... }

    function resolve(newValue) {
        ...
        state = true
        value = newValue
        finale()
    }

    function reject(newValue) {
        state = false
        value = newValue
        finale()
    }

    function finale() { ... }

    doResolve(fn, resolve, reject)
}

function Handler(onFulfilled, onRejected, resolve, reject) { ... }

function doResolve(fn, onFulfilled, onRejected) {  
    var done = false;
    try {
        fn(function (value) {
            if (done) return
            done = true
            onFulfilled(value)
        }, function (reason) {
            if (done) return
            done = true
            onRejected(reason)
        })
    } catch (ex) {
        if (done) return
        done = true
        onRejected(ex)
    }
}

我们重点看看这个 doResolve() 函数:doResolve() 接受三个参数,其中 fn 是传入 Promise 的回调函数,onFulfilled, onRejected 指向本次 Promise 上下文中的 resolve, reject 函数。

从这里可以看出,我们在实例化 Promise 对象时调用的 resolve, reject 就是上文中 resolve, reject 的封装,当我们实例化 Promise 并执行 resolve(result) 时,实际上是执行了 Promise 对象内部的 resolve 函数,将 state 掷到 true,并将异步处理结果赋予 valuereject() 同理。

那么,我们怎么才能得到 Promise 的运行结果呢?

Promise#then

Promise 提供了 then 的原型方法来获得它最终的执行结果,then 方法接受两个参数作为回调[2.2]

promise.then(function onFulfilled(result) {

}, function onRejected(error) {

});

其中:

  1. onFulfilled 指定了 Promise 对象被 resolve 时的处理
  2. onRejected 指定了 Promise 对象被 reject 时的处理

图片来自 JavaScript Promise迷你书(中文版)

我们来看看刚才折叠的部分代码,首先是 then 方法的实现:

this.then = function (onFulfilled, onRejected) {  
    return new self.constructor(function (resolve, reject) {
        handle(new Handler(onFulfilled, onRejected, resolve, reject))
    })
}

可以看到,then 方法返回了一个新的 Promise,并向其注册了一个回调函数 handle(new Handler(onFulfilled, onRejected, resolve, reject))。那么这个 Handler 构造函数是什么?

function Handler(onFulfilled, onRejected, resolve, reject) {  
    this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null
    this.onRejected = typeof onRejected === 'function' ? onRejected : null
    this.resolve = resolve
    this.reject = reject
}

Handler 构造函数创建了一个带有 onFulfilled, onRejected, resolve, reject 四个方法的对象。其中,onFulfilled, onRejected 是我们在 then 中传入对 Promise 的结果处理回调,resolve, reject 则是实例化 Promise 时,经由前文提到的 doResolve() 函数对 Promise 内置的 resolve, reject 函数的封装。好,接下来是核心部分 handle() 函数:

function handle(deferred) {  
    if (state === null) {
        deferreds.push(deferred)
        return
    }
    asap(function () {
        var cb = state ? deferred.onFulfilled : deferred.onRejected
        if (cb === null) {
            (state ? deferred.resolve : deferred.reject)(value)
            return
        }
        var ret
        try {
            ret = cb(value)
        } catch (e) {
            deferred.reject(e)
            return
        }
        deferred.resolve(ret)
    })
}

当 Promise 未获得结果时,状态 state == null,此时调用 then 方法则会向 deferreds 数组中推入新的 Handler 对象;而当 Promise 已完成时,则会根据此时的状态 state 选择对应的处理方式,其中:

  1. state == true:对应该 Promise 被 resolve 时的处理,若 then 方法有传入 onFulfilled 回调,则调用 deferred.onFulfilled(),否则调用 deferred.resolve()
  2. state == false:对应该 Promise 被 reject 时的处理,若 then 方法有传入 onRejected 回调,则调用 deferred.onRejected(),否则调用 deferred.reject()

被推入 deferreds 数组中的对象,会在 Promise 被 resolve 或 reject 时,经由 finale() 函数遍历并执行 handle(deferred)

当没有传入对应的 onFulfilled || onRejected 回调时,绑定在 Handler 对象上的内置 resolve || reject 函数会被执行,将得到的结果传递给 then 的内部,并由 finale() 函数引发新一轮的处理,使结果能沿着 Promise 链向后传递,直到被捕获

而当有传入对应的处理函数时,则执行该回调,并将得到的结果传递给 deferred.resolve(),然后执行同上一段中高亮的操作,把结果沿着 Promise 链向后传递。

由此可知:对于同一个 Promise 对象,then 方法可以被多次调用,每个 then 都返回一个新的 Promise。当 Promise 执行结束后,所有绑定的 then 方法会被依次执行。如果 then 中没有传入对应的处理函数(如:Promise rejected -> then(onFulfilled, undefined)),则该 Promise 执行结果会沿着 Promise 链[2.3]向后传递,直到遇上相应的处理函数。并且 then 的返回值会作为结果,沿着 Promise 链向后传递[1.3]。处理过程如图所示:

图片来自 JavaScript Promise迷你书(中文版)

Promise#catch

ECMAScript 6 中,为 Promise 对象提供了一个方便的错误捕获方法 catch,它实际是对 then 方法的封装:

Promise.prototype['catch'] = function (onRejected) {  
  return this.then(null, onRejected);
}

这样一来,我们就可以这样写 Promise:promise.then(taskA).then(taskB).catch(handleError)

thenable

我们在调用 deferred.resolve() 来处理回调结果,以便传给下一个 then 时,可能遇到这样的情况:回调函数的返回值是一个拥有 then 方法的类 Promise 对象(thenable)。这时我们就应该做一些处理,以便下一次调用 then 的时候能得到该 thenable 对象的运行结果[1.4]。下面是一个完整的 resolve 函数:

function resolve(newValue) {  
    try {
        if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.')
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then
            if (typeof then === 'function') {
                doResolve(then.bind(newValue), resolve, reject)
                return
            }
        }
        state = true
        value = newValue
        finale()
    } catch (e) {
        reject(e)
    }
}

then 上绑定的回调函数返回了一个 thenable 对象时,使用 doResolve() 函数调用该 thenable 对象上的 then 方法,并将内置的 resolve, reject 函数作为其 onFulfilled, onRejected 回调,把 thenable 的运行结果存入自身内部变量 value,以使结果能传递给下一个 then

至此我们就完成了 Promise 的核心实现。下面,我们来看一些 ECMAScript 6 中 Promise 对象的静态方法:

ES6 Promise 静态方法

ECMAScript 6 原生的 Promise 在 Promises/A+ 规范[1]基础上拓展了一些静态方法,这里就不一一介绍其实现过程,具体可以参照 promise/es6-extensions.js at master · then/promise

我们来看一下各自的用法:

Promise.resolve

Promise.resolve 方法返回了一个处于 fulfilled 状态的 Promise 对象[2.4],比如:

Promise.resolve(101);

// 等价于

new Promise(function (resolve) {  
    resolve(101);
});

Promise.reject

Promise.resolve 方法相反,Promise.reject 方法返回了一个处于 rejected 状态的 Promise 对象[2.5],比如:

Promise.reject(new Error('something wrong!'));

// 等价于

new Promise(function (resolve, reject) {  
    reject(new Error('something wrong!'));
});

Promise.all

Promise.all 方法接受一个由 Promise 对象组成的数组,当这个数组里的所有成员都得到结果(即变为 fulfilledrejected 状态)时,then 方法才会被调用,同时传递给 then 的是包含各个 Promise 运行结果的数组[2.6]。比如我们在开篇处提到的 concat 例子,当要合并目录下所有 js 文件时,为避免同步操作的阻塞,用传统的回调方法我们可能会这样书写:

function readFiles(files, cb) {  
    var results = [],
        done = false,
        remain = files.length;

    files.forEach(function (path, index) {
        console.log('read file ' + path);

        fs.readFile(path, 'utf-8', function (err, data) {
            if (err) {
                done = true;
                cb(err);
                return;
            }

            results[index] = data;

            if (!done && !--remain) {
                done = true;
                cb(null, results);
            }
        });
    });
};

而用 Promise 改写如下:

function readFiles(files) {  
    var promises = files.map(function (path) {
        return new Promise(function (resolve, reject) {
            console.log('read file ' + path);

            fs.readFile(path, 'utf-8', function (err, data) {
                if (err) {
                    return reject(err);
                }

                resolve(data);
            });
        });
    });

    return Promise.all(promises);
};

怎么样?是不是感觉逻辑要清晰得多?要注意的是,Promise.all 中的某一对象出现错误,并不会终止剩余 Promise 的执行。但是此时整个 Promise 的状态将被掷到 rejected

var defer = function (ms) {  
    return new Promise(function (resolve) {
        setTimeout(function () {
            console.log(ms + 'ms delay is running');
            resolve(ms);
        }, ms);
    });
};

Promise.all([  
    defer(1),
    defer(10),
    defer(100),
    defer(1000),
    Promise.reject(new Error('something wrong'))
]).then(function (value) {
    // 不会执行
    console.log(value);
}).catch(function (error) {
    // Error: something wrong
    console.log(error);
});

Promise.race

就像 Promise.resolvePromise.reject 的对立关系一样,Promise.race 较之 Promise.all,同样是接受一个由 Promise 对象组成的数组作为参数,但处理方式却不尽相同:在 Promise.race 中,只要队列中的一个 Promise 得到结果,then 方法就会被调用[2.7],比如:

Promise.race([  
    defer(1),
    defer(10),
    defer(100),
    defer(1000)
]).then(function (time) {
    // log: 这是延迟 1 毫秒后的 Promise
    console.log('这是延迟 ' + time + ' 毫秒后的 Promise');
});

运行以上代码,我们可以发现 then 只调用了最早完成的那个 Promise 的结果(1ms)。但是剩余的 Promise 并不会因此停止执行,所以我们能在控制台看到所有 Promise 的运行记录。

同样,Promise.race 中的某一对象出现错误,并不会终止剩余 Promise 的执行。但与 Promise.all 不同的是,Promise.race 的最终状态完全取决于最早完成的 Promise:

Promise.race([  
    defer(1),
    defer(10),
    defer(100),
    defer(1000),
    new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('Promise with error running');
            reject(new Error('something wrong'));
        }, 2000);
    })
]).then(function (time) {
    // log: 这是延迟 1 毫秒后的 Promise
    console.log('这是延迟 ' + time + ' 秒后的 Promise');
}).catch(function (error) {
    // 不会执行
    console.log(error);
});

总结

Promise 为异步操作提供了类似同步的写法,极大程度上避免了回调地狱。笔者才疏学浅,行文至此可能有很多不当之处,还望指出。

(全文完)

参考资料:

  1. Promises/A+ Specification
  2. JavaScript Promise迷你书(中文版)
  3. 深入理解Promise实现细节
  4. A Deeper Dive Into JavaScript Promises