Skip to content

手写Promise,promise原理解析

一. 最基础的Promise

1. 基本特点:

  1. 我们用Promise时候是通过new Promise来使用的,因此可以用class来实现
  2. new Promise时候,要传入一个executor执行器函数,这个执行器函数是立即执行的
  3. 这个执行器函数接收两个参数,resolve,reject
  4. promise有三个状态,padding,fulfilled,rejected,当调用resolve时候,会将状态置为fulfilled,当调用reject时候,会将状态置为rejected,且只有当状态为padding时候才能让改变,状态一经改变就不能再变。
  5. 调用resolve或者reject时候,可以传入成功或者失败原因,不传默认为undefined;
  6. 执行器函数内也可以抛出异常,抛出异常时候,会将promise状态置为rejected;
  7. 每个promise都有一个then方法,then方法有两个参数,onFulfilled,onRejected;
  8. 当promise状态为成功时候,会调用then方法的第一个参数,并将成功的原因传递出去
  9. 当promise状态为失败时候,会调用then方法的第二个参数,并将失败的原因传递出去

2. 代码实现

以上就是最基本的promise的原理或特点,根据这些特点,可以得到一个基本的promise骨架

js
// 4. promise有三个状态
const STATUS = {
  PADDING: "PADDING", // padding
  FULFILLED: "FULFILLED", // fulfilled
  REJECTED: "REJECTED", // rejected
};

class Promise {
  // 2. 传入一个执行器函数
  constructor(executor) {
    // 4. 默认情况下,Promise的状态就是padding;
    this.status = STATUS.PADDING;
    this.value = undefined; // 5. 成功的原因,默认undefined,为了方便在then里面取到,所以实例上放一份
    this.reason = undefined; // 5. 失败的原因,默认undefined,为了方便在then里面取到,所以实例上放一份

    // 成功回调(注意使用箭头函数,否则theis会有问题)
    const resolve = (val) => {
      // 4. 只有padding时候才能改变状态
      if (this.status === STATUS.PADDING) {
        // 4. 调用resolve时候,将状态置位成功fulfilled
        this.status = STATUS.FULFILLED;
        // 5. 调用resoove时候,接收到成功的原因val
        this.value = val;
      }
    };

    // 失败回调(注意使用箭头函数,否则theis会有问题)
    const reject = (reason) => {
      // 4. 只有padding时候才能改变状态
      if (this.status === STATUS.PADDING) {
        // 4. 调用reject时,将promise置位失败,rejected
        this.status = STATUS.REJECTED;
        // 5. 调用reject时候, 接收到失败原因reason;
        this.reason = reason;
      }
    };

    // 6.可以在执行器函数内抛出异常,这里可以用try catch来捕获异常
    try {
      // 2. 执行器函数默认就会执行,
      // 3.执行器函数接收个参数,分别是resolve,reject,都是函数
      executor(resolve, reject);
    } catch (e) {
      // 6. 捕获到异常会导致promise失败
      reject(e);
    }
  }

  // 7. then是promise的实例方法,有两个参数,onFulfilled,onRejected
  then(onFulfilled, onRejected) {
    //  8. 若是当前promise的状态是成功则调用第一个参数onFulfilled
    if (this.status === STATUS.FULFILLED) {
      onFulfilled(this.value); // 8. 将成功的原因传递出去
    }
    // 9.若是当前promise的状态是失败则调用第二个参数onRejected
    if (this.status === STATUS.REJECTED) {
      onRejected(this.reason); // 9.将失败的原因传递出去
    }
  }
}

module.exports = Promise;

3. 测试

js
// 引入自己的promise
const Promise = require("./myPromise");

let p = new Promise((resolve, reject) => {
//   throw new Error("抛出异常了");
//   reject('出错了')
  resolve('成功了')
});

p.then(
  (res) => {
    console.log("success:" + res);
  },
  (err) => {
    console.log("fail:" + err);
  }
);

本节githubCommit记录:promise基本骨架搭建

二. 改进:执行器内处理异步逻辑

1. 上一步存在的问题:

executor执行器里面不能执行异步方法,否则会得不到结果

例如:

js
// bad
const Promise = require("./myPromise");
let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("成功了");
  }, 1000);
});

p.then(
  (res) => {
    console.log("success:" + res); 
// 没有输出,因为这里的then在执行的时候,promise里面的状态既不是fulfilled,也不是rejeted,
// 但是在then里面,我们没有对其他状态做判断,因此这里不会有输出
  }
);

上面的例子,promise里面不能处理异步方法,只能处理同步。

2. 原因分析

我们在executor执行器函数内传入一个异步函数,执行器函数执行时,会将异步代码放入任务队列,等待执行,再调用then方法的时候,当前promise的状态可能还处于padding状态,但是在then方法里面,我们只处理了fulfilled和rejected状态下逻辑,因此then没有抛出结果,外面自然无法拿到结果。

3. 解决方案

  1. 发布订阅模式 executor里面传入异步方法时候,调用then方法,会进入padding状态,那么,此时可以将onFulfilled和onRejected方法缓存起来,当异步方法返回结果,会将promise状态改变,此时再根据状态将缓存起来的方法以此执行。
  2. 装饰模式,切片编程 我们在缓存onFulfilled和onRejected方法时候,可能还有其他额外逻辑,但是我们不能直接修改他们。因此可以用一个新的函数将其包装一下,并且加上额外的逻辑代码,不会改变onFulfilled和onRejected内部逻辑。

4. 代码实现

新增的发布订阅模式 image.pngimage.png

js
// 4. promise有三个状态
const STATUS = {
  PADDING: "PADDING", // padding
  FULFILLED: "FULFILLED", // fulfilled
  REJECTED: "REJECTED", // rejected
};

class Promise {
  // 2. 传入一个执行器函数
  constructor(executor) {
    // 4. 默认情况下,Promise的状态就是padding;
    this.status = STATUS.PADDING;
    this.value = undefined; // 5. 成功的原因,默认undefined,为了方便在then里面取到,所以实例上放一份
    this.reason = undefined; // 5. 失败的原因,默认undefined,为了方便在then里面取到,所以实例上放一份

    this.onFulfilledCallBack = []; // 存放成功回调的
    this.onRejectedCallBack = []; // 存放失败回调的

    // 成功回调(注意使用箭头函数,否则theis会有问题)
    const resolve = (val) => {
      // 4. 只有padding时候才能改变状态
      if (this.status === STATUS.PADDING) {
        // 4. 调用resolve时候,将状态置位成功fulfilled
        this.status = STATUS.FULFILLED;
        // 5. 调用resoove时候,接收到成功的原因val
        this.value = val;
        // 发布:调用成功时候,将所有成功的方法执行
        this.onFulfilledCallBack.forEach((fn) => fn());
      }
    };

    // 失败回调(注意使用箭头函数,否则theis会有问题)
    const reject = (reason) => {
      // 4. 只有padding时候才能改变状态
      if (this.status === STATUS.PADDING) {
        // 4. 调用reject时,将promise置位失败,rejected
        this.status = STATUS.REJECTED;
        // 5. 调用reject时候, 接收到失败原因reason;
        this.reason = reason;
        // 发布: 执行失败时候,将所有失败的函数执行
        this.onRejectedCallBack.forEach((fn) => fn());
      }
    };

    // 6.可以在执行器函数内抛出异常,这里可以用try catch来捕获异常
    try {
      // 2. 执行器函数默认就会执行,
      // 3.执行器函数接收个参数,分别是resolve,reject,都是函数
      executor(resolve, reject);
    } catch (e) {
      // 6. 捕获到异常会导致promise失败
      reject(e);
    }
  }

  // 7. then是promise的实例方法,有两个参数,onFulfilled,onRejected
  then(onFulfilled, onRejected) {
    //  8. 若是当前promise的状态是成功则调用第一个参数onFulfilled
    if (this.status === STATUS.FULFILLED) {
      onFulfilled(this.value); // 8. 将成功的原因传递出去
    }
    // 9.若是当前promise的状态是失败则调用第二个参数onRejected
    if (this.status === STATUS.REJECTED) {
      onRejected(this.reason); // 9.将失败的原因传递出去
    }
    // 订阅模式:状态为padding时候,将方法缓存起来,等待发布..
    if (this.status === STATUS.PADDING) {
      this.onFulfilledCallBack.push(() => {
        // todo..其他逻辑
        onFulfilled(this.value);
      });
      this.onRejectedCallBack.push(() => {
        // todo.. 其他逻辑
        onRejected(this.reason);
      });
    }
  }
}

module.exports = Promise;

本节githubCommit记录:exector加入可执行异步的逻辑,用了发布订阅模式和装饰模式

5. 测试

结果:两个then过两秒后同时输出结果

js
const Promise = require("./myPromise");

let p = new Promise((resolve, reject) => {
  setTimeout(() => {  // exector里面执行异步代码
    resolve("ok");
  }, 1000);
});

p.then(
  (res) => {
    console.log("success:" + res); // 过2秒后输出  success: ok
  },
  (err) => {
    console.log("fail:" + err);
  }
);

p.then(
  (res) => {
    console.log("resolve: " + res); // 过2秒后输出  resolve: ok
  },
  (err) => {
    console.log("shibai: " + err);
  }
);

三. 改进:链式调用(难点)

1. 要点

  1. 若then方法中返回的不是一个promise,无论成功(参数1)或者失败(参数2),都会将这个值传或错误递给下一次then的第一个参数成功结果内。
  2. 若是then方法中抛出异常,会将错误信息传递给下一次then的第二个参数的失败里去。
  3. 若then里面又返回了一个promise,则会将这个promise的结果作为下一次then的成功或者失败
  4. catch可以理解为就是then的别名,只有then的第二个参数.then(null,(err)=>{}
  5. 错误处理会先找then的第二个参数,若没有则往下一个继续找,找到了就不找了,不管后面还有没有then或catch

小结:只有当then里面抛出异常或返回的promise出错,才会走到下一次then的失败里去,其他都会走下一次then的成功。

2. then为什么可以链式调用?

因为每次调用then都会返回一个全新的promise,才可能实现链式调用,因为promise状态改变了就不可能再改变了。

3. 实现

3.1 第一个then返回的不是一个promise时的链式调用

then里面可以返回不是promise的数据,也可以抛出异常,对于抛出的异常,可以在下一个then的第二个参数里面取到,对于返回的是基本值或对象,则可以在下一个then的第一个参数内取到,若没有返回任何值(没有return),则下一个then里面取到的为undefined

3.1.1 链式第一步

由于promise的状态只能改变一次,且只有padding时候才能改变,因此,链式调用的关键就是每次调用then方法都得返回一个全新的promise,用Promise2表示

js
then(onFulfilled, onRejected) {
  // then返回一个promise才能链式调用,所以,每次调用then的时候都需要有一个全新的promise
  let promise2 = new Promise((resolve, reject) => {
	...原来的逻辑都放在这里
  });
  return promise2;
}

3.1.2 链式第二步

我们在使用promise时候,then方法的回调里面可以return出来一个值,那么在promise源码里面可以用一个x来接收,当我们在then的第一个参数里面return出来这个值,若promise是成功状态,走了then的第一个参数,那么在下一个then的第一个参数就可以得到上一个then里面return出来的值,我们在源码里面可以通过判断promise的状态,来决定将x放到这个promise2的resolve还是reject里面

js
// promise原型上的then方法
then(onFulfilled, onRejected) {
    // then返回一个promise才能链式调用,所以,每次调用then的时候都需要有一个全新的promise
    let promise2 = new Promise((resolve, reject) => {
        if (this.status === STATUS.FULFILLED) {
            // 这里的x就是then里面return 出来的结果
            let x = onFulfilled(this.value); // 8. 将成功的原因传递出去
            resolve(x);
        }
        ...其他状态同理
    });
    return promise2;
}

3.1.3 链式第三步

我们在调用then方法时候,,可以抛出异常,且抛出的异常会在下一个then调用时候会走到第二个参数。由于执行then方法的回调,在源码里面得到的是x,因此,可以在源码的x这里加上try catch,用来捕获异常,捕获到异常后,将异常交给promise2的`reject``,依便传给下一个then的第二个参数。

js
// promise原型上的then方法
then(onFulfilled, onRejected) {
    // then返回一个promise才能链式调用,所以,每次调用then的时候都需要有一个全新的promise
    let promise2 = new Promise((resolve, reject) => {
        if (this.status === STATUS.FULFILLED) {
           try {
              // 这里的x就是then里面return 出来的结果
              let x = onFulfilled(this.value); // 8. 将成功的原因传递出去
              resolve(x);
           } catch (error) {
              reject(error) 
           }
        }
        ...其他状态同理
    });
    return promise2;
}

本节的githubCommit记录:then里面return出非promise数据

3.2 第一个then返回的是一个promise时的链式调用

在使用原生promise时候,then方法里面可以return出来一个promise,比如下面的场景: 一个axios返回了数据,下一个接口需要用这个接口返回的数据再发起一次请求,那么就会在上一个接口的then里面去发请求,然后return出去,在下下个then里面去拿到数据(只是为了举例而举例)

js
this.$request
  .get("/getArticleInfo?id=1")
  .then(({ commentId }) => {
    return this.$request.get(`/getComment?commentId=${commentId}`);
  })
  .then(
    (res) => {
      console.log("评论信息" + res);
    },
    (err) => {
      console.log("err" + err);
    }
  );

这种情况下第一个then返回的就是promise,那么x就是一个promise,在3.1的源码里因为x就是一个值,因此直接将x给了promise2resolvereject,但是这里的x可能是一个promise,所以这一步的关键就是将x这个promise的值得到,然后再传给promise2resolvereject,问题就得以解决。

这里的判断和逻辑可以参考promise/A+规范里面的判断,只需要参照规范来实现具体逻辑即可,再者,我们的promise 按照规范来写 就可以和别人的promise共用

3.2.1 链式第四步

需要有一个方法,根据promise/A+规范,方法内部要判断x是否为promise,若x为promise,先将x的值获取到,再传给promise2的resolve和reject,若不是,则直接将x往下传,还需要判断x和promise2的关系,若x就是promise2,则抛出类型错误,因为不可能自己等待自己完成xxx

js
then(onFulfilled, onRejected) {
    let promise2 = new Promise((resolve, reject) => {
      if (this.status === STATUS.FULFILLED) {
        try {
          let x = onFulfilled(this.value); 
          // resolve(x);
          resolvePromise(x, promise2, resolve, reject); // 用来解析x是promise的情况
        } catch (error) {
          reject(error);
        }
      }
       ...其他状态同理
    });
    return promise2;
}

// 解析promise的方法
function resolvePromise(x, promise2, resolve, reject) {
  console.log(x, promise2, resolve, reject);
}

3.2.2 链式第五步

3.2.1中存在一个问题,就是在promise2内部去调用 resolvePromise(x, promise2, resolve, reject) 时候,明显拿不到promise2这个实参,因为在调用这个方法的时候,promise还没有初始化完成,因此,这里需要用一个宏任务或者微任务,等待promise2实例创建好后,再来取promise2,在promise/A+规范里面也有说明(跳转到Notes

js
then(onFulfilled, onRejected) {
    let promise2 = new Promise((resolve, reject) => {
      if (this.status === STATUS.FULFILLED) {
	// setTimeout这里可以使用其他宏任务或微任务,目的是让里面的resolvePromise方法能拿到promise2
        setTimeout(()=>{
            try {
                let x = onFulfilled(this.value); 
                // resolve(x);
                resolvePromise(x, promise2, resolve, reject); // 用来解析x是promise的情况
              } catch (error) {
                reject(error);
              }
        },0)
      }
	...其他状态同理
    });
    return promise2;
}

这一节之前的githubCommit记录:链式调用之then返回promise

3.2.3 链式第六步(核心)

现在就该编写resolvePromise方法的逻辑了,这是整个promise的核心,也是难点 前置条件:

  1. x:是使用promise时候,then里面回调的返回值,可能是普通js对象(引用和原始),也可能是一个promise实例对象。
  2. promise2是源码里面then方法返回的一个promise实例对象
  3. resolvepromise2执行器里面的resolve回调
  4. rejectpromise2执行器里面的reject回调

要做的

  1. xpromise,则递归解析,得到里面的具体值;
  2. x是普通js对象(包括原始值),则把值给resolv,因为resolvePromise是在fulfilled状态下调用的。
  3. xpromise2是同一个promise,则抛出异常,防止自己等于自己
  4. 还需要用一个状态锁保证状态不被重复修改,当状态改变后,就不能再掉resolve和reject来改变状态了

实现如下

js
function resolvePromise(x, promise2, resolve, reject) {
  if (x === promise2) {
    // 防止then里面return了和上一个then里面相同引用的promise,导致递归引用
    throw TypeError("不允许递归引用同一个promise对象");
  }

  // x可能是一个promise
  if ((typeof x === "object" && x !== null) || typeof x === "function") {
    let called; // 调用锁,防止多次改变状态
    try {
      let then = x.then;
      // 是promise
      if (typeof then === "function") {
        // 需要拿到x的状态,根据状态调用resolve和reject
        // 因为上面已经拿了一次x里面的then,为了避免出错,这里直接使用,不再次拿取,但是为了保证then里面的this指向,用call调用then
        // 调用返回的promise 用他的结果 作为下一次then的结果
        then.call( x, function (y) {
            if (called) return;
            called = true;
            // 递归解析成功后的值 直到他是一个promise为止
            resolvePromise(y, promise2, resolve, reject);
        },
        function (err) {
            // then的第二个参数
            if (called) return;
            called = true;
            reject(err);
        });
      } else {
        if (called) return;
        called = true;
        // x是普通对象,这里为什么不用判断状态,是因为resolvePromise就是fulfilled下调用的,因此直接resolve即可
        resolve(x);
      }
    } catch (error) {
      if (called) return;
      called = true;
      // 取then时候出错,进入到下一个then的第二个参数
      reject(error);
    }
  } else {
    // x就是一个普通值
    reject(x);
  }
}

本节githubCommit提交记录:链式调用完成

3.3 给then方法添加默认的回调

原生promise里面,then方法允许不传入回调,若没有传入回调,则也会将上一个then里面的内容传递给下一个then

js
let p2 = new Promise((resolve, reject) => {
  resolve("okk");
});
p2.then()
  .then()
  .then()
  .then(
    (data) => {
      console.log("data:" + data); // data:okk
    },
    (err) => {
      console.log("err:" + err);
  });

实现原理,其实只需要给源码里面的then方法加上默认的回调函数即可

js
// 默认处理
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : data=>data
onRejected = typeof onRejected === 'function' ? onRejected : err=>{ throw err } // 这里抛出异常或调用reject才会进入下一个then的第二个参数

本节githubCommit记录:给then方法添加默认回调

3.4 自动化测试promise是否符合promise/A+规范

自动化测试插件地址

myPromise.js里面编写测试代码

js
// 测试时会调用此方法 
Promise.defer = Promise.deferred = function () {
  let dfd = {};
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
};

然后全局安装自动化测试插件

js
npm install promises-aplus-tests -g

运行命令测试

js
promises-aplus-tests myPromise.js

之后会自动跑刚刚写好的peomise,若全绿就说明写的peomise是符合规范的,如下图: image.png

本节githubCommit记录:添加自动化测试

四. 其他原理

1. promisify原理

nodejs里面的异步api都是错误优先的api,即第一个参数都是err对象,这在开发中写起来错误处理和嵌套都不利于阅读,但是node也提供promise形式的异步API,这里基于promise封装了一个类似 node里面promisify的方法

js
function promisify(fn){
    return function (...args) {
        return new Promise((resolve,reject)=>{
            fn(...args,function (err,data) {
                if(err) reject();
                resolve(data);
            })
        });
    }
}
let read = promisify(fs.readFile);