大白话讲解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 相关文章推荐
Jquery Ajax学习实例7 Ajax所有过程事件分析示例
Mar 23 Javascript
Jquery 动态循环输出表格具体方法
Nov 23 Javascript
js冒泡、捕获事件及阻止冒泡方法详细总结
May 08 Javascript
jQuery实现的向下图文信息滚动效果
May 03 Javascript
jQuery+CSS实现滑动的标签分栏切换效果
Dec 17 Javascript
前端jquery部分很精彩
May 03 Javascript
总结Node.js中的一些错误类型
Aug 15 Javascript
JS实现滑动门效果的方法详解
Dec 19 Javascript
js css3实现图片拖拽效果
Mar 04 Javascript
原生JS实现图片懒加载(lazyload)实例
Jun 13 Javascript
JS 中LocalStorage和SessionStorage的使用
Aug 17 Javascript
Vue项目开发常见问题和解决方案总结
Sep 11 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静态文件返回304技巧分享
2015/01/06 PHP
深入认识javascript中的eval函数
2009/11/02 Javascript
js操作textarea 常用方法总结
2012/12/03 Javascript
网站如何做到完全不需要jQuery也可以满足简单需求
2013/06/27 Javascript
extjs每个组件要设置唯一的ID否则会出错
2014/06/15 Javascript
html的DOM中document对象anchors集合用法实例
2015/01/21 Javascript
JavaScript对表格或元素按文本,数字或日期排序的方法
2015/05/26 Javascript
JavaScript获取数组最小值和最大值的方法
2015/06/09 Javascript
JavaScript mixin实现多继承的方法详解
2017/03/30 Javascript
vue.js动态数据绑定学习笔记
2017/05/19 Javascript
详解win7 cmd执行vue不是内部命令的解决方法
2017/07/27 Javascript
微信小程使用swiper组件实现图片轮播切换显示功能【附源码下载】
2017/12/12 Javascript
Vue组件之极简的地址选择器的实现
2018/05/31 Javascript
解决vue-cli单页面手机应用input点击手机端虚拟键盘弹出盖住input问题
2018/08/25 Javascript
使用vue 国际化i18n 实现多实现语言切换功能
2018/10/11 Javascript
vue 本地服务不能被外部IP访问的完美解决方法
2018/10/29 Javascript
详解在React-Native中持久化redux数据
2019/05/22 Javascript
原生js实现分页效果
2020/09/23 Javascript
Python打印scrapy蜘蛛抓取树结构的方法
2015/04/08 Python
python清除指定目录内所有文件中script的方法
2015/06/30 Python
Python+树莓派+YOLO打造一款人工智能照相机
2018/01/02 Python
python之matplotlib学习绘制动态更新图实例代码
2018/01/23 Python
对DJango视图(views)和模版(templates)的使用详解
2019/07/17 Python
python中的逆序遍历实例
2019/12/25 Python
python实现凯撒密码、凯撒加解密算法
2020/06/11 Python
用python进行视频剪辑
2020/11/02 Python
个人贷款担保书
2014/04/01 职场文书
校园绿化美化方案
2014/06/08 职场文书
人力资源管理专业自荐书
2014/07/07 职场文书
管理岗位竞聘演讲稿
2014/08/18 职场文书
招商引资工作汇报材料
2014/10/28 职场文书
预备党员个人总结
2015/02/14 职场文书
2017元旦晚会开幕词
2016/03/03 职场文书
go设置多个GOPATH的方式
2021/05/05 Golang
MySQL中优化SQL语句的方法(show status、explain分析服务器状态信息)
2022/04/09 MySQL
零基础学java之带返回值的方法的定义和调用
2022/04/10 Java/Android