Skip to content

深入理解 ES6 #11-Promise 与异步编程

🏷️ 《深入理解 ES6》

异步编程的背景知识

JavaScript 引擎是基于单线程(Single-threaded)实际循环的概念构建的,同一时刻只允许一个代码块在执行。

所以需要跟踪即将运行的代码,那些代码被放在一个任务队列(job queue)中,每当一段代码准备执行时,都会被添加到任务队列。每当 JavaScript 引擎中的一段代码结束执行,事件循环(event loop)会执行队列中的下一个任务,它是 JavaScript 引擎中的一段程序,负责监控代码执行并管理任务队列。队列中的任务会从第一个一直执行到最后一个。

事件模型

用户点击按钮或者按下按钮会触发类似 onclick 这样的事件,它会向任务队列添加一个新任务来响应用户的操作,这是 JavaScript 中最基础的异步编程形式,直到事件触发时才执行事件处理程序,且执行时上下文与定义时的相同。

js
let button = document.getElementById("my-btn");
button.onclick = function(event) {
    console.log("Clicked");
}

单击 button 后赋值给 onclick 的函数被添加到任务队列中,只有当前面的任务都完成后它才会被执行。

回调模式

回调模式与事件模式类似,异步代码都会在未来的某个时间点执行,两者的区别是回调函数中被调用的函数是作为参数传入的。

js
readFile("example.txt", function(err, contents) {
    // 错误 (error first) 优先
    if (err) {
        throw err;
    }

    console.log(contents); // 文件读取结束后执行
});

Console.log("Hi!"); // 先执行

回调模式比事件模型更灵活,因为相比之下,通过回调模式链接多个调用更容易。

js
readFile("example.txt", function(err, contents) {
    if (err) {
        throw err;
    }

    // 再次使用回调函数
    writeFile("example.txt", function(err) {
        if (err) {
            throw err;
        }
        console.log("File was written!");
    })
    console.log(contents);
});

虽然上例中的模式运行的不错,但是当嵌套层数过多时将陷入回调地狱。

js
method1(function(err, result) {
    if (err) {
        throw err;
    }

    method2(function(err, result) {
        if (err) {
            throw err;
        }

        method3(function(err, result) {
            if (err) {
                throw err;
            }

            method4(function(err, result) {
                if (err) {
                    throw err;
                }
                
                method5(reuslt);
            })
        })
    })
})

如果想实现更复杂的操作,回调函数的局限性同样会显现出来。

例如,同步执行两个异步操作,当两个操作都结束时通知你;或者同时进行两个异步操作,只取优先完成的操作结果。

Promise 的基础知识

Promise 相当于异步操作结果的占位符,它不会去订阅一个事件,也不会传递一个回调函数给目标函数,而是让函数返回一个 Promise。

js
// readFile 承诺将在未来的某个时刻完成
let promise = readFile("example.txt");

readFile 不会立即执行,函数会先返回一个表示异步读取操作的 Promise 对象,未来对这个对象的操作完全取决于 Promise 周期。

Promise 生命周期

  • unsettled(未处理)

    • pending(进行中)
  • settled(已处理)

    • fulfilled(成功完成)
    • rejected(未成功完成)

内部属性 [[PromiseState]] 被用来表示 Promise 的三种状态:pending、fulfilled、rejected。这个属性不暴露在 Promise 对象上,所以不能以编程的方式检测 Promise 的状态,只有当 Promise 的状态改变时,通过 then() 方法来采取特定的行动。

所有的 Promise 都有 then() 方法,它接受两个参数:一个是当 Promise 的状态变为 fulfilled 时要调用的函数,与异步操作相关的附加数据都会传递给这个完成函数(fulfillment function);第二个是当 Promise 的状态变为 rejected 时要调用的函数,其与完成时调用的函数类似,所有与失败状态相关的附加数据都会传递给这个拒绝函数(rejection function)。

Note

如果一个对象实现了上述的 then() 方法,那这个对象我们称之为 thenable 对象。所有的 Promise 都是 thenable 对象,但并非所有 thenable 对象都是 Promise。

js
let promise = readFile("example.txt");

promise.then(function(contents) {
    // 完成
    console.log(contents);
}, function(err) {
    // 拒绝
    console.error(err.message);
});

promise.then(function(contents) {
    // 完成
    console.log(contents);
});

promise.then(null, function(err) {
    // 拒绝
    console.error(err.message);
});

Promise 还有一个 catch() 方法,相当于只给其传入错误处理程序的 then() 方法。

js
promise.then(function(err) {
    // 拒绝
    console.error(err.message);
});

// 与以下调用相同

promise.then(null, function(err) {
    // 拒绝
    console.error(err.message);
})

then() 方法和 catch() 方法一起使用才能更好地处理异步操作结果。

如果一个 Promise 处于已处理状态,在这之后添加到任务队列中的处理程序仍将执行。

js
let promise = readFile("example.txt");

// 最初的完成处理程序
promise.then(function(contents) {
    console.log(contents);

    // 现在又添加一个到任务队列中
    promise.then(function(contents) {
        cosnole.log(contents);
    })
})

Note

每次调用 then() 方法或 catch() 方法都会创建一个新任务,当 Promise 被解决(resolved)时执行。这些任务最重会被加入到一个为 Promise 量身定做的独立队列中,这个任务队列的具体细节对应理解如何使用 Promise 而言不重要,通常你只要理解任务队列是如何运作的就可以了。

创建未完成的 Promise

用 Promise 构造函数可以创建新的 Promise,构造函数只接受一个参数:包含初始化 Promise 代码的执行器(executor)函数。

执行器接受两个参数,分别是 resolve() 函数和 reject() 函数。

执行器成功时调用 resolve() 函数,失败时调用 reject() 函数。

js
// Node.js 示例
let fs = require("fs");

function readFile(filename) {
    return new Promise(function(resolve, reject) {
        fs.readFile(filename, { encoding: "utf8" }, function(err, contents) {
            if (err) {
                reject(err);
                return;
            }

            resolve(contents);
        });
    });
}

let promise = readFile("example.txt");

promise.then(function(contents) {
    // 完成
    console.log(contents);
}, function(err) {
    // 拒绝
    console.error(err.message);
});

使用 Promise 构造函数创建 Promise 时,Promise 的执行器会立即执行,然后才执行后续流程中的代码。

执行器中调用 resolve() 后会触发一个异步操作,传入 then()catch() 方法的函数会被添加到任务队列中并异步执行。

js
let promise = new Promise(function(resolve, reject) {
    console.log("Promise");
    resolve();
});

promise.then(function() {
    console.log("Resolved.");
});

console.log("Hi!");

// 输出内容
// Promise
// Hi!
// Resolved.

虽然 promise.then()console.log("Hi!") 之前,但仍然在其之后执行。这是因为完成处理程序和拒绝处理程序总是在执行器完成后被添加到任务队列的末尾。

创建已处理的 Promise

使用 Promise.resolve()

Promise.resolve() 方法只接受一个参数并返回一个完成态的 Promise。

js
let promise = Promise.resolve(32);

promise.then(function(value) {
    console.log(value); // 32
});

使用 Promsie.reject()

可以通过 Promsie.reject() 方法创建已拒绝 Promise。它与 Promise.resolve() 很像,唯一的区别是创建出来的是拒绝态的 Promise。

js
let promise = Promise.reject(32);

promise.catch(function(value) {
    console.log(value); // 32
});

非 Promise 的 Thenable 对象

Promise.resolve()Promise.reject() 方法都可以接受非 Promise 的 Thenable 对象作为参数。如果传入一个非 Promise 的 Thenable 对象,则这些方法会创建一个新的 Promise,并在 then() 函数中被调用。

拥有 then() 方法并且接受 resolvereject 这两个参数的普通对象就是非 Promise 的 Thenable 对象。

js
let thenable = {
    then: function(resolve, reject) {
        resolve(32);
    }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
    console.log(value); // 32
})

执行器错误

如果执行器内部抛出一个错误,则 Promise 的拒绝处理程序就会被调用。

js
let promise = new Promise(function(resolve, reject) {
    throw new Error("Explosion!");
});

promise.catch(function(error) {
    console.log(error.message); // Explosion!
});

每个执行器都隐含了一个 try-catch 块,所以错误会被捕获并传入拒绝处理程序。

js
let promise = new Promise(function(resolve, reject) {
    try {
        throw new Error("Explosion!");
    } catch (ex) {
        reject(ex);
    }
});

promise.catch(function(error) {
    console.log(error.message); // Explosion!"
})

全局的 Promise 拒绝处理

如果在没有拒绝处理程序的情况下拒绝一个 Promise,那么不会提示失败信息,这是 JavaScript 语言中唯一一处没有强制报错的地方。

Promise 的特性决定了很难检测一个 Promise 是否被处理过。

js
let rejected = Promise.reject(32);

// 此时,rejected 还没有被处理

rejected.catch(function(value) {
    // 现在 rejected 已经被处理了
    console.log(value);
});

任何时候都可以调用 then() 方法或 catch() 方法,无论 Promise 是否已解决,这两个方法都可以正常运行,但这样很难知道一个 Promise 何时被处理。

Node.js 环境的拒绝处理

在 Node.js 中,处理 Promise 拒绝时会触发 process 对象上的两个事件:

  • unhandledRejection
    在一个事件循环中,当 Promise 被拒绝,并且没有提供拒绝处理程序时被调用。

  • rejectionHandled
    在一个事件循环后,当 Promise 被拒绝,并且没有提供拒绝处理程序时被调用。

设计这些事件是用来识别那些拒绝却又没被处理的过的 Promise 的。

unhandledRejection 事件处理程序接受两个参数:拒绝原因及被拒绝的 Promise。

js
let rejected;

process.on("unhandledRejection", function(reason, promise) {
    console.log(reason.message); // "Explosion!"
    console.log(rejected === promise); // true
});

rejected = Promise.reject(new Error("Explosion!"));

rejectionHandled 事件处理程序接受一个参数:被拒绝的 Promise。

js
let rejected;

process.on("rejectionHandled", function(promise) {
    console.log(rejected === promise); // true
});

rejected = Promise.reject(new Error("Explosion!"));

// 等待添加拒绝处理程序
setTimeout(function() {
    rejected.catch(function(value) {
        console.log(value.message); // "Explosion!"
    });
}, 1000);

// 输出结果
// true
// Explosion!

浏览器环境的拒绝处理程序

浏览器也是通过触发两个事件来识别未处理的拒绝的,虽然这些事件是在 window 对象上触发的,但实际上与 Node.js 中的完全等效。

  • unhandledrejection
    在一个事件循环中,当 Promise 被拒绝,并且没有提供拒绝处理程序时被调用。

  • rejectionhandled
    在一个事件循环后,当 Promise 被拒绝,并且没有提供拒绝处理程序时被调用。

事件处理程序接受一个有以下属性的对象作为参数:

  • type
    事件名称("unhandledrejection" 或 "rejectionhandled")

  • promise
    被拒绝的 Promise 对象

  • reason
    来自 Promise 的拒绝值

js
let rejected;

window.onunhandledrejection = function(event) {
    console.log(event.type); // "unhandledrejection"
    console.log(event.reason.message); // 
    console.log(rejected === event.promise); // true
};

window.onrejectionhandled = function(event) {
    console.log(event.type); // "rejectionhandled"
    console.log(event.reason.message); // 
    console.log(rejected === event.promise); // true
};

rejected = Promise.reject(new Error("Explosion!"));

Note

在 Chrome 上运行没有执行自定义的异常处理。按照网上的文档,Chrome 49+ 应该是支持该特性的,本机装的版本是 61,但是仍然没有效果。

串联 Promise

每次调用 then() 方法或 catch() 方法时,实际上创建并返回了另一个 Promise,只有当第一个 Promise 完成或被拒绝后,第二个才会被解决。

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

p1.then(function(value) {
    console.log(value);
}).then(function() {
    console.log("Finished");
});

// 输出结果
// 32
// Finished

只有第一个 then() 方法被执行后,才会调用第二个 then() 方法。上例可以拆解成如下形式:

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

let p2 = p1.then(function(value) {
    console.log(value);
});

p2.then(function() {
    console.log("Finished");
});

// 输出结果
// 32
// Finished

捕获错误

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

p1.then(function(value) {
    throw new Error("Boooom!");
}).catch(function(error) {
    console.log(error.message); // "Boooom!"
}).then(function() {
    console.log("Finished"); // Finished
});

// 输出结果
// Boooom!
// Finished
js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

p1.then(function(value) {
    throw new Error("Boooom!");
}).catch(function(error) {
    console.log(error.message); // "Boooom!"
}).then(function() {
    console.log("Finished"); // "Finished"
});

也可以通过后面的拒绝处理程序捕获前面的拒绝处理程序中的错误。

js
let p1 = new Promise(function(resolve, reject) {
    throw new Error("Explosion!");
});

p1.catch(function(error) {
    console.log(error.message); // "Explosion!"
    throw new Error("Boooom!");
}).catch(function(error) {
    console.log(error.message); // "Boooom!"
}).then(function() {
    console.log("Finished"); // "Finished"
});

// 输出结果
// Explosion!
// Boooom!
// Finished

Note

务必在 Promise 链的末尾留有一个拒绝处理程序以确保能够正确处理所有可能发生的错误。

Promise 链的返回值

Promise 链的另一个重要特性是可以给下游 Promise 传递数据。

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

p1.then(function(value) {
    console.log(value); // 32
    return value + 1;
}).then(function(value) {
    console.log(value); // 33
});

在拒绝处理程序中也可以做相同的事情。

js
let p1 = new Promise(function(resolve, reject) {
    reject(32);
});

p1.catch(function(value) {
    console.log(value); // 32
    return value + 1;
}).then(function(value) {
    console.log(value); // 33
});

在 Promise 链中返回 Promise

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

let p2 = new Promise(function(resolve, reject) {
    resolve(33);
});

p1.then(function(value) {
    // 第一个完成处理程序
    console.log(value); // 32
    return p2;
}).then(function(value) {
    // 第二个完成处理程序
    console.log(value); // 33
});

关于这个模式,最需要注意的是,第二个完成处理程序被添加到了第三个 Promise 而不是 p2。上面的示例等价于:

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

let p2 = new Promise(function(resolve, reject) {
    resolve(33);
});

let p3 = p1.then(function(value) {
    // 第一个完成处理程序
    console.log(value); // 32
    return p2;
});

p3.then(function(value) {
    // 第二个完成处理程序
    console.log(value); // 33
});

p2 改成被拒绝,p3.then() 将永远不被执行。。

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

let p2 = new Promise(function(resolve, reject) {
    reject(33);
});

let p3 = p1.then(function(value) {
    // 第一个完成处理程序
    console.log(value); // 32
    return p2;
});

p3.then(function(value) {
    // 第二个完成处理程序
    console.log(value); // 永不执行
});

虽然使用的 return p2 返回给变量 p3,但 p2 是不等于 p3,是两个不同的变量。但是 p2p3[[PromiseStatus]][[PromiseValue]] 属性的值是一致的。也就是说,如果 p2 被拒绝了,那么 p3 也是被拒绝的。最终导致了上例中的 p3.then() 永远不会被调用。

在完成处理程序中创建新的 Promise 可以推迟完成处理程序的执行。

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

p1.then(function(value) {
    console.log(value); // 32

    // 创建一个新的 promise
    let p2 = new Promise(function(resolve, reject) {
        resolve(33);
    });

    return p2;
}).then(function(value) {
    console.log(value); // 33
});

跟前一个例子的区别在于,p2 只会在 p1 被解决后才会被执行。

相应多个 Promise

如果想通过监听多个 Promise 来决定下一步的操作,则可以使用 ECMAScript 6 提供的 Promise.all()Promise.race() 两个方法来监听多个 Promise。

Promise.all() 方法

Promise.all() 方法只接受一个参数并返回一个 Promise,该参数是一个含有多个受监视 Promise 的可迭代对象,只有当可迭代对象中所有 Promise 都被解决后返回的 Promise 才会被解决,只有当可迭代对象中所有 Promise 都被完成后返回的 Promise 才会被完成。

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

let p2 = new Promise(function(resolve, reject) {
    resolve(33);
});

let p3 = new Promise(function(resolve, reject) {
    resolve(34);
});

let p4 = Promise.all([p1, p2, p3]);

p4.then(function(value) {
    console.log(Array.isArray(value)); // true
    console.log(value[0]); // 32
    console.log(value[1]); // 33
    console.log(value[2]); // 34
});

所有传入 Promise.all() 方法的 Promise 只要有一个被拒绝,那么返回的 Promise 没等所有 Promise 都完成就立即被拒绝。

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

let p2 = new Promise(function(resolve, reject) {
    reject(33);
});

let p3 = new Promise(function(resolve, reject) {
    resolve(34);
});

let p4 = Promise.all([p1, p2, p3]);

p4.catch(function(value) {
    console.log(Array.isArray(value)); // false
    console.log(value); // 33
});

拒绝处理程序总是接受一个值而非数组,该值来自被拒绝 Promise 的拒绝值。

Promise.race() 方法

它也接受含多个受监视 Promise 的可迭代对象作为唯一参数并返回一个 Promise,但只要有一个 Promise 被解决返回的 Promise 就被解决,无需等到所有 Promise 都被完成。

一旦数组中的某个 Promise 被完成,Promise.race() 方法也会像 Promise.all() 方法一样返回一个特定的 Promise。

js
let p1 = Promise.resolve(32);

let p2 = new Promise(function(resolve, reject) {
    resolve(33);
});

let p3 = new Promise(function(resolve, reject) {
    resolve(34);
});

let p4 = Promise.race([p1, p2, p3]);

p4.then(function(value) {
    console.log(value); // 32
});

传给 Promise.race() 方法的 Promise 会进行竞选,以决出哪一个先被解决,如果先解决的是已完成 Promise,则返回已完成 Promise;如果先解决的是已拒绝 Promise,则返回已拒绝 Promise。

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

let p2 = Promise.reject(33);

let p3 = new Promise(function(resolve, reject) {
    resolve(34);
});

let p4 = Promise.race([p2, p1, p3]);

p4.catch(function(value) {
    console.log(value); // 33
});

自 Promise 继承

Promise 与其它内建类型一样,也可以作为基类派生其他类。

js
class MyPromise extends Promise {
    success(resolve, reject) {
        return this.then(resolve, reject);
    }

    failure(reject) {
        return this.catch(reject);
    }
}

let promise = new MyPromise(function(resolve, reject) {
    resolve(32);
});

promise.success(function(value) {
    console.log(value); //32
}).failure(function(value) {
    console.log(vlaue);
});

MyPromise 类型派生自 Promise,并且扩展了两个方法 success()failure()

由于静态方法会被继承,因此派生的 Promise 也拥有 MyPromise.resolve()MyPromise.reject()MyPromise.race()MyPromise.all() 这 4 个方法,后两者与内建方法完全一致,而前两者却稍有不同。

由于 MyPromise.resolve() 方法和 MyPromise.reject() 方法通过 Symbol.species 属性来决定返回 Promise 的类型,故调用这两个方法时无论传入什么值都会返回一个 MyPromise 的实例。

js
let p1 = new Promise(function(resolve, reject) {
    resolve(32);
});

let p2 = MyPromise.resolve(p1);
p2.success(function(value) {
    console.log(value); // 32
});

console.log(p2 instanceof MyPromise); // true

// 输出结果:
// true
// 32

基于 Promise 的异步任务执行

js
let fs = require("fs");

function run(taskDef) {
    // 创建可以在其他地方使用的迭代器
    let task = taskDef();

    // 开始执行任务
    let result = task.next();

    // 不断调用 next() 的递归函数
    function step() {
        // 如果有更多任务要做
        if (!result.done) {
            if(typeof result.value === "function") {
                result.value(function(err, data) {
                    if (err) {
                        result = task.throw(err);
                        return;
                    }

                    result = task.next(data);
                    step();
                });
            } else {
                result = task.next(result.value);
                step();
            }
        }
    }

    // 启动递归
    step();
}

// 定义一个可用于执行的函数
function readFile(filename) {
    return function(callback) {
        fs.readFile(filename, callback);
    };
}

// 执行一个任务
run(function*() {
    let contents = yield readFile("example.txt");
    // doSomethingWith(contents);
    console.log(contents);
    console.log("Done");
});

将上例中的异步操作改成使用 Promise 实现。

js
let fs = require("fs");

function run(taskDef) {
    // 创建迭代器
    let task = taskDef();

    // 开始执行任务
    let result = task.next();

    // 不断调用 next() 的递归函数
    (function step() {
        // 如果有更多任务要做
        if (!result.done) {
            let promise = Promise.resolve(result.value);
            promise.then(function(value) {
                result = task.next(value);
                step();
            }).catch(function(error) {
                result = task.throw(error);
                step();
            });
        }
    }());
}

// 定义一个可用于执行的函数
function readFile(filename) {
    return new Promise(function(resolve, reject) {
        fs.readFile(filename, function(err, contents) {
            if (err) {
                reject(err);
            } else {
                resolve(contents);
            }
        });
    });
}

// 执行一个任务
run(function*() {
    let contents = yield readFile("example.txt");
    // doSomethingWith(contents);
    console.log(contents);
    console.log("Done");
});