如何手把手实现一个 Promise/A+
一直就想手把手实现一下 Promise/A+ 了,碰巧最近在读已逝的 avalon 作者 司徒正美 的书《JavaScript框架设计》,算是奠念前辈吧。因此,程序员。除了有理性和清晰的逻辑思维,还要有强壮和灵活的身体及思想。看书有感记,以此共勉。
Talk is cheap, show me the code.
- 内部通过单向链表结果存储事件成功处理函数、事件失败处理函数,和链表中下一个 Promise 类型对象
- Promise 内部实例状态标识:Pending(初始状态)、Fulfilled(成功状态)、Rejected(失败状态)。且状态为单方向移动的
- Promise 实例的成功/失败事件函数是基于Promise的状态而被调用的
- Promise 总是返回值是一个新的 promise 实例
var p = Promise.resolve(0)
var p1 = p.then((value) => value)
p === p1 // false
PS: 原著中使用 ES4 编写,我使用 ES6 改写。后续有时间补上测试用例。
/**
* References:
* 1. 司徒正美《JavaScript框架设计》
* 2. [MDN MessageChannel](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel)
* 3. [MDN MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
* 3. [Can i use](https://caniuse.com/)
*/
const REJECTED = "rejected"
const PENDING = "pending"
const RESOLVED = "resolved"
class Promise {
static [Symbol.toStringTag] = "Promise"
/**
* @constructor
* @callback Executer
* @param {(resolve: (value) => {}, reject: (reason) => {} )} executer
* @param {} executer
* @returns {Promise<any>}
*/
constructor(executer) {
if (!executer || typeof executer !== "function") {
throw new TypeError(
`Promise resolver ${typeof executer} is not a function`
)
}
// Promise 内部实例状态标识:Pending(初始状态)、Fulfilled(成功状态)、Rejected(失败状态)。且状态为单方向移动的
this.status = PENDING
this.value = undefined
// 单向链表结果存储事件成功处理函数、事件失败处理函数,和链表中下一个 Promise 类型对象
this._deferred = [
/* nextPromiseInstance?: [
onResolved: Function,
onRejected: Function,
resolve: Function,
reject: Function] */
]
// Promise 实例化的回调函数被同步/立即执行
executer(
/* onResolve */ (value) => this._resolve(value),
/* onReject */ (reason) => this._reject(reason)
)
}
// when onFulfilled
/**
* Promise 总是返回值是一个新的 promise 实例
* @param {(value) => {}} onResolved
* @param {(reason) => {}} onRejected
*/
then(onResolved, onRejected) {
return new Promise((resolve, reject) => {
this._deferred.push([onResolved, onRejected, resolve, reject])
this._notify()
})
}
// when onReject
/**
* Promise 总是返回值是一个新的 promise 实例
* @param {(reason) => {}} onReject
*/
catch(onReject) {
return this.then(undefined, onReject)
}
// finally function
/**
* Promise 总是返回值是一个新的 promise 实例
* @param {Function} callback
*/
finally(callback) {
return new Promise(() => {
if (typeof callback === "function") {
callback()
}
})
}
/**
* 内部实现 _resolve 方法,更新 status 到 RESOLVED 并下发 _notify
* @param {Promise|any} value
*/
_resolve(value) {
if (this.status === PENDING) {
if (value === this) {
throw new TypeError(`Promise settled with itself.`)
}
let called = false
try {
const then = value && value["then"]
// if it is a Promise or Thenable object
// so executes it and pass its value to
// the current promise's resolve/reject
if (
value !== null &&
typeof value === "object" &&
typeof then === "function" &&
value instanceof Promise
) {
then.call(
value,
function onResolve(value) {
if (!called) {
this._resolve(value)
}
called = true
},
function onReject(reason) {
if (!called) {
this._reject(value)
}
called = false
}
)
return
}
} catch (error) {
if (!called) {
this._reject(error)
}
called = true
return
}
this.status = RESOLVED
this.value = value
this._notify()
}
}
/**
* 内部实现 _reject 方法,更新 status 到 REJECTED 并下发 _notify
* @param {Error|string} reason
*/
_reject(reason) {
if (this.status !== PENDING) return
this.status = REJECTED
this.value = reason
this._notify()
}
/**
* 内部实现 _notify 方法,以微/宏任务执行按序执行单向链表中的 Promise
*/
_notify() {
nextTick(() => {
if (this.status !== PENDING) {
while (this._deferred.length) {
const deferred = this._deferred.shift()
const [onResolved, onRejected, resolve, reject] = deferred
try {
if (this.status === RESOLVED) {
if (typeof onResolved === "function") {
resolve(onResolved.call(undefined, this.value))
} else {
resolve(this.value)
}
} else if (this.status === REJECTED) {
if (typeof onRejected === "function") {
resolve(onRejected.call(undefined, this.value))
} else {
reject(this.value)
}
}
} catch (error) {
reject(error)
}
}
}
})
}
static resolve(value) {
return new Promise((resolve, _reject) => {
resolve(value)
})
}
static reject(reason) {
return new Promise((_resolve, reject) => {
reject(new Error(reason))
})
}
static all() {}
static race() {}
// todo: what `allSettled` means?
static allSettled() {}
}
/**
* 以微/宏任务的方式运行回调,实际上它有个 callback 参数,很多 library 都有实现类似的方法
* @see {@link https://github.com/vuejs/vue/blob/52719ccab8/src/core/util/next-tick.js How Vue.js implements nextTick?}
*
* 这是其实现前的注释:
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
*/
function nextTick() {
// if it runs in nodeJS can use process.nextTick as well
// const nextTick = inNodeJS && process && process.nextTick
// return nextTick
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate}
* @see {@link https://caniuse.com/#search=setImmediate}
* Non-standard, it only works for IE10,IE10+
*
* This method is used to break up long running operations and run a callback function immediately
* after the browser has completed other operations such as events and display updates.
*/
if (window.setImmediate) {
return window.setImmediate.bind(window)
}
const callbacks = []
function flushCallbacks() {
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
/**
* 所以此处加上我们的 MessageChannel 实现
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessagePort}
* @see {@link https://caniuse.com/#search=MessageChannel}
*
* The MessageChannel interface of the Channel Messaging API allows us to create a
* new message channel and send data through it via its two MessagePort properties.
*/
if (window.MessageChannel) {
const channel = new MessageChannel()
// Returns port2 of the channel.
const port = channel.port2
channel.port1.onmessage = flushCallbacks
return function (fn) {
// push callback function into
callbacks.push(fn)
port.postMessage(1)
}
}
/**
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver}
* @see {@link https://caniuse.com/#search=MutationObserver}
* Works on IE10+ and all modern browsers.
*
* The MutationObserver interface provides the ability to watch for changes being made to the DOM tree.
* It is designed as a replacement for the older Mutation Events feature,
* which was part of the DOM3 Events specification.
*/
if (window.MutationObserver) {
const node = document.createTextNode("promise")
// Create an observer instance linked to the callback function
new MutationObserver(flushCallbacks).observe(
/* Select the node that will be observed for mutations */
node,
/* Options for the observer (which mutations to observe) */
{
characterData: true,
}
)
let bool = false
return function (fn) {
callbacks.push(fn)
bool = !bool
node.data = bool
}
}
// jQuery Deferred Object 实现中的 next_fast_way_image
// 另一实现是 next_fast_way_state_change 跟 image 类似
// 不过是通过动态插入一段 script 监听 onStateChange 实现
if (window.Image) {
const image = new Image()
image.onload = image.onerror = flushCallbacks
return function (fn) {
callbacks.push(fn)
image.src = "data:image/png," + Math.random()
}
}
// 最终回退到 setTimeout
return function (fn) {
const timerId = setTimeout(() => {
clearTimeout(timerId)
fn()
}, 4)
}
}
nextTick = nextTick()