浅谈ES6中的Promise对象

文章目录

    • 前言
    • 产生背景
    • 什么是Promise
    • Promise的特点
    • 方法介绍
    • 缺点
    • 总结
    • 参考文章

前言

ES6中绝大部分其实都是语法糖,为我们平常开发与学习提供了极大的便利。所以学习ES6是每一个前端工程师必备的技能,而我,作为前端的小“辣鸡”,居然还没有吃透ES6+,所以不能再懒惰下去,于是我建立了一个专栏,专门来学习ES6+。今天主要是来讲讲ES6超级超级重要的一个东西:Promise 对象,这东西简直不要太好用啊!注意哈:如果要理解这篇文章最好先理解了JS的运行机制(顺便把浏览器渲染机制也搞懂了)这样学Promise对象将十分简单!如果文章有错误的地方,欢迎大家指正!

产生背景

平时开发过程中难免要遇到异步操作比如:AJAX请求文件操作事件处理定时器计时等等,而JS引擎是单线程的,同一时间只能执行一个任务,异步操作是无法自己进行的,那么得交给其他同一进程下(Renderer进程)其他线程处理,如事件触发线程定时器线程等,等异步操作完毕,我们需要拿到异步操作的结果,这时候我们通常是设置回调函数,一个异步操作完成后就设置一个回调,那么有时候有几十个异步操作,那么我们是不是得设置十几个回调函数呢,简直太变态了,大家约定俗成地将这种情况称之为回调地狱。除此之外,我们经常把异步操作和异步结果都放在外头同一个函数里头写了,逻辑和结果看起来一点都不清晰,有时候我们并不关心异步操作是啥,就想要结果,那么回调就不是一个很好的选择了(但是不能抛弃回调哈,回调可谓是JS的精华)。我们看一个常用的例子,比如我们要异步请求数据,而且下一个异步操作需要根据上个异步操作的结果来执行。

下面是我用原生AJAX写的简单的请求:

const getJSON = function (url, methods, success) {
     const callback = function () {
       // 只考虑成功状态
       if (this.readyState !== 4) {
         return;
       } else {
       // 成功响应后回调,返回的数据作为回调函数参数
         this.status == 200 ? success(JSON.parse(this.response)) : console.log(new Error('请求失败'));
       }
     };

     const xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHttp");
     xhr.onreadystatechange = callback;
     xhr.open(methods, url);
     xhr.send();
   };
 // 请求a.json ,成功后将数据中的a存入数组,然后回调,下面是一样的
	getJSON('data/a.json', 'GET', function (data) {
	  let array = [];
	  if (data.a == 1) {
	    array.push(data.a);
	    getJSON('data/b.json', 'GET', function (data) {
	      if (data.b == 2) {
	        array.push(data.b);
	        getJSON('data/c.json', 'GET', function (data) {
	          if (data.c == 3) {
	            array.push(data.c);
	            console.log(array); // [1,2,3]
	          }
	        });
	      }
	    });
	  }
	});

下图是我在data中存储的数据:

浅谈ES6中的Promise对象_第1张图片
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们可以很清楚地看到,这样请求数据很麻烦,而且不符合我们平时的习惯。现在我只写了几个,那如果有几十条甚至上百个请求的时候,那简直无法想象了。。。那么,如果我们可以像写同步代码那样写异步代码,将是十分方便的,比如大概是这样(我没有写全):

   let result1 =  getJSON('data/a.json', 'GET');
   let result2 =  getJSON('data/b.json', 'GET');
   let result3 =  getJSON('data/c.json', 'GET');

一切的语法升级我认为只是图个方便,如方便效率,方便写代码等等,所以这个时候 Promise 对象就应运而生了。

什么是Promise

每次一看见新概念就头疼,然后上MDN看看这是个啥东西:

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象

OK,不得不说算你狠了,我还是看不懂,文字我是读完了,脑子里面没有任何画面,你能不能说人话呐!我咋知道你代理谁啊!看完后更加头疼了。。。

迷茫之际,打开了阮一峰ES6文档,介绍 Promise 写道:

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

看完之后我理解是: Promise 对象就是把异步操作(如请求AJAX)封装了,把返回的结果包括成功和失败两种情况都放在了返回的Promise对象里面,将来想要获取异步操作的消息,直接从这里拿就是。其实简单来说就是把处理异步操作结果的回调函数给替换了成了另一种方式,逻辑和结果被单独分开了,这种方式很符合人的思维习惯。

new Promise(function (resolve, reject) {
	// 异步操作
	...
	// 成功
	resolve();
	//失败
	reject();
}).then(resolve => {
	// 处理异步操作成功的结果
})
  .catch (error => {
	// 处理异步操作失败的结果
});

先统一执行逻辑,不关心如何处理结果,根据结果是成功还是失败,在将来的某个时候调用resolve函数或reject函数。

是不是大概理解一些了呢,把 promise对象当做一个容器,异步操作放在在 Promise对象这个容器里面,我在上面定义的两个回调方法是将来才执行的,而且保证你一定会执行。这两个方法保存着将来异步操作完成后的结果。

// 成功
resolve();
//失败
reject();

我们从字面上来理解,promise 的英文意思是承诺。也就是说,我在当前这个时间点我承诺你:将来等异步操作完成好我就来执行以上那两个方法,在此之前你可以假装你有了异步操作的结果,你可以自己定义如果处理结果的方法。像这样:

promise.then(resolve => {
	// 处理异步操作成功的结果
})
      .catch (error => {
		// 处理异步操作失败的结果
	});

Promise的特点

我们现在对Promise有了一定的理解后,下面介绍一下Promise有什么特点吧:

  1. Promise对象一旦被创建就会被执行
    这个还是很容易理解的,Promise 是一个构造函数,我们创建对象就需要 new 关键字,那么只要new了一个对象,这个构造函数就会立即执行。如果想根据情况来创建的话,最好是在外层再包裹一个函数。详细可以了解一下new关键字的原理。(个人理解,如果有异议,欢迎指正)
  2. 状态变化不会受外界影响
    Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  3. 一旦状态改变,就不会再变,任何时候都可以得到这个结果
    这句话意思就是说,Promise对象的根据异步操作的结果变变化状态,如果结果成功了那么状态从pending转化为fufilled,如果失败了,pending变为rejected。一旦变化状态后,后续任何操作都不会改变这个状态。(我们可以在resolve函数后面抛出一个错误,看看异步操作执行完后能不能在catch方法里面捕捉到这个错误,待会在下面我会介绍,因为目前还没讲到Promise的方法)

我们使用Promise对上面的代码进行改写:

// 封装AJAX请求,返回一个Promise对象
const getJSON = function (url, methods) {
	return new Promise(function(resolve, reject) {
		const callback = function () {
			if (this.readyState !== 4) {
				return;
			} else {
				this.status === 200 ? resolve(JSON.parse(this.response)) : reject(this.status);
			}
		};

		const xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHttp");
		xhr.onreadystatechange = callback;
    xhr.open(methods, url);
    xhr.send();
	});
};

// 以同步的写法来进行异步操作
let p1 = getJSON('data/a.json', 'GET');
let p2 = getJSON('data/b.json', 'GET');
let p3 = getJSON('data/c.json', 'GET');

// 当所有异步操作的完成后,回调的函数
Promise.all([p1, p2, p3])
.then(([data1, data2, data3]) => {
	console.log(data1, data2, data3);
})
.catch(error => {
	console.log(error);
});

在浏览器控制台的结果是:
浅谈ES6中的Promise对象_第2张图片

看是不是实现了我们的愿景呢,用同步的方式来写异步代码,而且我们无需对异步操作有过多的关注,重点对他们的结果进行分析操作就行啦。在这里我使用了一个Promise的一个all方法,这个方法是将所有的Promise对象放在一个数组里面,统一执行异步操作,然后再返回一个新的Promise对象,最后我们就可以得到所有异步操作的结果了。

方法介绍

不说别的我们先用 console.dir在浏览器打印一下便知道:
浅谈ES6中的Promise对象_第3张图片
我们先看看原型方法:

  • then
    这是一个最常用,最常见,最重要的方法了,只要用了Promise对象就会用到这个方法。有两个参数:resolvereject (名字任意)。第一个参数是异步操作成功后回调的函数,第二个参数是失败后回调的函数。(更准确说,应该是Promise的状态从pendingfullfilled,或者从从pendingfailed)。
promise.then(resolve => {}, reject => {});

当然这个方法还是返回了Promise对象,也就是说可以采用链式的写法,非常方便。

  • catch
    这个方法的作用和then方法第二个参数的作用是一样的,都是用于指定发生错误时的回调函数。那我肯定要质问一下,为啥还要有这个方法呢,直接用then的第二个参数不就完事了吗?(其实我前面的例子里早有暗示了,都是用的catch不信回头自己悄悄哦)
    但是经过我的学习,并且实践发现,catch方法确实方便,不信吗?先来瞧瞧catch的写法:
promise
	.then(resolve => {})
	.catch(error => {})

很明显了,从写法上来说catch更接近同步的写法,而且代码看起来很清晰。当然光这一点不足以显示它的厉害之处,我们再来看看:

new Promise(function(resolve, reject) {
	 	resolve('ok');
	 }).then((resolve) => {
	 	console.log(resolve);
	 	// 在then中抛出一个错误
	 	throw new Error('error');
	 }).catch(error => {
	 	console.log(error)
	 });

我们可以看到,在then的后面我又抛出了一个错误,那么返回的Promise对象就会携带一个错误被后面的catch方法捕获到。但是有几种情况catch是捕获不到的:

  1. 异步操作中抛出的错误捕获不到
new Promise(function(resolve, reject) {
	 	setTimeout(() => {
	 		throw new Error('throw an error');
	 	},1000);
	 	resolve('ok');
	 }).then((resolve) => {
	 	console.log(resolve);
	 }).catch(error => {
	 	console.log(error)
	 });
  1. Promise状态变从pending边为failed无法捕获
new Promise(function (resolve, reject) {
			resolve('OK');
			throw new Error('error');
		}).then((resolve) => {
			console.log(resolve);
		}).catch(error => {
			console.log(error);
		});
  1. 创建一个新的 Promise ,且已决议
var p1 = Promise.resolve("calling next");

var p2 = p1.catch(function (reason) {
    //这个方法永远不会调用
    console.log("catch p1!");
    console.log(reason);
});

p2.then(function (value) {
    console.log("next promise's onFulfilled"); /* next promise's onFulfilled */
    console.log(value); /* calling next */
}, function (reason) {
    console.log("next promise's onRejected");
    console.log(reason);
});
  • finally
    这个方法是ES9新出的方法,看名字就知道:不管 Promise 对象最后状态如何,都会执行的操作。平时用得比较少,简单说一句吧,就是不管Promse状态如何,在最后一定会执行的方法,所以一般这方法一般放在最后。
  • all

Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

在上面我们提前使用了这个方法,我们再来详细看看:

// 以同步的写法来进行异步操作
let p1 = getJSON('data/a.json', 'GET');
let p2 = getJSON('data/b.json', 'GET');
let p3 = getJSON('data/c.json', 'GET');

// 当所有异步操作的完成后,回调的函数
Promise.all([p1, p2, p3])
.then(([data1, data2, data3]) => {
	console.log(data1, data2, data3);
})
.catch(error => {
	console.log(error);
});

当有很多个异步操作的时候,创建多个Promise实例,然后返回一个新的Promise实例,这个实例同样包括了两种状态,而且是跟里面所有Promise实例息息相关的。有几个特点:

  1. 这个Promise的执行时间取决于它里面最慢的那个Promise中的异步操作
  2. 只有当所有Promise的状态为fullfilled的时候,这个对象才的状态才会变成fullfilled。但是,一旦有一个Promise状态变为failed。那么它也会变成failed

这个方法很适用于多个异步操作的情况,并且只关注他们的结果。all方法里面我放了一个数组来存储所有Promise对象,那么当状态发生改变,我便可以通过解构赋值得到他们每一个的值,超级方便是不是。

  • race
    这个方法和all正好相反,这个方法正如它的名字一样,多个Promise对象进行“赛跑”,哪个最快,那么这个方法就返回哪个Promise对象。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。
let p1 = new Promise((resovle, reject) => {
            setTimeout(resovle, 500, "P1");
       });

let p2 = new Promise((resovle, reject) => {
     setTimeout(resovle, 600, "P2");
});

Promise.race([p1, p2]).then(resovled => {
          console.log(resovled); // p1
      });

当我们并行地去执行很多异步操作的时候,并且具有相同特点,那么我们并不关注是谁的异步操作,只需让它最快显示出来就行,则这个方法是你的不二选择(当然我这么说可能不是很全面)。

注意:Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

  • resolve将现有的对象转化成Promise对象
    参数有很多情况,我看了阮一峰的文档后,我个人认为最常用的就是不加参数,直接返回一个带有fullfilled状态的Promise对象,也正好符合这个方法的名字。详细可以看看阮一峰的文档。
  • reject立即返回一个带有failed状态的Promise对象

缺点

有些时候优点也变成为了缺点,比如:

  • Promise一旦新建就会立即执行,那么中途停都停不下来。
  • Promise 内部的错误无法反应到外部,也就是说不设置回调函数,根本不知道里面出现了错误,所以会继续执行外部的代码。
  • Promise处于pending的时候,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

其实还有一个更为致命的缺点,在处理多个异步操作的时候,如果某个异步操作需要上一个异步操作的结果,也就是说需要等待上一个异步操作完成了才进行下面的操作(有点同步任务的感觉),需要处理复杂的逻辑,那么Promise显得很无力,在一开始的时候我在回调里面还是有if语句的,虽然它的写法很糟糕,但还是有逻辑的。那么Promise就这么完了?当然,既然有问题肯定会有解决方案的。比如:generatorasync/await(其实就是generator的语法糖)后面我会介绍一下如何解决这个问题。

总结

我个人十分喜欢廖雪峰和阮一峰的文章,两个大佬,搭配MDN是一个不错的选择。当然看MDN的概念实在是太难懂了,最好结合案例来分析,我这篇文章写了我足足三天时间,虽然有些地方和大佬的文档有些相似,也引用了一些文字,但是我自己总结出来的偏多,真的是受益匪浅,谢谢他们的文章。有时候自己写着写着就不知道怎么写了,想放弃。如今完成后,自己对Promise对象真的明亮了很多了,不敢说自己全部掌握了,但是至少,我好像当着很多人讲了一遍,哈哈,写文章就是这样的感觉。说了这么多,希望自己越来越好吧,学JS才4个多月时间,只能算是入了门,这门语言的深度着实令人喟叹!今后继续加油鸭!

参考文章

【1】阮一峰 Promise对象
【2】MDN Promise对象的catch方法
【3】廖雪峰 Promise对象

你可能感兴趣的:(ECMAScript,6+)