JS 探索异步Promise
Promise是异步编程的一种解决方案
学习promise前,我们先来了解下什么是异步
首先我们要知道js是单线程语言,也就是说,它并不能像JAVA语言那样,多个线程并发执行。
先讲下什么是同步。同步指的是代码一行一行依次执行,下面的代码必须等待上面代码的执行完成。当遇到一些耗时比较长的任务时,比如网络请求就容易发生阻塞,必须等数据请求过来后才能执行后面的操作。那么这里异步执行是在请求数据的同时还能执行后面的任务。
拿现实生活来举例,比如你又要煮饭又要炒菜。如果是同步执行,那么你必须先等饭煮完才能炒菜,或者是先炒完菜才能再去煮饭,这显然是非常耽搁时间的。如果是异步执行的话,你可以在遇到煮饭任务时将任务交给电饭煲,然后再去炒菜,炒完菜后再将饭取出来,这样的过程我们就可以称之为一个异步操作。
其中取饭的操作相当于执行了回调函数(异步执行的函数就是回调函数,因为这个函数最终会调入主线程执行),这里我已经简单解释了什么是同步执行与异步执行还有回调函数。
如果没有理解请再看一遍,或看这里异步执行的解析。
异步任务的影响
所以从上可以看出异步的执行是依赖于回调函数,那么在进行异步操作时回调函数会带来什么影响呢?那就是回调地狱。
回调地狱指的是:回调函数嵌套回调函数,形成的多层嵌套。
我们来看下如下例子,这里当你需要1s后打印3条hello,再过1秒后打印3条vue.js 再过1秒后打印3条node.js
首先你要知道setTimeout就是一个异步操作,其中传入的第一个参数就是回调函数,到了规定的时间就会回调执行。
setTimeout(() => { console.log("hello"); console.log("hello"); console.log("hello"); setTimeout(() => { console.log("vue.js"); console.log("vue.js"); console.log("vue.js"); setTimeout(() => { console.log("node.js"); console.log("node.js"); console.log("node.js"); }, 1000); }, 1000); }, 1000);
使用Promise解决回调地狱
如上代码就是产生了回调地狱,当代码过多会非常复杂。如下就是使用一种优雅的方式(promise)来解决如上的问题
这里我不打算特别详细的讲解Promise的用法,你可以根据下面的注释来分析代码。
//这里注意Promise()不用调用会自动执行,Promise括号中接收一个函数为参数,而函数中有两个参数resolve,reject //当然resolve与reject参数名不是固定的,但是为了语义化我们通常这样写更加的通俗易懂 new Promise((resolve, reject) => {//resolve表示异步操作成功时的回调。reject表示失败时的回调(暂时不谈)。 setTimeout(() => { resolve(); //因为resolve与reject是函数,所以加上括号调用 }, 1000); }).then(() => { //then表示然后,一旦执行了resolve()就会执行then()这个方法 //之后的所有的回调代码就能then()函数中处理,这样会使得代码变得更加清晰 console.log("hello"); console.log("hello"); console.log("hello"); setTimeout(() => { console.log("vue.js"); console.log("vue.js"); console.log("vue.js"); setTimeout(() => { console.log("node.js"); console.log("node.js"); console.log("node.js"); }, 1000); }, 1000); })
这样看起来then中还是有回调函数的嵌套,那么我们可以在then()方法中返回一个Promise实例,这样返回一个Promise就能继续的使用then()来解决嵌套,形成链式调用。那么要如何操作呢?如下:
new Promise((resolve, reject) => { //第一次回调执行的函数 setTimeout(() => { resolve(); }, 1000); }).then(() => { //第一次回调函数处理的代码 console.log("hello"); console.log("hello"); console.log("hello"); return new Promise((resolve, reject) => { //第二次回调执行的函数 setTimeout(() => { resolve(); }, 1000); }) }).then(() => { //第二次回调函数处理的代码 console.log("vue.js"); console.log("vue.js"); console.log("vue.js"); setTimeout(() => { console.log("node.js"); console.log("node.js"); console.log("node.js"); }, 1000); }) //最后还有一层嵌套那么交给你自己来解绝。
虽然代码变得复杂了,但是逻辑清晰了。即使嵌套再多层,代码依然很清晰。只要是类似于如上的回调函数嵌套(比如网络请求)就必须使用Promise包裹。
再来举个例子,每隔1s将数值加1,且打印加1后的结果,并将结果传入下一个回调,再进行加1操作并打印,持续两次。
new Promise(resolve => {//因为我并不打算调用reject,这个参数可以省略 setTimeout(() => { resolve(0);//resolve中传入的参数在then中接收 }, 1000) }).then(data => { data++; console.log(data);//1 return new Promise(resolve => { setTimeout(() => { resolve(data); }, 1000) }) }).then(data => { data++; console.log(data);//2 })
仔细观察setTimeout()中的内容,你会发现里面只有一行代码,加1还有输出的操作都是放在then()中执行,这就是Promise的操作思想,将回调函数中需要执行的所有代码放入then()中执行,再由resolve()调用。这里你可能还是会觉得麻烦,多此一举,但实际编码中回调函数的代码量是非常的多,使用这样的结构代码会更加清晰。
说了这么多我们来讲讲reject()这个函数。上面有注释有提到reject()是回调失败时的调用,那么当我们回调失败时,如果也把代码写入then()中再做判断,那么Promise()显得好像并不是很优雅,因为then既有成功的回调代码,也会有失败的回调代码。
当然Promise考虑到了这点,所以我们失败时的回调代码是写在catch()中的,catch就是“捕获”的意思,一旦你调用reject()就会来到catch()这个方法中。
举个简单的例子:
new Promise((resolve, reject) => { setTimeout(() => { let num = Math.floor(Math.random() * 11);//0-10的随机数 if (num >= 5) { resolve(num); } else { reject(num); } },1000) }).then(data => { console.log("执行了成功时的回调,数值为:"+data); }).catch(reason => { console.log("执行了失败时的回调,数值为:"+reason); })
异步获取一个随机数(0-10),1秒后执行。大于等于5我们认为是成功,所以调用resolve()修改Promimse的状态,否则是失败,调用reject()修改Promise的状态,reject()中的参数我们可以认为传入的是失败的原因,所以一般使用reason作为参数名。
那么现在你是不是又困惑了,什么是Promise的状态?
promise有三种状态:pending / fulfilled / rejected
- pending:等待状态,当异步任务的回调函数还没有执行Promise就处于pending状态,比如网络请求,定时器时间
- fulfilled:完成状态,当我们调用了resolve()方法,promise的状态就会从 pending转变为fulfilled,且会调用then()方法
- rejected:拒绝状态,当我们调用了reject()方法,promise的状态就会从 pending转变为rejected,且会调用catch()方法
有基础的小伙伴又会疑惑了then()中不是也可以写失败的回调代码吗?没错,下面来看下Promise的另外一种写法
还是以上面代码为例子。
new Promise((resolve, reject) => { setTimeout(() => { let num = Math.floor(Math.random() * 11);//0-10的随机数 if (num >= 5) { resolve(num); } else { reject(num); } },1000) }).then(data => { console.log("执行了成功时的回调,数值为:"+data); },reason => { console.log("执行了失败时的回调,数值为:"+reason); })
其实then()中有两个参数,第一个参数的函数是用于处理成功时的回调代码,而第二个参数的函数就是处理失败时的回调代码。
Promise的链式调用与简写方法
看如下代码:
new Promise(resolve => { //这里1s后回调,执行then()中的代码 setTimeout(() => { resolve('aaa'); }, 1000) }).then(data => { console.log(data);//aaa return new Promise(resolve => { resolve(data + 'bbb'); }) }).then(data => { console.log(data);//aaabbb return new Promise(resolve => { resolve(data + 'ccc'); }) }).then(data => { console.log(data);//aaabbbccc })
你会发现上面代码在后面的then中并没有进行异步操作,所以我们没有必要使用Promise包裹没有异步操作的代码,可以简写成如下代码:
new Promise(resolve => { setTimeout(() => { resolve('aaa'); }, 1000) }).then(data => { console.log(data); return Promise.resolve(data + 'bbb'); }).then(data => { console.log(data); return Promise.resolve(data + 'ccc'); }).then(data => { console.log(data); })
没想到吧除了上面还有更简单的方法:
new Promise(resolve => { setTimeout(() => { resolve('aaa'); }, 1000) }).then(data => { console.log(data); return data + 'bbb'; }).then(data => { console.log(data); return data + 'ccc'; }).then(data => { console.log(data); })
直接返回结果,实际内部会对返回的结果进行封装。
对于如上的操作,我们肯定不会只有成功的回调,当然也会有失败时的回调。
new Promise(resolve => { setTimeout(() => { resolve('aaa'); }, 1000) }).then(data => { console.log(data); //想必也猜到了失败的简写 return Promise.reject('error info'); //当然我们如果抛出一个异常也会执行catch中的代码 //throw 'error info'; }).then(data => { console.log(data); return Promise.resolve(data + 'ccc'); }).then(data => { console.log(data); }).catch(reason => { //如果执行失败时的回调catch()那么中间的then()都不会执行 console.log('执行了失败时的回调',reason); })
Promise.all()方法
Promise.all()主要用于同时处理多个异步任务。当我们需要完成一个业务,就是当两个网络请求都成功时,就执行回调函数中的代码。这个时候就需要使用到Promise.all()。下面我就使用setTimeout()来模拟网络请求(这里你需要知道网络请求是一个异步操作),第一个网络请求(p1)的时间是1s,第二个请求(p2)的时间是2s。
let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('结果1'); }, 1000); }) let p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('结果2'); }, 2000); }) //Promise.all([])接收多个异步任务,放入数组中 Promise.all([p1, p2]).then(results => {//results接收多个参数,所以是数组 console.log(results);//["结果1", "结果2"] })
当你执行代码后你会发现两秒后才执行完成,也就是等待所有网络请求成功时,回调代码才会执行。
Promise.race()方法
Promise.all()是等待每个异步任务都完成就执行,以最慢的异步任务为准。而Promise.race()就是谁先完成就执行谁,以最快的任务为准。所以你可以看到then中我都是写的result而不是复数results。如下你可以看作是两个网络请求,你可以理解为同时请求了两个服务器,但是p1请求的服务器最先响应,所以我们就可以从响应快的服务器中拿数据。
let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('结果1'); }, 1000); }) let p2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('结果2'); }, 2000); }) //Promise.race([])中接收多个异步任务,放入数组中 Promise.race([p1, p2]).then(result => {//result只会接收一个参数,谁先完成接收谁 console.log(result); })
总结
Promise就是对异步操作进行封装,其中的思想就是不希望你在回调函数中处理需要执行的代码,而是把回调执行的代码放入对应的区域也就是then()或catch()方法中处理,然后通过resolve()或reject()来调用。将代码分离后做的时链式的调用,你可以这样理解一个then或一个catch就是一个链条的连接点。一般有异步操作我们都要使用promise对异步操作进行封装,养成良好的习惯。