细嗅Promise

读完这篇文章,预计会消耗你 40 分钟的时间。

Ajax 出现的时候,刮来了一阵异步之风,现在 Nodejs 火爆,又一阵异步狂风刮了过来。需求是越来越苛刻,用户对性能的要求也是越来越高,随之而来的是页面异步操作指数般增长,如果不能恰当的控制代码逻辑,我们就会陷入无穷的回调地狱中。

ECMAScript 6 已经将异步操作纳入了规范,现代浏览器也内置了 Promise 对象供我们进行异步编程,那么此刻,还在等啥?赶紧学习学习 Promise 的内部原理吧!

第一章 了解 Promise

一、场景再现

由于 javascript 的单线程性质,我们必须等待上一个事件执行完成才能处理下一步,如下:

// DOM ready之后执行
$(document).ready(function(){
    // 获取模板
    $.get(url, function(tpl){
        // 获取数据
        $.get(url2, function(data){
            // 构建 DOMString
            makeHtml(tpl, data, function(str){
                // 插入到 DOM 中
                $(obj).html(str);
            });
        });
    });
});

为了减少首屏数据的加载,我们将一些模板和所有数据都放在服务器端,当用户操作某个按钮时,需要将模板和数据拼接起来插入到 DOM 中,这个过程还必须在 DOMReady 之后才能执行。这种情况是十分常见的,如果异步操作再多一些,整个代码的缩进让人看着很不舒服,为了优雅地处理这个问题,ECMAScript 6 引入了 Promise 的概念,目前一些现代浏览器已经支持这些新东西了!

二、模型

为了让代码流程更加清晰,我们假想着能够按照下面的流程来跑程序:

new Promise(ready).then(getTpl).then(getData).then(makeHtml).resolve();

先将要事务按照执行顺序依次 push 到事务队列中,push 完了之后再通过 resolve 函数启动整个流程。

整个流程的操作模型如下:

promise(ok).then(ok_1).then(ok_2).then(ok_3).reslove(value)------+
         |         |          |          |                       |
         |         |          |          |        +=======+      |
         |         |          |          |        |       |      |
         |         |          |          |        |       |      |
         +---------|----------|----------|--------→  ok() ←------+
                   |          |          |        |   ↓   |
                   |          |          |        |   ↓   |
                   +----------|----------|--------→ ok_1()|
                              |          |        |   ↓   |
                              |          |        |   ↓   |
                              +----------|--------→ ok_2()|
                                         |        |   ↓   |
                                         |        |   ↓   |
                                         +--------→ ok_3()-----+
                                                  |       |    |
                                                  |       |    ↓
@ Created By Barret Lee                           +=======+   exit

在 resolve 之前,promise 的每一个 then 都会将回调函数压入队列,resolve 后,将 resolve 的值送给队列的第一个函数,第一个函数执行完毕后,将执行结果再送入下一个函数,依次执行完队列。一连串下来,一气呵成,没有丝毫间断。

三、简单的封装

如果了解 Promise,可以移步下方,看看对 Promise 的封装:

Github: https://github.com/barretlee/myPromise
DEMO: http://barretlee.github.io/myPromise/index.html

如果还不是很了解,可以往下阅读全文,了解一二。

第二章 Promise 原理

一、什么是 Promise ?

那么,什么是 Promise ?

Promise 可以简单理解为一个事务,这个事务存在三种状态:

  1. 已经完成了 resolved
  2. 因为某种原因被中断了 rejected
  3. 还在等待上一个事务结束 pending

上文中我们举了一个栗子,获取模板和数据之后再将拼合的数据插入到 DOM 中,这里我们将整个程序分解成多个事务:

事务一:     获取模板
               ↓
事务二:     获取数据
               ↓
事务三: 拼合之后插入到 DOM

在事务一结束之前,也就是模板代码从服务器拉取过来之前,事务二和事务三都处于 pending 状态,他们必须等待上一个事务结束。而事务一结束之后会将自身状态标记为 resolved,并把该事务中处理的结果移交给事务二继续处理(当然,这里如果没有数据返回,事务二就不会获得上一个事务的数据),依次类推,直到最后一个事务操作结束。

在事务操作的过程中,若遇到错误,比如事务一获取数据存在跨域问题,那事务就会操作失败,此时它会将自身的状态标记为 rejected,由于后续事务都是承接前一事务的,前一事务已经宣告工程已经玩不成了,那么后续的所有事务都会将自己标记为 rejected,其标记理由(reason)就是出错事务的报错信息(这个报错信息可以使用 try…catch 来捕获,也可以通过程序自身来捕获,如 ajax 的 onerror 事件、ajax 返回的状态码为 404 等)。

小结:Promise 就是一个事务的管理器。他的作用就是将各种内嵌回调的事务用流水形式表达,其目的是为了简化编程,让代码逻辑更加清晰。

由于整个程序的实现比较难理解,对于 Promise,我们将分为两部分阐述:

  • 无错误传递的 Promise,也就是事务不会因为任何原因中断,事务队列中的事项都会被依次处理,此过程中 Promise 只有 pending 和 resolved 两种状态,没有 rejected 状态。
  • 包含错误的 Promise,每个事务的处理都必须使用容错机制来获取结果,一旦出错,就会将错误信息传递给下一个事务,如果错误信息会影响下一个事务,则下一个事务也会 rejected,如果不会,下一个事务可以正常执行,依次类推。

二、无错误传递的 Promise(简化版的 Promise)

首先,我们需要用一个变量(status)来标记事务的状态,然后将事务(affair)也保存到 Promise 对象中。

var Promise = function(affair){
    this.state = “pending”;
    this.affair = affair || function(o) { return o; };
    this.allAffairs = [];
};

Promise 有两个重要的方法,一个是 then,另一个是 resolve:

  • then,将事务添加到事务队列(allAffairs)中
  • resolve,开启流程,让整个操作从第一个事务开始执行

在操作事务之前,我们会先把各种事务依次放入事务队列中,这里会用到 then 方法:

Promise.prototype.then = function (nextAffair){
    var promise = new Promise();
    if (this.state == ‘resloved’){
        // 如果当前状态是已完成,则这个事务将会被立即执行
        return this._fire(promise, nextAffair);
    }else{
        // 否则将会被加入队列中
        return this._push(promise, nextAffair);
    }
};

如果整个操作已经完成了,那 then 方法送进的事务会被立即执行,

Promise.prototype._fire = function (nextPromise, nextAffair){
    var nextResult = nextAffair(this.result);
    if (nextResult instanceof Promise){
        nextResult.then(function(obj){
            nextPromise.resolve(obj);
        });
    }else{
        nextPromise.resolve(nextResult);
    }
    return nextPromise;
};
被立即执行之后会返回一个结果,这个结果会被传递到下一个事务中作为原料,但是这里需要考虑两种情况:
  1. 异步,如果这个结果也是一个 Promise,则需要等待这个 Promise 执行完毕再将最终的结果传到下一个事务中。
  2. 同步,如果这个结果不是 Promise,则直接将结果传递给下一个事务。

第一种情况还是比较常见的,比如我们在一个事务中有一个子事务队列需要处理,此时必须等待子事务完成才能回到主事务队列中。

Promise.prototype.resolve = function (obj){
    if (this.state != ‘pending’) {
        throw ‘流程已完成,不能再次开启流程!’;
    }
    this.state = ‘resloved’;
    // 执行该事务,并将执行结果寄存到 Promise 管理器上
    this.result = this.affair(obj);
    for (var i = 0, len = this.allAffairs.length; i < len; ++i){
        // 往后执行事务
        var affair = this.allAffairs[i];
        this._fire(affair.promise, affair.affair);
    }
    return this;
};

resolve 接受一个参数,这个数据是交给第一个事务来处理的,因为第一个事务的启动可能需要点原料,这个数据就是原料,它也可以是空。该事物处理完毕之后,将操作结果(result)寄存在 Promise 对象上,方便引用,然后将结果(result)作为原料送入下一个事务。依次类推。

我们看到 then 方法中还调用了一个 _push ,这个方法的作用是将事务推进事务管理器(Promise)。

Promise.prototype._push = function (nextPromise, nextAffair){
    this.allAffairs.push({
        promise: nextPromise,
        affair: nextAffair
    });
    return nextPromise;
};

以上操作,我们就实现了一个简单的事务管理器,可以测试下下面的代码:

// 初始化事务管理器
var promise = new Promise(function(data){
    console.log(data);
    return 1;
});
// 添加事务
promise.then(function(data){
    console.log(data);
    return 2;
}).then(function(data){
    console.log(data);
    return 3;
}).then(function(data){
    console.log(data);
    console.log(“end”);
});
// 启动事务
promise.resolve(“start”);

可以看到依次输出的结果为:

> start
> 1
> 2
> 3
> end

由于上述实现十分简陋,链式调用没做太好的处理,请读者自行完善:)

下面是一个异步操作演示:

var promise = new Promise(function(data){
    console.log(data);
    return “end”;
});
promise.then(function(data){
    // 这里需要返回一个 Promise,让主事务切换到子事务处理
    return (function(data){
        // 创建一个子事务
        var promise = new Promise();
        setTimeout(function(){
            console.log(data);
            // 一秒之后才启动子事务,模拟异步延时
            promise.resolve();
        }, 1000);
        return promise;
    })(data);
});
promise.resolve(“start”);
可以看到依次输出的结果为:
> start
> end (1s之后输出)

将函数写的稍微好看点:

function delay(data){
    // 创建一个子事务
    var promise = new Promise();
    setTimeout(function(){
        console.log(data);
        // 一秒之后才启动子事务,模拟异步延时
        promise.resolve();
    }, 1000);
    return promise;
}
// 主事务
var promise = new Promise(function(data){
    console.log(data);
    return “end”;
});
promise.then(delay);
promise.resolve(“start”);

三、包含错误传递的 Promise

真的很羡慕你能看到这么详细的文章,当然,后面会更加精彩!

没有错误处理的 Promise 只能算是一个半成品,虽说可以通过在最外层加一个 try..catch 来捕获错误,但没法具体定位是哪个事务发生的错误。并且这里的错误不仅仅包含 JavaScript Error,还有诸如 ajax 返回的 data code 不是 200 的情况等。

先看一个浏览器内置 Promise 的实例(该代码可在现代浏览器下运行):

new Promise(function(resolve, reject){
    resolve(“start”);
}).then(function(data){
    console.log(data);
    throw “error”;
}).catch(function(err){
    console.log(err);
    return “end”;
}).then(function(data){
    console.log(data)
});
Promise 的回调和 then 方法都是接受两个参数:
new Promise(function(resolve, reject){
    // …
});

promise.then(
    function(value){/* code here */},
    function(reason){/* code here */}
);

事务处理过程中,如果有值返回,则作为 value,传入到 resolve 函数中,若有错误产生,则作为 reason 传入到 reject 函数中处理。

在初始化 Promise 对象时,若传入的回调中没有执行 resolve 或者 reject,这需要我们主动去启动事务队列。

promise.resolve();
promise.reject();

上面两种都是可以启动一个队列的。这里跟第二章第二节的 resolve 函数用法类似。Promise 对象还提供了 catch 函数,起用法等价于下面所示:

promise.catch();
// 等价于
promise.then(null, function(reason){});

还有两个 API:

promise.all();
promise.race();

后续再讲。先看看这个有错误处理的 Promise 是如何实现的。

function Promise(resolver){
    this.status = “pending”;
    this.value = null;
    this.handlers = [];
    this._doPromise.call(this, resolver);
}

_doPromise 方法在实例化 Promise 函数时就执行。如果送入的回调函数 resolver 中已经 resolve 或者 reject 了,程序就已经启动了,所以在实例化的时候就开始判断。

_doPromise: function(resolver){
    var called = false, self = this;
    try{
        resolver(function(value){
            // 如果没有 call 则继续,并标记 called 为 true
            !called && (called = !0, self.resolve(value));
        }, function(reason){
            // 同上
            !called && (called = !0, self.reject(reason));
        });
    } catch(e) {
        // 同上,捕获错误,传递错误到下一个 then 事务
        !called && (called = !0, self.reject(e));
    }
},

只要 resolve 或者 reject 就会标记程序 called 为 true,表示程序已经启动了。

resolve: function(value) {
    try{
        if(this === value){
            throw new TypeError(‘流程已完成,不能再次开启流程!’);
        } else {
            // 如果还有子事务队列,继续执行
            value && value.then && this._doPromise(value.then);
        }
        // 执行完了之后标记为完成
        this.status = “fulfilled”;
        this.value = value;
        this._dequeue();
    } catch(e) {
        this.reject(e);
    }
},
reject: function(reason) {
    // 标记状态为出错
    this.status = “rejected”;
    this.value = reason;
    this._dequeue();
},

可以看到,每次 resolve 的时候都会用一个 try..catch 包裹来捕获未知错误。

_dequeue: function(){
    var handler;
    // 执行事务,直到队列为空
    while (this.handlers.length) {
        handler = this.handlers.shift();
        this._handle(handler.thenPromise, handler.onFulfilled, handler.onRejected);
    }
},

无论是 resolve 还是 reject 都会让程序往后奔流,直到结束所有事务,所以这两个方法中都有 _dequeue 函数。

_handle: function(thenPromise, onFulfilled, onRejected){
    var self = this;

    setTimeout(function() {
        // 判断下次操作采用哪个函数,reject 还是 resolve
        var callback = self.status == “fulfilled”
                       ? onFulfilled
                       : onRejected;
        // 只有是函数才会继续回调
        if (typeof callback === ‘function’) {
            try {
                self.resolve.call(thenPromise, callback(self.value));
            } catch(e) {
                self.reject.call(thenPromise, e);
            }
            return;
        }
        // 否则就将 value 传递给下一个事务了
        self.status == “fulfilled”
                        ? self.resolve.call(thenPromise, self.value)
                        : self.reject.call(thenPromise, self.value);
    }, 1);
},

这个函数跟上一节提到的 _fire 类似,如果 callback 是 function,就会进入子事务队列,处理完了之后退回到主事务队列。最后一个 then 方法,将事务推进队列。

then: function(onFulfilled, onRejected){
    var thenPromise = new Promise(function() {});

    if (this.status == “pending”) {
        this.handlers.push({
            thenPromise: thenPromise,
            onFulfilled: onFulfilled,
            onRejected: onRejected
        });
    } else {
        this._handle(thenPromise, onFulfilled, onRejected);
    }

    return thenPromise;
}

如果第二节没有理解清楚,这一节也会让人头疼,这一部分讲的比较粗糙。

第三章 异步编程

一、jQuery 中的 Defferred 对象

或许你在面试的时候,有面试官问你:

$.ajax() 执行后返回的结果是什么?

在 jQuery1.5 版本就已经引入了 Defferred 对象,当时为了引入这个东西,整个 jQuery 都被重构了。Defferred 跟 Promise 类似,它表示一个还未完成任务的对象,而 Promise 确切的说,是一个代表未知值的对象。

$.ajax({
    url: url
}).done(function(data, status, xhr){
    //…
}).fail(function(){
    //…
});

回忆下第二章第一节中的 Promise,是不是如出一辙,只是 jQuery 还提供了更多的语法糖:

$.ajax({
    url: url,
    success: function(data){
        //…
    },
    error: funtion(){
        //…
    }
});

他允许将 done 和 fail 两个函数的回调放在 ajax 初始化的参数 success 和 fail 上,其原理还是一样的,同样,还有这样的东西:

$.when(taskOne, taskTwo).done(function () {
    console.log(“都执行完毕后才会输出我!”);
}).fail(function(){
    console.log(“只要有一个失败,就会输出我!”)
});
当 taskOne 和 taskTwo 都完成之后才执行 done 回调,这个浏览器内置的 Promise 也有对应的函数:
Promise.all([true, Promise.resolve(1), …]).then(function(value){
    //....
});

浏览器内置的 Promise 还提供了一个 API:

Promise.race([true, Promise.resolve(1), …]).then(function(value){
    //....
}, function(reason){
    //…
});

只要 race 参数中有一个 resolve 或者 reject,then 回调就会出发。

二、基于事件响应的异步模型

@朴灵 写的 EventProxy 就是基于事件响应的异步模型,按理说,这个实现的逻辑是最清晰的,不过代码量稍微多一点。

function taskA(){
    setTimeout(function(){
        var result = “A”;
        E.emit(“taskA”, result);
    }, 1000);
}

function taskB(){
    setTimeout(function(){
        var result = “B”;
        E.emit(“taskB”, result);
    }, 1000);
}

E.all([“taskA”, “taskB”], function(A, B){
    return A + B;
});

我没有看他的源码,但是想想,应该是这个逻辑。只需要在消息中心管理各个 emit 以及消息注册。这里的错误处理值得思考下。

在半年前,也写过一篇关于异步编程的文章:JavaScript异步编程原理,感兴趣的可以去读一读。

第四章 小结

一、小结

文章比较长,阅读了好几天别人写的东西,自己提笔还是比较轻松的,本文大概花费了 6 个小时撰写。

本文主要解说了 Promise 的应用场景和实现原理,如果你能够顺畅的读完全文并且之处文中的一些错误,说明你已经悟到了:)

Promise 使用起来不难,但是理解其原理还是有点偏头痛的,所以下面列举的几篇相关阅读也建议读者点进去看看。

二、相关阅读

时间: 2024-10-24 16:26:12

细嗅Promise的相关文章

我心有猛虎,细嗅蔷薇下

兄弟连PHP培训学校是我经历过最放心的学校,我曾经历好几个被换走N个班主任的班级,老师的好坏,听一节就大致了解了. 从决定到来LAMP兄弟来只用了半天的时间,时间非常仓促,以至于我下了火车,站在北京西站茫然环顾,都觉得一切都是幻觉,像是在做梦. 经历很多的人和事的变化,让我发现,自己的目前的现状与我想要的状态已经产生了巨大鸿沟,而且这道鸿沟正在渐渐变大.改变现状成了我内心最大的冲动了. 来到兄弟连后,见到第一个人说的第一句话顿时让我忐忑的心情被安抚下去:你是新来的兄弟吧? 3.28号经历开班仪式

Promise实现原理

这两天在熟悉 kissy框架的时候,看到了 Promise 模块. Promise 对于一个Jser并不陌生, Promise 类似于一个事务管理器,它的作用就是将各种内嵌回调的事务用流水形式表达.利用 Promise 可以让异步编程更符合人的直觉,让代码逻辑更加清晰,把开发人员从回调地狱中释放出来.这么“高大上”的东西,以前写 nodejs 代码的时候只是简单的用用,还没有理解其基本的实现原理,罪过!个人认为,理解编程思想最好的途径就是阅读一份简易的实现源码.很幸运,网上有不少 Promise

前端资源

Webpack: [webpack-howto] [webpack] [http://www.infoq.com/cn/articles/react-and-webpack] JS相关: [javascript weekly] [javascript标准参考教程] --阮一峰的参考教程,可读性很好,非常不错的 [QuirksMode-DOM] [深入理解JAVASCRIPT系列] [User Timing API (window performence)] 关注ES6(ES2015): [ES6

你只有特别努力,才能过上自己想要的生活

梅林文苑 2017-07-14 01:29 王小波说过这样一段话: " 那一年,我二十一岁,在我一生的黄金时代,我有好多奢望.我想爱,想吃,还想在一瞬间变成天上半明半暗的云.后来我才知道,生活就是个缓慢受锤的过程. 文?梅林|巽木 上周参加一个三下乡活动,回来之后,我经常会一个人呆在屋子里,不愿说话.也就是在这段时间里我突然想明白了一个道理:在面对突如其来的困难,抱怨是没用的,你能做的就是接受结果,然后,非常努力寻找方法把结果导致的负面影响降到最低.就算你满口虎牙,就算你真的觉得自己委屈,你也不

《App研发录》面世

古者富贵而名灭,不可胜记,唯倜傥非常之人称焉.故西伯拘而演<周易>,屈原放逐,乃赋<离骚>.文人雅士一次次的谱写着千古绝唱,而我亦不能免俗,也要附庸风雅,写一部前不见古人.后不见来者的经典之作. 于是,历时一年,呕心沥血,结合自身3年来从事App领域的一线实战经验,再辅之以从事软件行业十余载的奇技淫巧,写下这洋洋洒洒三百多页十几万字.初稿完成后,就如同和氏璧般,竟找不到出版社愿意出版,而我又不肯妥协,去写那些无关痛痒的语法介绍和UI布局等入门级章节.就在这时,杜勇帮我介绍了机械工业

于我,过去,现在和未来 —— 西格里夫&#183;萨松

In me, past, present, future meet 于我,过去.现在和未来 To hold long chiding conference    商讨聚会 各执一词 纷扰不息 My lusts usurp the present tense 林林总总的 欲望,掠取着我的现在 And strangle Reason in his seat   把“理性”扼杀于它的宝座 My loves leap through the future's fence 我的爱情纷纷越过未来的藩篱 To

2016年,失望,开心,耐心,学习,梦想,领悟,惋惜,展望

2016年,失望,开心,耐心,学习,梦想,领悟,惋惜,展望 从十一月就开始动笔,准备把2016年总结一下,可是思路这东西,还真是比较难找到,一直断断续续的,所以也写的很艰难,实在是这2016年过的十分的坎坷,也十分的艰辛,我以为我会在一月底写,是啊,我总是我以为,今年并不怎么开心,但是又有一些比较开心的事情,慢慢的回顾一下吧! 这篇文章会在CSDN上首发,然后同步到我的个人微信公众号上去,如果你还没有加我的公众号,只要扫一下右下方的二维码或者直接搜索刘桂林,那个光头和尚就是我了,阿弥陀佛! 既然

前端PHP入门-004-数据类型,特别需要注意字符串

人类世界对万事万物都有种类划分,例如: 哺乳动物 人.猫.马.鸭嘴兽-.等等 蔬菜 西红柿.波菜.茄子-.等等 水果 西瓜.桃子.苹果-.等等 数据类型:就是对数据分类的一个划分而已 整型就是整数 我一直在讲,不要被名词的含义所吓唬住. 到底什么是整型呀? 所谓整型,就是大家数学中所学的整数,我们最经常使用的. 整型--整数也,英文称之:`integer`.英文简写:`int` 整型分为: 10进制 8进制 (了解,基本不用)- 0开头的 16进制(了解,基本不用)- 0X开头 整型(整数)在计

《青春》--席慕蓉

所有的结局都已写好 所有的泪水也都已启程 却忽然忘了是怎么样的一个开始 在那个古老的不再回来的夏日 无论我如何地去追索 年轻的你只如云影掠过 而你微笑的面容极浅极淡 逐渐隐没在日落后的群岚 遂翻开那发黄的扉页 命运将它装订得极为拙劣 含着泪 我一读再读 却不得不承认 青春是一本太仓促的书 油然而生:  处在青春的年纪却在纠结任性青春是什么,我希望应该是:窗前午后,阳光正好,捧一本书,心有猛虎,细嗅蔷薇.