深入解读JS承诺模式--Promise

351 阅读8分钟

Promise是什么

Promise是ES6中新增的内置类,主要作用是用来规划异步编程

let p = new Promise([executor])
  • Promise 是一个内置类
  • p 是类的一个实例
  • [executor] 可执行的回调函数(必须传递)

回调地狱问题

回调函数就是将一个函数作为参数,传递给另一个函数,在函数执行的时候,将传入的回调函数进行处理

  • 回调地狱:回调函数嵌套回调函数
let callName = function(name,callback){
    setTimeout(()=>{
        console.log(name)
        callback();
    },1000)
}
callName('first',()=>{
    callName('second',()=>{
        callName('third',()=>{
            console.log('end')
        })
    })
})
// 输出: first second third end
  • 这个问题在AJAX异步请求数据上尤为常见

当多个请求之间有相互依赖关系时,上一个请求完成执行才能执行下一个请求,这就是AJAX请求串行

// 以 jQuery 的 ajax 方法为例
let data = {};
$.ajax({
    url: '/api/info',
    success: function (result) {
        // 获取数据成功后执行回调函数
        // result就是从服务器获取的结果
        data = result;

        $.ajax({
            url: `/api/score?id=${data.id}`,
            success: function (result) {

                $.ajax({
                    url: `/api/ranking?val=${result.chinese}`,
                    success: function (result) {
                
                    }
                });
            }
        });
    }
});
// 上述代码需要在拿到`个人信息`后,再拿`成绩`,再拿语文`成绩排名`
  • 基于Promise解决上述回调地狱的问题
function queryInfo() {
    return new Promise(resolve => {
        $.ajax({
            url: '/api/info',
            success: function (result) {
                resolve(result);
            }
        });
    });
}

function queryScore(id) {
    return new Promise(resolve => {
        $.ajax({
            url: `/api/score?id=${id}`,
            success: function (result) {
                resolve(result);
            }
        });
    });
}

function queryPaiMing(val) {
    return new Promise(resolve => {
        $.ajax({
            url: `/api/ranking?val=${val}`,
            success: function (result) {
                resolve(result);
            }
        });
    });
}

// 基于Promise解决回调地狱
queryInfo().then(result => {
    return queryScore(result.id);
}).then(result => {
    return queryPaiMing(result.chinese);
}).then(result => {
    // 获取的排名信息
});

Promise类

Promise是类所以需要通过new关键字执行,并传入一个executor可执行的回调函数作为参数

Promise是异步的?

如果Promise是异步的,那么下面的例子中执行的结果就是 "2 1" ,但是执行结果却是"1 2"

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

从上述代码我们可以总结出,Promise本身是同步的,但是他是用来管理异步编程的,new Promise的时候会立即将executor函数执行

Promise实例的属性

那为什么有些人会认为Promise是异步的呢?

我们将p1在控制台中打印出来,会发现这个Promise实例上除了原型链之外还有两个私有属性,分别是[[PromiseState]]以及[[PromiseResult]]

  • [[PromiseState]]:用于存储Promise实例状态
    • pending 初始状态
    • fulfilled/resolved 成功状态(一般指的是异步请求成功,在不同浏览器版本中,成功返回的值不同,但是不会影响结果)
    • rejected 失败状态(一般指的是异步请求失败)
  • [[PromiseResult]]:Promise结果或者值
    • 初始值是undefined
    • 不论成功的结果还是失败的原因都会存储在这个值里面

executor可执行回调

  • 第一个参数:resolve函数--函数执行会修改Promise实例的状态为成功fulfilled/resolve
  • 第二个参数:reject函数--函数执行会修改Promise实例的状态为失败reject
  • 通常在executor函数中管理一个异步操作,异步操作成功执行resolve函数,让实例状态修改成成功,并且获取成功的结果;异步操作失败执行reject函数,让实例状态修改成失败,并获取失败原因
  • 只要Promise实例的状态从pending变为fulfilled或者rejected则状态就不能再改变了,即实例状态是不可逆
// new Promise(executor)
let p1 = new Promise((resolve,reject)=>{
    resolve(10)
})

Promise.prototype

  • then([A],[B]):基于then方法存放两个回调函数A/B,当Promise状态为成功时执行A,状态为失败时执行B,并将[[PromiseResult]]的值传递给对应函数
  • catch
  • finally

then

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

then方法也是同步操作,但是then的callback函数是异步的,then方法执行可以理解为将then中的callback函数存储在p1实例中,在Promise中resolve函数执行之后会通知存储在p1实例中对应的方法执行

执行then方法会返回一个新的Promise实例

let p1 = new Promise((resolve,reject)=>{
   setTimeout(()=>{
        let ran = Math.random();
        ran < 0.5 ? reject("NO") : resolve("OK");
   },1000);
});
let p2 = p1.then(A=>{
    console.log("成功"+A);
    return A + "1"
},B=>{
    console.log("失败"+B);
    return B + "0"
});
  • 执行then方法是为了将传递给then的成功的回调以及失败的回调存储在p1
  • 同时返回一个新的Promise实例 p2
    • 新实例p2的状态是由上一个实例基于then方法存放的A/B函数所决定
    • 不论是A还是B执行,只要执行不报错,则p2的状态就是成功只要报错状态就是失败
    • p2的结果是A/B函数执行的返回值,或者是报错的失败原因
    • 特殊情况:如果A/B返回的是一个新的Promise实例,则返回的Promise实例的成功失败以及结果,直接决定p2的状态和结果
  • p1的成功和失败会受到executor执行是否报错影响,执行报错,则p1的状态是失败的,PromiseResult的值就是失败的原因;如果执行不报错,再看执行的是resolve还是reject
new Promise((resolve, reject) => {
    // resolve(10);         //=>执行resolve函数时输出  10  100  1000
    reject(20);             //=>执行reject函数时的输出  20  2  20
}).then(result => {
    console.log("成功:" + result);
    return result * 10;
}, reason => {
    console.log("失败:" + reason);
    return reason / 10;
}).then(result => {
    console.log("成功:" + result);
    return Promise.reject(result * 10);
}, reason => {
    console.log("失败:" + reason);
    return reason / 10;
}).then(result => {
    console.log("成功:" + result);
    return result * 10;
}, reason => {
    console.log("失败:" + reason);
    return reason / 10;
}); 

resolve函数执行时会一直执行then中传入的第一个回调函数,而reject函数执行的时候将Promise实例的状态修改成失败,所以在第一个then中会执行传入的第二个函数,而第二个函数没有报错,则会直接将新的Promise实例的状态修改成成功,而后的所有then方法都是执行传入的第一个callback

then的顺延

then([A],[B]):如果其中一个函数没有传递,则会顺延,即[A]函数没有传递则顺着then链找下一个then的[A];,即[B]函数没有传递则顺着then链找下一个then的[B];

Promise.resolve(10).then(null, reason => {
    console.log("失败:" + reason);
    return reason / 10;
}).then(result => {
    console.log("成功:" + result);     //=> 成功:10
    return result * 10;
}, reason => {
    console.log("失败:" + reason);
    return reason / 10;
})

当then中[A]没有传递,他会默认 result=>{ return Promise.resolve(result); },当then中[B]没有传递,他会默认 reason=>{ return Promise.reject(reason); }

catch

明白了顺延之后,catch就相当于 then( null , reason=>{ ... } ),也就是then不传第一个参数。

// 真实项目中:then只是处理成功的  catch处理失败(一般写在最后)
Promise.reject(10).then(result => {
    console.log(`成功了 -> ${result}`);
}).catch(reason => {
    console.log(`失败了 -> ${reason}`);
});

在then链中只要报错的位置,和catch之间的then中没有传递第二个callback,都会直接将错误的原因顺延到catch,所以我们在真实项目中一般不会在then中传递第二个callback,防止传递之后将异常拦截,造成后期没必要的麻烦

返回失败但是没有失败处理

浏览器会在控制台报错,但是该错误不会阻塞代码的执行

Promise.reject(10).then(result=>{
    console.log(`成功了--${result}`);
    return result * 10;
})
setTimeout(()=>{
    console.log(1)
},1000)

直接在then链的尾部加一个catch(reson=>{})就可以解决这个报错

Promise的静态方法

  • resolve 返回一个状态为成功的Promise实例
  • reject 返回一个状态为失败的Promise实例
  • all管理多个promise实例
    • 当所有实例都为成功,Promise.all返回的总状态的实例才是成功的,结果是一个数组,按照顺序依次存储每一个实例成功的结果
    • 当有一个实例的结果是失败的,总实例的结果也是失败的,结果是当前实例失败的原因
  • race 管理多个promise实例,以返回运行最快的那个实例的结果,不过是成功还是失败

手写Promise

想要深入理解Promise,Promise的源码就一定要清楚

  • Promise类
function myPromise(executor){
    // 参数验证,保证executor是一个可执行的函数
    if(typeof executor !== 'function') throw new TypeError("myPromise resolver undefined is not a function")
    
    // 实例上挂在的属性
    this.myPromiseState = 'pending';
    this.myPromiseValue = undefined;
    this.resolveFunc = function(){};
    this.rejectFunc = function(){};
    
    var _this = this;
    var change = function(state,value){
        //实例的状态不为pending的时候才修改状态
        if(_this.MyPromiseState !== "pending") return;
        _this.MyPromiseState = state;
        _this.MyPromiseValue = value;
        //通过定时器来模拟异步的效果
        let timer = setTimeout(()=>{
            _this.MyPromiseState === "fulfilled" ?
            _this.resolveFunc(_this.MyPromiseValue):
            _this.rejectFunc(_this.MyPromiseValue);
        },0)
    }
    
    var resolve = function(){
        change("fulfilled",result);
    };
    var reject = function(){
        change("rejected",reson);
    };
    
    // 立即执行 executor 函数
    try{
        executor(resolve,reject);
    }catch(err){
        reject(err)
    }
}
  • myPromise.resolve/reject
MyPromise.resolve = function resolve(result){
    return new MyPromise(resolve=>{
        resolve(result);
    })
}
MyPromise.reject = function reject(reason){
    return new MyPromise((_,reject)=>{
        reject(reason);
    })
}
  • myPromise.prototype.then()
    • 每一次执行then方法都会返回一个新的Promise实例
    • 上一个Promise实例的resolve/reject的执行,控制新返回的实例的成功和失败
MyPromise.prototype.then = function then(resolveFunc,rejectFunc){
        //实现顺延效果
        if(typeof resolveFunc !== "function"){
            resolveFunc = function resolveFunc(result){
                return MyPromise.resolve(result);
            }
        }
        if(typeof rejectFunc !== "function"){
            rejectFunc = function rejectFunc(reason){
                return MyPromise.reject(reason);
            }
        }

        return new myPromise((resolve,reject)=>{
             //执行不报错,返回新实例的状态是成功的(如果返回值是新Promise实例,则该实例的状态的成功和失败决定了返回实例的状态)
             _this.resolveFunc = function(result){
                try {
                    var x = resolveFunc(result);
                    x instanceof Promise ?
                        x.then(resolve,reject) :
                        resolve(x);
                } catch (err) {
                    reject(err)
                }
            }

            _this.rejectFunc = function(reason){
                try {
                    var x = rejectFunc(reason);
                    x instanceof Promise ?
                        x.then(resolve,reject) :
                        resolve(x);
                } catch (err) {
                    reject(err)
                }
            }
           
        })
    }
  • myPromise.prototype.catch()

catch就相当于 p1.catch([function]) => p1.then(null,[function])

MyPromise.prototype.catch = function catch(rejectFunc){
	return this.then(null,rejectFunc)
}
  • MyPromise.all
    • MyPromise.all返回的也是MyPromise实例p
    • 数组中的每个MyPromise实例都是成功的,最后p才是成功
    • 数组中只要有一个实例是失败的,最后p都是失败
    • 如果最后都是成功,p的MyPromiseValue存储的也是一个数组按照之前存放MyPromise实例的顺序,存储每一个实例的结果
myPromise.all = function all(promiseAry){
    return new myPromise((resolve,reject)=>{
        let result = [],
        	index = 0;
        let fire = function fire(){
            if(index === promiseAry.length){
                resolve(result);
            }
        }
        for(let i = 0; i < promiseAry.length; i++){
            let item = promiseAry[i];
            if(!(item instanceof myPromise)){
                result[i] = item;
                index++;
                fire();
                return;
            }
            item.then(result=>{
                result[i] = result;
                index++;
                fire();
            },reason=>{
                reject(resson);	// 只要有一个函数是失败的,整体结果就是失败的
            })
        }
    })
}