转眼见,已经来到九月。
从Promise化一个API 谈起,
我们熟悉微信小程序开发的应该知道,我们使用 wx.request() 在微信小程序环境中发送一个网络请求。参考官方文档,具体用法如下:
wx.request({
url: 'test.php', // 仅为示例,并非真实的接口地址
data: {
x: '',
y: ''
},
header: {
'content-type': 'application/json' // 默认值
},
success(res) {
console.log(res.data)
}
})
配置化的 API 风格和我们早期使用 jQuery 中 Ajax 方法的封装类似。
这样的设计有一个小的问题,
就是容易出现“回调地狱”问题。
如果我们想先通过 ./userInfo 接口来获取登录用户信息数据,
再从登录用户信息数据中,通过请求 ./${id}/friendList 接口来获取登录用户所有好友列表,就需要:
wx.request({
url: './userInfo',
success(res) {
const id = res.data.id
wx.request({
url: `./${id}/friendList`,
success(res) {
console.log(res)
}
})
}
})
一级嵌套还没有到地狱级别,但是足以说明问题。
我们知道解决“回调地狱”问题的一个极佳方式就是 Promise,将微信小程序 wx.request() 方法进行 Promise 化:
const wxRequest = (url, data = {}, method = 'GET') =>
new Promise((resolve, reject) => {
wx.request({
url,
data,
method,
header: {
//通用化 header 设置
},
success: function (res) {
const code = res.statusCode
if (code !== 200) {
reject({ error: 'request fail', code })
return
}
resolve(res.data)
},
fail: function (res) {
reject({ error: 'request fail'})
},
})
})
上面就是promise 封装的简单基本例子。
Promise 基本概念不再过多介绍。
我们不仅可以对 wx.request() API 进行 Promise 化,更应该做的通用,能够 Promise 化更多类似(通过 success 和 fail 表征状态)的接口:
const promisify = fn => args =>
new Promise((resolve, reject) => {
args.success = function(res) {
return resolve(res)
}
args.fail = function(res) {
return reject(res)
}
})
使用:
const wxRequest = promisify(wx.request)
这个外包装实话说挺吊的。
通过上例,我们知道:
Promise 其实就是一个构造函数,我们使用这个构造函数创建一个 Promise 实例。该构造函数很简单,它只有一个参数,按照 Promise/A+ 规范的命名,把 Promise 构造函数的参数叫做 executor, executor
类型为函数。这个函数又“自动”具有resolve、reject 两个方法作为参数。
体验结论开始 promise 第一步:
function Promise(executor) {
}
Promise 初见雏形
在上面的 wx.request() 介绍中, 实现了 Promise 化,因此对于嵌套回调场景,可以:
wxRequest('./userInfo').then(
data => wxRequest(`./${data.id}/friendList`),
error => {
console.log(error)
}
).then(
data => {
console.log(data)
},
error => {
console.log(error)
}
)
通过观察使用例子,我们来剖析Promise 的实质
结论 Promise 构造函数返回一个 promise 对象实例,这返回的promise 对象具有一个 then 方法。 then 方法中,调用者可以定义俩个参数,分别是 onfulfilled 和 onrejected,它们都是函数类型。其中 onfulfilled 通过参数,可以获取 promise 对象和 resolved 的值,onrejected 获得 promise 对象 rejected 的 值。通过这个值,我们来处理异步完成后的逻辑
这些都是规范的基本内容: Promise/A+
因此,继续实现我们的Promise:
function Promise(executor){
}
Promise.prototyoe.then = function(onfulfilled, onrejected){
}
继续复习 Promise 的知识,看例子来理解:
let promise1 = new Promise((resolve, reject) => {
resolve('data')
})
promise1.then((data) => {
console.log(data)
})
let promise2 = new Promise((resolve, reject) => {
reject('error')
})
let promise2 = new Promise(() => {
reject('error')
})
promise2.then((data) => {
console.log(data)
}, error => {
console.log(error)
})
结论,我们在使用 new 关键字调用 Promise 构造函数时,在合适的时机(往往是异步结束时),调用 executor 的参数 resolve 方法,并将 resolved 的值作为 resolve 函数参数执行,这个值便可以在后续在 then 方法第一个参数 (onfulfilled) 中拿到;同理。在出现错误时,调用 executor 的参数reject 方法,并将错误信息作为reject函数参数执行,这个错误信息可以在后续的 then 方法,并将错误信息作为 reject 函数参数执行,这个错误信息可以在后续的 then 方法第二个函数参数(onrejected)中拿到。
因此,我们实现 Promise时, 应该有俩个值, 分别存储 resolved 的值,以及 rejected 的值(当然,因为 Promise 状态的唯一性,不可能同时出现 resolved 的值和 rejected 的值,因此可以使用一个变量来存储);同时也需要存在一个状态,这个状态就是 promise 实例的状态(pending,fulfilled,rejected);同时还要提供 resolve 方法以及 reject 方法,这俩个方法需要作为 exeutor的参数提供给开发者使用:
function Promise(executor) {
const self = this
this.status = 'pending'
this.value = null
this.reason = null
function resolve(value) {
self.value = value
}
function reject(reason) {
self.reason = reason
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled = Function.prototype, onrejected = Function.prototype) {
onfulfilled(this.value)
onrejected(this.reason)
}
为了保证 onfulfilled、 onrejected 能强健执行,我们为其设置了默认值,其默认值为一元函数(
Function.prototype
)。
注意,因为 resolve 的最终调用是由开发者在不确定环境下(往往是全局中)直接调用的。为了在resolve 函数中能够拿到promise 实例的值,我们需要对this进行保存,上述代码中用self变量记录this,或者使用箭头函数:
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
const resolve = value => {
this.value = value
}
const reject = reason => {
this.reason = reason
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled = Function.prototype, onrejected = Function.prototype) {
onfulfilled(this.value)
onrejected(this.reason)
}
为什么 then 放在 Promise 构造函数的原型上,而不是放在构造函数内部呢?
每个 promise 实例的 then 方法逻辑是一致的,在实例调用该方法时,可以通过原型(Promise.prototype)找到,而不需要每次实例化都新创建一个 then 方法,这样节省内存,显然更合适。
Promise 实现状态完善
我们先来看一到题目,判断输出:
let promise = new Promise((resolve, reject) => {
resolve('data')
reject('error')
})
promise.then(data => {
console.log(data)
}, error => {
console.log(error)
})
只会输出: data, 因为我们知道 promise 实例状态 只能从 pending 改变为 fulfilled, 或者从 pending
改变为 rejected。状态一旦变更完毕,就不可再变化或者逆转。也就是说:如果一旦变到 fulfilled,就不能再 rejected,一旦变到rejected,就不能 fulfilled。
而我们的代码实现,显然无法满足这一特性。执行上一段代码时,将会输出 data 以及 error。
因此,需要对状态进行判断和完善:
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
const resolve = value => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
}
}
const reject = reason => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
}
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled, onrejected) {
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
onrejected = typeof onrejected === 'function' ? onrejected : error => {throw error}
if (this.status === 'fulfilled') {
onfulfilled(this.value)
}
if (this.status === 'rejected') {
onrejected(this.reason)
}
}
我们看,在 resolve 和 reject 方法中,我们加入判断,只允许 promise 实例状态从 pending 改变为 fulfilled,或者从 pending 改变为 rejected。
同时注意,这里我们对 Promise.prototype.then 参数 onfulfilled 和 onrejected 进行了判断,当实参不是一个函数类型时,赋予默认函数值。这时候的默认值不再是函数元 Function.prototype 了。为什么要这么更改?后面会有介绍。
这样一来,我们的实现显然更加接近真实了。刚才的例子也可以跑通了:
let promise = new Promise((resolve, reject) => {
resolve('data')
reject('error')
})
promise.then(data => {
console.log(data)
}, error => {
console.log(error)
})
但是不要高兴得太早,promise 是解决异步问题的,我们的代码全部都是同步执行的,似乎还差了更重要的逻辑。
Promise 异步完善
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('data')
}, 2000)
})
promise.then(data => {
console.log(data)
})
正常的来讲,上诉代码会在2秒之后输出data,但是我们实现的代码,并没有任何输入信息。这是为什么呢。
理由就是,我们实现的逻辑全是同步的。在上面实例化一个promise的构造函数时,我们是在setTimeout 逻辑里才调用 resolve,也就是说, 2秒之后才会调用 resolve 方法,也才会去更改promise实例状态。而结合我们的实现,返回实例代码,then 方法中的 onfulfilled 执行是同步的,它在执行时 this.status 仍然为pending,并没有做到"2秒中之后再执行 onfulfilled"。
那该怎么办呢?我们似乎应该在“合适”的时间才去调用 onfulfilled 方法,这个合适的时间就应该是开发者调用 resolve 的时刻,那么我们先在状态(status)为 pending 时,把开发者传进来的 onfulfilled 方法存起来,在 resolve 方法中再去执行即可:
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.onFulfilledFunc = Function.prototype
this.onRejectedFunc = Function.prototype
const resolve = value => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onFulfilledFunc(this.value)
}
}
const reject = reason => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onRejectedFunc(this.reason)
}
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfullfilled, onrejected) {
onfulfilled = typeof onfulfilled === 'function'? onfulfilled: data => data
onrejected = typeof onrejected === 'function'? onrejected: error => { throw error}
if (this.status === 'fulfilled') {
onfulfilled(this.value)
}
if (this.status === 'rejected') {
onrejected(this.reason)
}
if( this.status === 'pending') {
this.onFulfilledFunc = onfulfilled
this.onRejectedFunc = onrejected
}
}
于此同时,我们知道 Promise 是异步执行的:
let promise = new Promise((resolve, reject) => {
resolve('data')
})
promise.then(data => {
console.log(data)
})
console.log(1)
正常情况下,这里会按照顺序,输出 1 再输出 data。
但是现实的情况确不是这样的。
引用大佬的一段话
而我们的实现,却没有考虑这种情况,先输出 data 再输出 1。因此,需要将 resolve 和 reject 的执行,放到任务队列中。这里姑且先放到 setTimeout 里,保证异步执行(这样的做法并不严谨,为了保证 Promise 属于 microtasks,很多 Promise 的实现库用了 MutationObserver 来模仿 nextTick)。
改动的代码是如下的:
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onFulfilledFunc(this.value)
}
})
}
const reject = reason => {
setTimeout(() => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onRejectedFunc(this.reason)
}
})
}
executor(resolve, reject)
这样一来,
在执行到 executor(resolve, reject) 时,也能保证在 nextTick 中才去执行,不会阻塞同步任务。
同时我们在 resolve 方法中,
加入了对 value 值是一个 Promise 实例的判断。看一下到目前为止的实现代码
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.onFulfilledFunc = Function.prototype
this.onRejectedFunc = Function.prototype
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onFulfilledFunc(this.value)
}
})
}
const reject = reason => {
setTimeout(() => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onRejectedFunc(this.reason)
}
})
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled, onrejected) {
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
onrejected = typeof onrejected === 'function' ? onrejected : error => {throw error}
if (this.status === 'fulfilled') {
onfulfilled(this.value)
}
if (this.status === 'rejected') {
onrejected(this.reason)
}
if (this.status === 'pending') {
this.onFulfilledFunc = onfulfilled
this.onRejectedFunc = onrejected
}
}
let promise = new Promise((resolve, reject) => {
resolve('data')
})
promise.then(data => {
console.log(data)
})
console.log(1)
此时就是按照顺序,输出1 在输出data。
貌似来说,我们目前实现的是一个完美状态。
但是,还是有多个then方法调用的一些细节需要实现。
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('data')
}, 2000)
})
promise.then(data => {
console.log(`1: ${data}`)
})
promise.then(data => {
console.log(`2: ${data}`)
})
应该输出:
1: data
2: data
然而,实际上,我们只是输出了 2: data,这是因为js函数的覆盖性,第二个方法会直接覆盖第一个方法。
第二个 then 方法中的 onFulfilledFunc 会覆盖第一个 then 方法中的 onFulfilledFunc。
这个问题也是有解决方案的,只需要将所有的then 方法中的 onFulfilledFunc 存储为一个数组 onFulfilledArray,在resolve时,依次执行。对于 onRejectedFunc 同理:
以下是,改动后的实现
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.onFulfilledArray = []
this.onRejectedArray = []
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onFulfilledArray.forEach(func => {
func(value)
})
}
})
}
const reject = reason => {
setTimeout(() => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onRejectedArray.forEach(func => {
func(reason)
})
}
})
}
try {
executor(resolve, reject)
} catch(e) {
reject(e)
}
}
Promise.prototype.then = function(onfulfilled, onrejected) {
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
onrejected = typeof onrejected === 'function' ? onrejected : error => {throw error}
if (this.status === 'fulfilled') {
onfulfilled(this.value)
}
if (this.status === 'rejected') {
onrejected(this.reason)
}
if (this.status === 'pending') {
this.onFulfilledArray.push(onfulfilled)
this.onRejectedArray.push(onrejected)
}
}
这里还有一个细节,在构造函数中如果出错,将会自动触发 promise 实例状态为 rejected,我们用 try...catch 块对 executor 进行包裹:
try {
executor(resolve, reject)
} catch(e) {
reject(e)
}
关于promise在真实中使用的案例,可以参看大概下面这篇文章,
promise 于websocket的结合使用,我在里面看到是 :
this._websocket.onmessage = (e) => {
const key = e.content.token;
const req = this.promisePool[key]
req.resolve(e);
delete this.promisePool[key];
};
它在open 方法,就是连接状态上面 onmessage 监听上面,返回了 send的 返回值,也就是说没有写到回调里面,

先挂起一个异步,等待回调成功完成后在执行函数。

执行过程就是这个样子的。
https://segmentfault.com/a/1190000014873336
文章采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接。