年前年后跳槽季,准备从面试内容入手看看前端相关知识点,旨在探究一个系列知识点,能力范围以内的深刻探究一下。重在实践,针对初级前端和准备面试的同窗,争取附上实际的代码例子以及相关试题~系列名字就用【秃破前端面试】—— 由于圈内你们共识,技术与发量成正比。😄但愿你们早日 秃 破瓶颈前端
关于面试题或者某个知识点的文章太多了,这里笔者只是想把我的的总结用代码仓库的形式记录下来并输出文章,毕竟理论不等于实践,知其然也要知其因此然,实践用过才能真正理解~ git
相关系列文章:github
凡事有因必有果,新事物的出现就表明着老的事物不能知足咱们的需求了。Promise 这个新事物就是在这个背景下出现的,而它代替的老事物就是ES6 以前常常被用的 callback(回调函数)。面试
虽然 ES6 Promise 已经并不能算是新事物了,可是就背景来讲,它刚出现的时候确实是来解决异步回调地狱问题的。数据库
什么是回调地狱,来看一个最简单的示例:json
setTimeout(() => {
console.log(111);
setTimeout(() => {
console.log(222);
setTimeout(() => {
console.log(333);
setTimeout(() => {
console.log(444);
// 你还能够放置更多
...
}, 4000);
}, 3000);
}, 2000)
}, 1000);
复制代码
通常来讲回调地狱就是出如今异步操做中,下一次的操做依赖上一次的结果,一环套一环,套着套着就套的咱们头痛难忍,写出了上面的代码。跨域
固然,上面有点为了黑而黑了,事实上,常用的场景应该是 AJAX 请求以及数据库的各类操做会产生回调地狱。下面代码就是一个标准的数据库查屡次表的一个操做(这里我只查了两次,可是也已经造成了嵌套)。数组
/** * 回调地狱示例 */
const db = Object.create(null); // 假设这就是链接数据库的对象
/** * 第一步,从 A 表查出 id 为 1 的用户 * 第二步,从 B 表查出文章做者是 id = 1 用户 username 的全部文章 **/
db.query('SELECT * FROM A WHERE id = 1', function(err, results) {
if (err) throw err;
// 完成第一步,开始第二步
db.query(`SELECT * FROM B WHERE author = ${results[0].username}`, function(err, results) {
if (err) throw err;
// 完成第二步,开始干坏事
console.log(results);
});
});
复制代码
上面代码,若是再继续查下去,必定跟上面的代码差不太多,而数据库查询也确实可能会出现上面的状况。promise
出现问题了,就得解决啊,Promise 就出现了,先来看看 Promise 怎么解决回调地狱的。bash
// promise 解决
function f1() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(111), 1000);
}).then(data => console.log(data));
}
function f2() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(222), 2000);
}).then(data => console.log(data));;
}
function f3() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(333), 3000);
}).then(data => console.log(data));;
}
function f4() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(444), 4000);
}).then(data => console.log(data));;
}
f1().then(f2).then(f3).then(f4);
复制代码
嗯,这么一看,确实是解决了,并无函数嵌套,而后调用也变成了链式调用。固然,这个例子也有点特殊,反过来看看数据库查询数据的例子:
/** * 使用 Promise * 由于 Promise 是 ES6,因此下面全部代码都使用 ES6 语法 **/
new Promise((resolve, reject) => {
db.query('SELECT * FROM A WHERE id = 1', (err, results) => {
if (err) reject(err);
resolve(results);
});
}).then(data => {
// 拿到第一步数据,开始第二步
db.query(`SELECT * FROM B WHERE author = ${results[0].username}`, (err, results) => {
if (err) reject(err);
// 完成第二步,开始干坏事
console.log(results);
});
}).catch(err => {
throw err;
});
复制代码
相比之下,看起来确实要好看一些。
Promise 对象用于表示一个异步操做的最终完成 (或失败),及其结果值。Promise 对象是一个代理对象(代理一个值),被代理的值在 Promise 对象建立时多是未知的。它容许你为异步操做的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法能够像同步方法那样返回值,但并非当即返回最终执行结果,而是一个能表明将来出现的结果的 promise 对象。
它的出现是为了解决 ES6 以前 JS 代码中频繁嵌套回调函数所致使的回调地狱问题,Promise 为 ES6 特性。
一个 Promise 对象值是未知的,状态是可变的,可是不管怎么变化,它的状态永远处于如下三种之间:
Promise 的状态会发生变化,成功时会从pending -> fulfilled
,失败时会从pending -> rejected
,可是此过程是不可逆的,也就是不能从另外两个状态变成pending
。fulfilled/rejected
这两个状态也被称为 settled 状态。
JS 万物皆对象,因此 Promise 也能够被咱们new
出来。咱们经过下面的语法来新建一个 Promise 对象:
new Promise( function(resolve, reject) {...} /* executor */ );
复制代码
Promise 的构造函数有一个参数 —— 是一个带有两个参数(resolve, reject)
的函数,这两个参数分别表明这次异步操做的结果也就是Promise的状态。resolve
和reject
函数被调用时,分别会将这次 Promise 的状态改为fulfilled
或者rejected
,一旦异步操做结束,Promise 的最终状态只能是两者之一,若是异步成功,该状态会被resolve
函数修改成fullfilled
;相反当异步过程当中抛出一个错误,那么该状态就会被reject
函数改为rejected
。
Promise 的原型链以及对象自己有一些方法供咱们使用,其中最经常使用也比较有可说性的就是下面这几个:
添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来 resolve。
这么看起来老是晦涩难懂的,仍是得实际代码来看:
new Promise((resolve, reject) => {
setTimeout(() => resolve(111), 1000);
}).then(data => {
console.log(data);
});
new Promise((resolve, reject) => {
setTimeout(() => reject(111), 1000);
}).then(data => {
console.log(data);
});
复制代码
能够看到,.then
里面拿到的是咱们 Promise resolve 事后的数据。而且他还会返回一个 Promise 继续供咱们调用,好比:
new Promise((resolve, reject) => {
setTimeout(() => resolve(111), 1000);
}).then(data => {
console.log(data); // 打印 111
return data + 111; // 至关于 resolve(data + 111)
}).then(data => {
console.log(data); // 打印 222
});
复制代码
then()
用法比较简单,你们确定也常常用,这里其实就知道.then()
是能够一直链式调用的,由于它的返回值也是一个 Promise,就能够了。
添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的 promise。当这个回调函数被调用,新 promise 将以它的返回值来 resolve,不然若是当前 promise 进入 fulfilled 状态,则以当前 promise 的完成结果做为新 promise 的完成结果。
new Promise((resolve, reject) => {
setTimeout(() => reject(111), 1000);
}).then(data => {
console.log('then data:', data);
}).catch(e => {
console.log('catch e: ', e);
});
复制代码
如上图所示:一般来讲,通常写到 catch 就表示发生异常了,通常就结束了,可是从文档说明来看,它返回的也是一个 Promise,我表示并无这么用过,可是仍是实验一下吧:
new Promise((resolve, reject) => {
setTimeout(() => reject(111), 1000);
}).then(data => {
console.log('then data:', data);
}).catch(e => {
console.log('catch e: ', e);
return e;
}).then(data => {
console.log('catch data: ', data);
});
复制代码
好吧,涨姿式了,可是仍是那句话,我的以为 catch 到错误就能够了,不必下一步了,除非你还要用错误作其余的事情~
上面提到了catch()
通常来讲用于捕获错误,因此大部分代码应该是到这一步就结束了,可是实际上 Promise 提供了标准结束方法 finally()
,只要 Promise 状态变成 settled,不管是 rejected 仍是 fulfilled,都会在 finally 里捕获。
new Promise((resolve, reject) => {
setTimeout(() => reject(111), 1000);
}).then(data => {
console.log('then data:', data);
}).catch(e => {
console.log('catch e: ', e);
return e;
}).then(data => {
console.log('catch data: ', data);
return data;
}).finally(() => {
console.log('promise finally');
return 222;
}).then(data => {
console.log('finally data: ', data);
});
复制代码
从上图能够看得出,finally 也会返回一个 promise,可是我劝你们善良,真的到 finally 就能够结束了!!!这里只是为了演示它的返回。
我想了一下,不经常使用的缘由多是本身太 low 了,其实它仍是有很明显的试用场景的。好比官方给出的Demo:
let isLoading = true;
fetch(myRequest).then(function(response) {
var contentType = response.headers.get("content-type");
if(contentType && contentType.includes("application/json")) {
return response.json();
}
throw new TypeError("Oops, we haven't got JSON!");
})
.then(function(json) { /* process your JSON further */ })
.catch(function(error) { console.error(error); /* this line can also throw, e.g. when console = {} */ })
.finally(function() { isLoading = false; });
复制代码
这个场景应该在实际开发过程当中很经常使用,若是不使用 finally,咱们会在 then 和 catch 里分别设置一次isLoading = false;
,而使用 finally 则只须要赋值一次,不只避免了重复代码并且优化了逻辑~这才是正确的使用之道啊~
Promise.finally(fn)须要注意如下两点:
这个方法返回一个新的 promise 对象,该 promise 对象在 iterable 参数对象里全部的 promise 对象都成功的时候才会触发成功,一旦有任何一个 iterable 里面的 promise 对象失败则当即触发该 promise 对象的失败。这个新的 promise 对象在触发成功状态之后,会把一个包含 iterable 里全部 promise 返回值的数组做为成功回调的返回值,顺序跟 iterable 的顺序保持一致;若是这个新的 promise 对象触发了失败状态,它会把 iterable 里第一个触发失败的 promise 对象的错误信息做为它的失败错误信息。Promise.all 方法常被用于处理多 个promise 对象的状态集合。
这个算是我常用的一个 API 了,上面的内容虽然有点长,可是总结起来其实也很简单,大概就是以下三个方面:
// promise 解决
function f1() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(111), 1000);
}).then(data => console.log(data));
}
function f2() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(222), 2000);
}).then(data => console.log(data));;
}
function f3() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(333), 3000);
}).then(data => console.log(data));;
}
Promise.all([f1, f2, f3]);
复制代码
// promise 解决
function f1() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(111), 1000);
});
}
function f2() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(222), 2000);
});;
}
function f3() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(333), 3000);
});;
}
Promise.all([f1(), f2(), f3()]).then(results => {
console.log(results);
});
复制代码
能够看到,返回值是一个数组,而且每一个元素对应的就是参数数组里对应事后的resolve值。
// promise 解决
function f1() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(111), 1000);
});
}
function f2() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(222), 2000);
});;
}
function f3() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(333), 3000);
});;
}
Promise.all([f1(), f2(), f3()]).then(results => {
console.log(results);
}).catch(e => {
console.log(e);
});
复制代码
能够看到,当我把第二个和第三个分别设置成 reject 的时候,Promise.all 进入了 catch 也就是捕获异常阶段,捕获到的是第二个 reject 内容,也就是第一次出现的 reject 的那个地方。
因此,通常来讲,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不一样接口的数据一块儿请求过来,不过,若是其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了~
当 iterable 参数里的任意一个子 promise 被成功或失败后,父 promise 立刻也会用子 promise 的成功返回值或失败详情做为参数调用父 promise 绑定的相应句柄,并返回该 promise 对象。
这个 API 讲道理,不常用,可是在某些场景下,仍是特别给力的。怎么说的,字面意义就是竞赛,想象一个场景,用户登陆和取消,登陆过程是一个请求过程,会耗时,假设我这边点击登陆以后,数据请求过程当中点击了取消,那么若是登陆还未响应回来,应该就是取消这个行为赢得了竞争,就不登陆了。
固然,登陆取消这个场景我没有实际使用过,我只在一个地方用到过 Promise.race —— fetch timeout,众所周知,前端若是使用 fetch 请求的时候,没办法设置超时时间,由于 fetch 内部并无 timeout 这个参数,那么若是咱们但愿前端能够设置超时时间,好比超过5s没有响应数据的话就认为请求超时了,这个时候可使用 Promise.race 来帮助咱们实现。由于 fetch 本质上也是 Promise,咱们只须要在 Promise.race 里将 fetch 和一个 5s 延时事后 reject/resolve 的 Promise 进行竞赛便可。具体代码以下:
// fetch timeout实现
timeoutPromise = () => new Promise((resolve) => {
setTimeout(() => {
resolve(
new Response(
'Timeout',
{
status: 408,
statusText: "Fetch timeout",
ok: false
}
)
);
}, timeout = 5000);
});
Promise.race([timeoutPromise(), fetch(url, opts)])
.then(res => res.json())
.then(data => {
return data;
});
复制代码
由于我比较喜欢用 fetch,因此刚好有这个场景的使用,亲测可用,具体细节内容你们能够根据本身的项目去修改,这里不过多介绍,感兴趣能够留言交流。
讲到这里,必定会有人问了,是否是又要手写一个 Promise 了?固然不会! 我说过了,重在实践,从实践角度出发,我以为并不会有人在项目里使用本身手写的 Promise 而是都直接 new Promise()
,所以,我再去画蛇添足浪费本身和你们的时间去写一个并不会有人用的 Promise,也没什么意义,若是大家想了解内部实现,建议直接去看源码~
还得再来一遍,新事物的出现就表明着老的事物不能知足咱们的需求,ES6 刚出 Promise 来解决异步问题,ES7 就又出了一个 Async/Await(其实官方名字是 async function),看来 Promise 并无达到你们伙的预期,因此官方就又搞了个更为优雅的异步解决方案。
为何说它是为了解决 Promise 带来的问题,能够看看 MDN 官网的下面这段话:
async/await 的目的是简化使用多个 promise 时的同步行为,并对一组 Promises 执行某些操做。正如 Promises 相似于结构化回调,async/await 更像结合了 generators 和 promises。
上面提到的那个异步嵌套 setTimeout的例子来讲,事实上,大部分人用 Promise 应该并不会像上面的代码那样写,而是下面这样:
/* Async/Await */
new Promise((resolve, reject) => {
setTimeout(() => resolve(111), 1000);
}).then(data => {
console.log(data);
new Promise((resolve, reject) => {
setTimeout(() => resolve(222), 2000)
}).then(data => {
console.log(data);
new Promise((resolve, reject) => {
setTimeout(() => resolve(333), 3000)
}).then(data => {
console.log(data);
new Promise((resolve, reject) => {
setTimeout(() => resolve(444), 4000)
}).then(data => {
console.log(data);
})
})
})
});
复制代码
嗯,说实话,其实 Promise.then() 若是使用过多,依然仍是回调地狱,嵌套依然没有消失,因此来讲,Promise 并不能称之为完美的异步方案,所以,ES7 提出了 async function,它用来更为优雅的解决异步。咱们此次就来看看它的魅力:
// 定时器嵌套
function f1() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(111), 1000);
}).then(data => console.log(data));
}
function f2() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(222), 2000);
}).then(data => console.log(data));;
}
function f3() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(333), 3000);
}).then(data => console.log(data));;
}
function f4() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(444), 4000);
}).then(data => console.log(data));;
}
async function timeoutFn() {
await f1(); // 开始执行第一个异步函数
await f2(); // 第一个执行完,开始执行第二个异步函数
await f3(); // 第二个执行完,开始执行第三个异步函数
await f4(); // 第三个执行完,开始执行第四个异步函数
}
timeoutFn();
复制代码
// 数据库查询
async function queryData() {
try {
// 第一步,获取数据
const step1Data = await db.query('SELECT * FROM A WHERE id = 1');
// 第二步,获取数据
const step2Data = await db.query(`SELECT * FROM B WHERE author = ${step1Data[0].username}`);
console.log(step2Data);
} catch(e) {
throw e;
}
}
复制代码
看看上面的代码,多么的优美,彻底的同步流程~称之为最完美异步解决方案一点也不为过。
关于 async function,其实并无过多的 API,由于它更像是一个高级语法糖,官方文档给出的也更多都是使用示例。在这里,其实咱们只须要知道并强调一件事 —— await 关键字用来暂停等待异步函数的执行结束,若是是 Promise,也就是等待它的 settled 状态,而且 await 只能出如今 async function 内部,不可单独使用。
官方给出了一个比较有意思的例子:
// 一个1秒的异步函数
var resolveAfter1Second = function() {
console.log("starting fast promise");
return new Promise(resolve => {
setTimeout(function() {
resolve("fast");
console.log("fast promise is done");
}, 1000);
});
};
// 一个2秒的异步函数
var resolveAfter2Seconds = function() {
console.log("starting slow promise");
return new Promise(resolve => {
setTimeout(function() {
resolve("slow");
console.log("slow promise is done");
}, 2000);
});
};
// 下面这种写法是一块儿执行异步函数,只不过由于await等待致使输出有前后
var concurrentStart = async function() {
console.log('==CONCURRENT START with await==');
const slow = resolveAfter2Seconds(); // starts timer immediately
const fast = resolveAfter1Second(); // starts timer immediately
// 1. Execution gets here almost instantly
console.log(await slow); // 2. this runs 2 seconds after 1.
console.log(await fast); // 3. this runs 2 seconds after 1., immediately after 2., since fast is already resolved
}
// 下面这种是标准的等待写法
var sequentialStart = async function() {
console.log('==SEQUENTIAL START==');
// 1. Execution gets here almost instantly
const slow = await resolveAfter2Seconds();
console.log(slow); // 2. this runs 2 seconds after 1.
const fast = await resolveAfter1Second();
console.log(fast); // 3. this runs 3 seconds after 1.
}
复制代码
具体来讲你们能够本身实际体验一下,第二种没什么可说的,想象中就是这个样子,由于 await 会暂停等待函数执行完以后再向下执行,所以等待时间不会重叠,先等待2秒执行 slow 后再等待1秒执行 fast。
而第一种
const slow = resolveAfter2Seconds();
const fast = resolveAfter1Second();
console.log(await slow);
console.log(await fast);
复制代码
上面这两个异步函数由于没有 await 关键字,都是当即执行,所以先输出promise start
,以后,两个函数延时不一样,虽然 slow 先执行,可是是2秒,而 fast 后执行是1秒,先输出fast done
再输出slow done
。最后,await 关键字发挥做用,虽然 fast 先执行完,可是你仍是要等 await slow 完事以后才能 await fast。
这里就不给相关面试题了,把背景和基础内容都了解了,API 都知道如何使用了,那么面试题也就百变不离其宗了,也没什么可说的了。写到此处突然想起来一个问题,那么仍是说一下吧。setTimeout 和 Promise 都是异步操做,那么谁更快呢?
function whoFast() {
setTimeout(() => console.log('settimeout'), 0);
new Promise(() => {
console.log('promise');
})
}
复制代码
实践是检验真理的惟一标准,promise 无关顺序更快执行,至于原理,你们就去看 js 的 event loop 机制吧,若是感兴趣,后续也能够写~
前面几篇我的以为写得很好的没啥人看,这一篇感受也没写什么竟然不少人评论,确实没想到,因此有一些细节并无考虑到,😄。在这里进行补充:
上面提到了,Promise.all([])
若是出现异常则会直接返回第一个错误,那么即便有的成功了也不会返回,这样作有时候会出现问题,一个页面两个接口,使用Promise.all()
来获取,若是一个成功一个失败你至少应该把成功那个展现才对,嗯,因此这时候就用到了Promise.allSettled()
,它返回的也是一个对应数组,里面是对应 Promise 的 setteld 状态,可能成功,也可能失败~
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];
Promise.allSettled(promises).
then((results) => results.forEach((result) => console.log(result.status)));
// expected output:
// "fulfilled"
// "rejected"
复制代码
直接上图上面说过,await 是等待异步代码执行结束,后面通常都会跟异步函数,可是若是你就是要跟同步代码会怎么样呢?不要紧,上图你也能看得出,跟同步代码,await 同步代码
依然会转换成 Promise~