手写Promise,通过Promise A+测试且执行时机与V8一致(代码量很少)

534 阅读4分钟

成果

先看结果: 通过了 Promise A+ 测试

1641305745406-e921a58f-ac3b-4c81-89a4-d9db6daa84d4.png 执行逻辑与V8一致。 下面道题与V8的 Promise 输出一致 0 1 2 3 4 5 6

MyPromise.resolve()
  .then(() => {
  console.log(0);
  return MyPromise.resolve(4);
})
  .then((res) => console.log(res))

MyPromise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2))
  .then(() => console.log(3))
  .then(() => console.log(5))
  .then(() => console.log(6))

原理

先谈一下我认为的promise原理,有错误欢迎支出。

  • 基于 订阅发布。
    • 订阅:then 进行订阅。
      • then(onFulfilled, onRejected)
    • 发布:resolve、reject 发布。
      • new Promise(resolve => resolve(1))
  • 异步。
    • 微任务(Microtask)
      • 我们可通过 queueMicrotask,创建一个微任务
  • 链式调用。
  • 状态确认后不可变。

实现

下面通过es6的 class,私有属性,箭头函数、queueMicrotask、来实现MyPromise。 逻辑参考了V8源码实现(在下面的参考资料中有读源码文章)。 代码很简单,直接看吧。

class MyPromise {
    status = Pending   // 初始 Pending状态
    reaction = []      // then的订阅队列
    value = undefined
    
    constructor(executor) {
        // callOnes 保证resolve, reject其中一个函数执行后,再次执行无效
        const [resolve, reject] = callOnes(this.#resolve, this.#reject)
        try {
            executor(resolve, reject)
        } catch (e) {
            reject(e)
        }
    }

    #resolve = (v) => {
          // resolve传入的值,要分多种情况处理
        this.#resolvePromise(v)
    }
    
    // Fulfille状态确认函数
    #fulfil = (v) => {
        if (this.status === Pending) {
            this.status = Fulfilled
            this.value = v
            this.#triggerPromiseReactions()
        }
    }
    
    // Rejected状态确认函数
    #reject = (reason) => {
        if (this.status === Pending) {
            this.status = Rejected
            this.value = reason
            this.#triggerPromiseReactions()
        }
    }
    
    // 处理 resolve中result的多种情况
    #resolvePromise = (result) => {
        // result是this
        if (this === result) {
            return this.#reject(new TypeError('The promise and the return value are the same'))
        }
        // 非object直接赋值.
        // 注意:function算object, null不算
        if (!isObject(result)) {
            return this.#fulfil(result);
        }
        let then;
        try {
            // 取 then
            then = result.then;
        } catch (error) {
            // 取 then 时抛出错误
            return this.#reject(error);
        }
        
        // 普通对象,非thenable对象
        if (typeof then !== 'function') {
            return this.#fulfil(result)
        }
        
        // result是myPromise或thenable
        // ecma规定,统一在下一个微任务处理
        queueMicrotask(() => {
            if (then === this.then) {
                // result 是传入的值,确认是MyPromise实例。
                
                // 确认this的状态和值,通过 result。
                // 为什么then?  因为result可能在Pending状态。
                result.then(this.#fulfil, this.#reject)
                // 注意微任务执行then,then又会触发微任务,这里会有2个微任务的执行时间。
            } else {
                // 是thenable(即带有"then"方法的对象)
                const [resolve, reject] = callOnes(this.#resolve, this.#reject)
                try {
                    // 执行,this指向thenable
                    then.call(result, resolve, reject)
                } catch (error) {
                    reject(error)
                }
            }
        })
    }
    
    // 订阅onFulfilled, onRejected
    then(onFulfilled, onRejected) {
        // 返回新promise
        const promise = new MyPromise(() => {})
        // 保存订阅onFulfilled和onRejected的订阅对象
        this.reaction.push({
            onFulfilled,
            onRejected,
            promise, // 保存返回的promise,用于链式调用
        })
        // 状态已确认,开始发布
        if (this.status !== Pending) {
            this.#triggerPromiseReactions()
        }
        return promise
    }
    
    // 发布:将then的订阅全部执行,且确认then返回的promise的状态。
    #triggerPromiseReactions = () => {
        const reaction = this.reaction
        this.reaction = []
        const handlerKeyMap = {
            [Fulfilled]: 'onFulfilled',
            [Rejected]: 'onRejected',
        }
        const handlerKey = handlerKeyMap[this.status]
        
         // 发布:将then的订阅全部执行,且确认then返回的promise的状态。
        for (let {[handlerKey]: handler, promise} of reaction) {
              // handler:根据status,取出对应订阅函数  
            // promise:then方法创建且返回的
              
           // 微任务,处理then方法的订阅
            queueMicrotask(() => {
                if (typeof handler !== 'function') {
                    // then没有订阅函数。 eg: then(null,null)       
                    // 确认promise的状态和值,通过 this的状态和值,
                    switch (this.status) {
                        case Fulfilled:
                            return promise.#resolve(this.value)
                        case Rejected:
                            return promise.#reject(this.value)
                    }
                } else {
                    // then有订阅函数。eg: then(()=>1))
                    // 确认promise的状态和值,通过 执行订阅函数
                    try {
                        let result = handler(this.value)
                        return promise.#resolve(result)
                    } catch (e) {
                        return promise.#reject(e)
                    }
                }
            })
        }
    }
    
    static resolve(v) {
        // ecma规定,Promise.resolve传入promise,直接返回.
        if (v instanceof MyPromise) { return v }
        return new MyPromise((resolve) => resolve(v))
    }
    
    static reject(reason) {
        return new MyPromise((_, reject) => reject(reason))
    }
}

const Pending = 'Pending'
const Fulfilled = 'Fulfilled'
const Rejected = 'Rejected'
// 只能执行一个
function callOnes(a, b) {
    let canCalled = true
    function warp(fn) {
        return (...args) => {
            if (canCalled) {
                fn(...args)
            }
            canCalled = false
        }
    }
    return [warp(a), warp(b)]
}
// 判断object
const isObject = v => v !== null && typeof v === 'object' || typeof v === 'function'

为什么没有 catch 等其它方法?

因为Promise A+ 只规定有then方法。 而且其余方法都是包装了then方法。 举例:catch

class MyPromise {
  catch(onReject) {
    return this.then(null, onReject)
  }
}

Promise A+ 测试

github地址,github.com/promises-ap… 配置package.json

{
  "name": "promise",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "devDependencies": {
    "promises-aplus-tests": "*"
  },
  "scripts": {
    "test": "promises-aplus-tests src/promise.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

开始测试

npm run test

完美通过 1641305745406-e921a58f-ac3b-4c81-89a4-d9db6daa84d4.png

执行时间是否与V8相同

找了几道题来测试下。 你可以将MyPromise替换为Promise,输出是一致的。

 MyPromise.resolve()
   .then(() => {
   console.log(0);
   throw Promise.resolve(4);
 })
   .catch((res) => console.log(res))
   .then(() => console.log(7))

MyPromise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2))
  .then(() => console.log(3))
  .then(() => console.log(5))
  .then(() => console.log(6))
//输出:0 1 2 3 4 5 6
let p1 = MyPromise.resolve()
  .then(v => console.log(1))
  .then(v => console.log(2))
  .then(v => console.log(3))

p1.then(v => console.log(4))
p1.then(v => console.log(5))

let p2 = MyPromise.resolve()
  .then(v => console.log(11))
  .then(v => console.log(22))
  .then(v => console.log(33))

p2.then(v => console.log(44))
p2.then(v => console.log(55))
//输出:1 11 2 22 3 33 4 5 44 55

总结

我们的MyPromise完成了以下要求:

  • 通过了 Promise A+ 测试。
  • 微任务执行时机 与V8 一致。

代码放到github上了,地址 github.com/liu-zhi-fei…

参考资料