【你应该掌握的】Promise基础知识&如何实现一个简单的Promise

1,421 阅读11分钟

团队:skFeTeam  本文作者:高扬

Promise是前端基础技能中较为重要的一部分,本文将从以下几个方面展开Promise的相关知识,与大家交流。

  • 什么是Promise
  • Promise可以解决什么问题
  • 该类问题是否有其他解决方案
  • 如何使用Promise
  • 如何自己实现Promise

什么是Promise?

Promise是一种异步编程的解决方案,已经被纳入ES6规范当中。在Promise出现之前,传统的异步编程方案主要是点击事件以及回调函数

Promise可以解决什么问题?

简单来说,Promise可以避免出现回调地狱

什么是回调地狱?

JQuery中发起一个异步请求可以写为:

$.ajax({
    type: 'GET',
    url: 'xxx',
    ...,
    success:function (data) {
        ...
    }
})

如果业务需要扩展,在获取到请求结果后再发起一个异步请求,则代码扩展为:

$.ajax({
    type: 'GET',
    url: 'xxx',
    ...,
    success:function (data1) {
        // 另一个异步请求
        $.ajax({
            url: 'xxx',
            success: function (data2) {
                ...
            }
        })
    }
})

如果业务更加复杂,需要依次执行多个异步任务,那么这些异步任务就会一层一层嵌套在上一个异步任务成功的回调函数中,我们称之为回调地狱,代码片段如下。

// 第一个异步请求
$.ajax({
    url: 'x',
    success:function (data1) {
        // 第二个异步请求
        $.ajax({
            url: 'xx',
            success: function (data2) {
                // 第三个异步请求
                $.ajax({
                    url:'xxx',
                    success: function (data3) {
                        // 第四个异步请求
                        $.ajax({
                            url: 'xxxx',
                            success: function (data4) {
                                // 第五个异步请求
                                $.ajax({
                                    url: 'xxxxx',
                                    success: function (data5) {
                                        // 第N个回调函数
                                        ...
                                    }
                                })
                            }
                        })
                    }
                })
            }
        })
    }
})

回调地狱会造成哪些问题?

  • 代码可读性差
  • 业务耦合度高,可维护性差
  • 代码臃肿
  • 代码可复用性低
  • 排查问题困难

因为Promise可以避免回调地狱的出现,因此以上问题也是Promise可以解决的问题。

该问题还有其他解决方案吗?

Promise规范推出后,基于该规范产生了许多回调地狱的解决方案,包括ES6原生Promise,bluebird,Q,then.js等。

此处可参考知乎nodejs异步控制「co、async、Q 、『es6原生promise』、then.js、bluebird」有何优缺点?最爱哪个?哪个简单? 不再赘述。

Promise如何使用?

构造函数及API

一个完整的Promise对象包括以下几个部分:

new Promise(function(resolve,reject) {
    ...
    resolve('success_result');
}).then(function (resolve) {
    console.log(resolve); // success_result
}).catch(function (reject) {
    console.log(reject);
});

对象声明主体:方法主体,发起异步请求,返回的成功结果用resolve包裹,返回的失败结果用reject包裹。
then:异步请求成功的回调函数,可以接收一个参数,即异步请求成功的返回结果,或不接收参数。
catch:异步请求失败的回调函数,处理捕获的异常或异步请求失败的后续逻辑,至多接收一个参数,即失败的返回结果。

每个Promise对象包含三种状态:

  • pending:初始状态
  • fulfilled/resolved:操作成功
  • rejected:操作失败

Promise对象的状态无法由外界改变,且当状态变化为fulfilled/resolved或者rejected时,不会再发生变更。

我们也可以构造一个特定状态的Promise对象,如

let fail = Promise.reject('fail');

let success = Promise.resolve(23);

不常用API之Promise.all()
将多个Promise对象包装成一个Promise,如果全部执行成功,则返回所有成功结果的数组,如果有任务执行失败,则返回最先失败的Promise对象的返回结果。
示例:

let p1 = new Promise(function (resolve, reject) {
  resolve('成功');
});

let p2 = new Promise(function (resolve, reject) {
  resolve('success');
});

let p3 = Promse.reject('失败');

Promise.all([p1, p2]).then(function (result) {
  console.log(result); // ['成功', 'success']
}).catch(function (error) {
  console.log(error);
});

Promise.all([p1,p3,p2]).then(function (result) {
  console.log(result);
}).catch(function (error) {
  console.log(error);  // '失败'
})

不常用API之Promise.race()
多个异步任务同时执行,返回最先执行结束的任务的结果,无论成功还是失败。
示例:

let p1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    resolve('success');
  },1000);
});

let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    reject('failed');
  }, 500);
});

Promise.race([p1, p2]).then(function (result) {
  console.log(result);
}).catch(function (error) {
  console.log(error);  // 'failed'
});

Promise支持链式调用

Promise的then方法中允许追加新的Promise对象。
因此回调地狱可以改写为:

var p1 = new Promise(function (resolve, reject) {
    ...
    resolve('success1');
});

var p2 = p1.then(function (resolve1) {
    ...
    console.log(resolve1); // success1
    resolve('success2');
});

var p3 = p2.then(function (resolve2) {
    console.log(resolve2); // success2
    resolve('success3');
});

var p4 = p3.then(...);

var p5 = p4.then(...);

也可以简写为:

new Promise(function (resolve, reject) {
    resolve('success1');
}).then(function (resolve1) {
    console.log(resolve1); // success1
    resolve('success2');
}).then(function (resolve2) {
    console.log(resolve2); // success2
    resolve('success3');
}).then(...);

以上逻辑均表示当接收到上一个异步任务返回的“success${N}”结果之后,才会执行下一个异步任务。
链式调用的一个特殊情况是透传,Promise也是支持的,因为无论当前then方法有没有接收到参数,都会返回一个Promise,这样才可以支持链式调用,才会有下一个then方法。

let p = new Promise(function (resolve, reject) {
    resolve(1);
});

p.then(data => 2)
.then()
.then()
.then(data => {
    console.log(data); //2
});

Promise在事件循环中的执行过程?

Promise在初始化时,代码是同步执行的,即前文提及的对象声明主体部分,而在then中注册的回调函数是一个微任务,会在浏览器清空微任务队列时执行。

关于浏览器中的事件循环请参考宏任务与微任务

Promise升级之async/await的执行过程

ES6中出现的async/await也是基于Promise实现的,因此在考虑async/await代码在事件循环中的执行时机时仍然参考Promise。

function func1() {
    return 'await';
};
let func2 = async function () {
    let data2= await func1();
    console.log('data2:', data2);
};

以上代码可以用Promise改写为:

let func1 = Promise.resolve('await');

let func2 = function (data) {
    func1.then(function (resolve) {
        let data2 = resolve;
        console.log('data2:', data2);
    });
};

从改写后的Promise可以看出 console.log('data2:', data2) 在微任务队列里,因此改写前的 console.log('data2:', data2) 也是在微任务队列中。

由此可推断出下列代码片段中

function func1() {
    console.log('func1');
};
let func2 = async function () {
    let data = await func1();
    console.log('func2');
}

console.log('func2') 也是微任务。

如何手写一个Promise?

首先,Promise对象包含三种状态,pending,fulfilled/resolved,rejected,并且pending状态可修改为fulfilled/resolved或者rejected,此外我们还需要一个变量存储异步操作返回的结果,因此可以得到以下基本代码。

// 定义Promise的三种状态
const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

function Promise(executor) {
    this.state = PENDING;
    this.value = undefined; // 用于存储异步操作的返回结果

    /**
     * 异步操作成功的回调函数
     * @param {*} value 异步操作成功的返回结果
     */
    function resolve(value) {

    }

    /**
     * 异步操作失败的回调函数
     * @param {*} value 异步操作失败的抛出错误
     */
    function reject(value) {

    }

}

module.exports = Promise;

为了增强代码的可读性我们把三种状态定义为常量。

每一个Promise对象都需要提供一个then方法用于处理异步操作的返回值。我们将它定义在原型上。

Promise.prototype.then = function (onFulfilled, onRejected) {
    console.log('then'); // 测试语句
};

此时我们写一段代码来测试这个Promise

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

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

输出

then

因为我们现在还没有对声明Promise对象以及then方法的入参做任何处理,因此pp then都不会打印。
首先我们给Promise的声明中增加代码执行入参。

function Promise(executor) {
    this.state = PENDING;
    this.value = undefined; // 用于存储异步操作的返回结果

    executor(resolve, reject); // 立刻执行

    /**
     * 异步操作成功的回调函数
     * @param {*} value 异步操作成功的返回结果
     */
    function resolve(value) {
    }
    /**
     * 异步操作失败的回调函数
     * @param {*} value 异步操作失败的抛出错误
     */
    function reject(value) {
    }
};

此时测试代码输出为

p
then

接下来我们来完善resolve和reject方法。因为Promise状态只可以由pending变化为resolved或者rejected,且变化后就不可以再变更。因此代码可扩充为:

function Promise(executor) {
    var _this = this;
    this.state = PENDING;
    this.value = undefined; // 用于存储异步操作的返回结果 成功与失败共用一个变量,也可以选择分开

    executor(resolve, reject); // 立刻执行

    /**
     * 异步操作成功的回调函数
     * @param {*} value 异步操作成功的返回结果
     */
    function resolve(value) {
        if(_this.state === PENDING) {
            _this.value = value;
            _this.state = RESOLVED;
        }
    }

    /**
     * 异步操作失败的回调函数
     * @param {*} value 异步操作失败的抛出错误
     */
    function reject(value) {
        if(_this.state === PENDING) {
            _this.value = value;
            this.state = REJECTED;
        }
    }
};

接下来完善then方法,成功时调用注册的成功回调函数,失败时调用注册的失败回调函数。

Promise.prototype.then = function (onFulfilled, onRejected) {
    if (this.state === RESOLVED) {
        if (typeof onFulfilled === 'function') {
            onFulfilled(this.value);
        }
    }

    if (this.state === REJECTED) {
        if (typeof onRejected === 'function') {
            onRejected(this.value);
        }
    }
};

考虑到后续代码逻辑会复杂化,为了减少在各个条件下都去判断onFulfilled和onRejected是否是一个方法的重复代码,代码可再次优化为:

Promise.prototype.then = function (onFulfilled, onRejected) {

    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (onFulfilled) => onFulfilled;
    onRejected = typeof onRejected === 'function' ? onRejected : (onRejected) => {
        throw onRejected;
    };

    if (this.state === RESOLVED) {
        onFulfilled(this.value);
    }

    if (this.state === REJECTED) {
        onRejected(this.value);
    }
};

此时修改测试代码为

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

p.then((value) => {
    console.log('p then', value);
});

输出

p
then
p then success

但是此时我们手写的Promise还不支持异步操作,运行如下测试代码

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

p.then((value) => {
    console.log('p then', value);
});

会发现p then 1并不会输出。这是因为setTimeout使得resolve延迟执行,所以当运行then方法时,state还没有变更为resolved,所以也不会调用onFulfilled方法。
为了解决这个问题,我们可以为成功的回调函数和失败的回调函数各建立一个数组,当执行到then方法时若对象状态还没有发生变化,就将回调函数寄存在数组中,等到状态发生改变后再取出执行。
首先,需要新增两个数组保存回调函数。

function Promise(executor) {
    var _this = this;
    this.state = PENDING;
    this.value = undefined; // 用于存储异步操作的返回结果 成功与失败共用一个变量,也可以选择分开
    this.onFulfilledFunc = []; // 保存成功的回调函数
    this.onRejectedFunc = []; // 保存失败的回调函数

    executor(resolve, reject); // 立刻执行
    ...
};

然后,我们在then方法中增加逻辑,若当前Promise对象还处于pending状态,将回调函数保存在对应数组中。

Promise.prototype.then = function (onFulfilled, onRejected) {
    ...
    if (this.state === PENDING) {
        this.onFulfilledFunc.push(onFulfilled);
        this.onRejectedFunc.push(onRejected);
    }
    
    if (this.state === RESOLVED) {
        ...
    }

    if (this.state === REJECTED) {
        ...
    }
};

保存好回调函数后,当状态改变,依次执行回调函数。

/**
 * 异步操作成功的回调函数
 * @param {Function} value 异步操作成功的返回结果
 */
function resolve(value) {
    if(_this.state === PENDING) {
        _this.value = value;
        _this.onFulfilledFunc.forEach(fn => fn(value));
        _this.state = RESOLVED;
    }
}

/**
 * 异步操作失败的回调函数
 * @param {Function} value 异步操作失败的抛出错误
 */
function reject(value) {
    if(_this.state === PENDING) {
        _this.value = value;
        _this.onRejectedFunc.forEach(fn => fn(value));
        this.state = REJECTED;
    }
}

此时重新执行测试代码,输出了p then 1,至此,我们已经支持了Promise的异步执行。 接下来我们再运行一段代码来测试一下Promise的链式调用。

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

p.then((value) => {
    console.log('p then', value);
    resolve(2);
}).then((value) => {
    console.log('then then ', value);
});

会发现不仅没有输出正确的结果,控制台还有报错。
支持链式调用的核心在于,每一次调用都会返回一个Promise,这样才能支持下一个then方法的调用。
其次,为了支持Promise的链式调用,需要递归比较前后两个Promise并按不同情况处理,此时我们需要分几种情况去考虑:

  • 当前then方法resolve的就是一个Promise -> 直接返回
  • 当前then方法resolve的是一个常量 -> 包装成Promise返回
  • 当前then方法没有resolve -> 视为undefined包装成Promise返回
  • 当前then方法既没有入参也没有resolve -> 继续向下传值,支持透传
  • 当前then方法执行出现异常 -> 调用reject方法并传递给下一个then的reject

接下来我们来改写then方法。

Promise.prototype.then = function (onFulfilled, onRejected) {

    let self = this;
    let promise2; // 用于保存最终需要return的promise对象

    if (this.state === RESOLVED) {
        promise2 = new Promise((resolve, reject) => {
            ...
        })
    }

    if (this.state === REJECTED) {
        promise2 = new Promise((resolve, reject) => {
            ...
        })
    }

    if (this.state === PENDING) {
        promise2 = new Promise((resolve, reject) => {
            ...
        })
    }

    return promise2;
};

抽取出独立的递归函数处理then方法。

/**
 * 根据上一个对象的then返回一个新的Promise
 * @param {*} promise 
 * @param {*} x 上一个then的返回值
 * @param {*} resolve 新的promise的resolve
 * @param {*} reject 新的promise的reject
 */
function resolvePromise(promise, x, resolve, reject) {
    if (promise === x && x !== undefined) {
        reject(new TypeError('发生了循环引用'));
    }
    if (x !== null && (typeof x === 'function' || typeof x === 'object')) {
        // 对象或函数
        try {
            let then  = x.then;
            if (typeof then === 'function') {
                then.call(x, (y) => {
                    // resolve(y);
                    // 递归调用
                    resolvePromise(promise, y, resolve, reject);
                }, (e) => {
                    reject(e);
                })
            } else {
                resolve(x);
            }
        } catch (error) {
            // 如果抛出异常,执行reject
            reject(error);
        }

    } else {
        // 常量等
        resolve(x);
    }
}

在then方法中补充完整逻辑并增加setTimeout支持异步:

Promise.prototype.then = function (onFulfilled, onRejected) {

    let self = this;

    let promise2; // 用于保存最终需要return的promise对象
    ...

    if (this.state === RESOLVED) {
        promise2 = new Promise((resolve, reject) => {
            // 异步执行
            setTimeout(() => {
                try {
                    let x = onFulfilled(self.value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (error) {
                    reject(error);
                }
            })
        })
    }

    if (this.state === REJECTED) {
        promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(self.value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (error) {
                    reject(error)
                }
            })
        })
    }

    if (this.state === PENDING) {
        promise2 = new Promise((resolve, reject) => {
            self.onFulfilledFunc.push(() => {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                })
            });

            self.onRejectedFunc.push(() => {
                setTimeout(() => {
                    try {
                        let x = onRejected(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (error) {
                        reject(error);
                    }
                })
            })
        })
    }
    return promise2;
};

至此,我们手写的Promise就基本可以使用了。

以上是对Promise相关知识的一些整理,其中浏览器的事件循环以及手写Promise也是前端面试中比较重要的考察点,如有错误,欢迎指正。

参考链接

[1] Promise精选面试题
[2] 理解和使用Promise.all和Promise.race
[3] Promise API
[4] 只会用?一起来手写一个合乎规范的Promise
[5] 手写Promise
[6] 【翻译】Promises/A+规范
[7] promise by yuet
[8] [es6-design-mode by Cheemi](

想了解skFeTeam更多的分享文章,可以点这里,谢谢~