大白话讲解JavaScript的Promise


Posted in Javascript onApril 06, 2017

去年6月份, ES2015正式发布(也就是ES6,ES6是它的乳名),其中Promise被列为正式规范。作为ES6中最重要的特性之

一,我们有必要掌握并理解透彻。本文将由浅到深,讲解Promise的基本概念与使用方法。

 ES6 Promise 先拉出来遛遛

复杂的概念先不讲,我们先简单粗暴地把Promise用一下,有个直观感受。那么第一个问题来了,Promise是什么玩意呢?是一个类?对象?数组?函数?

别猜了,直接打印出来看看吧,console.dir(Promise),就这么简单粗暴。

大白话讲解JavaScript的Promise

这么一看就明白了,Promise是一个构造函数,自己身上有all、reject、resolve这几个眼熟的方法,原型上有then、catch等同样很眼熟的方法。这么说用Promise new出来的对象肯定就有then、catch方法喽,没错。

那就new一个玩玩吧。

var p = new Promise(function(resolve, reject){
  //做一些异步操作
  setTimeout(function(){
    console.log('执行完成');
    resolve('随便什么数据');
  }, 2000);
});

Promise的构造函数接收一个参数,是函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。其实这里用“成功”和“失败”来描述并不准确,按照标准来讲,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected。不过在我们开始阶段可以先这么理解,后面再细究概念。

在上面的代码中,我们执行了一个异步操作,也就是setTimeout,2秒后,输出“执行完成”,并且调用resolve方法。

运行代码,会在2秒后输出“执行完成”。注意!我只是new了一个对象,并没有调用它,我们传进去的函数就已经执行了,这是需要注意的一个细节。所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:

function runAsync(){
  var p = new Promise(function(resolve, reject){
    //做一些异步操作
    setTimeout(function(){
      console.log('执行完成');
      resolve('随便什么数据');
    }, 2000);
  });
  return p;      
}
runAsync()

这时候你应该有两个疑问:1.包装这么一个函数有毛线用?2.resolve('随便什么数据');这是干毛的?

我们继续来讲。在我们包装好的函数最后,会return出Promise对象,也就是说,执行这个函数我们得到了一个Promise对象。还记得Promise对象上有then、catch方法吧?这就是强大之处了,看下面的代码:

runAsync().then(function(data){
  console.log(data);
  //后面可以用传过来的数据做些其他操作
  //......
});

在runAsync()的返回上直接调用then方法,then接收一个参数,是函数,并且会拿到我们在runAsync中调用resolve时传的的参数。运行这段代码,会在2秒后输出“执行完成”,紧接着输出“随便什么数据”。

 这时候你应该有所领悟了,原来then里面的函数就跟我们平时的回调函数一个意思,能够在runAsync这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。

你可能会不屑一顾,那么牛逼轰轰的Promise就这点能耐?我把回调函数封装一下,给runAsync传进去不也一样吗,就像这样:

function runAsync(callback){
  setTimeout(function(){
    console.log('执行完成');
    callback('随便什么数据');
  }, 2000);
}

runAsync(function(data){
  console.log(data);
});

效果也是一样的,还费劲用Promise干嘛。那么问题来了,有多层回调该怎么办?如果callback也是一个异步操作,而且执行完后也需要有相应的回调函数,该怎么办呢?总不能再定义一个callback2,然后给callback传进去吧。而Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。

链式操作的用法

所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:

runAsync1()
.then(function(data){
  console.log(data);
  return runAsync2();
})
.then(function(data){
  console.log(data);
  return runAsync3();
})
.then(function(data){
  console.log(data);
});

这样能够按顺序,每隔两秒输出每个异步回调中的内容,在runAsync2中传给resolve的数据,能在接下来的then方法中拿到。运行结果如下:

大白话讲解JavaScript的Promise

猜猜runAsync1、runAsync2、runAsync3这三个函数都是如何定义的?没错,就是下面这样:

function runAsync1(){
  var p = new Promise(function(resolve, reject){
    //做一些异步操作
    setTimeout(function(){
      console.log('异步任务1执行完成');
      resolve('随便什么数据1');
    }, 1000);
  });
  return p;      
}
function runAsync2(){
  var p = new Promise(function(resolve, reject){
    //做一些异步操作
    setTimeout(function(){
      console.log('异步任务2执行完成');
      resolve('随便什么数据2');
    }, 2000);
  });
  return p;      
}
function runAsync3(){
  var p = new Promise(function(resolve, reject){
    //做一些异步操作
    setTimeout(function(){
      console.log('异步任务3执行完成');
      resolve('随便什么数据3');
    }, 2000);
  });
  return p;      
}

在then方法中,你也可以直接return数据而不是Promise对象,在后面的then中就可以接收到数据了,比如我们把上面的代码修改成这样:

runAsync1()
.then(function(data){
  console.log(data);
  return runAsync2();
})
.then(function(data){
  console.log(data);
  return '直接返回数据'; //这里直接返回数据
})
.then(function(data){
  console.log(data);
});

那么输出就变成了这样:

大白话讲解JavaScript的Promise

reject的用法

到这里,你应该对“Promise是什么玩意”有了最基本的了解。那么我们接着来看看ES6的Promise还有哪些功能。我们光用了resolve,还没用reject呢,它是做什么的呢?事实上,我们前面的例子都是只有“执行成功”的回调,还没有“失败”的情况,reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调。看下面的代码。

function getNumber(){
  var p = new Promise(function(resolve, reject){
    //做一些异步操作
    setTimeout(function(){
      var num = Math.ceil(Math.random()*10); //生成1-10的随机数
      if(num<=5){
        resolve(num);
      }
      else{
        reject('数字太大了');
      }
    }, 2000);
  });
  return p;      
}

getNumber()
.then(
  function(data){
    console.log('resolved');
    console.log(data);
  }, 
  function(reason, data){
    console.log('rejected');
    console.log(reason);
  }
);

getNumber函数用来异步获取一个数字,2秒后执行完成,如果数字小于等于5,我们认为是“成功”了,调用resolve修改Promise的状态。否则我们认为是“失败”了,调用reject并传递一个参数,作为失败的原因。

 运行getNumber并且在then中传了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到下面两种结果:

大白话讲解JavaScript的Promise或者 大白话讲解JavaScript的Promise

catch的用法

我们知道Promise对象除了then方法,还有一个catch方法,它是做什么用的呢?其实它和then的第二个参数一样,用来指定reject的回调,用法是这样:

getNumber()
.then(function(data){
  console.log('resolved');
  console.log(data);
})
.catch(function(reason){
  console.log('rejected');
  console.log(reason);
});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:

getNumber()
.then(function(data){
  console.log('resolved');
  console.log(data);
  console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
  console.log('rejected');
  console.log(reason);
});

在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:

大白话讲解JavaScript的Promise

 也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能。

all的用法

Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。我们仍旧使用上面定义好的runAsync1、runAsync2、runAsync3这三个函数,看下面的例子:

Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
  console.log(results);
});

用Promise.all来执行,all接收一个数组参数,里面的值最终都算返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。那么,三个异步操作返回的数据哪里去了呢?都在then里面呢,all会把所有异步操作的结果放进一个数组中传给then,就是上面的results。所以上面代码的输出结果就是:

大白话讲解JavaScript的Promise

有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很酷?有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

race的用法

all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」,这就是race方法,这个词本来就是赛跑的意思。race的用法与all一样,我们把上面runAsync1的延时改为1秒来看一下:

Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
  console.log(results);
});

这三个异步操作同样是并行执行的。结果你应该可以猜到,1秒后runAsync1已经执行完了,此时then里面的就执行了。结果是这样的:

大白话讲解JavaScript的Promise

你猜对了吗?不完全,是吧。在then里面的回调开始执行时,runAsync2()和runAsync3()并没有停止,仍旧再执行。于是再过1秒后,输出了他们结束的标志。

 这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:

//请求某个图片资源
function requestImg(){
  var p = new Promise(function(resolve, reject){
    var img = new Image();
    img.onload = function(){
      resolve(img);
    }
    img.src = 'xxxxxx';
  });
  return p;
}

//延时函数,用于给请求计时
function timeout(){
  var p = new Promise(function(resolve, reject){
    setTimeout(function(){
      reject('图片请求超时');
    }, 5000);
  });
  return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
  console.log(results);
})
.catch(function(reason){
  console.log(reason);
});

requestImg函数会异步请求一张图片,我把地址写为"xxxxxx",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。运行结果如下:

大白话讲解JavaScript的Promise

总结

ES6 Promise的内容就这些吗?是的,能用到的基本就这些。

我怎么还见过done、finally、success、fail等,这些是啥?这些并不在Promise标准中,而是我们自己实现的语法糖。

本文中所有异步操作均以setTimeout为例子,之所以不使用ajax是为了避免引起混淆,因为谈起ajax,很多人的第一反应就是jquery的ajax,而jquery又有自己的Promise实现。如果你理解了原理,就知道使用setTimeout和使用ajax是一样的意思。说起jquery,我不得不吐槽一句,jquery的Promise实现太过垃圾,各种语法糖把人都搞蒙了,我认为Promise之所以没有全面普及和jquery有很大的关系。后面我们会细讲jquery。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
如何实现浏览器上的右键菜单
Jul 10 Javascript
js闭包的用途详解
Nov 09 Javascript
jQuery链式操作实例分析
Nov 16 Javascript
Google 地图事件实例讲解
Aug 06 Javascript
js HTML5多图片上传及预览实例解析(不含前端的文件分割)
Aug 26 Javascript
js实现常见的工具条效果
Mar 02 Javascript
基于jquery实现五星好评
Nov 18 jQuery
在Vue-cli里应用Vuex的state和mutations方法
Sep 16 Javascript
微信小程序实现带缩略图轮播效果
Nov 04 Javascript
详解可以用在VS Code中的正则表达式小技巧
May 14 Javascript
Angular8引入百度Echarts进行图表分析的实现代码
Nov 27 Javascript
Vue中的this.$options.data()和this.$data用法说明
Jul 26 Javascript
JS实现的二叉树算法完整实例
Apr 06 #Javascript
JavaScript结合HTML DOM实现联动菜单
Apr 05 #Javascript
js实现按座位号抽奖
Apr 05 #Javascript
Angularjs 实现移动端在线测评效果(推荐)
Apr 05 #Javascript
微信小程序中的onLoad详解及简单实例
Apr 05 #Javascript
微信小程序 页面跳转如何实现传值
Apr 05 #Javascript
微信小程序 数据遍历的实现
Apr 05 #Javascript
You might like
php download.php实现代码 跳转到下载文件(response.redirect)
2009/08/26 PHP
php另类上传图片的方法(PHP用Socket上传图片)
2013/10/30 PHP
PHP中file_get_contents高?用法实例
2014/09/24 PHP
使用PHPMailer发送邮件实例
2017/02/15 PHP
JS获取各种宽度、高度的简单介绍
2014/12/19 Javascript
DOM基础教程之事件类型
2015/01/20 Javascript
JS简单计算器实例
2015/01/20 Javascript
jQuery中noconflict函数的实现原理分解
2015/02/03 Javascript
jQuery实现当前页面标签高亮显示的方法
2015/03/10 Javascript
深入探究JavaScript中for循环的效率问题及相关优化
2016/03/13 Javascript
JavaScript-html标题滚动效果的简单实现
2016/09/08 Javascript
jstl中判断list中是否包含某个值的简单方法
2016/10/14 Javascript
jQuery  ready方法实现原理详解
2016/10/19 Javascript
jQuery is not defined 错误原因与解决方法小结
2017/03/19 Javascript
从vue源码解析Vue.set()和this.$set()
2018/08/30 Javascript
如何使用VuePress搭建一个类型element ui文档
2019/02/14 Javascript
jQuery表单元素过滤选择器用法实例分析
2019/02/20 jQuery
Angular8基础应用之表单及其验证
2019/08/11 Javascript
在Node.js中将SVG图像转换为PNG,JPEG,TIFF,WEBP和HEIF格式的方法
2019/08/22 Javascript
python写入中英文字符串到文件的方法
2015/05/06 Python
使用python存储网页上的图片实例
2018/05/22 Python
使用selenium模拟登录解决滑块验证问题的实现
2019/05/10 Python
pygame实现成语填空游戏
2019/10/29 Python
美国最受欢迎的度假租赁网站:VRBO
2016/08/02 全球购物
丝芙兰意大利官方网站:Sephora.it
2019/12/13 全球购物
Booking.com缤客中国:全球酒店在线预订网站
2020/05/03 全球购物
英语师范专业毕业生自荐信
2013/09/21 职场文书
精彩的推荐信范文
2013/11/26 职场文书
优秀共产党员先进事迹材料
2014/05/06 职场文书
身边的榜样活动方案
2014/08/20 职场文书
苏州园林导游词
2015/02/03 职场文书
小学生暑假安全保证书
2015/07/13 职场文书
tensorflow中的梯度求解及梯度裁剪操作
2021/05/26 Python
MyBatis 动态SQL全面详解
2021/10/05 MySQL
Win11如何设置右键单击显示所有选项?Win11右键单击显示所有选项设置教程
2022/04/08 数码科技
mysql 获取相邻数据项
2022/05/11 MySQL