从回调地狱说起 Callback Hell
在 Node 中,绝大部分操作都是异步的方式,例如读取一个文件内容:
fs.readFile('./config/pass.txt', (err, data) => {
if (err) throw err
console.log(data)
})
这里我们通过传入一个回调函数作为异步完成的后续操作。
如果我们异步读取多个文件,等到所有文件读取完毕执行特定操作呢?
代码修改如下:
fs.readFile('./config/pass.txt', (err, data) => {
if (err) throw err
fs.readFile('./config/pass1.txt', (err, data1) => {
if (err) throw err
fs.readFile('./config/pass2.txt', (err, data2) => {
if (err) throw err
fs.readFile('./config/pass3.txt', (err, data3) => {
if (err) throw err
console.log('success');
})
})
})
})
这里还是简单地敲套三层,想象下业务突然复杂,一不小心嵌套五层以上,这画面太美。。。
救世主 Promise
Promise 的音译是普罗米修斯,是希腊神话故事中的英雄,名字的意思是“先知”。
我们可以把上面 Node 读取文件的操作改造如下:
function asyncReadFile (filePath, options) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, options, (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
现在来看看 Promise 是如何解决 Callback Hell 的:
asyncReadFile('./config/pass.text').then(data => {
return asyncReadFile('./config/pass1.text')
}).then(data => {
return asyncReadFile('./config/pass2.text')
}).then(data => {
return asyncReadFile('./config/pass3.text')
}).then(data => {
console.log('success')
}).catch(err => {
console.log(err)
})
对于先知的我们,故事的安排在脑中是即时出现的,而非实际上的异步发生。因此,作为创世的编程者而言,这种与瞬间思考所同步的代码更符合现实世界下的思考方式。因此,更易读更易于理解。
Promise 是一种对异步操作的封装,在异步操作执行成功或者失败时执行指定方法。将横向的异步调用转换为纵向,因此更符合人类的思维方式。
一个 Promise 对象具备三种生命状态:pending、fulfilled 和 rejected。只能从最初的 pending 到 fulfilled 或者 rejected,而且状态的改变是不可逆的。
我们先简单看下 Promise 的工作原理。
Promise 大致的工作流程是:
- 创建 Promise 对象 => 进入等待处理阶段 Pending
- 处理完成后,切换到 Fulfilled 状态/ Rejected 状态
- 根据状态,执行 then 方法/执行 catch 方法 内的回调函数
- then 方法返回新的 Promise,此时就支持链式调用
这里创建一个 Promise 对象,Promise 内部维系着 resolve 和 reject 方法,resolve 会让 Promise 对象进入 Fulfilled 状态,并将 resolve 方法的第一个参数传给后续 then 所指定的 onFulfilled 函数。reject 方法同理,只不过是切换到 Rejected 状态,并将参数传给 catch 所指定的 onRejected 函数。
一步步打造心中的 Promise
基础实现
先抛开 rejected,实现一个 Promise 的调用链的简单代码如下:
function Promise (fn) {
var deferreds = []
this.then = function (onFulfilled) {
deferreds.push(onFulfilled)
}
function resolve (value) {
deferreds.forEach(deferred => {
deferred(value)
})
}
fn(resolve)
}
深入理解上面代码逻辑:
- then 方法将 onFulfilled 函数压入 deferreds 队列中。
- 将 resolve 传给创建 Promise 时传入的函数,resolve 的作用是将 deferreds 中的 onFulfilled 函数队列逐一执行。
但是这段代码暴露出一个严重的问题,如果 Promise 执行的是同步代码,resolve 是早于 then 方法的执行,这样造成一个问题:then 还没有及时把 onFulfilled 函数压入队列,此时 deferreds 还是空数组,resolve 执行后,后续注册到 deferreds 数组内的 onFulfilled 函数将不再执行。
这里我们可以把 deferreds 数组视为水桶,onFulfilled 视为饮用水,resolve 视为开关。then 操作就是将饮用水一点点地注入到水桶中。想想我们还没将水加到水桶中(执行 then 操作)就打开开关(执行 resolve),这肯定是接不到水的。
解决的办法就是将 resolve 函数的执行改为异步。
异步
Promises/A+ 规范明确要求回调需要通过异步方式执行,保证一致可靠的执行顺序。通过 setTimeout 方法,我们可以轻松实现:
function resolve (value) {
setTimeout(() => {
pending.forEach(deferred => {
deferred(value)
})
}, 0)
}
这样我们就可以把 resolve 执行放到下一个时钟周期。
引入状态
按照 Promise 规范,我们需要引入三种互斥的状态:pending、fulfilled、rejected。
执行 resolve 会将 pending 状态切换到 fulfilled,在此之后添加到 then 到函数都会立即被调用。
现在我们的代码如下:
function Promise (fn) {
const deferreds = []
var state = 'pending'
var value = null
this.then = function (onFulfilled) {
if (state === 'pending') {
deferreds.push(onFulfilled)
return this
}
onFulfilled(value)
return this
}
function resolve (_value) {
state = 'fulfilled'
value = _value
setTimeout(() => {
deferreds.forEach(deferred => {
deferred(value)
})
}, 0)
}
fn(resolve)
}
有了上面的基础,我们可以简单地调用 Promise:
asyncReadFile('./README.md', 'utf-8').then(data => {
console.log(data)
})
为了串行 Promise,我们在 then 中返回 this,并设置一个 value 来保存传给 resolve 的值。
asyncReadFile('./README.md', 'utf-8').then(data => {
console.log(data)
return asyncReadFile('./package.json', 'utf-8')
}).then(data => {
console.log(data)
})
像上面这样调用,虽然可以通过,但是两次输出的 data 是相同的值,并不是真正意义上的链式调用。
串行 Promise
只要 then 方法每次调用都返回一个 Promise 对象,前一个 Promise 对象持有后一个 Promise 对象的 resolve 方法,这样串行就变得非常简单了。
这里需要对 then 方法进行改造:
this.then = function (onFulfilled) {
return new Promise(resolve => {
handle({ onFulfilled, resolve })
})
}
function handle (deferred) {
if (state === 'pending') {
deferreds.push(deferred)
return
}
const ret = deferred.onFulfilled(value)
deferred.resolve(ret)
}
这里完成的主要任务是:
- then 方法中返回一个新的 Promise 对象,这样每次执行 then 方法,都返回一个 Promise 对象,让链式调用成为可能。
- 新创建的 Promise 对象调用上一级 Promise 的 handle 方法,传递自身的 resolve 方法和当前的 onFulfilled 函数。
handle 相比之前的 then 多了一行 deferred.resolve(ret)
,这一步是链式调用的关键点。此刻的 resolve 是下一级 Promise 的方法,上一级 Promise 执行这段方法调用,就开启了链式调用。
我们继续重构前面的 Promise 代码,这里主要修改的是 resolve 方法。
function Promise (fn) {
const deferreds = []
var state = 'pending'
var value = null
this.then = function (onFulfilled) {
// then 方法永远会返回一个 Promise 对象
return new Promise(resolve => {
// handle 为上一级 Promise 的方法
handle({ onFulfilled, resolve })
})
}
function handle (deferred) {
if (state === 'pending') {
// then 方法将 deferred 传入时,先压入到 deferreds 中
deferreds.push(deferred)
return
}
// 执行 Bridge Promise 前一个 Promise 对象的 then 方法的 onFulfilled 函数
const ret = deferred.onFulfilled(value)
// resolve 执行 deferreds 中的 onFulfilled 方法,即下一个 Bridge Promise 的 then 中的回调函数
deferred.resolve(ret)
}
function resolve (_value) {
// 如果是 promise 对象
if (_value && (typeof _value === 'object' || typeof _value === 'function')) {
const then = _value.then
if (typeof then === 'function') {
// 将 resolve 延迟到 promise 执行完毕后调用,切换 Bridge Promise 的状态。
then.call(_value, resolve)
return
}
}
// 如果是其它值
state = 'fulfilled'
value = _value
setTimeout(() => {
deferreds.forEach(deferred => {
handle(deferred)
})
}, 0)
}
fn(resolve)
}
Promise 具体流程
- 实例化一个最初的 Promise 对象,设置最初的状态为
pending
。 - 通过 then 方法,创建一个新的 Promise 对象,由于上一级 Promise 暂时处于 pending 状态,当前 then 方法的 onFulfilled 函数和新 Promise 的 resolve 方法放入到上一级 Promise 的 deferreds 数组中。
- 这样就形成这样一个画面:第一个 Promise 被实例化,调用 then 方法。then 会返回一个新的 Promise 对象,在上一个 then 方法的基础上继续通过新 Promise 的 then,形成一条调用链。 每一个被创建出来的新 Promise 的 resolve 都将传给上一级的 Promise 的 deferreds 数组来维护
- 在第一个 Promise 对象的回调函数中执行异步操作,完成后调用 Promise 的 resolve 方法。
- resolve 允许传入一个参数,该参数的值通过 Promise 内部的 value 变量维护。resolve 会把 Promise 的状态修改为
fulfilled
,然后异步调用 handle 依次处理 deferreds 数组中的每一个 deferred。 - 此时第一个 Promise 的状态在上一步骤中被改为 fulfilled,于是 handle 主要完成的工作是,执行 deferred 的 onFulfilled 函数,并调用下一个 Promise 的 resolve 方法。
- 下一个 Promise 的 resovle 在上一级被执行成功后,同样会将状态切换到
fulfilled
,重复步骤 6 直到结束。
这样 Promise 的核心逻辑,基本被我们实现了。至于 rejected 和 异常处理 就交给大家来思考吧(其实就是懒!)。
结束语:真的很难想象就这么几十行代码,竟然有如此强大的威力,理解 Promise 并不难,需要敬佩的是创造强大 Promise 魔法的第一批程序员。
我最近在看bluebird
源码,看得头都大了。没看懂…
Promise 的音译是普罗米修斯 😅
@dlyt 可以不用看bluebird的源码,因为它除了promise,还有很多其他的函数,导致他的结构会比较复杂,最好可以看 then/promise,去github搜一下就有了
@cctv1005s 好的,我去看看。
promise就是函数队列