美文网首页
Promise从入门到精通

Promise从入门到精通

作者: 广州芦苇科技web前端 | 来源:发表于2019-01-14 15:15 被阅读0次

    标签: Promise


    在ES6之前的JavaScript中处理异步的方法就是使用回调函数,当我们不知道一件事情会在什么时候结束,但是又希望在这件事情结束之后再去做一下其他的操作时,我们在这件事情执行之前就先规定好调用事情执行完之后再执行的操作,这也就是所谓的回调,在执行函数之前先告诉这个函数执行完之后下一步需要做什么。

    回调函数在异步操作相对较少的情况下使用起来确实不会有太大的问题,但是如果异步操作增多时,由于异步的不可预知性,那么整个代码的可读性就会比较差,因为无法清楚的知道代码实际的运行情况,同时多个异步之间的顺序性也是无法保证的。这也就是使用回调表达程序和管理并发的两个主要缺陷:缺乏顺序性和可信任性。

    什么是Promise

    关于promise的api在理解上并没有特别的困难,但是我们在学习一个新的工具的时候还是应该先理解其中更深层次的原理与概念,先从这一步来解释什么是promise,才能够更好的是使用promise。

    未来值

    以一个我们日常在饭店吃饭的例子来类比就是:当我们去一个快餐店吃饭,点了一个青椒炒肉,给收银员付了15元,很明显饭店的菜都是现炒的,我们不可能马上就拿到我们的午饭,这个时候收银员给了我一个小票,当我的青椒炒肉做好的时候,我可以凭借这个小票拿到它,这个小票就是我能够拿到午饭的依据,店员承诺(promise)拿着这个小票可以取到一份青椒炒肉。

    只要我好好的保存这个小票,我就不用担心吃不上午饭了,因为它代表了我未来的午饭。

    当然在这个等待的过程中,我还可以去做一些其他的事情,比如刷一下知乎。

    尽管我现在还没有拿到我的午饭,但是我有了这个小票,它相当于我的午饭的占位符,从本质上来讲,这个占位符使得这个值不再依赖时间。这是一个未来值。

    当服务员叫到我的订单号时,我拿着我的小票就能够换回我的午餐——青椒炒肉,也就是说只要我的午餐已经炒好了,那么我就能够用店员当初给我的承诺(小票)来换取这个值本身。

    当然也还有这么一种情况,店里面现在已经没有肉了,当我拿着小票去找店员拿我的午餐的时候,店员告诉我,做不了青椒炒肉了。从这里也就能够看得出来,我所需要的未来值可能失败也可能成功。

    也就是我每次去吃饭的时候,最终要么得到一份青椒炒肉,要么到的一个肉已经卖完了的消息。

    当然在代码中事情并不会这么简单,有可能我的订单被店员忘记了,那么我的订单将永远都不会被叫到,在这种情况下,我就永远处于一个未决议的状态。

    1. 现在值与未来值

    下面让我们使用代码来描述一下上面的情况,但是在具体解释promise之前,先使用比较好理解一点的方式——回调来解释一下未来值。

    当我们在写代码使用某一个变量的值的时候,实际上我们已经对这个值做了一个非常基本的假设,那就是他应该是我们已经确定他是一个具体的现在值了:

    let x,y = 2
    console.log(x + y)// NaN <-- 因为x还没有设定值
    

    在我们执行x+y的时候,我们假定了x和y都是一个具体的值了,用术语来说就是x和y的值都是已决议的。

    从上面的代码能够发现,我们在执行+运算的时候x的值其实并没有确定,而这个运算是无法检测x和y的值是否已经决议好,它也无法等待x和y都决议好之后再执行运算。如果在代码中有个语句现在完成,而有的语句是将来才完成,那么就会引起程序的混乱。

    如果两条语句中任意一个可能还没有完成,我们改如何追踪者两条语句的关系能?如果语句2依赖语句1的完成,那么就只有两种情况:语句1马上完成,一切顺利进行,要么语句1没有完成,而语句2也会因此失败。

    那么我们应该如何保证在运行x+y这个运算时是安全的呢?其实也就是在执行语句的时候要保证x和y的值都已经准备好了。换一种表述方式就是:“把x和y加起来,但是如果他们其中的任何一个还没有准备好,就等待两者都准备好。一旦可以立马执行加运算。”

    首先使用回调的形式:

    function add(getX, getY, cb) {
        var x,y
        getX(function (xVal) {
            x = xVal
            if(y !== undefined) {
                cb(x + y)
            }
        })
        
        getY(function (yVal) {
            y = yVal
            if(x !== undefined) {
                cb(x + y)
            }
        })
    }
    // fetchX() 和fetchY()是同步或者异步函数
    add(fetchX, fetchY, function (sum) {
        console.log(sum)
    })
    

    虽然我们为了执行一个x+y写了比较多的代码,但是在这段代码中我们把x和y当做未来值,并且表达了一个add()运算,这个运算并不会在意x和y现在是否可用。它把现在和将来归一化了,因此我们可以保证add()运算的输出是可预测的。

    说得更加直白一点就是,未来统一处理现在和将来,我们把它们都变成了将来,即所有的操作都变成了异步。

    1. Promise值

    下面先来看一下如何使用promise函数来表达这个x+y的例子:

    function add(xPromise, yPromise) {
        return Promise.all([xPromise, yPromise]).then(function (values) {
            return values[0] + values[1]
        })
    }
    add(fetchX(), fetchY()).then(function (sum) {
        console.log(sum)
    })
    

    直接掉调用fetchX(), fetchY(),它们的返回值被传递给add()。这些promise代表的底层值的可用的时间可能是现在,也可能是未来,但是不管怎么样,promise归一保证了行为的一致性。我们可以按照不依赖时间的方式追踪x和y。它们是未来值。

    当然就像去吃午饭一样,promise的决议结果可能是拒绝也可能是完成。拒绝值和完成的promise不一样:完成值是通过我们代码最终得到一个我们期望的值,而拒绝值通常是一个拒绝原因。可能是程序的逻辑直接设置的,也可能是从运行异常隐式得到的值。

    通过promise,调用then()实际上可以接受两个函数,第一个用于完成情况,第二用于拒绝情况。

    add(fetchX(), fetchY()).then(function (sum) {
        console.log(sum)
    }, function (err) {
        console.log(err)
    })
    

    从外部来看,由于Promise封装了依赖于时间的状态——等待底层的值完成或拒绝,所以Promise本身是与时间无关的。因此Promise可以按照可预测的方式组成,而不用关心时序或底层结果。

    Promise决议之后就是外部不可变的值,我们可以安全的把这个值传递给第三方,并确信它不会被修改。如果有多方同时查看同一个Promise决议时,任何一方都无法影响到另一方对Promise决议的观察结果。Promise决议之后的不可变性是Promise设计中最基础和最重要的因素!

    完成事件

    Promise的决议:一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的this-then-that。

    假如现在有一个异步任务foo,我们不知道也不关心它的任何细节,这个函数可能立即完成,也可能需要一段时间才能完成。

    我们只需要知道foo在什么时候结束,这样就可以继续继续下一个任务。换句话来说我们想要通过某种方式在foo完成的时候通知我们一下,以便可以继续下一步任务。

    在JavaScript中,如果需要侦听某一个通知,可能第一反应就是事件。所以可以把对通知的需求重新组织为对foo发出一个完成事件的监听。

    使用回调的话,通知就是任务调用的回调。而使用Promise的话,就把这个关系反转了过来,侦听来自foo的事件,然后得到通知的时候,根据情况进行下一步的操作。

    首先,根据上面的需求我们可以得到下面的伪代码:

    foo(x) {
        // 做一些异步操作
    }
    foo(88)
    on(foo "completion") {
        // foo完成开始下一步
    }
    on(foo “error”) {
        // 出错了
    }
    

    根据事件的特点,我们调用foo()然后建立两个事件侦听器,一个用于foo完成,一个用于foo出错,从本质上来说foo()并不需要了解调用代码订阅了那些事件,这样可以很好的实现关注点分离。

    当然原生的JavaScript并没有提供这样的东西。下面是在JavaScript中更加自然的表达方式。

    function foo(x) {
        // 做一些异步操作
        
        // 构造一个listener事件通知处理对象来返回
        return listener
    }
    let evt = foo(42)
    evt.on('completion', function () {
        // foo完成开始下一步
    })
    evt.on('error', function () {
        // 出错了
    })
    

    foo()显式创建并返回了一个事件订阅对象,调用代码得到这个对象,并且在其上注册了两个事件处理函数。

    相对于我们比较熟悉的面向回调来说,这里没有把回调传递给foo(),而是返回一个名为evt的事件注册对象,由它来接受回调,将控制返还给调用代码。

    还有一个重要的好处就是可以把这个事件侦听对象提供给代码中多个独立的部分,在foo()完成的时候,它们都可以独立的得到通知,然后执行下一步。

    var evt = foo(52)
    bar(evt)
    baz(evt)
    

    很明显使用上面的操作实现了更好的关注点分离,其中bar()和baz()不需要牵扯到foo()的调用细节。foo()也不需要知道或者关注bar()和baz()是否存在。

    从本质上来说,evt对象就是分离的关注点之间一个中立的第三方协商机制。

    Promise“事件”

    很明显,我们前面说到的事件侦听对象evt就是Promise的一个模拟。

    在基于Promise的方法中,前面的代码片段会让foo()创建并返回一个Promise实例,而且这个Promise会被传递到bar()和baz()。

    你可能会猜测bar()和baz()的内部实现或许如下:

    function bar(fooPromise) {
        fooPromise.then(
            function () {
                // foo执行完毕,执行bar
            },
            function () {
                // 出错了
            }
        )
    }
    

    Promise决议也不一定要像前面将Promise作为未来值查看时一样会涉及发送消息。它也可以只作为一种流程控制信号。

    另一种实现方式是:

    function bar() {
        // foo执行完毕,执行bar
    } 
    function baz() {
        // foo执行完毕,执行baz
    } 
    function oopsBar() {
        // 出错了
    }
    var p = foo(42)
    p.then(bar, oopsBar)
    p.then(baz, oopsBar)
    

    作者简介:李成文,芦苇科技web前端开发工程师,擅长网站建设、微信公众号开发、微信小程序开发、小游戏制作、企业微信制作、H5建设,专注于前端框架、交互设计、图像绘制、数据分析等研究。

    个人博客:LCW blog

    欢迎和我们一起并肩作战: web@talkmoney.cn
    访问 www.talkmoney.cn 了解更多

    提供深圳微信公众号制作,广东钉钉开发,专业的企业微信外包,高性价比的微信小程序建设,靠谱的小游戏制作,高质量的H5开发

    相关文章

      网友评论

          本文标题:Promise从入门到精通

          本文链接:https://www.haomeiwen.com/subject/fstjdqtx.html