手撕代码系列之 - Promise | 青训营笔记

91 阅读7分钟

笔记创作活动的第 10 天。

前言

相信大家对Promise一定不陌生,在日常开发中会经常遇到它,如果对Promise还不是很了解的小伙伴可以看一下 详解ES6之Promise对象 这篇文章,接下来让我们从零入手一步步实现一个简版的Promise吧!

首先,我们先来看一下平常是如何使用Promise的:

let p = new Promise((resolve, reject) => {
    if(成功){
        resolve(数据);
    } else{
        reject(原因);
    }
})

p.then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
})

需求:

  1. 实现resolvereject
  2. 状态一旦确定则不可变
  3. 实现then方法
  4. 解决定时器、请求等异步改变状态问题
  5. 实现then链式调用
  6. 模拟then的微任务特性
  7. 实现all race any等静态方法

一、resolve和reject

实现resolvereject

class MyPromise {
    constructor(callback) {
        // 初始化
        this.init();
        // 执行传进来的回调函数
        callback(this.resolve, this.reject);
    }

    init() {
        this.PromiseResult = null; // 结果值
        this.PromiseState = "pending"; // 状态
        // 绑定上下文
        this.resolve = this.resolve.bind(this);
        this.reject = this.reject.bind(this);
    }

    resolve(value) {
        // 修改状态
        this.PromiseState = "fulfilled";
        // 给 PromiseResult 赋值
        this.PromiseResult = value;
    }

    reject(reason) {
        this.PromiseState = "rejected";
        this.PromiseResult = reason;
    }
}

在初始化时有个很重要的点,就是给resolvereject函数绑定this上下文,目的是为了使它俩的this指向永远指向当前MyPromise的实例对象,避免随着函数执行环境的改变而改变。

使用try-catch捕获错误

constructor(callback) {
    // 初始化
    this.init();

    try {
        // 执行传进来的回调函数
        callback(this.resolve, this.reject);
    } catch (e) {
        // 捕捉到错误直接执行reject
        this.reject(e);
    }
}

我们来测试一下是否可用:

const mp = new MyPromise((resolve, reject) => {
    resolve("成功");
    reject("失败");
});
console.log(mp); // MyPromise { PromiseState: "rejected", PromiseResult: "失败" }

上面这段代码乍一看没啥问题,仔细思考一下!Promise的状态一旦被确定就不可以改变,上面已经resolve了却还是被reject改变了结果。接下来我们解决一下这个问题。

状态不可变

我们只需要短短的一行代码就能解决!

resolve(value) {
    // 判断 PromiseState 是否被修改,若是则直接 return
    if (this.PromiseState !== "pending") return; // 新增
    
    this.PromiseState = "fulfilled";
    this.PromiseResult = value;
}
reject(reason){
    // 判断 PromiseState 是否被修改,若是则直接 return
    if (this.PromiseState !== "pending") return; // 新增
    
    this.PromiseState = "rejected";
    this.PromiseResult = reason;
}

我们再来看看效果:

const mp = new MyPromise((resolve, reject) => {
    resolve("成功");
    reject("失败");
});
console.log(mp); // MyPromise { PromiseState: "fulfilled", PromiseResult: "成功" }

二、then方法

我们先来看看Promisethen怎么使用:

const p1 = new Promise((resolve, reject) => {
    resolve('成功')
})

p1.then(
    res => console.log(res), // 立即输出'成功'
    err => console.log(err)
)
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('失败')
    }, 3000)
})

p2.then(
    res => console.log(res), 
    err => console.log(err)  // 3秒后输出'失败'
)
const p3 = new Promise((resolve, reject) => {
    resolve(5)
})

p3.then(res => 2 * res)
  .then(res => console.log(res)) // 链式调用,输出 10

重点:

  1. then可以接受两个回调函数,分别是成功时执行的回调(对应fulfilled状态)和失败时执行的回调(对应fulfilled状态)。
  2. resolvereject在定时器等异步回调里,则定时器结束后再执行then
  3. then支持链式调用,后面的then的执行受上一次then返回值的影响。

实现then方法

then(onFulfilled, onRejected) {
    // 接收两个回调函数 onFulfilled, onRejected

    // 参数校验,确保一定是函数
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (val) => val;
    onRejected = typeof onRejected === "function" ? onRejected : (reason) => { throw reason };

    if (this.PromiseState === "fulfilled") {
        // 如果当前为成功状态,执行第一个回调
        onFulfilled(this.PromiseResult);
    } else if (this.PromiseState === "rejected") {
        // 如果当前为失败状态,执行第二个回调
        onRejected(this.PromiseResult);
    }
}

实现定时器等异步情况的处理

普通情况下的then我们已经实现了,但在定时器等异步状况下的,我们还有待处理。看个例子:

const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('success')
    }, 3000)
}).then(
    res => console.log(res)
)

我们期望达到的效果是,3秒后控制台输出 success。但是结合我们写的then函数来看,我们没有办法控制then函数的延迟执行,但是then函数执行它的两个回调主要是依靠判断PromiseState状态,那么我们就可以控制then函数执行它两个回调的时机(即暂时将then函数的参数保存起来,等定时器结束,PromiseState状态改变,再去调用回调函数)。

具体我们来看代码实现。

init(){
    this.PromiseResult = null; // 结果值
    this.PromiseState = "pending"; // 状态
    // 绑定上下文
    this.resolve = this.resolve.bind(this);
    this.reject = this.reject.bind(this);
    
    // 新增
    this.onFulfilledCallbacks = []; // 保存成功回调
    this.onRejectedCallbacks = []; // 保存失败回调
}

resolve(value) {
    if (this.PromiseState !== "pending") return;
    this.PromiseState = "fulfilled";
    this.PromiseResult = value;

    // 新增 - 执行保存的成功回调
    while (this.onFulfilledCallbacks.length) {
        this.onFulfilledCallbacks.shift()(this.PromiseResult);
    }
}

reject(reason) {
    if (this.PromiseState !== "pending") return;
    this.PromiseState = "rejected";
    this.PromiseResult = reason;

    // 新增 - 执行保存的失败回调
    while (this.onRejectedCallbacks.length) {
        this.onRejectedCallbacks.shift()(this.PromiseResult);
    }
}

then(onFulfilled, onRejected) {
    // ...省略
    // 新增
    if (this.PromiseState === "fulfilled") {
        // 如果当前为成功状态,执行第一个回调
        onFulfilled(this.PromiseResult);
    } else if (this.PromiseState === "rejected") {
        // 如果当前为失败状态,执行第二个回调
        onRejected(this.PromiseResult);
    } else if (this.PromiseState === "pending") {
        // 如果状态为待定状态,暂时保存两个回调
        this.onFulfilledCallbacks.push(onFulfilled.bind(this));
        this.onRejectedCallbacks.push(onRejected.bind(this));
    }
}

整理一下思路:

  1. 为什么使用数组保存then的回调?因为一个Promise实例可以调用多次then函数,所以有可能有多个回调。
  2. 使用了定时器等异步操作的Promise实例的then的回调不在then函数里面执行?因为PromiseState状态没有立即改变,我们需要通过状态来判断执行then的哪个回调,而状态的改变正是在resolvereject里面进行修改的。

我们来试试是否可用:

const mp = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('success')
    }, 3000)
}).then(
    res => console.log(res) // 3秒后输出success
)

实现链式调用

重点:

  • 下一次then的执行,受上一次then的执行情况的影响,实参是上一次then的返回值。
  • then方法本身会返回一个新的Promise对象。
  • 如果then的回调的返回值是一个Promise对象,则then返回的Promise会取决于回调的Promiseresolvereject
  • 如果then的回调的返回值不是一个Promise对象,则then返回的Promise的状态是fulfilled,值是那个返回值。(即Promise.resolve(value)

看个例子(搞懂这个例子就能明白上面四点):

let p = new Promise((resolve, reject) => {
    reject(2)
}) // p 是一个Promise的实例对象,它的状态是 rejected ,它的值是 2

// 执行 p.then 
let pt1 = p.then(
    res => { console.log(res) }, // 这里不会执行,因为p的状态是rejected
    err => {
        // then的失败回调,err 参数是 p 的值
        // 回调函数返回一个Promise实例,这个实例又reject了,值是 2 * 2 = 4
        return new Promise((resolve, reject) => {
            reject(2 * err)
        })
    }
) // pt1 是 p.then 返回的新的Promise实例对象,它的状态是 rejected ,它的值是 4
console.log(pt1)

// 因为pt1也是Promise实例,所以它也可以调用then,执行 pt1.then
let pt2 = pt1.then(
    res => { console.log(res) }, // 这里不会执行,因为pt1的状态是rejected
    err => {
        // 回调函数返回一个number类型的数值
        // 即便它是在失败回调里返回的,但是由于返回的是非Promise对象,所以pt1.then的返回值相当于Promise.resolve(2 * err)
        return 2 * err
    }
) // pt2 是 pt1.then 返回的新的Promise实例对象,它的状态是 fulfilled ,它的值是 8
console.log(pt2)
复制代码

理解了链式调用的基本规则后,我们来实现一下:

then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }

    let thenPromise = new MyPromise((resolve, reject) => {
        const handleCallback = cb => {
            try {
                // 执行then的回调函数
                const res = cb(this.PromiseResult) // 拿到then的回调函数的返回值
                
                if (res instanceof MyPromise) {
                    // 如果返回值是MyPromise实例对象,则我们也需要执行这个实例对象的then
                    res.then(resolve, reject)
                } else {
                    // 如果返回值不是MyPromise实例对象,就直接resolve
                    resolve(res)
                }
            } catch (err) {
                // 出现错误则直接reject,并再往控制台抛出一个错误
                reject(err)
                throw new Error(err)
            }
        }

        if (this.PromiseState === 'fulfilled') {
            handleCallback(onFulfilled)
        } else if (this.PromiseState === 'rejected') {
            handleCallback(onRejected)
        } else if (this.PromiseState === 'pending') {
            this.onFulfilledCallbacks.push(handleCallback.bind(this, onFulfilled))
            this.onRejectedCallbacks.push(handleCallback.bind(this, onRejected))
        }
    })

    // 返回这个新的MyPromise实例对象
    return thenPromise
}

浅尝一下:

const mp = new MyPromise((resolve, reject) => {
    resolve(2);
}).then((res) => res * 2)
  .then((res) => console.log("成功", res))  // 输出 '成功 4'
复制代码

现在还有一个比较细节的点需要完善,就是Promise.then是微任务,我们需要在代码中使用queueMicrotask模拟微任务的执行。

继续完善代码:

then(onFulfilled, onRejected){
    ...省略
    let thenPromise = new MyPromise((resolve, reject) => {        
        const handleCallback = (cb) => {
            // 新增
            queueMicrotask(()=>{
                try {
                    const res = cb(this.PromiseResult) 
                    if (res instanceof MyPromise) {
                        res.then(resolve, reject)
                    } else {
                        resolve(res)
                    }
                } catch (err) {
                    reject(err)
                    throw new Error(err)
                }
            })
        }
        ...省略
    }
}

来个测试代码测试一下:

setTimeout(() => {
    console.log(3);
});

const p = new MyPromise((resolve, reject) => {
    resolve(2);
}).then((res) => console.log(res));

console.log(1);
// 预期结果 1 2 3

写在最后

完整代码在Script里面。

如果文中有任何问题,还请各位大佬们指出,欢迎大家一起探讨~