手写 Promise 并描述其原理与实现

发布于 2023-12-31 08:53:03 字数 10396 浏览 53 评论 0

Promise 核心

  • Promise 概括来说是对异步的执行结果的描述对象。(这句话的理解很重要)
  • Promise 规范中规定了,promise 的状态只有 3 种:
    • pending
    • fulfilled
    • rejected
      Promise 的状态一旦改变则不会再改变。
  • Promise 规范中还规定了 Promise 中必须有 then 方法,这个方法也是实现异步的链式操作的基本。

ES6 Promise 细节

  • Promise 构造器中必须传入函数,否则会抛出错误。(没有执行器还怎么做异步操作。。。)
  • Promise.prototype 上的 catch(onrejected) 方法是 then(null,onrejected) 的别名,并且会处理链之前的任何的 reject。
  • Promise.prototype 上的 then 和 catch 方法总会返回一个全新的 Promise 对象。
  • 如果传入构造器的函数中抛出了错误,该 promise 对象的[[PromiseStatus]]会赋值为 rejected,并且[[PromiseValue]]赋值为 Error 对象。
  • then 中的回调如果抛出错误,返回的 promise 对象的[[PromiseStatus]]会赋值为 rejected,并且[[PromiseValue]]赋值为 Error 对象。
  • then 中的回调返回值会影响 then 返回的 promise 对象。(下文会具体分析)

动手实现

做了上面的铺垫,实现一个 Promise 的思路就清晰很多了,本文使用 ES6 来进行实现,
暂且把这个类取名为 GPromise 吧(不覆盖原生的,便于和原生进行对比测试)。
下文中 GPromise 代指将要实现的类,Promise 代指 ES6 中的 Promise 类。

内部属性

在浏览器中打印出一个 Promise 实例会发现其中会包括两用”[[ ]]”包裹起来的属性,这是系统内部属性,只有 JS 引擎能够访问。

[[PromiseStatus]]
[[PromiseValue]]

以上两个属性分别是 Promise 对象的状态和最终值。

我们自己不能实现内部属性,JS 中私有属性特性(#修饰符现在还是提案)暂时也没有支持,
所以暂且用”_”前缀规定私有属性,这样就模拟了 Promise 中的两个内部属性。

class GPromise {
        constructor(executor) {
            this._promiseStatus = GPromise.PENDING;
            this._promiseValue;
            this.execute(executor);
        }

        execute(executor){
            //...
        }

        then(onfulfilled, onrejected){
            //...
        }
    }

GPromise.PENDING = 'pedding';
GPromise.FULFILLED = 'resolved';
GPromise.REJECTED = 'rejected';

执行器

  • 传入构造器的 executor 为函数,并且在构造时就会执行。
  • 我们给 executor 中传入 resolve 和 reject 参数,这两个参数都是函数,用于改变改变 _promiseStatus 和 _promiseValue 的值。
  • 并且内部做了捕获异常的操作,一旦传入的 executor 函数执行抛出错误,GPromise 实例会变成 rejected 状态,
    即 _promiseStatus 赋值为’rejected’,并且 _promiseValue 赋值为 Error 对象。
execute(executor) {
    if (typeof executor != 'function') {
        throw new Error(` GPromise resolver ${executor} is not a function`);
    }
    //捕获错误
    try {
        executor(data => {
            this.promiseStatus = GPromise.FULFILLED;
            this.promiseValue = data;
        }, data => {
            this.promiseStatus = GPromise.REJECTED;
            this.promiseValue = data; 
        });
    } catch (e) {
        this.promiseStatus = GPromise.REJECTED;
        this.promiseValue = e;
    }
}

then 方法

异步实现

then 方法内部逻辑稍微复杂点,并且有一点一定一定一定要注意到: then 方法中的回调是异步执行的,思考下下段代码:

console.log(1);
new Promise((resolve,reject)=>{
    console.log(2);
    resolve();
})
.then(()=>console.log(3));
console.log(4);

执行结果是什么呢?答案其实是:1 2 4 3。

then 方法中的难点就是处理异步,其中一个方案是通过 setInterval 来监听 GPromise 对象的状态改变,
一旦改变则执行相应 then 中相应的回调函数(onfulfilled 和 onrejected),这样回调函数就能够插入事件队列末尾,
异步执行,实验证明可行,这种方案是最直观也最容易理解的。

then 返回值

then 方法的返回值是一个新的 GPromise 对象,并且这个对象的状态和 then 中的回调返回值相关,回调指代传入的 onfulfilled 和 rejected。

  1. 如果 then 中的回调抛出了错误,返回的 GPromise 的 _promiseStatus 赋值为’rejected’, _promiseValue 赋值为抛出的错误对象。
  2. 如果回调返回了一个非 GPromise 对象, then 返回的 GPromise 的 _promiseStatus 赋值为’resolved’, _promiseValue 赋值为回调的返回值。
  3. 如果回调返回了一个 GPromise 对象,then 返回的 GPromise 对象 的_promiseStatus 和 _promiseValue 和其保持同步。也就是 then 返回的 GPromise 记录了回调返回的状态和值,不是直接返回回调的返回值。

具体代码

then(onfulfilled, onrejected) {
        let _ref = null,
            timer = null,
            result = new GPromise(() => {});

        //因为 promise 的 executor 是异步操作,需要监听 promise 对象状态变化,并且不能阻塞线程
        timer = setInterval(() => {
            if ((typeof onfulfilled == 'function' && this._promiseStatus == GPromise.FULFILLED) ||
                (typeof onrejected == 'function' && this._promiseStatus == GPromise.REJECTED)) {
                //状态发生变化,取消监听
                clearInterval(timer);
                //捕获传入 then 中的回调的错误,交给 then 返回的 promise 处理
                try {
                    if (this._promiseStatus == GPromise.FULFILLED) {
                        _ref = onfulfilled(this._promiseValue);
                    } else {
                        _ref = onrejected(this._promiseValue);
                    }

                    //根据回调的返回值来决定 then 返回的 GPromise 实例的状态
                    if (_ref instanceof GPromise) {
                        //如果回调函数中返回的是 GPromise 实例,那么需要监听其状态变化,返回新实例的状态是根据其变化相应的
                        timer = setInterval(()=>{
                            if (_ref._promiseStatus == GPromise.FULFILLED ||
                                _ref._promiseStatus == GPromise.REJECTED) {
                                clearInterval(timer);
                                result._promiseValue = _ref._promiseValue;
                                result._promiseStatus = _ref._promiseStatus;
                            }
                        },0);

                    } else {
                        //如果返回的是非 GPromise 实例
                        result._promiseValue = _ref;
                        result._promiseStatus = GPromise.FULFILLED;
                    }
                } catch (e) {
                    //回调中抛出错误的情况
                    result._promiseStatus = GPromise.REJECTED;
                    result._promiseValue = e;
                }
            }
        }, 0);
        //promise 之所以能够链式操作,因为返回了 GPromise 对象
        return result;
    }

七段经典的 Promise

Promise 的 then 的 注册微任务队列 和 执行 是分离的。
注册 : 是完全遵循 JS 和 Promise 的代码的执行过程。
执行 : 先 同步,再 微任务 ,再 宏观任务。

demo1

/**
 * promise 是可连续执行的?
 * 是可以的!
 */

new Promise((resolve, reject) => {
  console.log(1);
  // return reject();
  return resolve();
})
    .then(() => {
      console.log(2);
    })
    .then(()=> {
      console.log(3);
    })
    .then(()=> {
      console.log(4);
    })
    .catch(()=> {
      console.log('catch');
    })
    .finally(()=> {
      console.log('finally');
    });

demo2

new Promise((resolve, reject) => {
  console.log(1);
  return resolve()
}).then(() => {
  console.log(2);
  // 外部第一个 then 方法里面 return 一个 Promise,这个 return ,代表 外部的第二个 then 的执行需要等待 return 之后的结果。
  return new Promise((resolve) => {
    console.log(3);

    return resolve()
  })
      .then(() => {
        console.log(4);
      })
      .then(() => {
        console.log(5);
      })
}).then(() => {
  console.log(6);
}).then(() => {
  console.log(7);
});

demo3

// 我们核心要看 then 的回调函数是啥时候注册的,我们知道,事件机制是 “先注册先执行”,
// 即数据结构中的 “栈” 的模式,first in first out。那么重点我们来看下他们谁先注册的。

// 外部的第二个 then 的注册,需要等待 外部的第一个 then 的同步代码执行完成。
// 当执行内部的 new Promise 的时候,然后碰到 resolve,resolve 执行完成,
// 代表此时的该 Promise 状态已经扭转,之后开始内部的第一个 .then 的微任务的注册,此时同步执行完成。
new Promise((resolve) => {
  console.log(1);
  return resolve()
}).then(() => {
  console.log(2);
  // 内部的 resolve 之后,当然是先执行内部的 new Promise 的第一个 then 的注册,这个 new Promise 执行完成,立即同步执行了后面的 .then 的注册。
  new Promise((resolve) => {
    console.log(3);
    return resolve()
  })
      .then(() => {
        console.log(4);
      })
      // 然而这个内部的第二个 then 是需要第一个 then 的的执行完成来决定的,而第一个 then 的回调是没有执行,仅仅只是执行了同步的 .then 方法的注册,所以会进入等待状态。
      .then(() => {
        console.log(5);
      })
      .then(()=> {
        console.log(6);
      })
}).then(() => {
  // 外部的第一个 then 的同步操作已经完成了,
  // 然后开始注册外部的第二个 then,此时外部的同步任务也都完成了。
  // 外部第二个 then 完成之后, 进入等待, 内部的第二个 then 注册之后在执行
  console.log(7);
}).then(() => {
  console.log(8);
}).then(()=> {
  console.log(9);
});

demo4

/**
 * 链式调用的注册是前后依赖的 比如上面的外部的第二个 then 的注册,是需要外部的第一个的 then 的执行完成。
 *
 * 变量定义的方式,注册都是同步的 比如这里的 p.then 和 var p = new Promise 都是同步执行的。
 */
new Promise(resolve=> {
  console.log('1');
  resolve();
})
  .then(()=> {
    console.log(2);
    const p = new Promise(resove=> {
      console.log(3);
      resove();
    });

    p.then(()=> {
      console.log(4);
    });

    p.then(()=> {
      console.log(5);
    });
  })
  .then(()=> {
    console.log(6)
  })
  .then(()=> {
    console.log(7)
  });

demo5

/**
 * 这段代码中,外部的注册采用了非链式调用的写法,根据上面的讲解,
 * 我们知道了外部代码的 p.then 是并列同步注册的。
 * 所以代码在内部的 new Promise 执行完,p.then 就都同步注册完了。
 *
 * 内部的第一个 then 注册之后,
 * 就开始执行外部的第二个 then 了(外部的第二个 then 和 外部的第一个 then 都是同步注册完了)。
 * 然后再依次执行内部的第一个 then ,内部的第二个 then。
 * @type {Promise}
 */
const p = new Promise(resolve => {
  console.log(1);
  resolve()
});

p.then(() => {
  console.log(2);
  new Promise(resolve => {
    console.log(3);
    resolve();
  })
    .then(() => {
      console.log(4);
    })
    .then(() => {
      console.log(5);
    })
});

p.then(() => {
  console.log(6);
});

p.then(() => {
  console.log(7)
});

demo6

new Promise(resolve => {
  console.log(1);
  resolve();
})
  .then(() => {
    console.log(2);
    new Promise(resolve => {
      console.log(3);
      resolve();
    })
      .then(() => {
        console.log(4);
      })
      .then(() => {
        console.log(5);
      });

    return new Promise(resolve => {
      console.log(6);
      resolve();
    })
      .then(() => {
        console.log(7);
      })
      .then(() => {
        console.log(8);
      })
  })
  .then(() => {
    console.log(9);
  })
  .then(() => {
    console.log(10);
  });

demo7

new Promise((resolve, reject) => {
  console.log('外部 promise');
  resolve();
})
  .then(() => {
    console.log('外部第一个 then');
    new Promise((resolve, reject) => {
      console.log('内部 promise');
      resolve();
    })
      .then(() => {
        console.log('内部第一个 then');
        return Promise.resolve();
      })
      .then(() => {
        console.log('内部第二个 then');
      })
  })
  .then(() => {
    console.log('外部第二个 then');
  })
  .then(() => {
    console.log('外部第三个 then');
  })

参考文章

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
1130 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文