JavaScript基础——Promise使用指南

本文介绍了JavaScript中的Promise,作为解决回调地狱的利器,Promise通过占位符装载未来值,简化异步编程。文章详细讲解了Promise的三种状态(Pending、Fulfilled、Rejected)及其转换,对比了Promise与回调函数的区别,并通过实例展示了Promise的构造、then()、catch()、resolve()、reject()、all()和race()等方法的用法。最后,文章以实际示例展示了如何使用Promise改写回调函数的代码。
摘要由CSDN通过智能技术生成

在上篇文章里《JavaScript基础——回调(callback)是什么?》我们一起学习了回调,明白了回调就是一个在另外一个函数执行完后要执行的函数,如果我们希望异步函数能够像同步函数那样顺序执行,只能嵌套使用回调函数,过多的回调嵌套会使得代码变得难以理解与维护,为了避免“回调地狱”让人发狂的行为,ES6原生引入了Promise的模式,通过这种方式,让我们代码看起来像同步代码,大大简化了异步编程,简直是ES6新特性中最让我们兴奋的特性之一。

640?wx_fmt=png

什么是promise?

640?wx_fmt=png

首先我们看看promise这个单词的中文释义,作为名词解释为承诺、诺言、誓言、约言,从中文释义可以看出,是一个未发生,将来一定会发生的某种东东…… 接下来我们来看看ECMA委员会怎么定义Promise的:

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

Promise是一个对象,用作占位符,用于延迟(可能是异步)计算的最终结果。

简单的来说,Promise就是装载未来值的容器。其实生活中有很多Promise的场景,设想以下的场景:我们去快餐店点餐,你点了一份牛肉面,你扫码付款后,会拿到一份带订单号的收据。订单号就是快餐店给我们的一份牛肉面的承诺(promise),保证了你会得到一份牛肉面。

所以我们一定要保管好我们的订单收据,因为我们知道了这个收据代表了我们未来会有一份牛肉面,尽管快餐店不能马上给我们一份牛肉面,但是我们大脑潜意识的把订单收据当做牛肉面的“占位符”了。

终于,我们听到服务员在喊“100号的牛肉面好了,请到窗口取餐”,然后我们拿着订单收据来到窗口递给服务员,我们换来了牛肉面。

说了很多,简单描述这个概念就是一旦我们需要的值准备好了,我们就用对我的承诺值换取这个值本身。

但是,还有一种不好的结果,服务员叫到我们的订单号,当我们去拿的时候,服务员会一脸歉意的告诉我们“十分抱歉,您的牛肉面卖完了”。作为顾客的我们对这个情况,除了愤怒之外只能换个地吃饭了或者点其它的。从中我们可以看出,未来值还有一个重要的特性:它可能成功也可能失败。

生活的例子很简单,我们都经历过,我们是不是特别着急如何用Promise呢?在使用之前,我们还是先了解下——Promise State(承诺状态,注:暂且这么翻译,小编也不知道如何翻译更好)  

640?wx_fmt=png

Promise State(承诺状态)

640?wx_fmt=png

Promise只会处在以下状态之一:

Pending(待处理): promise初始化的状态,正在运行,既未完成也没有失败的状态,此状态可以迁移至fulfilled和rejected状态。


Fulfilled(已完成):如果回调函数实现Promise的resolve回调(稍后介绍),那我们的promise实现兑现。


Rejected(已拒绝):如果Promise调用过程中遭到拒绝或者发生异常,那么我们的promise被拒绝,处于Rejected(状态)。


Settled(不变的,暂且这么翻译):Promise如果不处在Pending状态,状态就会改变,要不是Fulfilled要不是Rejected这两种状态。

Promise的状态转换,可以用下面一张图进行表示(图片来源:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#Methods)

640?wx_fmt=png 640?wx_fmt=png

Promise vs callback

640?wx_fmt=png

比如我们有个需求,需要通过AJAX实现三个请求,第二个和第三个请求都依赖上一个接口的请求,如果使用CallBack的方式,我们的代码可能是这样的:

ajaxCall('http://example.com/page1', response1 => {
   ajaxCall('http://example.com/page2'+response1, response2 => {
       ajaxCall('http://example.com/page3'+response2, response3 => {
           console.log(response3);
       })
   })
});

大家很快就会发现,这种多重嵌套的代码不但难以理解,而且难以维护,这就是著名的“回调地狱”现象。

如果使用Promise则会让我们的大脑更容易接受和理解,代码显得简单扁平化,代码调用如下,如何实现ajaxCallPromise稍后介绍:

ajaxCallPromise('http://example.com/page1')
   .then( response1 => ajaxCallPromise('http://example.com/page2'+response1) )
   .then( response2 => ajaxCallPromise('http://example.com/page3'+response2) )
   .then( response3 => console.log(response3) );

你是不是觉得代码的复杂性突然降低,代码看起来更简单易读呢,你也许会问ajaxCallPromise怎么写?,别着急,接着往下看

640?wx_fmt=png

Promise实现——(resolve, reject) 方法

640?wx_fmt=png

要实现回调函数转换成Promise对象,我们需要使用Promise构造函数,在上一小节,小编展示了ajaxCallPromise函数是如何调用的,ajaxCallPromise的实现内容如下,小编实现了(resolve,reject)相应的回调函数:

const ajaxCallPromise = url => {
   return new Promise((resolve, reject) => {
       // DO YOUR ASYNC STUFF HERE
       $.ajaxAsyncWithNativeAPI(url, function(data) {
           if(data.resCode === 200) {
               resolve(data.message)
           } else {
               reject(data.error)
           }
       })
   })
};

如何理解这段代码呢?

  1. 首先定义ajaxCallPromise返回类型为Promise,这意味我们会实现一个Promise的承诺。

  2. Promise接受两个函数参数,resolve(成功实现承诺)和reject(异常或失败)

  3. resolve和reject这两个特有的方法,会获取对应成功或失败的值

  4. 如果接口请求一切正常,我们将会通过resolve函数接收返回的值

  5. 如果接口请求失败,我们将会通过reject回调接收失败返回的值


再举个简单的例子,如果foo()和bar()函数都实现promise,我们改怎么写呢?

方式一:

foo().then( res => {
   bar().then( res2 => {
       console.log('Both done')
   })
});

方式二(建议这种,简单易读)

foo()
   .then( res => bar() ) // bar() returns a Promise
   .then( res => {
       console.log('Both done')
   });
640?wx_fmt=png

.then(onFulfilled, onRejected) 

640?wx_fmt=png

Promise的then()方法允许我们在任务完成后或拒绝失败后执行相应的任务,该任务可以是基于另外一个事件或基于回调的异步操作。

Promise的then()方法接收两个参数,即onFulfilled 和 onRejected 的回调,如果Promise对象完成,如果成功状态则执行onFulfilled回调,如果异常或失败则执行onRejected回调。

简单的来说,onFulfilled回调接收一个参数,及所谓的未来的值,同样 onRejected 也接收一个参数,显示拒绝的原因。让我们改动下上小节ajaxCallPromise的then()方法:

ajaxCallPromise('http://example.com/page1').then(
   successData => { console.log('Request was successful') },
   failData => { console.log('Request failed' + failData) }
);

如果请求过程失败,第二个函数将会执行输出而不是第一个函数输出。

我们一起再来看个简单的例子,我们在setTimeout()实现Promise回调,代码如下:

const PsetTimeout = duration => {
   return new Promise((resolve, reject) => {
       setTimeout( () => {
           resolve()
       }, duration);
   })
};
PsetTimeout(1000)
   .then(() => {
       console.log('Executes after a second')
   });

这里我们在这里实现了一个成功状态后没有返回成功状态值的Promise,函数执行后,成功返回后未来值将会是 undefined.

640?wx_fmt=png

catch(onRejected)方法

640?wx_fmt=png

除了then()方法可以处理错误和异常,使用Promise的catch()方法也能实现同样的功能,这个方法其实并没有什么特别,只是更容易理解而已,我们一眼就能明白是捕获异常的操作。

catch()方法只接收一个回调函数。catch()方法的onRejected回调的调用方式与then()方法的onRejected回调相同。

还记得我们上小节ajaxCallPromise的then()方法的实现吗:

ajaxCallPromise('http://example.com/page1').then(
   successData => { console.log('Request was successful') },
   failData => { console.log('Request failed' + failData) }
);

我们还可以使用catch()方法进行捕获异常或拒绝,效果是一致的。

ajaxCallPromise('http://example.com/page1’)
   .then(successData => {console.log('Request was successful')})
   .catch(failData => {console.log('Request failed' + failData)});
640?wx_fmt=png

Promise.resolve(value)

640?wx_fmt=png

Promise的resolve()方法接收成功返回值并返回一个Promise对象,用于未来值的传递,将值传递给.then(onFulfilled, onRejected) 的onFulfilled回调中。resolve()方法可以用于将未来值转化成Promise对象,下面的一段代码演示了如何使用Promise.resolve()方法:

const p1 = Promise.resolve(4);
p1.then(function(value){
   console.log(value);
}); //passed a promise object
Promise.resolve(p1).then(function(value){
   console.log(value);
});
Promise.resolve({name: "Eden"})
   .then(function(value){
       console.log(value.name);
   });

控制台将会输出以下内容:

4
4
Eden


640?wx_fmt=png

Promise.reject(value)

640?wx_fmt=png

Promise.reject(value)方法与上小节Promise.resolve(value)类似,唯一不同的是将值传递给.then(onFulfilled, onRejected) 的onRejected回调中,同时Promise.reject(value)主要用来进行调试。

让我们看看下面一段代码如何使用Promise.reject(value)方法:

const p1 = Promise.reject(4);
p1.then(null, function(value){
   console.log(value);
});
Promise.reject({name: "Eden"})
   .then(null, function(value){
       console.log(value.name);
   });

控制台将输出:

4
Eden
640?wx_fmt=png

Promise.all(iterable) 

640?wx_fmt=png

该方法传入迭代的Promise数组对象,并一起返回一个Promise对象,当所有的Promise迭代对象成功返回后,整个Promise才能返回成功状态的值。

好了,我们一起看看怎么实现Promise.all(iterable) 方法:

const p1 = new Promise(function(resolve, reject){
   setTimeout(function(){
       resolve();
   }, 1000);
});
const p2 = new Promise(function(resolve, reject){
   setTimeout(function(){
       resolve();
   }, 2000);
});
const arr = [p1, p2];
Promise.all(arr).then(function(){
   console.log("Done"); //"Done" is logged after 2 seconds
});

特别需要注意的一点,在迭代数组中,只要任意一个进入失败状态,那么该方法返回的对象也会进入失败状态,并将那个进入失败状态的错误信息作为自己的错误信息,示例代码如下:

const p1 = new Promise(function(resolve, reject){
   setTimeout(function(){
       reject("Error");
   }, 1000);
});
const p2 = new Promise(function(resolve, reject){
   setTimeout(function(){
       resolve();
   }, 2000);
});
const arr = [p1, p2];
Promise.all(arr).then(null, function(reason){
   console.log(reason); //"Error" is logged after 1 second
});
640?wx_fmt=png

Promise.race(iterable)

640?wx_fmt=png

与Promise.all(iterable) 不同的是,Promise.race(iterable) 虽然也接收包含若干个Promise对象的可迭代对象,不同的是这个方法会监听所有的Promise对象,并等待其中的第一个进入完成或失败状态的Promise对象,一旦有Promise对象满足,整个Promise对象将返回这个Promise对象的成功状态或失败状态,下面的示例展示了返回第一个成功状态的值:

var p1 = new Promise(function(resolve, reject){
   setTimeout(function(){
       resolve("Fulfillment Value 1");
   }, 1000);
});
var p2 = new Promise(function(resolve, reject){
   setTimeout(function(){
       resolve("fulfillment Value 2");
   }, 2000);
});
var arr = [p1, p2];
Promise.race(arr).then(function(value){
   console.log(value); //Output "Fulfillment value 1"
}, function(reason){
   console.log(reason)});


640?wx_fmt=png

用Promise改写上篇文章回调

640?wx_fmt=png

看过《JavaScript基础——回调(callback)是什么?》文章的同学,文章的最后我们用回调函数实现了一个真实的业务场景——用NodeJs实现从论坛帖子列表中显示其中的一个帖子的信息及留言列表信息,如果使用本篇文章学习到的内容,我们如何实现呢, 代码如下:

index.js

const fs = require('fs');
const path = require('path');
const postsUrl = path.join(__dirname, 'db/posts.json');
const commentsUrl = path.join(__dirname, 'db/comments.json');
//return the data from our file
function loadCollection(url) {
   return new Promise(function(resolve, reject) {
       fs.readFile(url, 'utf8', function(error, data) {
           if (error) {
               reject('error');
           } else {
               resolve(JSON.parse(data));
           }
       });
   });
}
//return an object by id
function getRecord(collection, id) {
   return new Promise(function(resolve, reject) {
       const data = collection.find(function(element){
           return element.id == id;
       });
       resolve(data);
   });
}
//return an array of comments for a post
function getCommentsByPost(comments, postId) {
   return comments.filter(function(comment){
       return comment.postId == postId;
   });
}
//initialization code
loadCollection(postsUrl)
   .then(function(posts){
       return getRecord(posts, "001");
   })
   .then(function(post){
       console.log(post);
       return loadCollection(commentsUrl);
   })
   .then(function(comments){
       const postComments = getCommentsByPost(comments, "001");
       console.log(postComments);
   })
   .catch(function(error){
       console.log(error);
   });
640?wx_fmt=png

本篇的内容就介绍到这里,各位是否看的很过瘾,虽然Promise已经比回调函数好许多,但是还是不够简洁,不够符合我们人类大脑思考逻辑,如果我们能以书写更接近同步的方法书写异步代码,那该多好啊,ES8引入了async/await让我们能使用更接近同步的方式书写异步代码,想想就很激动,小编将会在下篇文章进行介绍,敬请期待!

专注分享当下最实用的前端技术。关注前端达人,与达人一起学习进步!

长按关注"前端达人"

640?wx_fmt=gif 640?wx_fmt=jpeg
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值