是什么?

Promise是JavaScript的一种替换回调的解决方案,绝大多数情况下,是在程序出现异步任务的时候使用他。他诞生于社区,原先只是一种约定俗成的范式,直到 ES6 提供了原生Promise对象的实现,至此Promise成为了JavaScript的语言标准




未来值

在描述Promise是如何改变JavaScript的异步生态之前,我们需要先理解一个概念——未来值

未来值是一个凭证,他意味着某个在将来的某个时间点程序将会获取到的一个值。
试想一下,如果没有这个值的存在,你依然需要异步回调吗?
答案是否定的


这样你会说,不对啊。那串行的动画,难道就不是没有未来值而需要异步的动作吗?一串连贯的动画之间存在顺序关系,但是动画和主线程之间是并行的

如果你这么想,那说明你的问题很严重,因为:


所以我们讨论的前提,就是我们面对的并不是多线程,而是一段不知道会在将来哪个时间点突然执行完成的异步任务。正因为这种不确定性,所以我们需要一个机制,来确保无论这个异步任务在什么时候执行完毕,程序都可以按照我们所预想的那样执行


我们的异步回调,很明显就需要围绕这个不确定什么时候会出现的未来值去操作。而且很显然,我们在回调中需要获取这个未来值,并操作他



回调和未来值

未来值的概念是从JavaScript诞生一来就一直存在的了。比如Ajax中服务器返还的内容,某个定时任务中被修改的状态。就像我们之前说的那样,在promise出现之前,回调是我们处理异步的唯一方式


在回调环境下这么和未来值交互?

回调采用的是一种类似托孤的方式。由于JavaScript引擎没有时间的概念,也没办法直接和宿主平台中的内容打交道。所以JavaScript在把异步任务提交给宿主之后,JavaScript引擎是不知道这段代码会在什么时候,怎么被执行的。而要在未来值出现后回调的函数里面的内容也会在提交异步任务的时候一起被提交给宿主,由宿主在调用了异步任务后,自己去调用回调函数


群居的未来值

一个很关键的问题,未来值未必是孤立的。这个未来值可能会和某个已有的变量有关联;最极端的状况下, 也许未来值会和某个其他未来值存在关联


试想这样一个需求,我们有x和y两个字段,这两个字段我们都需要各自通过一个耗时操作才能得到他们。
最终,在回调函数中,我们需要把x和y相加,得到z。

  1. 我不希望程序因为等待这两个耗时操作而让程序假死,所以获取x和y的动作一定是异步的,而x和y分别是这两个异步任务的返回值(也就是未来值)
  2. x和y各自有不同的异步任务,所以也有不同的回调函数。在两个函数中要共享相同的变量,你必须要升域,把x和y定义于两个回调函数之外,就像这样:
//采用回调的方式实现 x和y相加
function fetchX(xCallback){
    //通过一个耗时操作得到X
    setTimeout(function(){
        xCallback(10);
    },Math.floor(500 * Math.random()));
}

function fetchY(yCallback){
    //通过一个耗时操作得到y
    setTimeout(function(){
        yCallback(5);
    },Math.floor(500 * Math.random()));
}

function add(getX,getY){
    let x,y;

    //回调函数定义
    function xCallback(value){
        x = value;

        if(y !== undefined){
            console.log("我是通过xCallback得到的结果=" + (x + y));
        }
    }

    function yCallback(value){
        y = value;
        if(x !== undefined){
            console.log("我是通过yCallback得到的结果=" + (x + y));
        }
    }

    //调用
    getX(xCallback);
    getY(yCallback);
}

在实际执行的过程中,你会发现光升域是不够的。因为最后的相加动作,必须是在x和y的任务都执行完的情况下才可以执行。那在每一个回调函数里面,我们都需要判断另一个变量是否已经有值了(相当于判断另一个异步任务是否已经执行完了),然后再决定是否继续执行下一步 以此来实现异步任务之间的通讯


这样写可以实现目标效果。可是,这样写优雅吗?

现在还只是x和y,如果是N个对象之间的联动,那岂不是妥妥的回调地狱?



英雄登场

很显然,上面那个问题的答案是否定的。于是,promise登场了
上文的处理方式,虽然可以完成我们的需求。但是他总让我们感觉很复杂,因为我们要在每个回调里面都要去判断可能出现的所有情况。


那有没有可能我们把情况统一呢?
既然我这么问了,那答案就一定是肯定的。对于异步任务来说,可能出现的情况无非就两种,完成 or 未完成


在promise中,我们把 所有的情况都视为未完成,视为将来要发生的事情。用promise来表达刚刚那个例子,那他长这样:

function addByPromise(xPromise,yPromise){
    return Promise.all([xPromise,yPromise]);
}

addByPromise(new Promise(function(resolve,reject){
    setTimeout(function(){
        resolve(10);
    },1000);
}),new Promise(function(resolve,reject){
    setTimeout(function(){
        resolve(5);
    },1000);
}))
.then(function(values){
    //当程序走到这里的时候,事实上JavaScript引擎已经取回了程序的执行权
    console.log("使用Promise实现加法同步,结果为:" + (values[0] + values[1]));
});

关键词:then

显然,在then的回调函数里面的那个value参数,就是我们上面聊的【未来值】。而这个【未来值】其实也就是promise的本质。
promise翻译过来的意思是承诺。而value对于then中的回调函数来说,相当于一个对【未来值】的承诺。promise可以保证你写入的then回调,一定会被执行,并获取到正确的【未来值】。

Promise是怎么做到的呢?


关键词:回调

注意到了吗,我刚刚又提到了这个关键字,回调。

没错,在promise中也有回调。那你就会说了,既然promise中有回调,那就必然会出现因为回调而存在的问题(回调地狱且不提,单说宿主可信任程度的问题)

是的,是存在这些问题,但是promise帮我们解决了。怎么解决的呢?这就涉及到了promise的两个重要特性:状态不可逆&值不可变
promise有三种状态,而相同的api,会根据promise当前状态的不同回应不同的行为:

  1. pending:进行中

    promise一被创建出来,就是pending状态。fulfilled和rejected是同级的,一次异步任务只能是fulfilled/rejected

  2. fulfilled:已成功

    promise的状态是 不可逆 的,只要被切换成fulfilled/rejected之后,就不可能再回到pending中

  3. rejected:已异常

托孤式 的回调不同,promise的回调只做一件事:

  • 如果,异步任务成功执行,并拿到【未来值】,把【未来值】传给JavaScript引擎,引擎会把promise的状态从pending转换成fulfilled
  • 如果,异步任务执行失败,并抛出异常,把异常传给JavaScript引擎,引擎会把promise的状态从pending转换成rejected

无论是哪个分支,当JavaScript引擎拿到 【未来值】or 异常 的时候,他就会重新获得对这个任务的主导权。




为什么promise不需要start函数

细心点你可能会发现,在我们创建promise的时候,其实异步任务就已经开始执行了。


那为什么我们不需要担心我们还没有写入then回调的时候,异步任务就执行完的情况呢?
在Java、C#这些语言的多线程中,我们可是习惯了先把所有的准备动作都做好再开始线程的做法啊?

  • 第一个原因
    你注意到了吗,promise并不是自动执行then回调的,他用了一个resolve函数,然你在异步任务里面显式的调用then回调

  • 第二个原因
    promise的内部构造显然是参考了【状态(State)】的设计模式。相同的api会根据调用时promise不同的状态,执行不同的操作。
    也就是说,当我调用then的时候,promise只可能处于 未完成&已完成 的状态其中一种。

    • 如果,我调用then的时候promise未完成,那么promise会把我写入的回调维护起来,等完成后一起调用
    • 如果,我调用then的时候promise已经完成了,那么promise也不会立刻调用我写入的回调。promise会异步执行我写入的回调




promise和不靠谱的回调

不靠谱的回调

如果说回调地狱带来的困扰勉强还可以克服的话,那么不稳定的宿主环境带来的问题,就是无论如何都不能忽视的了。因为我们对回调的行为的无法确定,所以我们失去了对回调的信任。


不被信任的回调,到底会做出些什么诡异操作?

  • 调用回调过早
  • 调用回调过晚
  • 回调次数不确定
  • 未能传递所需的环境和参数
  • 异常丢失

这些问题是怎么出现的?

调用回调过早

异步任务总是有长有短,他可不会等你把回调托付给宿主后再让自己执行完。这样一来异步任务和回调之间就出现了一种竟态,总会有你还没有写入回调,异步任务就执行完的情况。
这样的话宿主就抓不到回调,从而也就不执行回调了


调用回调过晚

当我们对一个异步任务有多个回调的时候,通常我们是没有办法判断这些回调的执行顺序的。我们知道,对于回调来说,每次触发回调,回调函数都需要到事件循环的结尾重新排队的

回调次数不确定(甚至可能是0次)* & *未能传递所需的环境和参数

这两个问题很常见,会不会出现完全取决于宿主的心情。根据定义,正确的回调调用次数应该是1次。但是有些宿主就是这么叛逆

异常丢失

这里的异常是指异步任务在执行的时候出现的异常
异步任务是在宿主环境被调用的,所以抛出的异常也会先体现到宿主环境中。是否要捕获并处理这些异常,需要你自己在回调中定义



Promise的解决方案

调用回调过早

调用过早根源在于回调函数的输入时间点和异步任务执行完成的时间点存在竟态
而在promise中,只存在“未来”才要完成的异步任务,其中的原理上文已经描述得很清楚了


调用回调过晚

在我们显性调用resolve或者reject触发then之后。剩下的回调已经由JavaScript引擎掌握了主动权了,更何况在ES6之后,还有任务队列的概念。promise是在任务队列中执行的 微任务。根本不需要担心调用过晚的问题
而且,then之间是用链状模式连接的,就像这样:

JavaScript和promise——0_1 promise-LMLPHP

promise可以保证这一段一定输出ABC

但是不同的promise之间的then执行顺序是不确定的,就像这样:

JavaScript和promise——0_1 promise-LMLPHP

这段代码的结果是:A B


回调次数不确定

首先要达成一个共识,没有任何东西(甚至JavaScript错误)可以阻止promise向你履行他的承诺。如果你的promise的定义是完整的(即,带有resolve、reject和catch的任务),那么他一定会调用其中的某一个。

  • 一次都不调用
    除开上面说的所有不确定性以外,还是有一种情况会让promise的回调不执行。那就是你输入的异步操作无法结束。那么promise就永远拿不到【未来值】。这时候一般我们会给promise中的异步任务增加一个“定时器”,当promise超时的时候,就视为异步操作无法结束。就像这样:

    JavaScript和promise——0_1 promise-LMLPHP

    输出:

    JavaScript和promise——0_1 promise-LMLPHP

  • 调用次数过多
    promise只可能通过一次决议,所以不用担心调用次数过多的问题。promise的then回调,有且只会跑一次


未能传递所需的环境和参数

这个问题和上一个问题一起被解决了
而当你有多个【未来值】的时候,promise会把他们组合形成一个数组,一起传给then回调


异常丢失

promise通过在定义异步任务的时候所传入的reject回调来实现对这个问题的处理。
你可以显性的调用reject(就像上面那个例子一样),但是即使你没有那样做,当出现异常的时候,promise也会自动调用reject回调。因为promise拿不到【未来值】
所以除非你没有定义reject,否则promise中就不会出现异常丢失的情况




为什么promise值得被信任

我们使用promise的初衷,是因为我们发现回调不值得被信任。所以我们追求一种新的方式以实现异步回调的效果
但是现在我们发现,就算我们用promise,也没有摆脱回调。promise只是改变了回调的位置和方式





万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【JavaScript笔记】有关的内容

06-27 03:01