使用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 相关文章推荐
超级酷和最实用的jQuery实例收集(20个)
Apr 21 Javascript
超简单的jquery的AJAX用法
May 10 Javascript
js实现鼠标拖动图片并兼容IE/FF火狐/谷歌等主流浏览器
Jun 06 Javascript
JS实现简洁、全兼容的拖动层实例
May 13 Javascript
jQuery控制DIV层实现由大到小,由远及近动画变化效果
Oct 09 Javascript
js省市县三级联动效果实例
Apr 15 Javascript
微信小程序 定位到当前城市实现实例代码
Feb 23 Javascript
jQuery点击页面其他部分隐藏下拉菜单功能
Nov 27 jQuery
vue请求本地自己编写的json文件的方法
Apr 25 Javascript
webpack proxy 使用(代理的使用)
Jan 10 Javascript
VUE动态生成word的实现
Jul 26 Javascript
js基础语法与maven项目配置教程案例
Jul 15 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 编程安全性小结
2010/01/08 PHP
php获取本地图片文件并生成xml文件输出具体思路
2013/04/27 PHP
php实现的简易扫雷游戏实例
2015/07/09 PHP
浅析Yii2缓存的使用
2016/05/10 PHP
浅谈PHP array_search 和 in_array 函数效率问题
2019/10/15 PHP
php使用pthreads v3多线程实现抓取新浪新闻信息操作示例
2020/02/21 PHP
编写Js代码要注意的几条规则
2010/09/10 Javascript
基于jQuery的history历史记录插件
2010/12/11 Javascript
了解jQuery技巧来提高你的代码(个人觉得那个jquery的手册很不错)
2012/02/10 Javascript
javascript提取URL的搜索字符串中的参数(自定义函数实现)
2013/01/22 Javascript
采用call方式实现js继承
2014/05/20 Javascript
在Vue中如何使用Cookie操作实例
2017/07/27 Javascript
Vue项目自动转换 px 为 rem的实现方法
2018/10/29 Javascript
Vue源码中要const _toStr = Object.prototype.toString的原因分析
2018/12/09 Javascript
详解微信小程序获取当前时间及日期的方法
2019/04/28 Javascript
jQuery+ThinkPHP实现图片上传
2020/07/23 jQuery
原生js实现移动小球(碰撞检测)
2020/12/17 Javascript
[01:14:34]DOTA2上海特级锦标赛C组资格赛#2 LGD VS Newbee第一局
2016/02/28 DOTA
Python中使用logging模块代替print(logging简明指南)
2014/07/09 Python
Python基于time模块求程序运行时间的方法
2017/09/18 Python
基于python中的TCP及UDP(详解)
2017/11/06 Python
在numpy矩阵中令小于0的元素改为0的实例
2019/01/26 Python
python实现AES加密和解密
2019/03/27 Python
python爬虫简单的添加代理进行访问的实现代码
2019/04/04 Python
如何定义TensorFlow输入节点
2020/01/23 Python
python实现扑克牌交互式界面发牌程序
2020/04/22 Python
Python实现清理微信僵尸粉功能示例【基于itchat模块】
2020/05/29 Python
Python趣味实例,实现一个简单的抽奖刮刮卡
2020/07/18 Python
CSS3 清除浮动的方法示例
2018/06/01 HTML / CSS
Pretty Little Thing美国:时尚女性服饰
2018/08/27 全球购物
班长岗位职责
2013/11/10 职场文书
旅游管理专业生自荐信范文
2014/01/02 职场文书
我们的节日中秋活动方案
2014/08/19 职场文书
竞选班干部演讲稿100字
2014/08/20 职场文书
六查六看心得体会
2014/10/14 职场文书
2014年转正工作总结
2014/11/08 职场文书