如何手把手实现一个 Promise/A+

一直就想手把手实现一下 Promise/A+ 了,碰巧最近在读已逝的 avalon 作者 司徒正美 的书《JavaScript框架设计》,算是奠念前辈吧。因此,程序员。除了有理性和清晰的逻辑思维,还要有强壮灵活身体思想。看书有感记,以此共勉。

Talk is cheap, show me the code.

  1. 内部通过单向链表结果存储事件成功处理函数、事件失败处理函数,和链表中下一个 Promise 类型对象
  2. Promise 内部实例状态标识:Pending(初始状态)、Fulfilled(成功状态)、Rejected(失败状态)。且状态为单方向移动的
  3. Promise 实例的成功/失败事件函数是基于Promise的状态而被调用的
  4. 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()

posted @ 2020-04-25 23:57  月光宝盒造梦师  阅读(182)  评论(0编辑  收藏  举报