使用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-tableDnD 拖拽的基本使用介绍
Jul 04 Javascript
Extjs grid添加一个图片状态或者按钮的方法
Apr 03 Javascript
jQuery获取元素父节点的方法
Jun 21 Javascript
JavaScript学习小结之被嫌弃的eval函数和with语句实例详解
Aug 01 Javascript
Bootstrap modal 多弹窗之叠加关闭阴影遮罩问题的解决方法
Feb 27 Javascript
Node.js中流(stream)的使用方法示例
Jul 16 Javascript
JS原生带小白点轮播图实例讲解
Jul 22 Javascript
Vue的实例、生命周期与Vue脚手架(vue-cli)实例详解
Dec 27 Javascript
代码整洁之道(重构)
Oct 25 Javascript
Vue.js实现的购物车功能详解
Jan 27 Javascript
Vue项目页面跳转时浏览器窗口上方显示进度条功能
Mar 26 Javascript
如何在CocosCreator里画个炫酷的雷达图
Apr 16 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/05 PHP
php格式化json函数示例代码
2016/05/12 PHP
php设计模式之组合模式实例详解【星际争霸游戏案例】
2020/03/27 PHP
模仿JQuery sortable效果 代码有错但值得看看
2009/11/05 Javascript
js实现时间显示几天前、几小时前或者几分钟前的方法集锦
2015/05/29 Javascript
js实时获取并显示当前时间的方法
2015/07/31 Javascript
快速解决Canvas.toDataURL 图片跨域的问题
2016/05/10 Javascript
JS组件Bootstrap Table使用实例分享
2016/05/30 Javascript
js防阻塞加载的实现方法
2016/09/09 Javascript
AngularJS自定义控件实例详解
2016/12/13 Javascript
如何获取元素的最终background-color
2017/02/06 Javascript
微信小程序 商城开发(ecshop )简单实例
2017/04/07 Javascript
vue解决弹出蒙层滑动穿透问题的方法
2018/09/22 Javascript
详解IOS微信上Vue单页面应用JSSDK签名失败解决方案
2018/11/14 Javascript
微信小程序页面缩放式侧滑效果的实现代码
2018/11/15 Javascript
微信小程序swiper使用网络图片不显示问题解决
2019/12/13 Javascript
node.js使用 http-proxy 创建代理服务器操作示例
2020/02/10 Javascript
[01:00:52]2018DOTA2亚洲邀请赛 4.4 淘汰赛 EG vs LGD 第一场
2018/04/05 DOTA
用python实现面向对像的ASP程序实例
2014/11/10 Python
Python数组遍历的简单实现方法小结
2016/04/27 Python
浅谈Python 对象内存占用
2016/07/15 Python
matplotlib 对坐标的控制,加图例注释的操作
2020/04/17 Python
python如何处理程序无法打开
2020/06/16 Python
PyTorch中clone()、detach()及相关扩展详解
2020/12/09 Python
python不同版本的_new_不同点总结
2020/12/09 Python
python 基于PYMYSQL使用MYSQL数据库
2020/12/24 Python
一款css实现的鼠标经过按钮的特效
2014/09/11 HTML / CSS
Html5自定义字体解决方法
2019/10/09 HTML / CSS
英国现代绅士品牌:Hackett
2017/12/17 全球购物
数组越界问题
2015/10/21 面试题
2014年平安创建工作总结
2014/11/24 职场文书
费城故事观后感
2015/06/10 职场文书
中学生国庆节演讲稿2015
2015/07/30 职场文书
初中班主任教育随笔
2015/08/15 职场文书
详解Python中__new__方法的作用
2022/03/31 Python
MySQL的存储函数与存储过程的区别解析
2022/04/08 MySQL