「前端面试」如何用20行代码实现一个Promise?

1,041 阅读6分钟

前言

徒手实现一个Promise是前端面试高频题,最近小编拜读了一些博客文章,get到一份最简Promise的代码,只有20行代码实现了核心异步链式调用功能(不考虑失败回调),经一番梳理把理解过程记录下来与大家分享!

实现代码

如下是最终代码,可以先一睹为快,接下来慢慢品;

function Promise(fn) {//fn是实例化时传入的,客户定义的,参数为resolve,reject的函数
    this.successList = [];// 存储成功回调函数successFn,每执行一次then方法就push一个
    const resolve = (value) => {
        setTimeout(() => {
            this.data = value;
            this.successList.forEach((successFn) => successFn(value));
        });
    }
    fn(resolve);// 需要直接执行fn, 并给其传入所需的参数,而这个参数是promise内部定义的resolve函数
}

Promise.prototype.then = function (successFn) {
    // promise2
    return new Promise((resolve) => {
        this.successList.push(() => {
            const result = successFn(this.data);// result对应的是userPromise
            if (result instanceof Promise) {
                // 给userPromise来个then方法执行定义成功回调
                result.then((res) => {
                    resolve(res)
                });// 或者写成result.then(resolve)
            } else {
                // 如果return的不是个promise,是其他值或者没有值,
                // 就把这个结果值传给resolve,那么promise2中的成功回调(successFn)就可拿到并做进一步的处理
                resolve(result)
            }
        });
    })
};

剖析过程

接下来开始一步步解析,来读懂并掌握上面的源码,首先我们从Promise的使用入手,测试案例如下:

测试案例

先不考虑then的链式调用,如下是最简单的使用案例,给Promise传入一个执行函数fn,当执行到resolve时,可以在给then传入的参数函数中拿到结果值res;

const p = new Promise((resolve) => {
    setTimeout(() => {
        resolve(1);
    }, 500);
})

p.then((res) => {
    console.log("成功", res);
})

由此我们开始进一步思考,如果Promise是我们自己定义那应该长什么样?

  • 1.是一个构造函数能接收一个外部定义的函数并执行,这里把这个外部函数称作fn
  • 2.提供一个then方法,接收一个函数,这里称作successFn,then方法可以把successFn收集起来
  • 3.当执行fn时,会走到resolve(1),那么resolve这个函数也需要提供
  • 4.执行resolve(1)时,之前收集起来的各个successFn开始依次执行
  • 5.successFn的参数是成功的结果值res,也就是案例中的1,所以1这个值应该会被存起来,然后在successFn执行时传给它

第一步:基本实现

依据上面的思路,我们实现了如下的代码:

function Promise(fn) {//fn是实例化时传入的,客户定义的,参数为resolve,reject的函数
    this.successList = [];// 存储成功回调函数successFn,每执行一次then方法就push一个
    const resolve = (value) => {
        this.data = value;
        this.successList.forEach((successFn) => successFn(value));
    }
    fn(resolve);// 需要直接执行fn, 并给其传入所需的参数,而这个参数是promise内部定义的resolve函数
}

Promise.prototype.then = function (successFn) {
    this.successList.push(() => {
        successFn(this.data);// 这里this.data就是"p.then((res) => {...}"中的res,起初并不知道res的值是多少,执行了resolve才知道
    });
};

第二步:细化代码

再基于上述的代码,我们继续考虑如下几点:

  • 1.Promise是异步的,那么其中resolve函数里的实现应该也是异步的,我们可以用setTimeout;
  • 2.现在需要考虑p.then(()=>{}).then(()=>{})这样的链式操作,那么在then方法里一定是返回了一个新的Promise,这样才能继续调用其的then方法;
  • 3.同时为了测试链式操作,测试示例也要在then的成功回调函数(successFn)里增加返回一个Promise的逻辑进行测试使用; 所以,测试示例如下:
// promise1
const p = new Promise((resolve) => {
    setTimeout(() => {
        resolve(1);
    }, 500);
})

p.then(function (res) {
    console.log("成功1", res);
    // userPromise
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(2);
        }, 500);
    });
}).then(function (res) {
    console.log("成功2", res);
})

// 期望得到的结果是 
// “成功1 1”
// “成功2 2”

根据上面说的第一点,需要把Promise构造函数改成如下代码:

function Promise(fn) {//fn是实例化时传入的,客户定义的,参数为resolve,reject的函数
    this.successList = [];// 存储成功回调函数successFn,每执行一次then方法就push一个
    const resolve = (value) => {
        setTimeout(() => {
            this.data = value;
            this.successList.forEach((successFn) => successFn(value));
        });
    }
    fn(resolve);// 需要直接执行fn, 并给其传入所需的参数,而这个参数是promise内部定义的resolve函数
}

then方法实现

由于要实现链式操作,那么then方法必须返回一个promise,那么可以暂时先写成下面的样子:

Promise.prototype.then = function (successFn) {
    // promise2
    return new Promise((resolve) => {
    	console.log("push ----")
        this.successList.push(() => {
            successFn(this.data);
        });
    })
};

接下来为了方便解释, 给不同位置的promise起了名字,在代码中有标注promise1,promise2和userPromise:

  • p=new Promise()=>promise1
  • Promise.prototype.then中的return new Promise()=>promise2
  • 测试示例p.then(function (res) {return new Promise(...)})中的return new Promise()=>userPromise

同时看着测试示例,我们来梳理一下:

  • 1.执行第一个p.then()方法时,开始创建promise2,同时在promise2中执行的this.successList.push()会把成功回调函数successFn放到promise1的successList中待执行,所以下面console.log("成功1", res)的地方还未执行。
p.then(function (res) {
    console.log("成功1", res);
    // userPromise
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(2);
        }, 500);
    });
})
  • 2.第一个p.then()执行完后,已经返回了promise2,那么可以继续执行第二个p.then(),那么此时是在promise2successList放入了带执行成功回调;
  • 3.当过了500ms,promise1开始执行resolve时,那么接着就拿出promise1中的successList,遍历依次执行成功回调函数
  • 4.执行到上面successFn(this.data)这句时,也就是开始执行如下这个函数:
function (res) {
    console.log("成功1", res);
    // userPromise
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(2);
        }, 500);
    });
}

打印完“成功1 1”后,又开始创建userPromise,并执行userPromise中传的Fn函数,也就是如下这段:

(resolve) => {
    setTimeout(() => {
        resolve(2);
    }, 500);
}

接着就是等待500ms后执行userPromiseresolve(2),此时你会发现successFn(this.data)这句执行完时,得到的是创建并返回userPromise

但是,重点来了!

  • 5.userPromise并没有then方法去到自己的successList中push对应的成功回调函数,那么当resolve(2)时,没有可执行的成功回调!

  • 6.并且,由上面第2点知道,我们把第二个p.then(successFn)中的successFn给了promise2

所以,现在要做的就是!

  • 7.给userPromise一个then方法执行,让userPromise有成功回调函数,当userPromise成功回调时就去通知promise2让其也执行resolve()

  • 8.综上得到的结果是userPromise成功resolve时,promise2也成功resolve,这样之前放在promise2上的successList就会依次遍历执行其函数项。

最后,再经过修改,then方法的终极版如下:

Promise.prototype.then = function (successFn) {
    // promise2
    return new Promise((resolve) => {
        console.log("push ----")
        this.successList.push(() => {
            const result = successFn(this.data);// result对应的是userPromise
            if (result instanceof Promise) {
                // 给userPromise来个then方法执行定义成功回调
                result.then((res) => {
                    resolve(res)
                });// 或者写成result.then(resolve)
            } else {
                // 如果return的不是个promise,是其他值或者没有值,
                // 就把这个结果值传给resolve,那么promise2中的成功回调(successFn)就可拿到并做进一步的处理
                resolve(result)
            }
        });
    })
};

总结

通过battle一遍代码感受到这样一个思路,我们外部使用的时候是只看到了promise1userPromise,内部使用一个promise2promise1userPromise链接起来了!

分享了这个最简Promise的代码思路,希望对你有帮助!如有疏漏,请不吝赐教~ 😊