浅谈对Promise的理解以及在工作中的应用
- Promise的概念
- 背景知识
- JavaScript的同步和异步
- JavaScript事件循环
- 回调函数进行异步操作
- 解决方案:Promise
- Promise 在工作中的运用
- 创建Promise
- Promise封装AJAX
- Promise链式操作
- Promise.all()
- Promise.race()
- async和await
- 总结
Promise的概念
在开始讲解Promise前,我们先大致了解一下js的运行机制以及多个任务是怎么运作的。
背景知识
众所周知,JavaScript是一门单线程语言,也就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着,这样会造成浏览器处于假死状态,严重影响用户体验。为了解决这个问题,js引入了异步的概念,在执行任务时挂起处于等待中的任务,先运行排在后面的任务。等到刚才挂起的任务返回了结果,再回过头,把挂起的任务继续执行下去。于是,js将所有的任务分成了两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
JavaScript的同步和异步
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行;
除了广义的同步任务和异步任务,对任务更精细的定义:
宏任务(macro-task):包括整体代码script,setTimeout,setInterval
微任务(micro-task):Promise,process.nextTick
宏任务中有微任务,一定要将宏任务中的微任务执行完毕,再去执行下一个宏任务。
那究竟什么是任务队列呢,同步任务和异步任务又是怎么执行的,这就要引入js的运行机制:事件循环(event loop)
JavaScript事件循环
js事件循环运作机制如下:
- 同步任务和异步任务分别进入不同的“场所”,同步任务进入主线程,异步任务进入Event Table (事件表)并注册函数
- 指定的事件完成后,Event Table (事件表)会将这个函数移入到Event Queue (事件队列)
- 当主线程的任务完成后,会检查Event Queue (事件队列),如果有任务就全部执行,如果没有就进入下一个宏任务
- 这个过程会不断的重复,这就叫事件循环
回调函数进行异步操作
和同步操作不同,异步操作是不会立即返回结果的(如发起网络请求,下载文件,操作数据库等)。如果我们后续的函数需要之前返回的结果,又怎样使之前的异步操作在其完成时通知到后续函数来执行呢?
通常,我们可以将这个函数先定义,存储在内存中,将其当做参数传入之前的异步操作函数中,等异步操作结束,就会调用执行这个函数,这个函数就叫做回调函数(callback)
// 下载
function download(callback){
// 模拟异步操作
setTimeout(function(){
// 调用回调函数
callback('下载完成');
}, 1000);
}
function callback(value){
// 下载完成的处理
console.log(value);
}
download(callback);
// 这段代码将在1秒后在控制台打印“下载完成”
但假如callback函数同样是个异步函数,且callback里又嵌入了callback呢? 例如需求是等待第一个文件下载完成后,再下载第二个文件,等待第二个文件下载完成后,再下载第三个文件…,这样的话,上面这种方法就不可取了,因为会产生很多的函数嵌套,嵌套太深容易引发回调地狱(指的是回调函数里嵌套回调函数,使得代码可读性非常差,容易陷于无止尽的循环)
//回调地狱
setTimeout(function () { //第一层
console.log('张三');//等3秒打印张三在执行下一个回调函数
setTimeout(function () { //第二层
console.log('李四');//等2秒打印李四在执行下一个回调函数
setTimeout(function () { //第三层
console.log('王五');//等一秒打印王五
}, 1000)
}, 2000)
}, 3000)
解决方案:Promise
所以为了回调地狱的问题,promise方案应运而生,它是对回调方法的一种封装,是用来处理异步操作的,可以让我们写异步调用的时候写起来更加优雅,更加美观。
以下是promise的一些特点
- Promise 对象代表一个异步操作,对象的状态不受外界影响,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)、settled (结束);只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,且一旦状态改变就不可再变
- Promise对象的then方法用来接收处理成功时响应的数据,catch方法用来接收处理失败时相应的数据,其中then方法的两个参数是resolve(成功回调),reject(失败回调)。异步任务执行成功时调用resolve函数返回结果,反之调用reject,根据不同的任务,由开发者来决定resolve和reject在函数体内的位置
- then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then/catch方法,链式调用可以保证代码的执行顺序
promise解决刚才的地狱回调问题:
// promise解决方式
function fn(str) {
var promise = new Promise(function (resolve, reject) { //resolve是成功的方法 reject是失败的方法
//处理异步任务
var flag = true;
setTimeout(function () {
if (flag) {
resolve(str)
}
else {
reject('失败')
}
})
})
return promise;
}
fn('张三')
.then((res) => { //then是成功执行的方法 返回的还是一个promise对象
console.log(res);//打印张三 res是结果
return fn('李四');
})
.then((res) => {
console.log(res);
return fn('王五')
})
.then((res) => {
console.log(res);
})
.catch((res) => { //catch是失败执行的方法
console.log(res);
})
Promise 在工作中的运用
创建Promise
promise构造器只接收一个参数,该参数被称为执行器(executor)的函数。该函数会被传递两个参数(方法),一个叫做resolve,另一个叫做reject。
resolve函数在成功时调用,reject函数在失败时被调用。并且resolve和reject只能被使用一次,如果之后还有resolve和reject也不会被执行了,有点儿类似于return,但是不同点在于,其他代码还会被照常执行。
new Promise((resolve, reject)=> {
resolve('我是第一次调用resolve');
console.log('我是其他代码');
resolve('我是第二次调用resolve'); // 不在起作用
reject('我来调用reject'); // 不在起作用
})
也可以直接使用Promise.resolve或者Promise.reject来创建成功或者失败的Promise
let p1 = Promise.resolve('我是成功的Promise'),
p2 = Promise.reject('我是失败的Promise');
Promise封装AJAX
var ajax = function(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readystate == 4 && xhr.status == 200) {
resolve(xhr.responseText)
} else if (xhr.readystate == 4 && xhr.status !== 200) {
reject(xhr.statusText)
}
}
})
}
ajax.then(console.log(xhr.responseText)); //打印出返回的数据
Promise链式操作
由于Promise的then 方法始终返回一个 Promise 对象, 所以Promise 可以一直调用 then 方法,从而实现链式调用(解决地狱回调)。不管 new Promise 创建出来的执行状态是成功 / 失败,只要在 then / catch方法中通过 return 返回一个结果,不管这个值是 Promise 对象还是普通值,都可以通过链式调用的 .then 方法中获取到这个值,因为 promise.then 方法会默认在返回值的外层包裹一层 Promise 对象,这样才可以实现 Promise 一直通过 .then 的方式去链式调用
let p1 = new Promise((resolve, reject)=> {
let name = '张三'
setTimeout(()=> {
resolve(name);
}, 2000)
})
let p2 = new Promise((resolve, reject)=> {
let name = '李四'
setTimeout(()=> {
resolve(name);
}, 1000)
})
let p3 = new Promise((resolve, reject)=> {
let name = '王五'
setTimeout(()=> {
resolve(name);
}, 3000)
})
//方式一:链式操作返回promise对象
p1.then((res) => {
console.log(res+'第一个出场');
return p2
}).then((res) => {
console.log(res+'第二个出场');
return p3
}).then((res) => {
console.log(res+'第三个出场');
})
//链式操作返回promise对象输出结果
张三第一个出场
李四第二个出场
王五第三个出场
//方式二:链式操作返回普通值
p1.then((res) => {
console.log('我是'+res);
return res
}).then((res) => {
console.log('我是'+res+'的儿子');
return res
}).then((res) => {
console.log('我是'+res+'的孙子');
})
//链式操作返回普通值输出结果
张三第一个出场
李四第二个出场
王五第三个出场
Promise.all()
这个方法返回一个新的promise对象,该promise对象在参数对象promises里所有的promise对象都成功的时候才会触发成功,一旦有任何一个promises里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含promises里所有promise返回值的数组作为成功回调的返回值,顺序跟promises的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。
简而言之,all方法会将传入的异步操作并行执行,等到它们都执行完后才会进到then方法,从时间上来看取决于最后一个异步任务执行完成的时间
let p1 = new Promise((resolve, reject)=> {
let name = '张三'
setTimeout(()=> {
resolve(name);
}, 2000)
})
let p2 = new Promise((resolve, reject)=> {
let name = '李四'
setTimeout(()=> {
resolve(name);
}, 1000)
})
let p3 = new Promise((resolve, reject)=> {
let name = '王五'
setTimeout(()=> {
resolve(name);
}, 3000)
})
// promise.all用法
Promise.all([p1,p2,p3]).then((res) => {
console.log(res+'都已到达终点');
})
// promise.all输出结果
张三,李四,王五都已到达终点
Promise.race()
race就是赛跑的意思,谁先出结果就由谁决定,采用第一个 promise 的值作为它的值,当promises参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。
let p1 = new Promise((resolve, reject)=> {
let name = '张三'
setTimeout(()=> {
resolve(name);
}, 2000)
})
let p2 = new Promise((resolve, reject)=> {
let name = '李四'
setTimeout(()=> {
resolve(name);
}, 1000)
})
let p3 = new Promise((resolve, reject)=> {
let name = '王五'
setTimeout(()=> {
resolve(name);
}, 3000)
})
// promise.race用法
Promise.race([p1,p2,p3]).then((res) => {
console.log(res+'第一个到达终点');
})
// promise.all输出结果
李四第一个到达终点
async和await
async 是“异步”的简写,而 await 可以认为是 async wait(等待) 的简写。
所以应该很好理解 async 用于申明一个 function 是异步的,返回的是一个 Promise 对象.
而 await 用于等待一个异步方法执行完成,后面必须跟一个Promise对象,但是不必写then(),直接就可以得到返回值
//基本用法的async函数
let asyncFun = async function(){
return 1
}
console.log(asyncFun())
//会返回一个promise对象
//使用场景
//摇色子方法
function dice(){
return new Promise((resolve,reject)=>{
let sino = parseInt(Math.random()*6+1) //生成一个1~6之间的随机小数
setTimeout(()=>{
resolve(sino)
},2000)
})
}
//异步方法
async function text(){
let n= await dice()
//await 关键字后面调用摇色子方法执行完毕之后,才进行变量赋值
console.log("摇出来"+n) //最后打印出摇出来的数
}
text()
//输出结果
Promise { 1 }
摇出来5
总结
以上就是我个人关于Promise的理解以及在工作中的应用,总的来说Promise在日常开发工作中的使用还是比较多的,他最大的用途在于让多个异步的任务按照我们想要的方式去执行。但想要彻底理解promise的运行方式及原理,还需要了解js引擎的运行逻辑,任务队列、宏任务微任务等。上文中如果有错误的地方欢迎指正,大家共同进步,越秃越强!