理解 JavaScript(ECMAScript 6)—— 异步
JavaScript 作为主要面向 Web 编程而创建的语言,其诞生初期即具有了应对异步的用户交互(如点击鼠标、按下键盘等)的能力。后续的 Node.js 引入了 callbacks 作为除事件模型以外的另一种实现异步编程的方式,而之后的 Promise 又使得 JavaScript 处理异步需求的能力更为强大。
一、异步编程基础
Event Model
当用户点击鼠标或按下键盘上的某个按键时,一个对应的特殊事件(比如 onclick
)触发,该事件关联的一系列响应动作即被添加到工作队列中最终被执行。这是 JavaScript 中最基本的异步编程方式。
<button id="my-btn">Click Me</button>
<script>
let button = document.getElementById("my-btn")
button.onclick = function(event) {
console.log("Clicked")
}
</script>
callback
callback 模式与基于事件模型的异步编程类似,异步代码都是在后续的某个特定时间点执行。不同的是,其异步执行的操作(函数)需要作为参数传递。
let fs = require("fs")
fs.readFile("example.txt", { encoding: "utf8" }, function(err, contents){
if (err) {
throw err
}
console.log(contents)
})
console.log("Hi!")
// Hi!
// This is an example text file
上面的例子使用了经典的 Node.js error-first 回调函数模式。readFile()
函数从硬盘读取某个文件,在文件读取完成之后执行 callback 函数。如果读取文件时发生错误,则传递给回调函数的 err
参数为错误对象;未发生错误则 content
参数中包含了读取的文件内容。
在 callback 模式中,readFile()
会立即开始执行,并且在文件读取的进度开启之后暂停。紧接着 readFile()
后面的 console.log("Hi!")
会立即执行并输出 Hi! 到屏幕上(此时 readFile()
处于暂停状态,回调函数中的 console.log(contents)
也并未执行)。
文件读取结束后,一个新的任务(即 callback 函数以及传递给它的参数)被添加到任务队列中等待最终被执行。
从输出中可以看到,Hi! 要先于 contents 被打印。
callback 模式的问题在于,当有过多的 callback 函数嵌套时,会出现称为 callback hell 的情况:
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(result);
});
});
});
});
二、Promise
除了前面提到的 callback hell
会使代码变得过于繁杂以至于难以理解和调试之外,callback 模式对于处理某些较复杂的逻辑也有一定的局限性。
比如希望两个异步操作并行执行,在双方都完成之后提醒用户;或者两个异步任务同时开始但是只获取第一个任务执行完后的结果。在这些情景下,就需要同时追踪多个 callback 函数的状态。
promise 则针对以上情况做了相应的提升。
promise 是一种对应异步操作执行结果的“占位符”。
// readFile “保证”会在未来的某个时间点完成
let promise = readFile("example.txt")
readFile()
并不会立即开始读取文件。相反,它会直接返回一个 promise 对象表示异步的读取操作,方便在后续的代码中通过这个 promise 对象访问读取任务的结果。该 promise 代表的结果是否可用取决于其生命周期所处的阶段。
Promise 生命周期
promise 的生命周期起始于 pending (unsettled)
状态,表明对应的异步操作还未完成。比如前面的 let promise = readFile("example.txt")
,在 readFile()
函数返回后 promise 就立即进入了 pending 状态。
而异步操作最终完成时,promise 则进入 settled
状态,具体包含两种情况:
- Fulfilled:promise 对应的异步操作成功执行完毕
- Rejected:promise 对应的异步操作未执行完毕(出现错误或其他情况)
内部属性 [[PromiseState]]
用来标记其生命周期状态(如 pending
、fulfilled
、rejected
),该属性不对 promise 对象外部暴露,因此不可以人为修改 promise 对象的生命周期。但是可以在 promise 的状态改变时通过 then()
方法自动触发一系列动作。
所有的 promise 对象都具有 then()
方法,该方法可以接收两个函数作为参数。第一个参数为当 promise 状态为 fulfilled 时调用的函数,所有与异步操作相关的数据都会被传递给该函数;第二个参数为当 promise 状态为 rejected 时调用的函数。这两个参数都是可选的。
let promise = readFile("example.txt");
promise.then(function(contents) {
// fulfillment
console.log(contents)
}, function(err) {
// rejection
console.error(err.message)
});
promise.then(function(contents) {
// fulfillment
console.log(contents);
});
promise.then(null, function(err) {
// rejection
console.error(err.message);
});
promise.catch(function(err) {
// rejection
console.error(err.message);
});
创建 Promise
promise 可以使用 Promise
构造器创建,该构造器接收一个称为 executor 的函数作为参数,包含了初始化 promise 的代码。
executor 接收 resolve()
、reject()
两个函数作为参数,resolve()
将在 executor 执行成功后调用,传递 promise 已做好准备的信号;executor 执行失败了则调用 reject()
。
一个 Promise 的完整示例:
let fs = require("fs")
function readFile(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename, { encoding: "utf8" }, function(err, contents) {
// check for errors
if (err) {
reject(err)
return
}
// the read succeeded
resolve(contents)
})
})
}
let promise = readFile("example.txt")
// listen for both fulfillment and rejection
promise.then(function(contents) {
// fulfillment
console.log(contents)
}, function(err) {
// rejection
console.error(err.message)
})
console.log("Hi!")
// Hi!
// This is an example text file
Promise 的执行流程
参考如下代码:
console.log("At code start")
var delayedPromise = new Promise((resolve, reject) => {
console.log("delayedPromise executor")
setTimeout(() => {
console.log("Resolving delayedPromise")
resolve("Hello")
}, 1000)
})
console.log("After creating delayedPromise")
delayedPromise.then(contents => {
console.log("delayedPromise resolve handled with", contents)
})
const immediatePromise = new Promise((resolve, reject) => {
console.log("immediatePromise executor")
resolve("World")
})
immediatePromise.then(contents => {
console.log("immediatePromise resolve handled with", contents)
})
console.log("At code end")
// At code start
// delayedPromise executor
// After creating delayedPromise
// immediatePromise executor
// At code end
// immediatePromise resolve handled with World
// Resolving delayedPromise
// delayedPromise resolve handled with Hello
具体的执行逻辑为:
- 代码开始执行,通过 Promise 构造器创建一个 delayedPromise,其中的
console.log()
和setTimeout()
(也可以是其他异步操作)函数立即执行 - delayedPromise 创建之后,其最终的结果和状态(是否成功执行)不能立即知晓,因此处于 pending 状态
- 调用
delayedPromise
的then
方法,将一个当 promise 成功 resolve 后才执行的 callback 函数放到执行计划中 - 继续创建另一个 immediatePromise,该 promise 会在创建的过程中立即 resolve,因此其创建完成后即处于 resolved 状态
- 调用
immediatePromise
的then
方法,注册一个当 promise 成功 resolve 后才执行的 callback 函数
从最终的结果中可以看出,即便 immediatePromise 在创建后即处于 resolved 状态,At code end
实际上是先于前面的 immediatePromise.then()
输出的。
原因是 promise 被设计成专门针对异步操作,then()
方法中的 callback 会永远在当前事件循环中所有代码执行完后才开始触发。
因此实际的执行顺序为:
At code start
-> 创建 delayedPromise
-> 通过 then() 注册 delayPromise 状态为 resolved 时触发的 callback
-> 创建 immediatePromise
-> 通过 then() 注册 immediatePromise 状态为 resolved 时触发的 callback
-> At code end
-> immediatePromise 先 resolved,其关联的 callback 执行
-> delayedPromise resolved,其关联的 callback 执行
三、Chaining Promises
截止到前面的介绍,promise 看起来只不过在 callback 的基础上做了一点点有限的提升。实际 promise 支持多种形式的连接,足以完成更加复杂的异步逻辑。
每次对 promise 的 then()
或 catch()
方法的调用,实际上都会创建和返回另一个 promise 对象。第二个 promise 对象只有在第一个 promise fulfilled 或 rejected 后才会被 resolve。
let p1 = new Promise(function(resolve, reject) {
resolve(42)
})
p1.then(function(value) {
console.log(value)
}).then(function() {
console.log("Finished")
})
// 42
// Finished
unchained 版本:
let p1 = new Promise(function(resolve, reject) {
resolve(42)
})
let p2 = p1.then(function(value) {
console.log(value)
})
p2.then(function() {
console.log("Finished")
})
p2.then()
也会返回一个 promise 对象,只不过它没有在代码中使用。
错误捕获
Promise chaining 允许用户捕获之前的 promise 中出现的错误。
let p1 = new Promise(function(resolve, reject) {
resolve(42)
})
p1.then(function(value) {
throw new Error("Boom!")
}).catch(function(error) {
console.log(error.message)
})
// Boom!
p1 的 fulfillment handler 抛出异常,第二个 promise 的 catch()
方法通过它的 rejection handler 接收到该异常。同样的方式也适用于 rejection handler 抛出异常:
let p1 = new Promise(function(resolve, reject) {
throw new Error("Explosion!")
})
p1.catch(function(error) {
console.log(error.message)
throw new Error("Boom!")
}).catch(function(error) {
console.log(error.message)
})
// Explosion!
// Boom!
executor 抛出异常触发 p1 的 rejection handler,该 handler 又抛出另一个异常触发第二个 promise 的 rejection handler。
Promise Chain 中的返回值
Promise Chain 中另一个很重要的特性即在两个 promise 之间传递数据。之前的代码中,可以通过 executor 中的 resovle()
函数将值传递给该 promise 的 fulfillment handler。此外,还可以通过为 fulfillment handler 指定一个返回值,将该值沿着 promise chain 传递。
let p1 = new Promise(function(resolve, reject) {
resolve(42)
})
p1.then(function(value) {
console.log(value)
return value + 1
}).then(function(value) {
console.log(value)
})
// 42
// 43
同样的操作也可以用在 rejection handler 上:
let p1 = new Promise(function(resolve, reject) {
reject(42)
})
p1.catch(function(value) {
console.log(value)
return value + 1
}).then(function(value) {
console.log(value)
})
// 42
// 43
四、响应多个 Promise
之前的代码中都是一次只响应一个 promise,但是有时候需要监控多个 promise 的状态并决定之后的动作。ECMAScript 6 提供了两种方法(Promise.all()
和 Promise.race
)应对这些情况。
Promise.all()
Promise.all()
方法只接收一个包含所有需要监控的 promise 的可迭代对象(如列表)作为参数,并且只有当这些需要监控的 promise 全部 resolved 时,Promise.all()
返回的 promise 才会 resolved。
let p1 = new Promise(function(resolve, reject) {
resolve(42)
})
let p2 = new Promise(function(resolve, reject) {
resolve(43)
})
let p3 = new Promise(function(resolve, reject) {
resolve(44)
})
let p4 = Promise.all([p1, p2, p3])
p4.then(function(value) {
console.log(Array.isArray(value)) // true
console.log(value[0]) // 42
console.log(value[1]) // 43
console.log(value[2]) // 44
})
Promise.all()
创建了 promise p4。只有当列表中的 promise p1,p2,p3 全部 fulfilled 之后,p4 最终才会 fulfilled。
前面 3 个 promise resolve 的数字组成列表传递给 p4 的 fulfillment handler,这些数字与生产它们的 promise 的位置是一一对应的。
如果任意一个传入 Promise.all()
的 promise 状态是 rejected,则 Promise.all()
返回的 promise 也会立即 rejected,不会等待其他 promise 结束。
let p1 = new Promise(function(resolve, reject) {
resovle(42)
})
let p2 = new Promise(function(resolve, reject) {
reject(43)
})
let p3 = new Promise(function(resolve, reject) {
resolve(44)
})
let p4 = Promise.all([p1, p2, p3])
p4.catch(function(value) {
console.log(Array.isArray(value)) // false
console.log(value) // 43
})
在上面的代码中,p2 的状态为 rejected,p4 的 rejection handler 会立即调用,不会等待 p1 和 p3 执行完毕(p1 和 p3 最终会执行完毕,只是 p4 不会等它们)。
Promise.race()
Promise.race() 同样接收一个包含需要监控的多个 promise 的可迭代对象,返回一个新的 promise。但是不同于 Promise.all()
会等待所有监控中的 promise resolved,Promise.race()
会在列表中任意一个 promise resolve 后立即返回。
let p1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 500, 42)
})
let p2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 43)
})
let p3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 200, 44)
})
let p4 = Promise.race([p1, p2, p3])
p4.then(function(value) {
console.log(value)
})
// 43
传递给 Promise.race()
的 promise 像是处在一个赛道中,看哪一个先执行完毕。如果第一个运行完的 promise 状态为 fulfilled,则最后返回的 promise 状态为 fulfilled;如果第一个运行完的 promise 状态为 rejected,则最后返回的 promise 状态为 rejected。
参考资料
Understanding ECMAScript 6
Secrets of the JavaScript Ninja, Second Edition