一点感悟
Promise 是编写异步的另一种方式,鄙人愚见,它就是 Callback 的一种封装
相比 Callback ,它有以下特点
- Promise 将异步结果保存起来,可以随时获取
- 链式调用 then 方法会返回一个新的 Promise ,从而避免了回调地狱
决定一次异步有两个环节
- 发起异步事件
- 处理异步结果
Promise 可以给一个异步事件注册多个处理函数,举个栗子,就像这样
let p1 = new Promise((resolve) => {
fs.readFile('./test.js', 'utf8', (err, data) => {
resolve(data)
})
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))
用 Callback 实现一样的效果
- 用 callbacks 将所有注册的函数保存
- 待异步事件返回结果,再遍历 callbacks ,依次执行所有注册的函数
就像这样
let callbacks = []
function resolve(data){
callbacks.forEach(cb => cb(data))
}
fs.readFile('./test.js', 'utf8', (err, data) => {
resolve(data)
})
callbacks.push(data => console.log(data))
callbacks.push(data => console.log(data.toUpperCase()))
将上述代码封装一下
const fs = require('fs')
class FakePromise {
constructor(fn){
this.callbacks = []
resolve = resolve.bind(this)
function resolve(data){
this.callbacks.forEach(cb => cb(data))
}
fn(resolve)
}
then(onFulfilled){
this.callbacks.push(onFulfilled)
}
}
let p1 = new FakePromise(resolve => {
fs.readFile('./test.js', 'utf8', (err, data) => {
resolve(data)
})
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))
哈?是不是和真的 Promise 有点像
从发布-订阅模式的角度来看:
- FakePromise 中通过
.then(onFulfilled)
来订阅消息,注册处理异步结果的函数 - 通过
resolve(data)
来发布消息,触发处理异步结果的函数去执行,发布的时机是异步事件完成时
延时 resolve
先前的代码存在一个问题,如果在执行 p1.then(data => console.log(data))
之前,resolve(data)
就已经执行了,那么再通过 .then(onFulfilled)
注册的处理异步结果的函数将永远不会执行
为了避免这种情况,改造 resolve 函数,在其内部添加 setTimeout,从而保证那些注册的处理函数是在下一个事件队列中执行,就像这样
function resolve(value) {
setTimeout(() => {
this.callbacks.forEach(cb => cb(value))
}, 0)
}
通过延时执行 resolve 内部的函数,保证了先订阅消息,再发布消息
但是 Promise 还有个额外的功能是在发布消息后,仍然可以订阅消息,并且立即执行,就像这样
const fs = require('fs')
let p1 = new Promise(resolve => {
fs.readFile('./test.js', 'utf8', (err, data) => resolve(data))
})
p1.then(data => console.log(data))
setTimeout(function(){
p1.then(data => console.log(data.toUpperCase()))
}, 5000)
5s之内,文件早已读取成功,但是在5s之后,依然可以通过 .then
注册处理事件,并且该事件会立即执行
先发布,再订阅
实现先发布,再订阅的基础是将消息保存下来。其次要记录状态,判断消息是否已被发布,如果未发布消息,则通过 .then
来注册回调时,是将回调函数添加到内部的回调队列中;如果消息已发布,则通过 .then
来注册回调时,直接将消息传至回调函数,并执行
Promise 规范中采用的状态机制是 pending
、fulfilled
、rejected
pending
可以转化为 fulfilled
或 rejected
,并且只能转化一次。
转化为 fulfilled
和 rejected
后,状态就不可再变
修改代码如下
class FakePromise {
constructor(fn) {
this.value = null
this.state = 'pending'
this.callbacks = []
resolve = resolve.bind(this)
function resolve(value) {
setTimeout(() => {
this.value = value
this.state = 'fulfilled'
this.callbacks.forEach(cb => cb(value))
}, 0)
}
fn(resolve)
}
then(onFulfilled) {
if (this.state === 'pending') {
this.callbacks.push(onFulfilled)
} else {
onFulfilled(this.value)
}
}
}
既然实现了先发布,再订阅,那么 resolve 中的 setTimeout 是不是可以去掉了?
并不可以,因为人家正经的 Promise 是这样的
let p1 = new Promise(resolve => {
resolve('haha')
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))
console.log('xixi')
// xixi
// haha
// HAHA
只有保留 resolve 中 setTimeout 才能使 FakePromise 实现相同的效果
let p1 = new FakePromise(resolve => {
resolve('haha')
})
p1.then(data => console.log(data))
p1.then(data => console.log(data.toUpperCase()))
console.log('xixi')
// xixi
// haha
// HAHA
没有 setTimeout 的输出结果
// haha
// HAHA
// xixi
链式 Promise
正经的 Promise 可以链式调用,从而避免了回调地狱
let p1 = new Promise(resolve => {
fs.readFile('./test.js', 'utf8', (err, data) => {
resolve(data)
})
}).then(res => {
return new Promise(resolve => {
fs.readFile('./main.js', 'utf8', (err, data) => {
resolve(data)
})
})
}).then(res => {
console.log(res)
})
正经的 Promise 调用 then 方法会返回一个新的 Promise 对象
我们伪造的 FakePromise 并没有实现这一功能,原来的 then 方法
...
then(onFulfilled){
if (this.state === 'pending') {
this.callbacks.push(onFulfilled)
} else {
onFulfilled(this.value)
}
}
...
原来的 then 方法就是根据 state 判断是注册 onFulfilled 函数,还是执行 onFulfilled 函数
为了实现 FakePromise 的高仿,我们要改造 then 方法,使其返回一个新的 FakePromise ,为了方便区分,将返回的 FakePromise 取名为 SonFakePromise ,而先前调用 then 的对象为 FatherFakePromise
那么问题来了
- 那么构造这个 SonFakePromise 的函数参数是什么
- 这个 SonFakePromise 什么时候 resolve ?
首先,当构造一个新的 SonFakePromise 时,会将传入的函数参数 fn 执行一遍,且这个函数有 resolve 参数
...
then(onFulfilled){
if(this.state === 'pending'){
this.callbacks.push(onFulfilled)
let SonFakePromise = new FakePromise(function fn(resolve){
})
return SonFakePromise
}else{
onFulfilled(this.value)
let SonFakePromise = new FakePromise(function fn(resolve){
})
return SonFakePromise
}
}
...
现在的问题是这个 SonFakePromise 什么时候 resolve ?即构造函数中的函数参数 fn 如何定义
结合正经 Promise 的例子来看
let faherPromise = new Promise(resolve => {
fs.readFile('./test.js', 'utf8', (err, data) => {
resolve(data)
})
}).then(res => {
return new Promise(resolve => {
fs.readFile('./main.js', 'utf8', (err, data) => {
resolve(data)
})
})
}).then(res => {
console.log(res)
})
// 等同于
let faherPromise = new Promise(resolve => {
fs.readFile('./test.js', 'utf8', (err, data) => {
resolve(data)
})
})
let sonPromise = faherPromise.then(function onFulfilled(res){
return new Promise(function fn(resolve){
fs.readFile('./main.js', 'utf8', (err, data) => {
resolve(data)
})
})
}).then(res => {
console.log(res)
})
在例子中,onFulfilled 函数如下,且其执行后返回一个新的 Promise,暂时取名为 fulPromise
function onFulfilled(res) {
return new Promise(function fn(resolve){
fs.readFile('./main.js', 'utf8', (err, data) => {
resolve(data)
})
})
}
现在来分析一下,fatherPromise,sonPromise 和 fulPromise 这三者的关系
- sonPromise 是调用 fatherPromise 的 then 方法返回的
- 而调用这个 then 方法需要传入一个函数参数,取名为 retFulPromise
- retFulPromise 函数执行的返回值 fulPromise
希望下面的代码能有助于理解
let fatherPromise = new Promise(function fatherFn(fatherResolve){
fs.readFile('./test.js', 'utf8', (err, data) => {
fatherResolve(data)
})
})
let sonPromise = fatherPromise.then(retFulPromise)
function retFulPromise(res) {
let fulPromise = new Promise(function fulFn(fulResolve){
fs.readFile('./main.js', 'utf8', (err, data) => {
fulResolve(data)
})
})
return fulPromise
}
fatherPromise 的状态为 fulfilled 时,会执行 retFulPromise,其返回 fulPromise ,当这个 fulPromise 执行 fulResolve 时,即完成读取 main.js 时, sonPromise 也会执行内部的 resolve
所以可以看成,sonPromise 的 sonResolve 函数,也被注册到了 fulPromise 上
So,了解了整个流程,该怎么修改自己的 FakePromise 呢?
秀操作,考验技巧的时候到了,将 sonResolve 的引用保存起来,注册到 fulFakePromise 上
const fs = require('fs')
class FakePromise {
constructor(fn) {
this.value = null
this.state = 'pending'
this.callbacks = []
resolve = resolve.bind(this)
function resolve(value) {
setTimeout(() => {
this.value = value
this.state = 'fulfilled'
this.callbacks.forEach(cb => {
let returnValue = cb.onFulfilled(value)
if (returnValue instanceof FakePromise) {
returnValue.then(cb.sonResolveRes)
}
})
})
}
fn(resolve)
}
then(onFulfilled) {
if (this.state === 'pending') {
let sonResolveRes = null
let sonFakePromise = new FakePromise(function sonFn(sonResolve) {
sonResolveRes = sonResolve
})
this.callbacks.push({
sonFakePromise,
sonResolveRes,
onFulfilled
})
return sonFakePromise
} else {
let value = onFulfilled(this.value)
let sonResolveRes = null
let sonFakePromise = new FakePromise(function sonFn(sonResolve) {
sonResolveRes = sonResolve
})
if (value instanceof FakePromise) {
value.then(sonResolveRes)
}
return sonFakePromise
}
}
}
多角度测试
let fatherFakePromise = new FakePromise(resolve => {
fs.readFile('./test.js', 'utf8', (err, data) => {
resolve(data)
})
})
let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
return new FakePromise(function fn(resolve) {
fs.readFile('./main.js', 'utf8', (err, data) => {
resolve(data)
})
})
}).then(res => {
console.log(res)
})
let fatherFakePromise = new FakePromise(resolve => {
fs.readFile('./test.js', 'utf8', (err, data) => {
resolve(data)
})
})
setTimeout(function () {
let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
return new FakePromise(function fn(resolve) {
fs.readFile('./main.js', 'utf8', (err, data) => {
resolve(data)
})
})
}).then(res => {
console.log(res)
})
}, 1000)
let fatherFakePromise = new FakePromise(resolve => {
resolve('haha')
})
let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
return new FakePromise(function fn(resolve) {
fs.readFile('./main.js', 'utf8', (err, data) => {
resolve(data)
})
})
}).then(res => {
console.log(res)
})
let fatherFakePromise = new FakePromise(resolve => {
resolve('haha')
})
setTimeout(function () {
let sonFakePromise = fatherFakePromise.then(function onFulfilled(res) {
return new FakePromise(function fn(resolve) {
fs.readFile('./main.js', 'utf8', (err, data) => {
resolve(data)
})
})
}).then(res => {
console.log(res)
})
}, 1000)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。