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

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

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

第一章 了解 Promise

一、场景再现

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

  1. // DOM ready之后执行
  2. $(document).ready(function(){
  3. // 获取模板
  4. $.get(url, function(tpl){
  5. // 获取数据
  6. $.get(url2, function(data){
  7. // 构建 DOMString
  8. makeHtml(tpl, data, function(str){
  9. // 插入到 DOM 中
  10. $(obj).html(str);
  11. });
  12. });
  13. });
  14. });

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

二、模型

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

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

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

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

  1. promise(ok).then(ok_1).then(ok_2).then(ok_3).reslove(value)------+
  2. | | | | |
  3. | | | | +=======+ |
  4. | | | | | | |
  5. | | | | | | |
  6. +---------|----------|----------|--------→ ok() ←------+
  7. | | | | |
  8. | | | | |
  9. +----------|----------|--------→ ok_1()|
  10. | | | |
  11. | | | |
  12. +----------|--------→ ok_2()|
  13. | | |
  14. | | |
  15. +--------→ ok_3()-----+
  16. | | |
  17. | |
  18. @ 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 中,这里我们将整个程序分解成多个事务:

  1. 事务一: 获取模板

  2. 事务二: 获取数据

  3. 事务三: 拼合之后插入到 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 对象中。

  1. var Promise = function(affair){
  2. this.state = pending”;
  3. this.affair = affair || function(o) { return o; };
  4. this.allAffairs = [];
  5. };

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

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

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

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

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

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

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

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

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

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

  1. Promise.prototype._push = function (nextPromise, nextAffair){
  2. this.allAffairs.push({
  3. promise: nextPromise,
  4. affair: nextAffair
  5. });
  6. return nextPromise;
  7. };

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

  1. // 初始化事务管理器
  2. var promise = new Promise(function(data){
  3. console.log(data);
  4. return 1;
  5. });
  6. // 添加事务
  7. promise.then(function(data){
  8. console.log(data);
  9. return 2;
  10. }).then(function(data){
  11. console.log(data);
  12. return 3;
  13. }).then(function(data){
  14. console.log(data);
  15. console.log(“end”);
  16. });
  17. // 启动事务
  18. promise.resolve(“start”);

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

  1. > start
  2. > 1
  3. > 2
  4. > 3
  5. > end

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

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

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

将函数写的稍微好看点:

  1. function delay(data){
  2. // 创建一个子事务
  3. var promise = new Promise();
  4. setTimeout(function(){
  5. console.log(data);
  6. // 一秒之后才启动子事务,模拟异步延时
  7. promise.resolve();
  8. }, 1000);
  9. return promise;
  10. }
  11. // 主事务
  12. var promise = new Promise(function(data){
  13. console.log(data);
  14. return end”;
  15. });
  16. promise.then(delay);
  17. promise.resolve(“start”);

三、包含错误传递的 Promise

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

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

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

  1. new Promise(function(resolve, reject){
  2. resolve(“start”);
  3. }).then(function(data){
  4. console.log(data);
  5. throw error”;
  6. }).catch(function(err){
  7. console.log(err);
  8. return end”;
  9. }).then(function(data){
  10. console.log(data)
  11. });
  1. Promise 的回调和 then 方法都是接受两个参数:
  1. new Promise(function(resolve, reject){
  2. // …
  3. });
  4.  
  5. promise.then(
  6. function(value){/* code here */},
  7. function(reason){/* code here */}
  8. );

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

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

  1. promise.resolve();
  2. promise.reject();

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

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

还有两个 API:

  1. promise.all();
  2. promise.race();

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

  1. function Promise(resolver){
  2. this.status = pending”;
  3. this.value = null;
  4. this.handlers = [];
  5. this._doPromise.call(this, resolver);
  6. }

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

  1. _doPromise: function(resolver){
  2. var called = false, self = this;
  3. try{
  4. resolver(function(value){
  5. // 如果没有 call 则继续,并标记 called 为 true
  6. !called && (called = !0, self.resolve(value));
  7. }, function(reason){
  8. // 同上
  9. !called && (called = !0, self.reject(reason));
  10. });
  11. } catch(e) {
  12. // 同上,捕获错误,传递错误到下一个 then 事务
  13. !called && (called = !0, self.reject(e));
  14. }
  15. },

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

  1. resolve: function(value) {
  2. try{
  3. if(this === value){
  4. throw new TypeError(‘流程已完成,不能再次开启流程!’);
  5. } else {
  6. // 如果还有子事务队列,继续执行
  7. value && value.then && this._doPromise(value.then);
  8. }
  9. // 执行完了之后标记为完成
  10. this.status = fulfilled”;
  11. this.value = value;
  12. this._dequeue();
  13. } catch(e) {
  14. this.reject(e);
  15. }
  16. },
  17. reject: function(reason) {
  18. // 标记状态为出错
  19. this.status = rejected”;
  20. this.value = reason;
  21. this._dequeue();
  22. },

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

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

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

  1. _handle: function(thenPromise, onFulfilled, onRejected){
  2. var self = this;
  3.  
  4. setTimeout(function() {
  5. // 判断下次操作采用哪个函数,reject 还是 resolve
  6. var callback = self.status == fulfilled
  7. ? onFulfilled
  8. : onRejected;
  9. // 只有是函数才会继续回调
  10. if (typeof callback === function’) {
  11. try {
  12. self.resolve.call(thenPromise, callback(self.value));
  13. } catch(e) {
  14. self.reject.call(thenPromise, e);
  15. }
  16. return;
  17. }
  18. // 否则就将 value 传递给下一个事务了
  19. self.status == fulfilled
  20. ? self.resolve.call(thenPromise, self.value)
  21. : self.reject.call(thenPromise, self.value);
  22. }, 1);
  23. },

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

  1. then: function(onFulfilled, onRejected){
  2. var thenPromise = new Promise(function() {});
  3.  
  4. if (this.status == pending”) {
  5. this.handlers.push({
  6. thenPromise: thenPromise,
  7. onFulfilled: onFulfilled,
  8. onRejected: onRejected
  9. });
  10. } else {
  11. this._handle(thenPromise, onFulfilled, onRejected);
  12. }
  13.  
  14. return thenPromise;
  15. }

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

第三章 异步编程

一、jQuery 中的 Defferred 对象

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

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

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

  1. $.ajax({
  2. url: url
  3. }).done(function(data, status, xhr){
  4. //…
  5. }).fail(function(){
  6. //…
  7. });

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

  1. $.ajax({
  2. url: url,
  3. success: function(data){
  4. //…
  5. },
  6. error: funtion(){
  7. //…
  8. }
  9. });

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

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

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

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

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

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

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

  1. function taskA(){
  2. setTimeout(function(){
  3. var result = A”;
  4. E.emit(“taskA”, result);
  5. }, 1000);
  6. }
  7.  
  8. function taskB(){
  9. setTimeout(function(){
  10. var result = B”;
  11. E.emit(“taskB”, result);
  12. }, 1000);
  13. }
  14.  
  15. E.all([“taskA”, taskB”], function(A, B){
  16. return A + B;
  17. });

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

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

第四章 小结

一、小结

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

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

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

二、相关阅读

细嗅Promise的更多相关文章

  1. 《App研发录》面世

    古者富贵而名灭,不可胜记,唯倜傥非常之人称焉.故西伯拘而演<周易>,屈原放逐,乃赋<离骚>.文人雅士一次次的谱写着千古绝唱,而我亦不能免俗,也要附庸风雅,写一部前不见古人.后不 ...

  2. OI生涯回忆录 2018.11.12~2019.4.15

    上一篇:OI生涯回忆录 2017.9.10~2018.11.11 一次逆风而行的成功,是什么都无法代替的 ………… 历经艰难 我还在走着 一 NOIP之后,全机房开始了省选知识的自学. 动态DP,LC ...

  3. 以虎嗅网4W+文章的文本挖掘为例,展现数据分析的一整套流程

    本文转自知乎 作者:苏格兰折耳喵 ----------------------------------------------------- 本文作者将结合自身经验,并以实际案例的形式进行呈现,涉及从 ...

  4. Go语言中的字符串处理

    1 概述 字符串,string,一串固定长度的字符连接起来的字符集合.Go语言的字符串是使用UTF-8编码的.UTF-8是Unicode的实现方式之一. Go语言原生支持字符串.使用双引号(“”)或反 ...

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

    人类世界对万事万物都有种类划分,例如: 哺乳动物 人.猫.马.鸭嘴兽-.等等 蔬菜 西红柿.波菜.茄子-.等等 水果 西瓜.桃子.苹果-.等等 数据类型:就是对数据分类的一个划分而已 整型就是整数 我 ...

  6. Go语言的数据类型

    1 概述 Go语言作为类C语言,支持常规的基础数据类型的的同时,支持常用的高级数据类型.他们是: 整数,int,uint,int8,uint8,int16,uint16,int32,uint32,in ...

  7. 手写SpringMVC 框架

    手写SpringMVC框架 细嗅蔷薇 心有猛虎 背景:Spring 想必大家都听说过,可能现在更多流行的是Spring Boot 和Spring Cloud 框架:但是SpringMVC 作为一款实现 ...

  8. C# 利用AForge进行摄像头信息采集

    概述 AForge.NET是一个专门为开发者和研究者基于C#框架设计的,提供了不同的类库和关于类库的资源,还有很多应用程序例子,包括计算机视觉与人工智能,图像处理,神经网络,遗传算法,机器学习,机器人 ...

  9. A·F·O小记

    看过很多的游记,也看过很多的退役记.回忆录,而当自己真正去面对的那一刻,却又不知道从何说起,也不知道能用怎样的形式和语言,才能把这段珍贵的记忆封存起来,留作青春里的一颗璀璨明珠…… 还是随便写写吧…… ...

随机推荐

  1. Win7 U盘安装Ubuntu16.04 双系统详细教程

    Win7 U盘安装Ubuntu16.04 双系统详细教程 安装主要分为以下几步: 一. 下载Ubuntu 16.04镜像软件: 二. 制作U盘启动盘使用ultraISO: 三. 安装Ubuntu系统: ...

  2. PHPCMS_V9 模型字段添加单文件上传功能

    后台有“多文件上传”功能,但是对于有些情况,我们只需要上传一个文件,而使用多文件上传功能上传一个文件,而调用时调用一个文件URL太麻烦了. 使用说明: 1.打开phpcms\modules\conte ...

  3. ConcurrentAsyncQueue 2014-09-07

    #define NET45 namespace Test { using System; using System.Threading; using System.Threading.Tasks; u ...

  4. Java集合框架练习-计算表达式的值

    最近在看<算法>这本书,正好看到一个计算表达式的问题,于是就打算写一下,也正好熟悉一下Java集合框架的使用,大致测试了一下,没啥问题. import java.util.*; /* * ...

  5. iOS 常用第三方类库、完整APP示例

    一.第三方类库 1:基于响应式编程思想的oc地址:https://github.com/ReactiveCocoa/ReactiveCocoa2:hud提示框地址:https://github.com ...

  6. 根据url下载图片

    如题:在我要动手写的时候才发现不搜索根本就是写不出来,究其原因还是因为基础不扎实,由于用的少已经没有能力写出了 首先需要获取url数据流,然后写进文件里即可,仅仅两步可惜我写不出来啊跟着搜来的内容写一 ...

  7. [VijosP1764]Dual Matrices 题解

    题目大意: 一个N行M列的二维矩阵,矩阵的每个位置上是一个绝对值不超过1000的整数.你需要找到两个不相交的A*B的连续子矩形,使得这两个矩形包含的元素之和尽量大. 思路: 预处理,n2时间算出每个点 ...

  8. Python读取文件内容并将内容插入到SSDB中

    import os import linecache import time from SSDB import SSDB ssdb = SSDB('127.0.0.1', 8888) print(&q ...

  9. Github初学者教程(一)

    如果你是一名程序员,或者是相关专业的学生,那么Github你不应不知道.很多开源组织和大神,会选择在Github这个平台上,发布他们的开源项目,学会使用Github将能够给你的学习和工作带来巨大帮助! ...

  10. Day 2:增加SplashScreen

    If you want to add just single image, then create a pic in the size of 480*800 and name it as Splash ...