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

团队:skFeTeam  本文做者:高扬前端

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

  • 什么是Promise
  • Promise能够解决什么问题
  • 该类问题是否有其余解决方案
  • 如何使用Promise
  • 如何本身实现Promise

什么是Promise?

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

Promise能够解决什么问题?

简单来讲,Promise能够避免出现回调地狱git

什么是回调地狱?

JQuery中发起一个异步请求能够写为:es6

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

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

$.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能够解决的问题。ajax

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

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

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

Promise如何使用?

构造函数及API

一个完整的Promise对象包括如下几个部分: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更多的分享文章,能够点这里,谢谢~

相关文章
相关标签/搜索