在早期 JavaScript
的 ES5
语法中,多层函数的回调嵌套是一件让人很头疼的事儿,行内黑话一般称之为回调地狱 。
可能有些伙计还没遇到过此类业务场景,但是没关系,只要在前端圈里混,苍天会绕过谁呢?所以为了大家,我就举个特别常见的业务场景:
URL-A
, URL-B
, URL-C
(都是 get
请求),我们需要分别向这三个接口请求获取数据。URL-B
时需要带上 URL-A
返回的数据,同理,请求 URL-C
时也要带上 URL-B
返回的数据。我们来看看用早期的 jquery ajax
会怎么处理:
$.get('/URL-A', function(resA){
// do Something
$.get('/URL-B?query=' + resA,function(resB){
// do Something
$.get('/URL-C?query=' + resB, function(resC){
// do Something
})
})
})
从上面我们可以看出,这一段代码是很不健康的,为什么这么说?有以下几点理由:
当然,我们也可以用函数内 callback
的形式来改写上面的这段代码,使之变得更直观些:
// 请求 URL-C
function getURLCData(res){
$.get('/URL-C?query=' + res, function(res){
// do Something
})
}
// 请求 URL-B
function getURLBData(res){
$.get('/URL-B?query=' + res, function(res){
// do Something
getURLCData(res)
})
}
// 请求 URL-A
function getURLAData(){
$.get('/URL-A', function(res){
// do Something
getURLBData(res)
})
}
这样我们就避免了函数的纵向发展,公共代码与业务代码也可以抽离,但是这种方式还不够直观,在复杂业务,超高并发请求下,业务代码依旧晦涩。
所以,在 ES6
中提出了 Promise
用来解决回调嵌套的问题。
以上代码的 Promise
改写我们在下文再讲,我们先讲讲何为 Promise
。
Promise
的基本用法对于 Promise
我们可以这么理解,如果一个函数 Promise
(数据准备好了)了,那么我们就可以 then
干点事情。
MDN
对其有以下描述:
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象
Promise
有以下四个函数可以调用:
Promise.all(iterable)
Promise.race(iterable)
Promise.resolve(value)
Promise.reject(reason)
我们来看个例子:
实现一个简单的定时 promise:
function delayLogNum(){
return new Promise((resolve, reject) => {
setTimeout(()=> {
console.log('success');
resolve('ok');
}, 3000)
})
}
delayLogNum().then(res => {
console.log(res)
})
Promise
处理串行和并行在 JavaScript
中已经有同步,异步,串行,并行这些概念了,大家需分清楚其中的区别:
在 ES7
中新增加了 async
和 await
关键字:
async
用于定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。await
操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。ok,梳理完了前置知识点,我们来看看利用 Promise
和 async await
怎么处理串行。
举一个例子:遍历一个 Number
数组并且在每次遍历时延时 1
秒输出遍历的数值。
function delay(){
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
async function eachEveryVal(item){
await delay();
console.log(item)
}
async function eachArr(data){
for(var val of data){
await eachEveryVal(val)
}
}
eachArr([1, 2, 3, 4])
我们再举一个例子,就拿最开始那段代码来说,就是典型的异步串行操作,我们可以这么来改写它:
function getURL(url){
return new Promise((resolve, reject) => {
$.get(url, res => {
resolve(res)
})
})
}
async getData(){
let dataA = await getURL('URL-A');
let dataB = await getURL('URL-B?query=' + dataA);
let dataC = await getURL('URL-C?query=' + dataB);
};
getData();
讲完了串行,我们再来讲讲异步并行。假设我们有以下需求:
URL-A
, URL-B
, URL-C
(都是 get
请求),我们需要分别向这三个接口请求获取数据。这就是一个典型的并行的业务需求,我们也可以用 promise
来实现它。
const URI_LIST = ["URL-A", "URL-B", "URL-C"];
function getURL(url){
return new Promise((resolve, reject) => {
$.get(url, res => {
resolve(res)
})
})
}
async function getData(){
const promises = URI_LIST.map(url => getURL(url));
Promise.all(promises).then(res => console.log(res))
};
getData();
我大概解释下这段代码:
getURL()
函数返回一个 promise
,并在传入 url
参数给 $.get()
调用,请求成功后调用 reslove(res)
来返回请求结果。getData()
函数声明一个 promises
来存放 getURL(url)
返回的 promise
对象,通过 URI_LIST.map()
来得到我们在基础用法中所讲的 iterable
参数对象,并将此对象传入 Promise.all()
中,最后通过 then()
获取结果。要注意的是,此结果是三个请求返回的数据组成的数组。Promise
常见特性有以下5个特性需要大家理解:
下面我来一一解释这 5 点特性:
1.Promise 捕获错误与 try catch 等同
这句话的意识就是说,在 new Promise(()=>{})
中直接去 throw err
,是可以通过 Promise.catch()
方法捕捉的,这也就意味中 Promise
内部也通过 try catch
进行了异常处理。
2.Promise 拥有状态变化
Promise
有以下三种状态:
而 Promise.resolve()
和 Promise.reject()
都会改变 promise
的状态值,其中 resolve
会将此状态值修改为 fulfilled
, 而 reject
会将此状态值修改为为 rejected
。
特别的,一旦 Promise
的状态值被改变,就会被固定,不再发生变化。也就是说只要你 resolve()
或者 reject()
了一次,在这之后无论你再调用几次这两个方法都不起效果。
3.Promise 方法中的回调是异步的
先解释一下,Promise
方法中的回调是异步的这句话中的方法是指 Promise
中的 catch
,then
,finally
这些方法,而不是指 new Promise()
中的 executor
函数,这个函数你可以把它理解为一个立即执行函数。
想要真正理解 Promise 方法中的回调是异步的
这句话,还没有这么简单,为什么这么说,因为 setTimeout
也是异步的,如果它们两同时存在且作用域平级,那么谁先执行,谁后执行,它们之间的竞争关系怎么确认?
想要了解这其中的原理,我们就需要了解一个概念:微任务(microtasks)和宏任务(tasks)。
我们已经知道,JavaScript
是单线程的操作,正是因为如此,才有了现在的同步和异步之分。在主线程中,一般是按顺序执行同步任务。而其他的异步任务则会挂起,当它们有返回值后会添加到任务队列中。等到主线程的同步任务执行完毕后,它会去任务队列中读取(按先进先出的原则)异步任务执行。以此形成一个反复的过程被称为事件循环。借用一个掘金上的图片,侵删:
而在异步任务中,其实又可以细分为宏任务和微任务。
setTimeout
,setInterval
, MessageChannel
(Web Worker中的管道通信)。而在整个异步流程中,JavaScript
会先进入整体代码执行宏任务,然后再检查是否有微任务需要执行,如果有,则需要立即执行;如果没有则检查队列,开始执行下一批宏任务并检查微任务。借用一个掘金上的图片,侵删:
总结一下, Promise
中的 executor
函数是处于主线程同步队列中执行(立即执行函数),而其他的方法诸如 then
, catch
等,则是异步任务队列中的微任务,诸如 setTimeout
,setInterval
等函数必须在微任务执行完毕后再开始执行。
所以,看到这儿,整个 Promise
中的函数内部在整个执行栈的执行顺序和竞争关系就已经很清晰了。
4.Promise 方法每次都返回一个新的 Promise
这儿的意思很直白,意味着无论是 then
,catch
亦或是 finally
都会返回 一个新的 Promise
对象。
5.Promise 会存储返回值
一般情况下我们都会这样来使用 Promise
:
function p(flag){
return new Promise((resolve, reject) => {
if(flag){
resolve('success')
}else{
reject('error')
}
})
};
p(true).then(res => console.log('res', res))
可以看到,我们通常会把一些参数或者函数在成功状态下通过 resolve()
传递给 then()
函数来接收并作相应处理;在失败状态下通过 reject()
把错误信息传递给 catch()
函数来处理。
特别的,如果你在 Promise
直接返回某些参数, Pormise
也会捕捉到你返回的参数并把它包装成 Promise
对象并传递给对应的接收函数。
Promise
面试题5.1 请用 Pormise
实现以下流水灯,已知红黄绿三个函数,要求红灯3秒执行一次,黄灯2秒执行一次,绿灯1秒执行一次:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
function delay(time){
return new Promise((resolve, reject) => {
setTimeout(resolve,time)
})
}
async function runTask(){
await delay(3000);
red();
await delay(2000);
green();
await delay(1000);
yellow();
// 递归循环播放
runTask()
}
runTask();
5.2 请用 Pormise
实现 mergePromise 函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组 data 中:
const timeout = ms => new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
const ajax1 = () => timeout(2000).then(() => {
console.log('1');
return 1;
});
const ajax2 = () => timeout(1000).then(() => {
console.log('2');
return 2;
});
const ajax3 = () => timeout(2000).then(() => {
console.log('3');
return 3;
});
const mergePromise = ajaxArray => {
// 在这里实现你的代码
};
mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log('done');
console.log(data); // data 为 [1, 2, 3]
});
// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]
我们先分析一下题目,看到这个题目是不是就有一种很熟悉的感觉?像不像我们在上面改写的异步并行?
你的感觉没错,实际上这道题考查的就是让你手写一个简单的 Promise.all()
函数。
所以,我们就能知道,上题中 ajaxArray
参数实际上就是一个包含多个 Promise
对象的数组,我们可以用并行遍历的方式来处理它。
const mergePromise = ajaxArray => {
let seq = Promise.resolve();
let data = [];
ajaxArray.map(func => {
seq = seq.then(func).then(res => {
data.push(res);
return data;
})
})
return seq;
};
如果你看到这了这,那么恭喜你,不管你有没有吸收其中的内容,你至少你知道了整个 Promise
应该怎么去学。实际上在工作中 Promise
的应用是很多的,包括我们使用的 babel
中也会有 Promise-polyfill
。现在已经是 9102
年了,前端圈已经逐渐稳定下来,这意味着你我的时间已然不多,所以加油吧,伙计们。