玩转Promise

791 阅读19分钟

1. 回调地狱

在讲述Promise之前,我们有必要了解到为什么会有Promise的产生?Promise解决了什么问题?带着这样的问题,我们来看两个🌰:

1.1 模拟回调地狱

现在有个方法 doSth,我想在执行这个方法几次之后执行一个回调函数 callback。再写个方法 workSth,打印一些内容。然后将workSth作为参数之一,并执行方法 doSth,用变量fn来接收,只有在fn执行4次后,才会显示打印的内容。

let doSth = function(t, callbak) {
    return function() {
        if (--t === 0) {
            callbak();
        }
    }
}

function workSth() {
    console.log('Symon is working from home.');
}

let fn = doSth(4, workSth);

fn();
fn();
fn();
fn();

现在,我要在workSth中写入函数行参cb,在打印内容后我执行这个cb,这时候还需要一个方法studySth,那么我希望studySth作为实参传入workSth。也就是说在fn执行四次后,callbak即workSth开始执行,那么它对应的cb即studySth开始执行。

function workSth(cb) {
    console.log('Symon is working from home.');
    cb();
}

function studySth() {
    console.log('Symon has not posted a technical article on Juejin for one week.');
}

let fn = doSth(4, workSth.bind(null, studySth));

那如果在studySth中再传入一个参数呢?那这时候又需要一个方法healthyHabitSth。

function studySth(cb) {
    console.log('Symon has not posted a technical article on Juejin for one week.');
    cb();
}

function healthyHabitSth() {
    console.log('Symon should remember to form the good habit of going to bed early and getting up early.');
}

这时候需要我们对doSth进行改写:

function doSth(t) {
    return function() {
        if (--t === 0) {
            workSth(function() {
                studySth(function() {
                    healthyHabitSth();
                });
            });
        }
    }
}

let fn = doSth(4);

输出的结果:

image.png

那么如果有更多的sth方法呢?

我们发现上面代码大量使用了回调函数(将一个函数作为参数传递给另个函数)并且有许多 '})' 结尾的符号,使得代码看起来很混乱,呈现金字塔状,我们很亲切地称它为——“回调地狱”。

1.2 基于回调函数的方式封装ajax发送请求

上述情况是模拟的回调地狱,接下来我们在异步请求中看看回调地狱的情形:

这里我们以ajax串形(多个ajax请求间存在依赖,只有上一个请求成功,才能发送下一个请求)的场景来实现一下:

$.ajax({
    url: 'api/test1',
    method: 'GET',
    datatype: 'json',
    success(value) {
        console.log('第一个请求结果:', value);
        
        $.ajax({
            url: 'api/test2',
            method: 'GET',
            datatype: 'json',
            success(value) {
                console.log('第二个请求结果:', value);
                
                $.ajax({
                    url: 'api/test3',
                    method: 'GET',
                    datatype: 'json',
                    success(value) {
                        console.log('第三个请求结果:', value);
                    }
                });
            }
        });
    }
});

回调地狱简单来说就是回调函数里嵌套回调函数再嵌套...

在没有Promise之前,基于回调函数的方式管理ajax请求,在串行的需求中,很容易产生“回调地狱”。而Promise是ES6中专门用来管理异步编程的,基于它可以避免"回调地狱"问题。

同样的需求,如果用Promise怎么写呢?

const p1 = () => {
    return new Promise(resolve => {
        $.ajax({
            url: 'api/test1',
            method: 'GET',
            datatype: 'json',
            // success(value) {
            //    resolve(value);
            // }
            success: resolve
        });
    })
};

const p2 = () => {
    return new Promise(resolve => {
        $.ajax({
            url: 'api/test2',
            method: 'GET',
            datatype: 'json',
            success: resolve
        });
    })
};

const p3 = () => {
    return new Promise(resolve => {
        $.ajax({
            url: 'api/test3',
            method: 'GET',
            datatype: 'json',
            success: resolve
        });
    })
};


p1()
    .then(value => {
        console.log('第一个请求结果:', value);
        return p2();
    })
    .then(value => {
        console.log('第二个请求结果:', value);
        return p3();
    })
    .then(value => {
        console.log('第三个请求结果:', value);
    });

每个请求都用Promise包裹起来,通过Promise把jquery的异步ajax请求去管理起来。

上述代码可以利用Promise语法糖做到精简效果:

(async function() {
    let value = await p1();
    console.log('第一个请求结果:', value);
    value = await p2();
    console.log('第二个请求结果:', value);
    value = await p3();
    console.log('第三个请求结果:', value);
})()

明明是异步的代码,通过 async、await 写出了同步的效果。

2.初识Promise

2.1 Promise的含义

Promise是ES6新增的内置类,是异步编程的一种解决方案。用来规划异步编程代码,解决回调地狱等问题。

Promise中文翻译过来是“承诺”,意思是在未来某一个时间点承诺返回数据给你。比如说我要和你做个约定,下周末一起去看电影,这就是一个承诺。承诺会有三种状态,一种是实现承诺😄,一种是承诺石沉大海😣,另一种是承诺等待结果中...😓...苦苦等待。

Promise有以下几个特点:

  • 不兼容IE浏览器(edge可以兼容)
  • 使用的时候是创建这个类的实例:实例具有私有属性,可以使用Promise.prototype上的方法,Promise作为对象存在一些静态的私有属性方法
  • 状态不受外界影响。Promise代表一个异步操作,有三种状态:fulfilled(已成功)、rejected(已失败)和pending(进行中)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

Promise也有一些缺点。 首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

2.2 基本用法

2.2.1 创建实例

ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。

let p = new Promise(() => {

});

Promise 接受一个函数作为参数,这个函数叫做 executor(执行器)。而该函数有两个参数,分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

let p = new Promise(() => {
    console.log(1);
})
console.log(2);

输出的结果是 1、2

executor 必须是一个函数,而且 new Promise 的时候会将其立即执行。

来打印一下 Promise 实例 p:

image.png

由上图可以看出,实例 p 有两个内置私有属性,分别是:

  • [[PromiseState]]: "pending"/"fulfilled"/"rejected" 实例的状态
  • [[PromiseResult]]: undefined 实例的值(成功的结果或者失败的原因)

实例 p 的公共属性方法 Promise.prototype:

  • then
  • catch
  • finally
  • Symbol(Symbol.toStringTag): "Promise"

拓展:Q:Symbol(Symbol.toStringTag): "Promise" 是用来干嘛的?

A:检测数据类型

当我们使用 Object.prototype.toString.call() 这个万能的检测数据类型的方法检测 p 实例的时候,得到👇的结果:

image.png

得到这样的结果就是通过 Symbol(Symbol.toStringTag) 这个属性来决定的。当前实例对象的该属性的值就对应打印的结果。没有该属性的时候,才会按照内部规则去找其对应所属类。

2.2.2 修改实例的状态和值

修改 Proise 实例的状态和值的方法有三种(或者说Promise实例变成成功/失败的状态取决于哪三种情况):

1.手动执行 executor 函数中的 resolve/reject

[executor] 中执行 resolve/reject 都是为了改变 Promise 实例的状态和值,一但状态被改变成 fulfilled/rejected 则不能在改为其他的状态。

let p = new Promise((resolve, reject) => {
    resolve('OK');
});
console.log(p);

image.png

let p = new Promise((resolve, reject) => {
    reject('NO');
});
console.log(p);

image.png

总结:

  • resolve('OK'); [[PromiseState]]:fulfilled [[PromiseResult]]:'OK'
  • reject('NO'); [[PromiseState]]:rejected [[PromiseResult]]:'NO'

2.executor函数中出现报错的代码

[executor] 函数执行报错,Promise实例状态也会发生改变,实例值是报错原因。

  • [[PromiseState]]: rejected
  • [[PromiseResult]]: 报错原因
let p = new Promise((resolve, reject) => {
    console.log(a);
});
console.log(p);

image.png

Promise内部做了异常信息捕获(try/catch)

let p = new Promise((resolve, reject) => {
    console.log(a)
});
console.log(p);

p.then(() => {}).catch(() => {});

这时候再看控制台,就没有报错了

image.png

3.通过.then存放的 onfulfilledCallback/onrejectedCallback 执行是否报错,影响了通过.then返回的那个全新Promise实例是成功/失败

这一种情况在下文中会提到。

2.3 Promise.prototype.then()

2.3.1 then方法的基本使用

Promise 实例具有then方法,也就是说,then方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。

then 方法的第一个参数是状态成功的回调函数 onfulfilledCallback,第二个参数是状态失败的回调函数onrejectedCallback,它们都是可选的。状态改变后执行对应的回调函数并且将 [[PromiseResult]] 的值传递给方法。

let p = new Promise((resolve, reject) => {
    resolve('OK');
});
p.then(result => {
    console.log('成功 ——>', result);
}, reason => {
    console.log('失败 ——>', reason);
});

image.png

let p = new Promise((resolve, reject) => {
    reject('NO');
});
p.then(result => {
    console.log('成功 ——>', result);
}, reason => {
    console.log('失败 ——>', reason);
});

image.png

2.3.2 Promise中的同步异步问题

接下来探究一下关于 Promise 同步异步的问题,先看道开胃菜:

let p = new Promise((resolve, reject) => {
    resolve('OK'); // *
});
console.log(p); // $

resolve 是同步还是异步呢?如果它是异步的,执行 * 处代码后什么效果都没有,继续执行 $ 处代码,那么 p 的状态没有发生改变。

但是通过打印发现 p 的状态发生了改变。所以 resolve 是同步的,也就是说在 [executor] 函数中我们没有管控异步代码。

image.png

接下来,我们来看一个稍微复杂一些的场景:

let p = new Promise((resolve, reject) => {
    console.log(1);
    resolve('OK');
    console.log(2);
});
console.log(p); // &
p.then(result => {
    console.log('成功 ——>', result);
});
console.log(3);

分析:首先,new Promise 的时候立即执行 [executor] 函数,因为里面是同步任务,所以先输出1,执行 resolve 的时候会同步修改其状态和值,修改完之后按理来说应该通知then方法里面的onfulfilledCallback 执行,但执行了 resolve 之后并没有执行then,因此接下来输出2。来到 & 处代码,因为在执行 resolve 的时候就已经同步修改了实例的状态和值,所以这时候输出的就是实例对象 p,并且是成功状态,值为 'OK'。

接下来执行 p.then(onfulfilledCallback,onrejectedCallback),内部做了两件事:

  • 首先把传递进来的onfulfilledCallbackonrejectedCallback存储起来(存储在一个容器中:因为可以基于then给其存放多个回调函数)
  • 其次再去验证当前实例的状态
    • 如果实例状态是pending,则不做任何的处理
    • 如果已经变为 fulfilled/rejected,则会通知对应的回调函数执行(但是不是立即执行,而是把其放置在 EventQueue 中的微任务队列中)

所以这时先输出后面的同步代码得到3,同步代码都执行完之后,再去将 EventQueue 中的微任务执行。

最终的打印结果:

image.png

基于此,我们可以得出一个结论:

promise本身不是异步的,是用来管理异步的,但是then方法是异步的(微任务)

我们难度升级一下,看👇代码:

let p = new Promise((resolve, reject) => {
    console.log(1);
    setTimeout(() => {
       resolve('OK');
       console.log(p);
       console.log(4);
    }, 1000);
    console.log(2);
});
console.log(p);
p.then(result => {
    console.log('成功 ——>', result);
});
console.log(3);

分析:new Promise 的时候立即执行 [executor] 函数,先输出1,定时器放到异步宏任务,输出2。因为这时候实例状态并没有发生改变且代码执行也没有报错,所以输出的实例 p 的状态为 pending,值为 undefined。接下来,p.then() 接受onfulfilledCallback的时候,状态还是pending,此时只把方法存储起来,不做其他处理。输出后面同步代码3。等1000ms后,把异步宏任务拿出来执行,即执行定时器中的函数 resolve,这时候做了两件事:

  • 同步改变实例的状态和值
  • 通知之前基于then存放的onfulfilledCallback执行(异步的微任务:也是把执行方法的事情放置在 EventQueue 中的微任务队列中)

通知基于then存放的onfulfilledCallback执行是个异步伟任务,所以先输出实例 p,状态是 fulfilled,值为 'OK'。然后输出4,最后执行 EventQueue 中的微任务。

最终打印结果:

image.png

Q:那么执行 resolve/reject 是同步还是异步?

A:修改状态和结果是同步的,但是通知方法执行是异步的。

所以Promise中的异步指的是 then、resolve、reject

2.3.3 then方法的链式调用

let p1 = new Promise((resolve, reject) => {
    resolve('OK');
    // reject('NO'); // !
});
let p2 = p1.then(result => {
    console.log('p1成功 ——>', result);
    // return Promise.reject('xxx'); // *
    // return 10; // @
}, reason => {
    console.log('p1失败 ——>', reason);
});
console.log(p2);

按照之前一样的分析,我们可以知道最先输出的是实例 p2。

最终打印结果:

image.png

那么 p2 恒等于 p1 么?

image.png

显然,二者并不相等,即 p2 是全新的 Promise 实例。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

Q:p2的状态和值是怎么改变的?

A:不论执行的是基于 p1.then 存放的 onfulfilledCallback/onrejectedCallback 两个方法中的哪一个

  • 只要方法执行不报错
    • 如果方法中返回一个全新的Promise实例,则“全新的Promise实例”的成功和失败决定p2的成功和失败(eg: 加上 * 处代码)
    • 如果不是返回promise呢?则 [[PromiseState]]:fulfiled [[PromiseResult]]:返回值(eg: 加上 @ 处代码)
  • 如果方法执行报错:p2的 [[PromiseState]]:rejected [[PromiseResult]]:报错原因

综上,也就不难发现如果只执行 ! 处代码,即只执行 reject 函数,最终输出的实例 p2 的状态一定是 fulfiled !!!

image.png

Promise.resolve() 返回成功的Promise实例;Promise.reject() 返回失败的Promise实例

牛刀小试:

let p1 = new Promise((resolve, reject) => {
    resolve('OK');
});
let p2 = p1.then(result => {
    console.log('p1成功 ——>', result);
    return Promise.reject(10);
}, reason => {
    console.log('p1失败 ——>', reason);
});
let p3 = p2.then(result => {
    console.log('p2成功 ——>', result);
}, reason => {
    console.log('p2失败 ——>', reason);
    return Promise.resolve(10);
});
p3.then(result => {
    console.log('p3成功 ——>', result);
}, reason => {
    console.log('p3失败 ——>', reason);
    return Promise.resolve(10);
});

接下来,看下这两种场景:

new Promise((resolve, reject) => {
    resolve('OK');
}).then(null, reason => {
    console.log('失败 ——>', reason);
}).then(result => {
    console.log('成功 ——>', result);
}, reason => {
    console.log('失败 ——>', reason);
}).then(result => {
    console.log('成功 ——>', result);
}, reason => {
    console.log('失败 ——>', reason);
});

打印的结果:

image.png

new Promise((resolve, reject) => {
    reject('NO');
}).then(result => {
    console.log('成功 ——>', result);
}, null)
.then(result => {
    console.log('成功 ——>', result);
}, reason => {
    console.log('失败 ——>', reason);
}).then(result => {
    console.log('成功 ——>', result);
}, reason => {
    console.log('失败 ——>', reason);
});

打印的结果:

image.png

如果 onfulfilledCallback/onrejectedCallback 不传递,则状态和结果都会“顺延/穿透”到下一个同等状态应该执行的回调函数上(内部其实是自己补充了一些实现效果的默认函数)

2.4 Promise.prototype.catch()

Promise.prototype.catch()方法用于指定发生错误时的回调函数。也就是说,catch只处理状态为失败时做的事情。

Promise.prototype.catch = function (onrejectedCallback) {
    return this.then(null, onrejectedCallback);
};

因此,平时写关于.then的代码时,里面只放成功状态下做的事情,失败状态下的事情写到catch里。

new Promise((resolve, reject) => {
    reject('NO');
}).then(result => {
    console.log('成功 ——>', result);
}).then(result => {
    console.log('成功 ——>', result);
}).catch(reason => {
    console.log('失败 ——>', reason);
});

2.5 Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

new Promise(resolve => {
    ...
})
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

finally本质上是then方法的特例。

new Promise(resolve => {
    ...
})
.finally(() => {
  ...
});

// 等同于
new Promise(resolve => {
    ...
})
.then(
  result => {
    return result;
  },
  reason => {
    throw error;
  }
);

3.Promise进阶

3.1 Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

let p = Promise.all([p1, p2, p3]);

Promise.all()方法传递的参数是由多个 Promise 实例组成的集合,一般是数组。如果集合中的某一项不是 Promise 实例,则默认状态是成功他、值是本身的 Promise 实例。Promise.all()方法会返回一个新的 Promise 实例,该实例的状态取决于数组中每一个Promise实例的状态。只要有一个失败的状态,那么新的 Promise 实例的状态就是失败的,只有数组中每个Promise实例都是成功的状态时,新的 Promise 实例状态才会是成功。

function fn(interval) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(interval);
      }, interval)
    });
}
let p1 = fn(3000);
let p2 = fn(1000);
let p3 = Promise.resolve(0); // ~

Promise.all([p1, p2, p3])
.then(result => {
    console.log(result);
})
.catch(reason => {
    console.log(reason);
})

打印的结果:

image.png

不论数组中的哪个 Promise 实例先知道状态,最后结果的顺序和传递数组的顺序要保持一致。

如果将 ~ 处的 resolve 改为 reject,打印的结果是 0

在对数组中的 Promise 实例只要遇到有一个是失败状态的,那么Promise.all()方法返回的新的实例的状态就是失败的,结果就是数组中状态失败的 Promise 实例失败的原因。

使用场景:ajax并行,比如一个页面需要请求两个甚至更多的Ajax请求数据后才能正常显示。

3.2 Promise.race()

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

let p = Promise.race([p1, p2, p3]);

Promise.race()方法和Promise.all()方法不同之处在于,其返回的新的 Promise 的实例的状态取决于传递的数组参数中最先知道状态的 Promise 实例,不管这个状态是成功还是失败。

使用场景:把异步操作和定时器放到一起,如果定时器先触发,认为超时,告知用户。

3.3 Promise.any()

Promise.any()方法和前两者不同之处在于传入的 Promise 状态只要有一个为 resolve,整个则返回成功状态;全为 reject,即返回 reject 状态。

使用场景:从最快的服务器检索资源,如果存在多台服务器,从最快的一台服务器获取资源。

3.手撕Promise

手写出一套符合 PromiseA+ 规范的代码

因为Promise是ES6新增的API,所以不兼容IE浏览器(IE11都不兼容),那如果平时项目中需要兼容IE浏览器呢?

比如说做一个 to C 的项目,那肯定要考虑IE,最起码IE10及以上,处理兼容需要用到babel。正常情况下babel-preset只能把ES6的语法转换为ES5,对于ES6中的内置API是无法转换的,此时需要@babel/polyfill,它会将ES6中很多内置的API基于ES5进行了重写,让其可以兼容IE浏览器。这其中就有对Promise的重写(基于PromiseA+ 规范)。接下来基于这一规范我们来对Promise进行重写。

首先为了避免自己的写的代码和外面发生冲突,写个闭包将函数包裹起来。创建Promise类,并将API暴露出去。Promise必须要有一个函数类型的参数executor,所以需要对传入的参数进行校验,且要保证Promise这个类只能通过new来执行。

拓展: ES6新增的一个语法:new.target,用来记录new的目标值(存储new的那个构造函数),如果是当作普通函数来执行,则值为undefined

但我们手写的话需要考虑兼容性,所以一律不用ES6语法。那怎么判断是不是通过new执行的呢?new Promise的特点是Promise这个构造函数当中会默认创建一个当前类的实例对象,并且this会指向这个实例对象;如果当作普通函数执行的话,其中的this指向的是window或undefined(严格模式下)。

给实例加上state和result两个属性。立即执行executor函数,而executor函数要传入两个函数参数resolve和reject。

(function() {
   'use strict'
   /** 核心代码 */
   function Promise(executor) {
      var self = this;
      // init params
      if (typeof executor !== 'function') throw new TypeError('Promise resolve executor is not a function');
      if (!(self instanceof Promise)) throw new TypeError('undefined is not a promise');

      // property
      self.state = 'pending';
      self.result = undefined;

      // 执行 executor
      executor(
        function resolve() {}, 
        function reject() {}
      );
    }

    /** 暴露API */
    if (typeof window !== 'undefined') window.Promise = Promise;
    if (typeof module === 'object' && typeof module.exports === 'object') module.exports = Promise;
})();

此时看的不是resolve/reject执行,而是优先考虑到executor函数执行是否会报错的问题,我们用try...catch来捕获异常。

无论是executor函数执行报错,还是resolve/reject函数执行,都会改变实例的状态,那我们统一写一个改变实例的状态和值的方法,叫做change。当执行change方法的时候,需要传递两个参数,一个是状态state,一个是值result

另外要考虑到实例的状态一旦从pending变成fulfilled/rejected将不再改变,所以在change方法中要做个判断。

var change = function change(state, result) {
    if (self.state !== 'pending') return;
    self.state = state;
    self.result = result;
};

try {
    executor(function resolve(value) {
      change('fulfilled', value);
    }, function reject(reason) {
      change('rejected', reason);
    });
} catch(err) {
    change('rejected', err);
}

接下来,向Priomise原型对象上添砖加瓦。finally一般不用,所以我们只实现then方法和catch方法即可。👇图中圈住的浅色的方法表示都是不可枚举的。

image.png

Q:为啥必须是不可枚举的呢?

A:防止使用for...in或Object.keys的时候将公共属性也迭代到

正常情况下,我们可以通过直接在原型对象上挂方法就行了

Promise.prototype.then = function then() {};

但要做到是否可枚举,我们就要用到ES5中的Object.defineProperty这个API。Object.defineProperty可以给当前对象(即Promise的原型对象)设置属性then/catch,然后去写它的规则。Object.defineProperty一方面可以给当前对象的属性值做劫持,另一方面设置一些属性,比如说enumerable表示是否可枚举,writable表示是否可以重写值,configurable表示能否被删除。另外通过value来设置值。

可以通过Object.getOwnPropertyDescriptor来查看内置的Promise原型对象上then方法的具体规则

console.log(Object.getOwnPropertyDescriptor(Promise.prototype, 'then'));

image.png

手写的属性规则就按照👆图中来即可

无论是then还是catch,都是要往原型对象上扩展方法,所以抽出来写一个方法define,传递两个参数 key(属性名)和 value(属性值)

/** 原型对象 */
var define = function define(key, value) {
  Object.defineProperty(Promise.prototype, key, {
    enumerable: false,
    writable: true,
    configurable: true,
    value: value
  });
};

define(Symbol.toStringTag, 'Promise');

define('then', function then() {});

define('catch', function myCatch() {});

注:因为catch是关键字,所以换名myCatch

then方法里面接收两个参数onfulfilled和onrejected。首先会校验当前方法中this是不是Promise实例,写一个工具方法checkInstance。

其次,需要知道状态是成功还是失败,这时候需要做判断。而对于状态还不知道是成功/失败的情况,要先存储起来,存储的目的是当resolve/reject执行,即这里是执行了change方法的时候,状态和值就会进行修改,修改完了后就会通知之前存储的方法执行。那么存储到呢?因为then方法中的self和change方法中的self指向的都是当前类的实例,只要都存储到实例上,以后就可以直接在change方法中通知执行了。因此可以在实例上加上两个集合,然后在then方法中通过push将参数存储到集合中,执行change方法将状态和值立即修改之后,我们要异步通知集合中的方法执行。根据当前状态来确定执行哪个集合中的方法。

然后,因为这一块是一个异步微任务,在不考虑兼容的情况下,可以基于 queueMicrotask 来实现。

queueMicrotask(() => {
    // 创建一个异步微任务
});

因为手写需要考虑兼容,所以用定时器来模拟异步微任务。

...
  // property
  self.state = 'pending';
  self.result = undefined;
  self.onfulfilledCallbacks = [];
  self.onrejectedCallbacks = [];
  var change = function change(state, result) {
    if (self.state !== 'pending') return;
    self.state = state;
    self.result = result;
    // 异步通知集合中的方法执行
    var callbacks = state === 'fulfiled' ? self.onfulfilledCallbacks : self.onrejectedCallbacks;
    if (callbacks.length > 0) {
      setTimeout(function() {
        callbacks.forEach(function(callback) {
          callback(self.result);
        });
      });
    }
 };
...
/** 工具方法 */
// 检测是否为Promise实例
var checkInstance = function checkInstance(self) {
  if(!(self instanceof Promise)) {
    throw new TypeError('Method then called on incompatible receiver #<Promise>');
  }
};

define('then', function then(onfulfilled, onrejected) {
  checkInstance(this);
  var self = this;
  switch(self.state) {
    case 'fulfilled': 
      setTimeout(function() {
        onfulfilled(self.result);
      });
      break;
    case 'rejected': 
      setTimeout(function() {
        onrejected(self.result);
      })
      break;
    default:
      self.onfulfilledCallbacks.push(onfulfilled);
      self.onrejectedCallbacks.push(onrejected);
  }
});

Q: then方法里面传进来的成功/失败时的参数不是只有一个吗,为什么要用数组去存储呢? A:同一个实例可以多次调用then方法(注意和then链的区别)

至此,Promise基础版的手写完成!