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

前言

第一次接触到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带给我们的好处。

如果你有把啰嗦的代码提炼成单独函数的好习惯,那就更加画美不看了:

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解决多层异步调用的简单学习心得就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • JavaScript异步编程Promise模式的6个特性

    在我们开始正式介绍之前,我们想看看Javascript Promise的样子: 复制代码 代码如下: var p = new Promise(function(resolve, reject) {  resolve("hello world");}); p.then(function(str) {  alert(str);}); 1. then()返回一个Forked Promise 以下两段代码有什么区别呢? 复制代码 代码如下: // Exhibit Avar p = new Pr

  • NodeJS中利用Promise来封装异步函数

    在写Node.js的过程中,连续的IO操作可能会导致"金字塔噩梦",回调函数的多重嵌套让代码变的难以维护,利用CommonJs的Promise来封装异步函数,使用统一的链式API来摆脱多重回调的噩梦. Node.js提供的非阻塞IO模型允许我们利用回调函数的方式处理IO操作,但是当需要连续的IO操作时,你的回调函数会多重嵌套,代码很不美观,而且不易维护,而且可能会有许多错误处理的重复代码,也就是所谓的"Pyramid of Doom". 复制代码 代码如下: ste

  • Javascript异步编程模型Promise模式详细介绍

    Promise 编程模式也被称为 thenable,可以理解为 延迟后执行.每个 Promise 都拥有一个叫做 then 的唯一接口,当 Promise 失败或成功时,它就会进行回调.它代表了一种可能会长时间运行而且不一定必须完成的操作结果.这种模式不会阻塞和等待长时间的操作完成,而是返回一个代表了承诺的(promised)结果的对象. 当前的许多 JavaScript 库(如 jQuery 和 Dojo.AngularJS)均添加了这种称为 Promise 的抽象.通过这些库,开发人员能够在

  • Javascript中的异步编程规范Promises/A详细介绍

    Javascript里异步编程逐渐被大家接受,先前大家一般通过回调嵌套,setTimeout.setInterval等方式实现,代码看起来非常不直观,不看整个代码逻辑很难快速理解.Javascript里异步函数大概有I/O函数(Ajax.postMessage.img load.script load等).计时函数(setTimeout.setInterval)等. 这些我们都很熟悉,在复杂的应用中往往会嵌套多层,甚至以为某些步骤未完成而导致程序异常,最简单的例子:比如你往DOM中注入节点,你必

  • 有关Promises异步问题详解

    迄今为止,可能每个JavaScript开发者和他们的祖母都听说过Promises.如果你没有,那么你即将会.promises的概念是由CommonJS小组的成员在 Promises/A规范 中提出来的.Promises被逐渐用作一种管理异步操作回调的方法,但出于它们的设计,它们远比那个有用得多.事实上,由于它们的多种用法,有无数人告诉我--在我写过一些关于promises的东西后--我"遗漏了promises的重点".那么什么是promises的重点呢? 一点关于Promises的东西

  • javascript使用Promise对象实现异步编程

    Promise对象是CommonJS工作组为异步编程提供的统一接口,是ECMAScript6中提供了对Promise的原生支持,Promise就是在未来发生的事情,使用Promise可以避免回调函数的层层嵌套,还提供了规范更加容易的对异步操作进行控制.提供了reject,resolve,then和catch等方法. 使用PROMISE Promise是ES6之后原生的对象,我们只需要实例化Promise对象就可以直接使用. 实例化Promise: var promise = new Promis

  • 简单实现异步编程promise模式

    异步编程 javascript异步编程, web2.0时代比较热门的编程方式,我们平时码的时候也或多或少用到,最典型的就是异步ajax,发送异步请求,绑定回调函数,请求响应之后调用指定的回调函数,没有阻塞其他代码的执行.还有像setTimeout方法同样也是异步执行回调的方法. 如果对异步编程还不太熟悉的话,直接戳 阮一峰大牛的教程 ,这篇文章介绍了四种异步编程的方式: 回调函数 事件监听 发布/订阅 promise模式 这几种方式的可维护性逐级递增,理解难度也逐级递增.这篇总结也是针对prom

  • JavaScript异步回调的Promise模式封装实例

    网页的交互越来越复杂,JavaScript 的异步操作也随之越来越多.如常见的 ajax 请求,需要在请求完成时响应操作,请求通常是异步的,请求的过程中用户还能进行其他的操作,不会对页面进行阻塞,这种异步的交互效果对用户来说是挺有友好的.但是对于开发者来说,要大量处理这种操作,就很不友好了.异步请求完成的操作必须预先定义在回调函数中,等到请求完成就必须调用这个函数.这种非线性的异步编程方式会让开发者很不适应,同时也带来了诸多的不便,增加了代码的耦合度和复杂性,代码的组织上也会很不优雅,大大降低了

  • javascript异步编程代码书写规范Promise学习笔记

    最近工作轻松了点,想起了以前总是看到的一个单词promise,于是耐心下来学习了一下. 一:Promise是什么?为什么会有这个东西? 首先说明,Promise是为了解决javascript异步编程时候代码书写的方式产生的. 随着javascript的发展,异步的场景越来越多.前端有AJAX,setTimeout等,后端Node异步更多.按照传统的做法,那么就是各种回调嵌回调.代码可以把人绕晕. 这个时候,CommonJS社区提出了一个叫做Promise/A+的规范,这个规范定义了如何书写异步代

  • 异步JavaScript编程中的Promise使用方法

    异步? 我在很多地方都看到过异步(Asynchronous)这个词,但在我还不是很理解这个概念的时候,却发现自己常常会被当做"已经很清楚"(* ̄? ̄). 如果你也有类似的情况,没关系,搜索一下这个词,就可以得到大致的说明.在这里,我会对JavaScript的异步做一点额外解释. 看一下这段代码: var start = new Date(); setTimeout(function(){ var end = new Date(); console.log("Time elap

随机推荐