Promise原理和问题集锦

参考网站:

大白话讲解Promise(一) - 吕大豹 - 博客园

模拟ES6中的Promise实现,让原理一目了然 - 狂奔的小马扎 - 博客园

ES6关于Promise的用法 - SegmentFault 思否

promise原理

其实,promise就是三个状态。利用观察者模式的编程思想,只需要通过特定书写方式注册对应状态的事件处理函数,然后更新状态,调用注册过的处理函数即可。 

这个特定方式就是then,done,fail,always…等方法,更新状态就是resolve、reject方法。

手写promise

/**
 * Promise类实现原理
 * 构造函数传入一个function,有两个参数,resolve:成功回调; reject:失败回调
 * state: 状态存储 [PENDING-进行中 RESOLVED-成功 REJECTED-失败]
 * then: 同时注册成功和失败处理函数
 * always: 一个处理函数注册到成功和失败
 * resolve: 更新state为:RESOLVED,并且执行成功处理队列
 * reject: 更新state为:REJECTED,并且执行失败处理队列
**/
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

const resolvePromise = (promise2, x, resolve, reject) => {
  // 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise  Promise/A+ 2.3.1
  if (promise2 === x) { 
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  // Promise/A+ 2.3.3.3.3 只能调用一次
  let called;
  // 后续的条件要严格判断 保证代码能和别的库一起使用
  if ((typeof x === 'object' && x != null) || typeof x === 'function') { 
    try {
      // 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候)  Promise/A+ 2.3.3.1
      let then = x.then;
      if (typeof then === 'function') { 
        // 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty  Promise/A+ 2.3.3.3
        then.call(x, y => { // 根据 promise 的状态决定是成功还是失败
          if (called) return;
          called = true;
          // y就是x的返回值
          // 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1
          resolvePromise(promise2, y, resolve, reject); 
        }, r => {
          // 只要失败就失败 Promise/A+ 2.3.3.3.2
          if (called) return;
          called = true;
          reject(r);
        });
      } else {
        // 如果 x.then 是个普通值就直接返回 resolve 作为结果  Promise/A+ 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      // Promise/A+ 2.3.3.2
      if (called) return;
      called = true;
      reject(e)
    }
  } else {
    // 如果 x 是个普通值就直接返回 resolve 作为结果  Promise/A+ 2.3.4  
    resolve(x)
  }
}

class Promise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks= [];

    let resolve = (value) => {
      if(this.status ===  PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onResolvedCallbacks.forEach(fn=>fn());
      }
    } 

    let reject = (reason) => {
      if(this.status ===  PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn=>fn());
      }
    }

    try {
      executor(resolve,reject)
    } catch (error) {
      reject(error)
    }
  }

  then(onFulfilled, onRejected) {
    //解决 onFufilled,onRejected 没有传值的问题
    //Promise/A+ 2.2.1 / Promise/A+ 2.2.5 / Promise/A+ 2.2.7.3 / Promise/A+ 2.2.7.4
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    //因为错误的值要让后面访问到,所以这里也要跑出个错误,不然会在之后 then 的 resolve 中捕获
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    // 每次调用 then 都返回一个新的 promise  Promise/A+ 2.2.7
    let promise2 = new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        //Promise/A+ 2.2.2
        //Promise/A+ 2.2.4 --- setTimeout
        setTimeout(() => {
          try {
            //Promise/A+ 2.2.7.1
            let x = onFulfilled(this.value);
            // x可能是一个proimise
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            //Promise/A+ 2.2.7.2
            reject(e)
          }
        }, 0);
      }

      if (this.status === REJECTED) {
        //Promise/A+ 2.2.3
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e)
          }
        }, 0);
      }

      if (this.status === PENDING) {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e)
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(()=> {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0);
        });
      }
    });

    return promise2;
  }
}

解释:

resolvePromise方法的x参数(成功回调返回值),为对象(可能为promise对象)或非函数类型时直接返回,是函数是使用then.call方法递归

promise使用及方法

复杂的概念先不讲,我们先简单粗暴地把Promise用一下,有个直观感受。那么第一个问题来了,Promise是什么玩意呢?是一个类?对象?数组?函数?

别猜了,直接打印出来看看吧,console.dir(Promise),就这么简单粗暴。

这么一看就明白了,Promise是一个构造函数,自己身上有all、reject、resolve这几个眼熟的方法,原型上有then、catch等同样很眼熟的方法。这么说用Promise new出来的对象肯定就有then、catch方法喽,没错。

那就new一个玩玩吧。

var p = new Promise(function(resolve, reject){

    //做一些异步操作

    setTimeout(function(){

        console.log('执行完成');

        resolve('随便什么数据');

    }, 2000);

});

我们执行了一个异步操作,也就是setTimeout,2秒后,输出“执行完成”,并且调用resolve方法。

Promise的构造函数接收一个参数,是函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。其实这里用“成功”和“失败”来描述并不准确,按照标准来讲,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected。不过在我们开始阶段可以先这么理解,后面再细究概念。

Promise的状态

我们可以把Promise对象看成是一条工厂的流水线,对于流水线来说,从它的工作职能上看,它只有三种状态,一个是初始状态(刚开机的时候),一个是加工产品成功,一个是加工产品失败(出现了某些故障)。同样对于Promise对象来说,它也有三种状态:

  1. pending
    初始状态,也称为未定状态,就是初始化Promise时,调用executor执行器函数后的状态。
  2. fulfilled
    完成状态,意味着异步操作成功。
  3. rejected
    失败状态,意味着异步操作失败。

它只有两种状态可以转化,即

  • 操作成功
    pending -> fulfilled
  • 操作失败
    pending -> rejected

并且这个状态转化是单向的,不可逆转,已经确定的状态(fulfilled/rejected)无法转回初始状态(pending)。

用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:

function runAsync(){

    var p = new Promise(function(resolve, reject){

        //做一些异步操作

        setTimeout(function(){

       if (/* success */) {
            // ...执行代码
            resolve();
        } else { /* fail */
            // ...执行代码
            reject();
        }

        }, 2000);

    });

    return p;            

}

runAsync()

这时候你应该有两个疑问:1.包装这么一个函数有毛线用?2.resolve(‘随便什么数据’);这是干毛的?

我们继续来讲。在我们包装好的函数最后,会return出Promise对象,也就是说,执行这个函数我们得到了一个Promise对象。还记得Promise对象上有then、catch方法吧?这就是强大之处了,看下面的代码:

runAsync().then(function(data){

    console.log(data);

    //后面可以用传过来的数据做些其他操作

    //......

});

在runAsync()的返回上直接调用then方法,then接收一个参数,是函数,并且会拿到我们在runAsync中调用resolve时传的的参数。运行这段代码,会在2秒后输出“执行完成”,紧接着输出“随便什么数据”。

这时候你应该有所领悟了,原来then里面的函数就跟我们平时的回调函数一个意思,能够在runAsync这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。

你可能会不屑一顾,那么牛逼轰轰的Promise就这点能耐?我把回调函数封装一下,给runAsync传进去不也一样吗,就像这样:

function runAsync(callback){

    setTimeout(function(){

        console.log('执行完成');

        callback('随便什么数据');

    }, 2000);

}

runAsync(function(data){

    console.log(data);

});

效果也是一样的,还费劲用Promise干嘛。那么问题来了,有多层回调该怎么办?如果callback也是一个异步操作,而且执行完后也需要有相应的回调函数,该怎么办呢?总不能再定义一个callback2,然后给callback传进去吧。而Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。

链式操作的用法

所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:

runAsync1()

.then(function(data){

    console.log(data);

    return runAsync2();

})

.then(function(data){

    console.log(data);

    return runAsync3();

})

.then(function(data){

    console.log(data);

});

这样能够按顺序,每隔两秒输出每个异步回调中的内容,在runAsync2中传给resolve的数据,能在接下来的then方法中拿到。运行结果如下:

猜猜runAsync1、runAsync2、runAsync3这三个函数都是如何定义的?没错,就是下面这样

function runAsync1(){

    var p = new Promise(function(resolve, reject){

        //做一些异步操作

        setTimeout(function(){

            console.log('异步任务1执行完成');

            resolve('随便什么数据1');

        }, 1000);

    });

    return p;            

}

function runAsync2(){

    var p = new Promise(function(resolve, reject){

        //做一些异步操作

        setTimeout(function(){

            console.log('异步任务2执行完成');

            resolve('随便什么数据2');

        }, 2000);

    });

    return p;            

}

function runAsync3(){

    var p = new Promise(function(resolve, reject){

        //做一些异步操作

        setTimeout(function(){

            console.log('异步任务3执行完成');

            resolve('随便什么数据3');

        }, 2000);

    });

    return p;            

}

在then方法中,你也可以直接return数据而不是Promise对象,在后面的then中就可以接收到数据了,比如我们把上面的代码修改成这样:

runAsync1()

.then(function(data){

    console.log(data);

    return runAsync2();

})

.then(function(data){

    console.log(data);

    return '直接返回数据';  //这里直接返回数据

})

.then(function(data){ 

    console.log(data); // 这个then还是会执行,且data为上面return出来的东西(上方不return则data为undefined),不走runAsync3方法

});

那么输出就变成了这样:

Promise.resolve()

它返回一个Promise对象,状态为fulfilled。但是,当解析时发生错误时,返回的Promise对象将会置为rejected态。

// 参数为普通值
var p4 = Promise.resolve(5);
p4.then(function(data) {
  console.log(data); // 5
});
// 参数为含有then()方法的对象
var obj = {
  then: function() {
    console.log('obj 里面的then()方法');
  }
};

var p5 = Promise.resolve(obj);
p5.then(function(data) {
  // 这里的值时obj方法里面返回的值
  console.log(data); // obj 里面的then()方法
});


// 参数为Promise实例
var p6 = Promise.resolve(7);
var p7 = Promise.resolve(p6);

p7.then(function(data) {
  // 这里的值时Promise实例返回的值
  console.log(data); // 7
});

// 参数为Promise实例,但参数是rejected态
var p8 = Promise.reject(8);
var p9 = Promise.resolve(p8);

p9.then(function(data) {
  // 这里的值时Promise实例返回的值
  console.log('fulfilled:'+ data); // 不执行
}).catch(function(err) {
  console.log('rejected:' + err); // rejected: 8
});

Promise.reject()

Promise.reject()和Promise.resolve()正好相反,它接收一个参数值reason,即发生异常的原因。此时返回的Promise对象将会置为rejected态。如下:

var p10 = Promise.reject('手动拒绝');
p10.then(function(data) {
  console.log(data); // 这里不会执行,因为是rejected态
}).catch(function(err) {
  console.log(err); // 手动拒绝
}).then(function(data) {
 // 不受上一级影响
  console.log('状态:fulfilled'); // 状态:fulfilled
});

always的用法

es6-promise-always正是对ES6的功能做了一个扩充,使其支持always,并同时支持node和browser.

表示无论成功或失败时都要做一些事.

1.安装

1

npm install es6-promise-always --save

2.引入使用

1

2

3

4

5

6

7

require("es6-promise-always")

axios.get("/").then(()=>{

 //处理逻辑

}).always(()=>{

 console.log("请求结束")

 hideLoading();

})

always(data, error)

  • data: resolve的数据。
  • error: reject的数据。

catch的用法

我们知道Promise对象除了then方法,还有一个catch方法,它是做什么用的呢?其实它和then的第二个参数一样,用来指定reject的回调,用法是这样:

getNumber()

.then(function(data){

    console.log('resolved');

    console.log(data);

})

.catch(function(reason){

    console.log('rejected');

    console.log(reason);

});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:

getNumber()

.then(function(data){

    console.log('resolved');

    console.log(data);

    console.log(somedata); //此处的somedata未定义

})

.catch(function(reason){

    console.log('rejected');

    console.log(reason);

});

在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:

也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能。

all的用法

Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。我们仍旧使用上面定义好的runAsync1、runAsync2、runAsync3这三个函数,看下面的例子:

Promise

.all([runAsync1(), runAsync2(), runAsync3()])

.then(function(results){

    console.log(results);

});

用Promise.all来执行,all接收一个数组参数,里面的值最终都算返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。那么,三个异步操作返回的数据哪里去了呢?都在then里面呢,all会把所有异步操作的结果放进一个数组中传给then,就是上面的results。所以上面代码的输出结果就是:

有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很酷?有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

race的用法

all方法的效果实际上是「谁跑的慢,以谁为准执行回调」(全部完成才回调),那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」(有一个完成就开始回调),这就是race方法,这个词本来就是赛跑的意思。race的用法与all一样,我们把上面runAsync1的延时改为1秒来看一下:

Promise

.race([runAsync1(), runAsync2(), runAsync3()])

.then(function(results){

    console.log(results);

});

这三个异步操作同样是并行执行的。结果你应该可以猜到,1秒后runAsync1已经执行完了,此时then里面的就执行了。结果是这样的:

你猜对了吗?不完全,是吧。在then里面的回调开始执行时,runAsync2()和runAsync3()并没有停止,仍旧再执行。于是再过1秒后,输出了他们结束的标志。

function runAsync1() {

        var p = new Promise(function (resolve, reject) {

            //做一些异步操作

            setTimeout(function () {

                console.log('异步任务1执行完成');

                resolve('随便什么数据1');

            }, 3000);

        });

        return p;

    }

    function runAsync2() {

        var p = new Promise(function (resolve, reject) {

            //做一些异步操作

            setTimeout(function () {
                console.log('异步任务2执行完成');
                resolve('随便什么数据2');
            }, 2000);

        });


    Promise.race([runAsync1(),runAsync2()])

        .then(function (data) {

            console.log(data); // 只执行第一个返回resolve的promise对象

        })

        .catch(function(reason){
            console.log(reason);
        })

        return p;

    }

结果

这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:

//请求某个图片资源

function requestImg(){

    var p = new Promise(function(resolve, reject){

        var img = new Image();

        img.onload = function(){

            resolve(img);

        }

        img.src = 'xxxxxx';

    });

    return p;

}

//延时函数,用于给请求计时

function timeout(){

    var p = new Promise(function(resolve, reject){

        setTimeout(function(){

            reject('图片请求超时');

        }, 5000);

    });

    return p;

}

Promise

.race([requestImg(), timeout()])

.then(function(results){

    console.log(results);  // 只执行第一个返回resolve的promise的对象

})

.catch(function(reason){

    console.log(reason); // 即使有一个出现了reject,其他的promise依然会跑完

});

requestImg函数会异步请求一张图片,我把地址写为”xxxxxx”,所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。运行结果如下:

总结

ES6 Promise的内容就这些吗?是的,能用到的基本就这些。

我怎么还见过done、finally、success、fail等,这些是啥?这些并不在Promise标准中,而是我们自己实现的语法糖。

本文中所有异步操作均以setTimeout为例子,之所以不使用ajax是为了避免引起混淆,因为谈起ajax,很多人的第一反应就是jquery的ajax,而jquery又有自己的Promise实现。如果你理解了原理,就知道使用setTimeout和使用ajax是一样的意思。说起jquery,我不得不吐槽一句,jquery的Promise实现太过垃圾,各种语法糖把人都搞蒙了,我认为Promise之所以没有全面普及和jquery有很大的关系。后面我们会细讲jquery。

问题集锦

1、then,catch 返回的是什么?

因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回promise 对象, 所以它们可以被链式调用。

2、如图的返回结果是?

3catch方法跟reject回调的区别

catch和then的第二个参数一样,用来指定reject的回调,用法是这样:

getNumber()

.then(function(data){

    console.log('resolved');

    console.log(data);

})

.catch(function(reason){

    console.log('rejected');

    console.log(reason);

});

catch效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中(并且后续的then不执行 )。

4all方法和race方法的区别

all方法全部完成才回调,race有一个完成就开始回调

5$.ajax和promise的区别

jquery Promise和ES6 Promise的区别 - 看风景就 - 博客园

query的Promise对象是一个受限的Deferred对象,即没有resolve和reject方法的对象

通过deferred.promsie()可以返回一个jquery版本的promise对象,其实就是一个去除了resolve和reject方法的deferred对象,

目的是防止意外的修改了promise对象的状态,影响真正的异步操作

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值