使用Promise解决多层异步调用的简单学习心得


Posted in Javascript onMay 17, 2016

前言

第一次接触到Promise这个东西,是2012年微软发布Windows8操作系统后抱着作死好奇的心态研究用html5写Metro应用的时候。当时配合html5提供的WinJS库里面的异步接口全都是Promise形式,这对那时候刚刚毕业一点javascript基础都没有的我而言简直就是天书。我当时想的是,微软又在脑洞大开的瞎捣鼓了。

结果没想到,到了2015年,Promise居然写进ES6标准里面了。而且一项调查显示,js程序员们用这玩意用的还挺high。

讽刺的是,作为早在2012年就在Metro应用开发接口里面广泛使用Promise的微软,其自家浏览器IE直到2015年寿终正寝了都还不支持Promise,看来微软不是没有这个技术,而是真的对IE放弃治疗了。。。

现在回想起来,当时看到Promise最头疼的,就是初学者看起来匪夷所思,也是最被js程序员广为称道的特性:then函数调用链。

then函数调用链,从其本质上而言,就是对多个异步过程的依次调用,本文就从这一点着手,对Promise这一特性进行研究和学习。

Promise解决的问题

考虑如下场景,函数延时2秒之后打印一行日志,再延时3秒打印一行日志,再延时4秒打印一行日志,这在其他的编程语言当中是非常简单的事情,但是到了js里面就比较费劲,代码大约会写成下面的样子:

var myfunc = function() {  
  setTimeout(function() {
    console.log("log1");
    setTimeout(function() {
      console.log("log2");
      setTimeout(function() {
        console.log("log3");
      }, 4000);
    }, 3000); 
  }, 2000);
}

由于嵌套了多层回调结构,这里形成了一个典型的金字塔结构。如果业务逻辑再复杂一些,就会变成令人闻风丧胆的回调地狱。

如果意识比较好,知道提炼出简单的函数,那么代码差不多是这个样子:

var func1 = function() {
  setTimeout(func2, 2000);
};

var func2 = function() {
  console.log("log1");
  setTimeout(func3, 3000);
};

var func3 = function() {
  console.log("log2");
  setTimeout(func4, 4000);
};

var func4 = function() {
  console.log("log3");
};

这样看起来稍微好一点了,但是总觉得有点怪怪的。。。好吧,其实我js水平有限,说不上来为什么这样写不好。如果你知道为什么这样写不太好所以发明了Promise,请告诉我。

现在让我们言归正传,说说Promise这个东西。

Promise的描述

这里请允许我引用MDN对Promise的描述:

Promise 对象用于延迟(deferred) 计算和异步(asynchronous ) 计算.。一个Promise对象代表着一个还未完成,但预期将来会完成的操作。

Promise 对象是一个返回值的代理,这个返回值在promise对象创建时未必已知。它允许你为异步操作的成功或失败指定处理方法。 这使得异步方法可以像同步方法那样返回值:异步方法会返回一个包含了原返回值的 promise 对象来替代原返回值。

Promise对象有以下几种状态:

•pending: 初始状态, 非 fulfilled 或 rejected。
•fulfilled: 成功的操作。
•rejected: 失败的操作。

pending状态的promise对象既可转换为带着一个成功值的fulfilled 状态,也可变为带着一个失败信息的 rejected 状态。当状态发生转换时,promise.then绑定的方法(函数句柄)就会被调用。(当绑定方法时,如果 promise对象已经处于 fulfilled 或 rejected 状态,那么相应的方法将会被立刻调用, 所以在异步操作的完成情况和它的绑定方法之间不存在竞争条件。)

更多关于Promise的描述和示例可以参考MDN的Promise条目,或者MSDN的Promise条目。

尝试使用Promise解决我们的问题

基于以上对Promise的了解,我们知道可以使用它来解决多层回调嵌套后的代码蠢笨难以维护的问题。关于Promise的语法和参数上面给出的两个链接已经说的很清楚了,这里不重复,直接上代码。

我们先来尝试一个比较简单的情况,只执行一次延时和回调:

new Promise(function(res, rej) {
  console.log(Date.now() + " start setTimeout");
  setTimeout(res, 2000);
}).then(function() {
  console.log(Date.now() + " timeout call back");
});

看起来和MSDN里的示例也没什么区别,执行结果如下:

$ node promisTest.js
1450194136374 start setTimeout
1450194138391 timeout call back

那么如果我们要再做一个延时呢,那么我可以这样写:

new Promise(function(res, rej) {
  console.log(Date.now() + " start setTimeout 1");
  setTimeout(res, 2000);
}).then(function() {
  console.log(Date.now() + " timeout 1 call back");
  new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout 2");
    setTimeout(res, 3000);
  }).then(function() {
    console.log(Date.now() + " timeout 2 call back");
  })
});

似乎也能正确运行:

$ node promisTest.js
1450194338710 start setTimeout 1
1450194340720 timeout 1 call back
1450194340720 start setTimeout 2
1450194343722 timeout 2 call back

不过代码看起来蠢萌蠢萌的是不是,而且隐约又在搭金字塔了。这和引入Promise的目的背道而驰。

那么问题出在哪呢?正确的姿势又是怎样的?

答案藏在then函数以及then函数的onFulfilled(或者叫onCompleted)回调函数的返回值里面。

首先明确的一点是,then函数会返回一个新的Promise变量,你可以再次调用这个新的Promise变量的then函数,像这样:

new Promise(...).then(...)
  .then(...).then(...).then(...)...

而then函数返回的是什么样的Promies,取决于onFulfilled回调的返回值。

事实上,onFulfilled可以返回一个普通的变量,也可以是另一个Promise变量。

如果onFulfilled返回的是一个普通的值,那么then函数会返回一个默认的Promise变量。执行这个Promise的then函数会使Promise立即被满足,执行onFulfilled函数,而这个onFulfilled的入参,即是上一个onFulfilled的返回值。

而如果onFulfilled返回的是一个Promise变量,那个这个Promise变量就会作为then函数的返回值。

关于then函数和onFulfilled函数的返回值的这一系列设定,MDN和MSDN上的文档都没有明确的正面描述,至于ES6官方文档ECMAScript 2015 (6th Edition, ECMA-262)。。。我的水平有限实在看不懂,如果哪位高手能解释清楚官方文档里面对着两个返回值的描述,请一定留言指教!!!

所以以上为我的自由发挥,语言组织的有点拗口,上代码看一下大家就明白了。

首先是返回普通变量的情况:

new Promise(function(res, rej) {
  console.log(Date.now() + " start setTimeout 1");
  setTimeout(res, 2000);
}).then(function() {
  console.log(Date.now() + " timeout 1 call back");
  return 1024;
}).then(function(arg) {
  console.log(Date.now() + " last onFulfilled return " + arg);  
});

以上代码执行结果为:

$ node promisTest.js
1450277122125 start setTimeout 1
1450277124129 timeout 1 call back
1450277124129 last onFulfilled return 1024

有点意思对不对,但这不是关键。关键是onFulfilled函数返回一个Promise变量可以使我们很方便的连续调用多个异步过程。比如我们可以这样来尝试连续做两个延时操作:

new Promise(function(res, rej) {
  console.log(Date.now() + " start setTimeout 1");
  setTimeout(res, 2000);
}).then(function() {
  console.log(Date.now() + " timeout 1 call back");
  return new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout 2");
    setTimeout(res, 3000);
  });
}).then(function() {
  console.log(Date.now() + " timeout 2 call back");
});

执行结果如下:

$ node promisTest.js
1450277510275 start setTimeout 1
1450277512276 timeout 1 call back
1450277512276 start setTimeout 2
1450277515327 timeout 2 call back

如果觉得这也没什么了不起,那再多来几次也不在话下:

 

new Promise(function(res, rej) {
  console.log(Date.now() + " start setTimeout 1");
  setTimeout(res, 2000);
}).then(function() {
  console.log(Date.now() + " timeout 1 call back");
  return new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout 2");
    setTimeout(res, 3000);
  });
}).then(function() {
  console.log(Date.now() + " timeout 2 call back");
  return new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout 3");
    setTimeout(res, 4000);
  });
}).then(function() {
  console.log(Date.now() + " timeout 3 call back");
  return new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout 4");
    setTimeout(res, 5000);
  });
}).then(function() {
  console.log(Date.now() + " timeout 4 call back");
});

 

$ node promisTest.js
1450277902714 start setTimeout 1
1450277904722 timeout 1 call back
1450277904724 start setTimeout 2
1450277907725 timeout 2 call back
1450277907725 start setTimeout 3
1450277911730 timeout 3 call back
1450277911730 start setTimeout 4
1450277916744 timeout 4 call back

可以看到,多个延时的回调函数被有序的排列下来,并没有出现喜闻乐见的金字塔状结构。虽然代码里面调用的都是异步过程,但是看起来就像是全部由同步过程构成的一样。这就是Promise带给我们的好处。

如果你有把??碌拇?胩崃冻傻ザ篮??暮孟肮撸?蔷透?踊?啦豢戳耍?/p>

function timeout1() {
  return new Promise(function(res, rej) {
    console.log(Date.now() + " start timeout1");
    setTimeout(res, 2000);
  });
}

function timeout2() {
  return new Promise(function(res, rej) {
    console.log(Date.now() + " start timeout2");
    setTimeout(res, 3000);
  });
}

function timeout3() {
  return new Promise(function(res, rej) {
    console.log(Date.now() + " start timeout3");
    setTimeout(res, 4000);
  });
}

function timeout4() {
  return new Promise(function(res, rej) {
    console.log(Date.now() + " start timeout4");
    setTimeout(res, 5000);
  });
}

timeout1()
  .then(timeout2)
  .then(timeout3)
  .then(timeout4)
  .then(function() {
    console.log(Date.now() + " timout4 callback");
  });
$ node promisTest.js
1450278983342 start timeout1
1450278985343 start timeout2
1450278988351 start timeout3
1450278992356 start timeout4
1450278997370 timout4 callback

接下来我们可以再继续研究一下onFulfilled函数传入入参的问题。

我们已经知道,如果上一个onFulfilled函数返回了一个普通的值,那么这个值为作为这个onFulfilled函数的入参;那么如果上一个onFulfilled返回了一个Promise变量,这个onFulfilled的入参又来自哪里?

答案是,这个onFulfilled函数的入参,是上一个Promise中调用resolve函数时传入的值。

跳跃的有点大一时间无法接受对不对,让我们来好好缕一缕。

首先,Promise.resolve这个函数是什么,用MDN上面文邹邹的说法

用成功值value解决一个Promise对象。如果该value为可继续的(thenable,即带有then方法),返回的Promise对象会“跟随”这个value

简而言之,这就是异步调用成功情况下的回调。

我们来看看普通的异步接口中,成功情况的回调是什么样的,就拿nodejs的上的fs.readFile(file[, options], callback)来说,它的典型调用例子如下

fs.readFile('/etc/passwd', function (err, data) {
 if (err) throw err;
 console.log(data);
});

因为对于fs.readFile这个函数而言,无论成功还是失败,它都会调用callback这个回调函数,所以这个回调接受两个入参,即失败时的异常描述err和成功时的返回结果data。

那么假如我们用Promise来重构这个读取文件的例子,我们应该怎么写呢?

首先是封装fs.readFile函数:

function readFile(fileName) {
  return new Promise(function(resolve, reject) {
    fs.readFile(fileName, function (err, data) {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

其次是调用:

readFile('theFile.txt').then(
  function(data) {
    console.log(data);
  }, 
  function(err) {
    throw err;
  }  
);

想象一下,在其他语言的读取文件的同步调用接口的里面,文件的内容通常是放在哪里?函数返回值对不对!答案出来了,这个resolve的入参是什么?就是异步调用成功情况下的返回值。

有了这个概念之后,我们就不难理解“onFulfilled函数的入参,是上一个Promise中调用resolve函数时传入的值”这件事了。因为onFulfilled的任务,就是对上一个异步调用成功后的结果做处理的。

哎终于理顺了。。。

总结

下面请允许我用一段代码对本文讲解到的要点进行总结:

function callp1() {
  console.log(Date.now() + " start callp1");
  return new Promise(function(res, rej) {
    setTimeout(res, 2000);
  });
}

function callp2() {
  console.log(Date.now() + " start callp2");
  return new Promise(function(res, rej) {
    setTimeout(function() {
      res({arg1: 4, arg2: "arg2 value"});
    }, 3000);
  });
}

function callp3(arg) {
  console.log(Date.now() + " start callp3 with arg = " + arg);
  return new Promise(function(res, rej) {
    setTimeout(function() {
      res("callp3");
    }, arg * 1000);
  });
}

callp1().then(function() {
  console.log(Date.now() + " callp1 return");
  return callp2();
}).then(function(ret) {
  console.log(Date.now() + " callp2 return with ret value = " + JSON.stringify(ret));
  return callp3(ret.arg1);
}).then(function(ret) {
  console.log(Date.now() + " callp3 return with ret value = " + ret);
})
$ node promisTest.js
1450191479575 start callp1
1450191481597 callp1 return
1450191481599 start callp2
1450191484605 callp2 return with ret value = {"arg1":4,"arg2":"arg2 value"}
1450191484605 start callp3 with arg = 4
1450191488610 callp3 return with ret value = callp3

以上这篇使用Promise解决多层异步调用的简单学习心得就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript回调(callback)函数概念自我理解及示例
Jul 04 Javascript
node.js中的path.delimiter方法使用说明
Dec 09 Javascript
使用jquery清空、复位整个输入域
Apr 02 Javascript
jQuery插件Zclip实现完美兼容个浏览器点击复制内容到剪贴板
Apr 30 Javascript
功能强大的Bootstrap效果展示(二)
Aug 03 Javascript
JavaScript使用键盘输入控制实现数字验证功能
Aug 19 Javascript
基于javascript实现按圆形排列DIV元素(一)
Dec 02 Javascript
详解JS中的柯里化(currying)
Aug 17 Javascript
jQuery动态移除与增加onclick属性的方法详解
Jun 07 jQuery
JavaScript学习笔记之基于定时器实现图片无缝滚动功能详解
Jan 09 Javascript
angular 实现下拉列表组件的示例代码
Mar 09 Javascript
Vue elementui字体图标显示问题解决方案
Aug 18 Javascript
js字符串截取函数slice、substring和substr的比较
May 17 #Javascript
javascript Promise简单学习使用方法小结
May 17 #Javascript
关于安卓手机微信浏览器中使用XMLHttpRequest 2上传图片显示字节数为0的解决办法
May 17 #Javascript
Web前端新人笔记之jquery入门心得(新手必看)
May 17 #Javascript
Angularjs中的事件广播 —全面解析$broadcast,$emit,$on
May 17 #Javascript
iScroll.js 使用方法参考
May 16 #Javascript
BootStrap的JS插件之轮播效果案例详解
May 16 #Javascript
You might like
PHP中Session可能会引起并发问题
2015/06/26 PHP
在Mac OS上搭建PHP的Yii框架及相关测试环境
2016/02/14 PHP
php compact 通过变量创建数组
2016/11/15 PHP
Javascript select下拉框操作常用方法
2009/11/09 Javascript
javascript判断用户浏览器插件安装情况的代码
2011/01/01 Javascript
原生Js与jquery的多组处理, 仅展开一个区块的折叠效果
2011/01/09 Javascript
十个迅速提升JQuery性能让你的JQuery跑得更快
2012/12/10 Javascript
jQuery Validate 验证,校验规则写在控件中的具体实例
2014/02/27 Javascript
JS实现局部选择打印和局部不选择打印
2014/04/03 Javascript
JavaScript实现短信倒计时60s
2017/10/09 Javascript
JavaScript复制内容到剪贴板的两种常用方法
2018/02/27 Javascript
浅谈vue中.vue文件解析流程
2018/04/24 Javascript
vue router的基本使用和配置教程
2018/11/05 Javascript
python网络编程学习笔记(三):socket网络服务器
2014/06/09 Python
在Python的setuptools框架下生成egg的教程
2015/04/13 Python
python从网络读取图片并直接进行处理的方法
2015/05/22 Python
pandas.DataFrame 根据条件新建列并赋值的方法
2018/04/08 Python
python使用webdriver爬取微信公众号
2018/08/31 Python
Python 学习教程之networkx
2019/04/15 Python
Python实现随机生成任意数量车牌号
2020/01/21 Python
Django mysqlclient安装和使用详解
2020/09/17 Python
python实现在列表中查找某个元素的下标示例
2020/11/16 Python
python 将html转换为pdf的几种方法
2020/12/29 Python
Python3+SQLAlchemy+Sqlite3实现ORM教程
2021/02/16 Python
希腊品牌鞋类销售网站:epapoutsia.gr
2020/03/18 全球购物
JoJo Maman Bébé爱尔兰官网:英国最受欢迎的精品母婴品牌
2020/12/20 全球购物
外贸业务员岗位职责
2013/11/24 职场文书
门卫岗位安全职责
2013/12/13 职场文书
单位绩效考核方案
2014/05/11 职场文书
民用住房租房协议书
2014/10/29 职场文书
优秀新员工事迹材料
2019/05/13 职场文书
zabbix agent2 监控oracle数据库的方法
2021/05/13 Oracle
javascript拖曳互换div的位置实现示例
2021/06/28 Javascript
python编程实现清理微信重复缓存文件
2021/11/01 Python
Redis集群节点通信过程/原理流程分析
2022/03/18 Redis
Java死锁的排查
2022/05/11 Java/Android