手写Promise: 揭秘JavaScript异步编程的核心

引言

在 JavaScript 的世界里,异步编程是处理复杂应用逻辑时不可或缺的部分,而 Promise 无疑是这一领域的核心角色。它巧妙地解决了回调地狱这一棘手难题,为异步操作提供了一种既优雅又强大的解决方案。然而,你是否曾好奇,我们日常频繁使用的 Promise 究竟是如何在幕后运作的呢?它背后的实现机制又是怎样的呢?本文将带你踏上一段探索之旅,从零开始手写一个 Promise 的实现,一步步揭开它神秘的面纱。通过这个过程,你不仅能深入理解 Promise 的工作原理,还能领悟其设计哲学,从而在实际开发中运用起来更加得心应手。

Promises/A+ 规范概述

Promises/A+ 规范为 Promise 的行为提供了统一的标准,了解它是深入理解 Promise 实现的基础。

Promises/A+ 具有以下几个关键特性

  1. 初始化异步操作 :当创建一个 Promise 时,需要传入一个 executor 函数。这个函数接收 resolvereject 两个参数,它们分别用于在异步操作成功或失败时改变 Promise 的状态。例如:

    ts 复制代码
    const likePromise = new LikePromise((resolve, reject) => {
      // 这里可以开始异步操作,如发起网络请求或读取文件等;
      // 当操作成功时,调用 resolve(result);
      // 当操作失败时,调用 reject(error);
    });
  2. Promise的三种状态 :一个 Promise 在其生命周期内只能处于三种状态之一:等待态(Pending)、完成态(Fulfilled)或拒绝态(Rejected)。初始时 Promise 处于等待态,当异步操作成功完成后,通过 resolve 函数将其状态转变为完成态,并传递成功结果;若异步操作失败,则通过 reject 函数将状态转变为拒绝态,并传递失败原因。

  3. then方法的规范行为 :Promise 必须提供一个 then 方法,它允许为 Promise 的成功和失败状态分别指定处理函数。then 方法的链式调用以及错误处理都有着严格的规范要求,这使得我们能够以一种条理清晰、连贯有序的方式去处理异步操作最终的结果和可能出现的错误。

深入剖析 Promise 创建时的 executor 函数

executor 函数的执行时机

executor 函数是同步执行的,这一点需要特别注意

ts 复制代码
const p = new LikePromise((resolve, reject) => {
  console.log("likePromise-executor");
});
console.log("end");
// 输出:
// "likePromise-executor"
// "end"

从输出结果可以清晰地看到,executor 函数内的代码会先于后续的 console.log("end") 执行,这体现了它同步执行的特性。

executor 函数中的错误处理

1.同步错误处理executor 函数在同步执行过程中出现错误,Promise 会捕获该错误,并将自身状态修改为拒绝态(Rejected)。

ts 复制代码
const p = new LikePromise((resolve, reject)=>{
    throw new Error("出错了");
}).then(undefined, (res)=>{
    console.log(res)
})
console.log("end");

// 输出:
// "end"
// "出错了"

在上述代码中,executor 函数内直接抛出了一个错误,此时 Promise 会捕获这个错误,将状态转变为拒绝态,后续通过 then 方法的第二个参数(处理拒绝态的回调函数)就可以获取到这个错误信息并进行相应处理。

2.异步错误处理executor 函数中的异步操作 出现错误(如在 setTimeout 回调中抛出错误),Promise 本身并不会直接捕获该异步错误。这是因为在异步操作发生错误时,无法确定何时应该处理该错误,所以需要在异步操作的回调函数中自行处理并调用 reject 函数来改变 Promise 的状态。

ts 复制代码
const p = new LikePromise((resolve, reject)=>{
    setTimeout(()=>{
        try {
            throw new Error("出错了");
        } catch(error){
            reject(error);
        }
    })
}).then(undefined, (res)=>{
    console.log(res)
})
console.log("end");

在这个例子里,我们在 setTimeout 的回调函数中手动捕获错误,并通过 reject 函数将错误传递出去,以便后续通过 then 方法来处理这个拒绝态的情况。

Promise 状态流转&最终值的设置

状态流转规则

Promise 的状态流转有着严格的规则,只能在 resolvereject 函数中进行操作,并且状态一旦发生改变,就是不可逆的。也就是说,一旦从等待态(Pending)转变为完成态(Fulfilled)或者拒绝态(Rejected),就再也不能切换回其他状态了。

ts 复制代码
const p = new LikePromise((resolve, reject) => {
  resolve(1);
  // 后续再次调用 resolve 或 reject 都不会改变已经确定的状态
  reject(2) // 这个是无效的,无法再次改变状态
});

在这里,当调用了 resolve(1) 后,Promise 的状态已经变为完成态,后续再调用 reject(2) 是不会起作用的,状态不会再发生改变。

最终值的设置

Promise 状态转变为完成态时,通过 resolve 函数传递的值将会成为这个 Promise 的最终值;而当状态转变为拒绝态时,通过 reject 函数传递的原因就会作为最终值。

ts 复制代码
const p = new LikePromise((resolve, reject) => {
  resolve("成功结果");
});

p.then(result => {
  console.log(result); // 输出:"成功结果"
});
// ---
const p1 = new LikePromise((resolve, reject) => {
  reject("拒绝原因");
});

p1.then(undefined, reason => {
  console.log(reason); // 输出:"拒绝原因"
});

Promise 的 then 方法的实现

then 方法的链式调用原理

then 方法之所以能够实现链式调用,关键在于它返回一个新的 Promise 实例。如果直接返回 this,由于 Promise 的状态一旦改变就不可逆转,后续链式调用的 then 方法将无法正确处理不同的状态。

ts 复制代码
class LikePromise {
  constructor(executor) {
    // 初始化代码
  }

  then(onFulfilled, onRejected) {
    // 错误的返回方式,不能直接返回 this
    // return this;
    // 正确的返回方式
    return new LikePromise((resolve, reject)=>{
        // TODO
    })
  }
}

then 方法中回调函数的执行时机

then 方法的回调函数应该在 Promise 状态确定后异步执行 。为了确保这一点,我们可以借助微任务队列(如 queueMicrotaskprocess.nextTick 或通过 MutationObserver 模拟)来确保回调函数在当前宏任务执行完后、下一个宏任务开始前执行。

ts 复制代码
function isPromiseA(value){
    return value && typeof value.then === 'function';
}
function microTask(fn) {
    // Promise.resolve().then(fn); 原生Promise.then包装是异步任务
    if (typeof queueMicrotask === 'function') {
        queueMicrotask(fn);
    } else if(typeof process === 'object' && typeof process.nextTick === 'function') {
        // Node 环境下
        process.nextTick(fn);
    } else if(typeof MutationObserver === 'function') {
        const textNode = document.createTextNode('');
        const observer = new MutationObserver(fn);
        observer.observe(textNode, { characterData: true });
        textNode.data = 'promise';
    } else {
        setTimeout(fn, 0);
    }
}
class LikePromise {
    constructor(executor) {
        // 初始化代码
    }
    then(onFulfilled, onRejected) {
        return new LikePromise((resolve, reject)=>{
           this.handlers.push(microTask(()=>{
               try {
                   const cb = this.state === STATE.FULFILLED ? onFulfilled : onRejected;
                   if (cb && typeof cb === "function") {
                       const data = cb(this.result);
                       if (isPromiseA(data)) {
                           data.then(resolve, reject);
                       } else {
                           resolve(data);
                       }
                   }else{
                       resolve(this.result);
                   }
               } catch (error) {
                   reject(error);
               }
           }))
        })
    }
}

在上述代码中,通过 microTask 函数将回调函数放入微任务队列,保证了其异步执行的特性,并且在回调函数内部对返回值进行了相应处理,以符合 Promise 的规范要求。

then 方法对回调函数的具体处理

1.默认值处理 若没传 onFulfilledonRejected 回调,就会默认获取之前 Promise 的最终值并传给下一个 then方法。

ts 复制代码
const p = new LikePromise((resolve, reject) => {
  resolve("成功结果");
}).then().then();

p.then(result => {
  console.log(result); // 输出:"成功结果"
});

从这个例子可以看到,即使中间的 then 方法没有传递具体的回调,依然可以正确获取到最初 Promise 的成功结果,这就是默认值处理机制在起作用。

2.函数返回值处理 当传入的 onFulfilledonRejected 是函数时,会在 Promise 状态确定后被调用。调用后要判断返回值:如果是符合 Promises/A+ 规范的对象(有 then 方法),就调用它的 then 方法并传入新 Promiseresolvereject,以便处理其状态流转;若不是,就直接用新 Promise 的 resolve 传递返回值,让后续 then 能接着处理。

ts 复制代码
const p = new LikePromise((resolve, reject) => {
  resolve("成功结果");
}).then((res)=>{
    return new LikePromise((resolve, reject)=>{
        resolve("then-返回Promise")
    })
}).then();

p.then(result => {
  console.log(result); // 输出:"then-返回Promise"
});

在这段代码中,第一个 then 方法返回了一个新的 Promise,后续的 then 方法能够正确获取到这个新 Promise 中传递的值,体现了对函数返回值的正确处理方式。

LikePromise 源码

ECMAScript 6 Promise 静态方法与实例方法

catch 实现

catch 方法实际上是Promise.prototype.then(undefined, onRejected) 的一种简写形式

ts 复制代码
class LikePromise {
    constructor(executor) {
        // 初始化代码
    }
    catch(onRejected){
        return this.then(undefined, onRejected)
    }
}

通过这样简单的实现,我们就可以方便地使用 catch 方法来统一处理 Promise 被拒绝的情况了。

finally 实现

finally 方法对应的是 Promise.prototype.then(onFinally, onFinally)

  1. onFinally 不接受参数
  2. finally 方法是不修改 Promise 状态的
ts 复制代码
class LikePromise {
    constructor(executor) {
        // 初始化代码
    }
    finally(onFinally){
        return this.then((value)=>{
            onFinally();
            return value;
        }, (reason)=>{
            onFinally();
            // 如果这里直接return 那么会修改状态为完成
            // return reason;
            // 通过抛出异常来保持状态不变
            throw reason;
        })
    }
}

通过上述代码实现,finally 方法就能在无论 Promise 成功还是失败的情况下,都执行指定的最终操作,同时不改变原有的状态。

resolve 实现

resolve 方法的描述
resolve 方法的实现遵循以下逻辑:

  1. 首先判断传入的参数是否是 Promise,如果是,直接返回该 Promise
  2. 接着判断参数是否是 isPromiseA(也就是 thenable 对象,即具有 then 方法的对象),若是,则调用其 then 方法并传入resolvereject两个回调函数。
  3. 最后,如果都不符合上述情况,就使用 resolve 函数进行包裹返回。
ts 复制代码
class LikePromise {
    constructor(executor) {
        // 初始化代码
    }
    static resolve(value?: any) {
        if(value instanceof Promise) return value;
        return new Promise((resolve, reject) => {
            if (isPromiseA(value)) {
                value.then(resolve, reject);
            }else{
                resolve(value);
            }
        })
    }
}

reject 实现

reject 方法的实现相对比较简单,它的作用就是返回一个新的 Promise,并将传入的参数作为拒绝的原因。代码如下:

ts 复制代码
class LikePromise {
    constructor(executor) {
        // 初始化代码
    }
    static reject(reason?: any) {
        return new Promise((_, reject) => {
            reject(reason);
        })
    }
}

all 实现

all 方法用于处理多个 Promise 实例,它会等待所有传入的 Promise 都完成后,将它们各自的结果组成一个数组返回,如果其中有任何一个 Promise 被拒绝,那么整个 all 方法返回的 Promise 就会立即被拒绝,并传递出第一个被拒绝的 Promise 的原因。

ts 复制代码
class LikePromise {
    constructor(executor) {
        // 初始化代码
    }
    static all(promises: Promise<any>[]) {
        return new Promise((resolve, reject) => {
            let count = 0;
            const results: any[] = [];
            for (let i = 0; i < promises.length; i++) {
                promises[i].then((value) => {
                    results[i] = value;
                    count++;
                    if (count === promises.length) {
                        resolve(results);
                    }
                }, (reason)=>{
                    reject(reason);
                });
            }
        })
    }
}

感谢阅读,敬请斧正!

相关推荐
Jay_帅小伙10 分钟前
前端编辑器JSON HTML等,vue2-ace-editor,vue3-ace-editor
前端·编辑器·json
小小怪_下士23 分钟前
Vue3:el-table组件存在多列操作数据,页面渲染导致进入页面卡顿问题优化。
前端·javascript·vue.js
小马超会养兔子29 分钟前
如何画一个网格
前端·vue
m0_7482309436 分钟前
ctfshow-web入门-爆破(web21-web24)
前端·数据库
横冲直撞de1 小时前
EventSource和WebSocket用法
前端·javascript·网络·websocket·网络协议·vue
m0_748254661 小时前
悬赏任务源码(悬赏发布web+APP+小程序)开发附源码
前端·小程序
大地爱1 小时前
日志平台--graylog-web配置、接入微服务日志
前端·微服务·graylog
codecodegirl2 小时前
ios h5中在fixed元素中的input被focus时,键盘遮挡input (van-popup、van-feild)
前端·vue.js·html5
Li3702349402 小时前
在react中使用组件的标签页写订单管理页面
前端·javascript