VueBloghyhero6

promise手写一个?promise实现(只是初版,链式调用的实现的看第二版)

2019-09-09 / 2019-10-10 / 351次浏览

转眼见,已经来到九月。
从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