JS异步编程与Promise
Promise是异步编程的一种解决方案
什么是异步编程?
首先我们要理解什么是同步编程。同步指的是代码一行一行执行,下面的代码必须等待上面代码的执行完成。当遇到一些耗时比较长的任务时,比如网络请求就容易发生阻塞,必须等数据请求过来后才能执行后面的操作。那么这里异步执行是在请求数据的同时还能执行后面的任务。
拿现实生活来举例,比如你又要煮饭又要炒菜。如果是同步执行,那么你必须先等饭煮完才能炒菜,或者是先炒完菜才能再去煮饭,这显然是非常耽搁时间的。如果是异步执行的话,你可以在遇到煮饭任务时将任务交给电饭煲,然后再去炒菜,炒完菜后再将饭取出来,这样的过程我们就可以称之为一个异步操作。
其中取饭的操作相当于执行了回调函数,这里我已经解释了什么是同步执行与i异步执行还有回调函数,如果没有理解请再看一遍,或看这里异步执行的解析。
异步操作的影响?
所以从上可以看出异步的执行是依赖于回调函数,那么在进行异步操作时回调函数会带来什么影响呢?那就是回调地狱。
回调地狱指的是:回调函数嵌套回调函数,形成的多层嵌套。
我们来看下如下例子,这里当你需要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
})
相信细心的你已经观察到我加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转变为