Promise

Promise

写JavaScript的都是好人,因为渣男的字典里没有Promise

借着高程4,把Promise好好看一下,以前都是浅尝辄止,能用就行,对于发送请求都使用axios库,没有留意具体代码的编写,这次跟着书来仔细总结一下。

JavaScript的异步模型

高程4里提到的一点非常关键,就是异步代码会生成一个消息压入JS的单线程事件消息队列中,虽然不知道具体什么时候得到执行,但一定会在当前线程的同步执行代码之后,我尝试了一个例子,果然发现以前没有注意到这句话:

    let x = 3;
    console.log(x + 1);

    setTimeout(() => {
        console.log(100);}, 0);

    console.log(x + 2);
    console.log(x + 3);

这个setTimeout中设置的延时是0,如果仅仅从字面意思去理解,则是立刻执行,然而并没有,程序运行的结果是:

4
5
6
100

这就说明setTimeout这行代码向消息队列中放入了一个消息,这个消息要等到当前线程的同步代码执行完毕之后,才会被取出来执行。

想要获得异步执行完的x的值来进行下一步处理,很显然就不能在当前线程中等着x改变,永远也等不到。必须依赖消息队列中的回调函数,假如想让x增加100之后再使用x,那必须让setTimeout执行一个回调函数才行:

    let x = 3;

    function printX(x) {
        console.log(x);
    }

    console.log(x + 1);

    setTimeout(() => {
        x = x + 100;
        printX(x);}, 0);

    console.log(x + 2);
    console.log(x + 3);

此时结果是

4
5
6
103

可以看到,x的值想要被使用,要依赖回调函数。如果回调过多,就构成回调地狱。

Promise

Promise用高程4的话说,是对尚不存在结果的一个替身,这个立刻让我想起了之前看Java多线程编程的时候用到的Future对象。Future对象也是异步运行代码的一个入口,不会阻挡接下来代码的执行,在适当的时候去Future内获取数据。ES6就使用了Promise这样一个类型,来作为这种机制的实现。

Promise的状态

既然是对象,就可以用new来实例化看看,这个实例化必须传入一个函数,叫做执行器函数

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

不传入执行器函数就会报错,打印的结果如下:

Promise {<pending>}

后边的pending其实是Promise的三种状态之一,三种状态分别是:

  1. 待定(pending
  2. 兑现、解决(fulfilled,resolved
  3. 拒绝(rejected

新创建一个期约,执行器函数中不做任何动作,期约的状态就是pendingpending状态的期约可以转换成为解决或者拒绝状态,一旦一个期约达到了解决或者拒绝状态,其状态就不能再被改变。

Promise的状态在自身内部改变

既然有三种状态,如何控制呢,由于异步操作的特性,在同步代码里去改变Promise的状态是没用的,Promise需要在其自身代码中改变状态,这就需要在执行器函数中进行操作。这里特别需要注意的是,执行器函数的代码是同步的,这是因为期约在初始化的时候就会运行执行器函数

    console.log(1)
    let p1 = new Promise((resolve, reject) => {
        console.log(10);
        console.log(11);
        console.log(12);
    });
    console.log(2);
    console.log(3);

这一段打印的结果如下:

1
10
11
12
2
3

说明创建Promise对象和执行器函数中的代码,都是同步在当前线程完成的。

通过执行器函数控制状态

执行器函数有两个固定的参数,第一个是resolve,第二个是reject,这代表两个函数,只要执行其一,就会将当前Promise的状态改变到解决或者拒绝状态,比如:

    console.log(1)
    let p1 = new Promise((resolve, reject) => {
        resolve()
    });
    console.log(2)
    console.log(p1);
    console.log(3);

打印出的结果是:

1
2
Promise {<fulfilled>: undefined}
3

这是由于执行器函数同步执行,会立刻将p1的状态改变成<fulfilled>,进而在后边打印出来。

如果将状态的修改放到另外一个线程里稍后改变:

    console.log(1)
    let p1 = new Promise((resolve, reject) => {
        setTimeout(resolve, 0);
    });
    console.log(2)
    console.log(p1);
    console.log(3);

这里打印出来是:

1
2
Promise {<pending>}
3

这就符合之前说的异步代码需要在当前同步代码执行完毕之后再执行的特点。
如果之前使用的是reject,会看到浏览器控制台中打印出一个Uncaught (in promise)错误,这个错误不能使用try/catch,只能使用拒绝处理函数来处理,这是因为这个错误不是在当前线程抛出的,而是进入了异步消息队列。

通过Promise类的静态方法控制状态

Promise类有两个静态方法,可以直接生成这两个状态的Promise对象,也就是如下两个静态方法:

  1. Promise.resolve()
  2. Promise.reject()

看一个例子:

    console.log(1)
    let p1 = Promise.reject();
    let p2 = Promise.resolve();
    console.log(2)
    console.log(p1);
    console.log(2.5);
    console.log(p2);
    console.log(3);

打印出来是:

1
2
Promise {<rejected>: undefined}
2.5
Promise {<fulfilled>: undefined}
3

这两个静态方法的区别需要了解期约的解决值拒绝理由

Promise的解决值和拒绝理由

期约封装的是异步操作,这些异步操作可能会成功的计算出一个值,然后改变期约状态至解决,也可能由于各种情况而失败,然后改变期约状态至拒绝,这两种情况下肯定需要获取结果或者失败的理由,默认的情况下这两个东西都是undefined

如果要获取特定的结果,实际上就要把解决值传递给执行器函数的resolve()函数作为参数,把失败理由传递给执行器函数的reject()函数作为参数。
对于Promise类的两个静态方法,也是如此。区别在于Promise.resolve()的参数如果也是一个期约,会类似一个空包装,返回其中的参数,只有传入其他类型才会被解析为解决值。

注意之前打印出来的结果,Promise {<rejected>: undefined}中花括号内部冒号后边的地方,就是解决值或者拒绝理由,可以用简单的代码试验一下:

    let p1 = Promise.reject(3);
    let p2 = Promise.resolve("cony");
    console.log(p1);
    console.log(p2);

打印出的结果是:

Promise {<rejected>: 3}
Promise {<fulfilled>: "cony"}

不过究竟要如何使用这个解决值和拒绝理由,来进行进一步的操作呢,对于一个期约,需要给其添加处理程序,这就需要用到Promise实例方法。

then()方法处理解决值和拒绝理由

期约在自己的状态发生改变的时候,提供解决值和拒绝理由,对于所有的期约对象,都实现了then()方法,在这个方法中,可以传递两个函数作为参数,第一个函数参数用于处理解决值,第二个用于处理拒绝理由,这两个函数的位置是固定的,所以仅仅想处理拒绝理由的话,第一个函数参数要传入null。传递给then()的任何类型为非函数类型的参数都会被自动忽略。

此外,也就是我之前没有仔细看到的非常重要的知识,then()中的处理函数,会自动接收到Promise的解决值和拒绝理由,而且处理函数只会在Promise有了两个确定的状态时候才会触发:

    function onResolve(id) {
        setTimeout(console.log, 0, id, 'resolved');
    }

    function onReject(id) {
        setTimeout(console.log, 0, id, 'rejected');
    }

    let p1 = new Promise(() => {});

    p1.then(onResolve, onReject);

这段代码执行后什么也没有,因为Promise的状态还是pending,但如果尝试来改变一下状态就知道了:

    let p1 = new Promise((resolve,reject) => {
        resolve("resolve value");
    });
    p1.then(onResolve, onReject);

就会立刻打印出resolve value resolved,可见状态改变才能触发处理函数,即使改用异步,若干秒之后再触发也可以:

    let p1 = new Promise((resolve,reject) => {
        setTimeout(()=>{
            resolve("resolved in 4s");
        }, 4000);
    });

    p1.then(onResolve, onReject);

其实有了这个最基本的模式,就可以用来做很多事情了。不过then()还有一个特性,就是也会返回一个Promise对象,其中包含的是处理函数的返回值,这个值会继续向下传递,这就让then()可以很方便的连锁使用,针对异步返回的值,由于每一次都有两个处理函数,因此可以成为一个二叉树选择操作:

    function onResolve(id) {
        return id + ' first';
    }

    function onReject(id) {
        return id + ' first reject';
    }

    let p1 = new Promise((resolve,reject) => {
        setTimeout(()=>{
            reject("resolved in 4s");
        }, 4000);
    });

    function onAny(id) {
        console.log(id);
    }

    p1.then(onResolve, onReject).then(onAny, onAny);

在p1达成reject之后,会把"resolved in 4s"向后传递到onReject函数中,onReject函数返回的结果是 "resolved in 4s" + ' first reject',这个结果会再向后传递到onAny函数中并且最终被打印出来,实际执行这段代码,就会在4秒左右输出结果:

resolved in 4s first reject

如果处理函数没有返回值,then()会包装undefined来返回一个Promise对象;如果处理函数中抛异常,就会返回一个rejected状态的Promise,而如果只是返回一个错误对象,那依然是resolved状态;处理reject状态函数的返回结果,也会被包装成一个resolved状态的Promise,这是因为这个函数本来就是处理reject状态的:

    function onResolve(id) {
        return Error("Error occured onResolve");
    }

    function onReject(id) {
        throw "Error occured onReject";
    }

    let p1 = new Promise((resolve,reject) => {
        setTimeout(()=>{
            resolve("resolved in 4s");
        }, 4000);
    });


    function onLastResolve(id) {
        console.log(id + " lastResolve");
    }

    function onLastReject(id) {
        console.log(id + " lastReject");
    }

    p1.then(onResolve, onReject).then(onLastResolve, onLastReject);

这里添加了一系列处理方法:

                       onLastResolve
                     /
           onResolve
        /            \
Promise                onLastReject 
        \            /
           onReject 
                     \
                      onLastResolve

具体的执行顺序是:

  1. Promise 4秒钟后变成resolve状态,解决值是"resolved in 4s"
  2. then()中的onResolve方法触发,接受的参数是"resolved in 4s",返回一个Error对象
  3. then()返回一个resolved状态的Promise,解决值是刚才的Error对象
  4. 触发第二个then()中的onLastResolve函数执行,打印出Error对象+" lastResolve"

这里可以自行尝试把开始的Promise对象,和中间的onResolveonReject函数更改成返回各种异常来试验一下。

看到这个地方,我算是彻底掌握了Promise的基本模式,对于那些异步代码也了解的更加清晰了。

catch()方法添加拒绝处理程序

这个方法仅仅接收一个参数,就是作为拒绝处理程序的函数,这个玩意实际上就是一个语法糖,相当于then()方法给第一个参数传入null,这个方法既然是语法糖,所以和then()一样都返回一个Promise对象,所以可以接着继续处理:

    function onResolve(id) {
        return Error("Error occured onResolve");
    }

    function onReject(id) {
        console.log(id);
    }

    let p1 = new Promise((resolve,reject) => {
        setTimeout(()=>{
            reject("resolved in 4s");
        }, 2000);
    });

    p1.catch(onReject);

如果将Promise设置为resolve状态,onReject就不会执行。

finally()方法添加必定处理程序

这个一般用来清理,把要清理的代码放入其中即可,一般不会在这个里边处理期约的状态,因为在其中只会原样传递期约,无法了解期约的状态:

    function onFinally() {
        console.log("Finally");
    }

    p1.then(onResolve, onReject)
        .then(onLastResolve, onLastReject)
        .finally(onFinally);

有了then()之后的异步模型

之前知道了,使用setTimeout等函数开启的异步代码,会一直到当前线程同步代码执行完毕之后才会执行,Promise中的执行器代码也属于当前线程的同步代码。

有了then()之后,在执行then()的时候,并不会立刻要求Promise达到某个状态并执行处理程序,依然是传递一个异步消息到消息队列中,当前的同步代码依旧要执行完毕才可以,哪怕Promise已经达到了then()中所需要的状态,也就是说本质上then()setTimeout没有区别,看几个例子:

    let p1 = Promise.resolve();

    p1.then(() => console.log("resolved"),
        () => console.log("rejected"));

    console.log(1);
    console.log(2);

打印出的结果是:

1
2
resolved

先添加处理程序,再创建Promise也是一样,所有的then()catch()finally()中的处理程序都符合这个要求,即只是排一个消息,然后继续执行之后的同步代码,同步代码执行完毕之后,消息队列中的消息才会被处理。

到了这里,基本上就能够自如的使用Promise了,异步代码也变得更加好懂了,后边的期约连锁和合成也都是在这个模式上的继续变化。下一次就来看看async/await这对兄弟。

LICENSED UNDER CC BY-NC-SA 4.0
Comment