从0开始手写一个promise

195 阅读20分钟

1、promise的特点

  1. 参数接收一个函数,不传会报错
  2. new 一个 promise 会立即执行函数
  3. 默认pending状态,例:console.log(new Promise(() => {}))); // Promise {<pending>}
  4. 接收的函数有两个状态:resolved、rejected,一旦调用其中一个就不能被改变了
  5. 调用then方法:p.then(fn1, fn2) // fn1 -- 正确的处理、fn2 -- 失败的处理
  6. then 返回 promise对象,可以链式调用

通过这几个特点,我们先写一个大体的框架:

function MyPromise(fn) {
    // 默认接收函数
    if (typeof fn !== 'function') {
        throw Error(`Promise resolver ${fn} is not a function`);
    }
    const self = this; // 修改status时有一个this指向问题,需要保存一下
    this.status = 'pending'; // promise默认状态
    function resolved() {
        self.status = 'resolved';
    }

    function rejected() {
        self.status = 'rejected';
    }

    // 创建函数立即执行
    // 这时发现没有resolved和rejected函数,说明两个函数是构造函数内部的,创建
    fn(resolved, rejected);
}

上述代码实现了以下功能:

  1. 容错,new MyPromise时如果传的不是函数会报错,与Promise保持一致
  2. 正确传函数进去可以立即执行,同时如果没有调用resolved或者rejected,状态默认为pending
  3. 调用resolved和rejected方法后会改变成相应的状态(这里有个坑,status可以多次修改,下一步进行优化)

2、实现then

  • then接收两个参数:resolved、rejected
  • 两个参数都是方法同时是都能将方法中的实参返回
  • then 返回 promise对象,可以链式调用

上代码:

2.1、前面代码的改造

function MyPromise(fn) {
    if (typeof fn !== 'function') {
        throw Error(`Promise resolver ${fn} is not a function`);
    }
    const self = this; 
    this.status = 'pending'; 
    this.data = null; // 保存resolved或者rejected的实参
    function resolved(data) {
        // 填坑:pending只能是resolve或者rejected状态,且只能改一次
        if (self.status === 'pending') {
            self.status = 'resolved';
            self.data = data;
        }
    }

    function rejected(err) {
        // 填坑:pending只能是resolve或者rejected状态,且只能改一次
        if (self.status === 'pending') {
            self.status = 'rejected';
            self.data = err;
        }
    }

    fn(resolved, rejected);
}

2.2、then方法的实现

MyPromise.prototype.then = function (onResolved, onRejected) {
    const self = this; // 保存MyPromise的this
    // 这里可以用this,因为在原型上写的函数,this指向MyPromise
    if (this.status === 'resolved') {
        // then 返回 promise对象
        return new MyPromise(function(resolved, rejected) {
            var res = onResolved(self.data);
            // 为什么要判断?2.3进行详解
            if (res instanceof MyPromise) {
                res.then(resolved, rejected);
            } else {
                resolved(res);
            }
        });
    }
    if (this.status === 'rejected') {
        return new MyPromise(function(resolved, rejected) {
            var res = onRejected(self.data);
            // 为什么要判断?2.3进行详解
            if (res instanceof MyPromise) {
                res.then(resolved, rejected);
            } else {
                resolved(res);
            }
        });
    }
}

2.3、解释

为什么需要要做判断?

then链式调用返回有两种情况:

1、正常返回,这个时候不管是resolved还是rejected下一个then都是resolve接收

上述代码,p1创建的时候执行内部的函数,执行resolved并传入‘123’,执行第一个then的时候直接返回参数,执行第二个then时走的第一个函数,所以打印123res;

p2创建时执行的rejeceed,所以执行第一个then时走的是第二个函数,直接返回err,开始执行第二个then,你会发现这个then方法执行的是第一个函数,所以打印的也是123res

2、then返回一个新的promise的情况

上述代码,创建p3走的是resolved,在执行第一个then时,执行的是参数中的第一个函数。该函数返回一个新的promise且执行了rejected,会发现打印的结果是123err,说明第二个then执行的是其第二个形参

总结:

  1. 如果then返回的是promise,那么链式调用时,下一个then接收的是新promise的状态
  2. 如果不是,那么下一个then接收的就是resolved状态

3、异步处理

我们先看一下原生的promise怎么处理的,复制下面代码到控制台,查看结果:

new Promise((resolved, rejected) => {
    setTimeout(() => resolved('123'), 3000);
}).then((data) => console.log(data));

我们会发现是等了3s之后再打印的123,再看下我们的:

new MyPromise((resolved, rejected) => {
    setTimeout(() => resolved('123'), 3000);
}).then((data) => console.log(data));

只打印了undefined,why?

原因是new MyPromise时执行计时器,then并没有等待计时器执行完就先执行了,所以打印的是undefined。

**解决方法:**实现then方法时再判断一下self.status === 'pending',如果等于,那么将这些一次保存进一个数组中,等到执行resolved或者rejected方法的时候依次执行即可。

上代码:

function MyPromise(fn) {
    ...
    // 定义两个数组保存异步代码中的函数
    this.resolvedList = [];
    this.rejectedList = [];
    function resolved(data) {
        if (self.status === 'pending') {
            self.status = 'resolved';
            self.data = data;
            // 当状态改变的时候执行对应的数组里的函数
            self.resolvedList.forEach((fn) => fn());
        }
    }
    function rejected(err) {
        if (self.status === 'pending') {
            self.status = 'rejected';
            self.data = err;
            // 当状态改变的时候执行对应的数组里的函数
            self.rejectedList.forEach((fn) => fn());
        }
    }
    fn(resolved, rejected);
}

then方法

MyPromise.prototype.then = function (onResolved, onRejected) {
    ...
    if (this.status === 'pending') {
        return new MyPromise(function(resolved, rejected) {
            // 需要通过立即执行函数保存onResolved
            self.resolvedList.push((function(onResolved) {
                // 即status === 'resolved' 那一段代码
                return function() {
                    var res = onResolved(self.data);
                    if (res instanceof MyPromise) {
                        res.then(resolved, rejected);
                    } else {
                        resolved(res);
                    }
                }
            }(onResolved)))
            // rejected也需要保存
            self.rejectedList.push((function(onRejected) {
                // 即status === 'rejected' 那一段代码
                return function() {
                    var res = onRejected(self.data);
                    if (res instanceof MyPromise) {
                        res.then(resolved, rejected);
                    } else {
                        resolved(res);
                    }
                }
            }(onRejected)))
        })
    }
}

4、执行顺序

var p = new Promise((resolved, rejected) => {
    console.log('1');
    resolved('2');
})
p.then((res) => console.log(res));
console.log('3');
上述代码的执行顺序是:1、3、2,显然前面的代码没有实现。具体原因可以了解js的执行机制。
**解决**:这块的实现是将resolved和rejeceed中的执行代码放到setTimeout中
function MyPromise(fn) {
    ...
    function resolved(data) {
        // 将resolved中的执行代码放到setTimeout中
        setTimeout(() => {
            if (self.status === 'pending') {
                self.status = 'resolved';
                self.data = data;
                self.resolvedList.forEach((fn) => fn());
            }
        }, 0);
    }
    function rejected(err) {
        // 将rejeceed中的执行代码放到setTimeout中
        setTimeout(() => {
            if (self.status === 'pending') {
                self.status = 'rejected';
                self.data = err;
                self.rejectedList.forEach((fn) => fn());
            }
        }, 0);
    }
    fn(resolved, rejected);
}

完整的代码详见:github.com/White-lamb/…