「星辰大海」你可能需要知道的 promise 知识的总结(内附思维导图)
前言
接上一期 「高频面试题」女友:消息队列 和 事件循环系统终于弄明白了!(内附思维导图) 文章出炉后,微信好友也多了不少,还挺好的~
最近又重新整理一下 Promise
相关知识,一方面和上一期文章有个接应,另一方面,方便自己日后复盘回顾。
本篇文章参考视频学习而来,都是自己一个字一个字敲出来的,也加了自己的理解,视频就在参考处,建议大家先看视频,后续回顾的话通过本文几个例题就好了。✿✿ヽ(°▽°)ノ✿
学完本篇文章,你将会明白如下几大模块知识:
- 迭代器
- 生成器
- promise
- async、await
(手机端可能看不清)获取高清PDF,请在微信公众号【小狮子前端】回复【promise知识】
迭代器是什么
提出问题
在了解迭代器之前,我们先来提出一个问题:
遍历和迭代有什么区别?
如下代码,对于数组、字符串、对象等我们可以按照如下方式进行遍历。
var arr = [1, 2, 3, 4]; |
但对于这些数据类型的不同,我们可能需要不同的遍历方式,比如对象我们需要通过 for...in
,函数也不一样,那么我们想着是否对于不同数据类型可以用同一种遍历方式呢?
没错,当然可以,这就是迭代,到此你应该明白了上文提出的问题:遍历和迭代有什么区别?
迭代就是从目标源依次按逐个抽取的方式来提取数据,其中目标源满足:1、有序的 2、连续的 而遍历就没有这些要求,对于不同数据类型会有不同遍历方式。
迭代引入
而对于数据类型呢,在这里先提前告知大家,有哪些可以直接迭代出来的:
Array、Map、Set、String、TypeArray、arguments、NodeList
上述都是可以通过 for..of
来迭代出来。可能此时你会有疑问了,对象不可以么?(那我们不妨来试试)
let arr = [1, 2, 3, 4] |
通过打印之后,我们发现当我们遍历 obj
对象的时候会抛出错误:Uncaught TypeError: obj is not iterable
通过打印,我们发现,如果一个数据类型能迭代的话,会在原型上存在 Symbol(Symbol.iterator)
方法,我们不妨来试着用一用这个方法,如下代码所示:
let arr = [1, 2, 3, 4] |
此时会打印 {value: 1, done: false}
,发现 done
为 false
,这意味着还没有完成迭代,即可以继续迭代。对于这个数组而言,有4个元素,那我们可以通过迭代5次来完成对数组的迭代,如下代码所示:
let arr = [1, 2, 3, 4] |
总结:调用
Symbol(Symbol.iterator)
方法将会返回一个迭代器对象
,而这个迭代器对象具备next()
方法,通过这个方法来实现逐步调用,同时每次调用的时候会返回一个对象,包括value
和done
两个属性,当value
为undefined
,done
为true
时迭代完成。
自己动手实现 Symbol.iterator
实现代码如下,我们依然可以获得与上文一样的结果。
let arr = [1, 2, 3, 4] |
实现对象的自定义迭代器对象
上文我们知道了对象是没办法进行迭代的,因为它的原型上没有
Symbol(Symbol.iterator)
方法,那么有了上文自己动手实现Symbol.iterator
前置知识后,我们现在来实现对象的自定义迭代器对象。
var obj = { |
从上文我们知道,我们可以通过 for...of
的方式来迭代,因此,对于上述代码我们可以换一种方式,如下:
for(let item of obj){ |
结果也是能迭代打印对象数据,那么也就是说 for...of
就是调用 [Symbol.iterator]()
方法来实现迭代,它能一次性迭代出对象中所有属性和属性值。
生成器的使用
生成器初识
上文我们介绍了迭代器是什么,发现对象没办法使用迭代,然后我们自己动手实现了对对象的自定义迭代器,然后也发现了说
for...of
就是调用[Symbol.iterator]()
方法来实现迭代,它能一次性迭代出对象中所有属性和属性值。
接下来,我们来讲解生成器了。首先,生成器它能像迭代一样返回一个迭代器对象,然后调用 next
方法进行抽取,下面结合一个例子就能明白了:
function* test(){ |
同样,我们也可以通过 for...of
方式实现,这里就不再演示了。
探讨生成器
我们不妨来看一下下面这段代码,看看会有怎样的结果:
function* test(){ |
结果显示是 没有打印结果
,你是否以为会执行了 test
函数之后会默认输出 0
呢,答案不是,这很奇怪! 那么我们不妨执行一下:
console.log(iter.next()); |
从结果上来看,当我们调用一下 next
方法之后,打印 0
的那行代码被执行了,而没有调用的时候就没执行。
那么总结一下,yeild
实际上能够中断函数,这里就有 return
的味道了,不妨看看下面几个例子:
function* test(){ |
function* test(){ |
这里因为在抽取的时候没有遇到 yield
因此全部抽取完,然后 done
设置为 true
,此时代表迭代完成。因此,生成器函数一般不会 return
一个值。
function* test(){ |
探究生成器传参
上文我们介绍了生成器抽取相关问题,也提及了一般不会使用 return
返回一个值,下面我们来探究一下生成器传参是怎样的情况,例如如下代码:
function* test(){ |
打印结果如下:
从上述打印结果来看,我们发现,第一个
yield
的返回值是在调用第二个next
方法时的传参
。
然而上述传参其实不太符合我们想象中的逻辑,可以有解决办法嘛?
当然有办法,因此之后推出了
async/await
。
Promise
关于 Promise
讲解的话,网上也是一大堆,但本篇还是以简单便捷的方式给大家呈现 promise
知识点,因此,可能需要你起初对 promise
有一定了解,本文不会从零去讲 promise
,比如它的来源,为什么要使用 promise
这些问题,本文不会过多涉及,更多内容小狮子们可以参考文末提供的学习视频,通过系统学习过后,如若后续还想继续复盘的话,就可以参考本文,而无需再看一遍视频了,我想读者应该明白我这些话的意思了哈,下面我们正式进入 promise
章节。
Promise的特性
1、promise 状态不受外界影响
2、promise 的固化(一旦 promise 状态发生变化后就不能再更改)
下面来看一个例子,看看会输出什么,这也就是 promise
的执行方式,通过 微任务 的方式来执行 promise
注册的回调函数,不管是成功还是失败。
let promise = new Promise(function (resolve, reject) { |
打印结果如下:
promise |
这道题主要还是与事件循环相关,小狮子们可以阅读我的上一篇文章,看完我想你对事件循环的理解会更加深入一些:
「高频面试题」女友:消息队列 和 事件循环系统终于弄明白了!(内附思维导图)
thenable 对象
直接看下面例子,你觉得会输出什么呢?
let obj = { |
答案是输出 11
,解释一波,当绑定一个对象参数为 thenable
的时候,那么会默认调用对象的 then
方法,之后then
方法里面 resolve
传递的值,就会成为之后 then
回调传入的值,即上述代码中的 data
。
(如果
then
方法是resolve
则为成为状态,如果是reject
则成为失败状态)
上一句话怎么理解呢,我们不妨修改一下上文代码,如下:
let obj = { |
输出结果是 err10
,也就是说当我们Promise
执行 reslove
方法时,传入参数为对象并且包含 then
方法,那么就会改变当前 promise
的状态。
但是,我们再试试改变一下上述代表,将 Promise
执行 reject
方法,会有什么变化呢?
let obj = { |
打印结果是 { then: [Function: then] }
,发现并没有执行 thenable
对象,因为此时 Promise
执行的是 reject
。
总结:当
Promise
是reslove
状态时,我们可以通过传入对象并且对象包含then
方法,那么可以通过这个then
方法来改变promise
状态,并且进行传参。
在此,穿插一道 promise
相关题目,看看输出结果是什么?
Promise.resolve().then(function () { |
输出结果如下:
promise1 |
这里要注意的是首先执行上面微任务,是整个微任务底下的代码加入微任务队列,因此第一个定时器宏任务会在微任务执行的时候才会加入延迟队列(或者叫做
Web APIs
)而第二个定时器宏任务在预编译代码的时候就在微任务之后加入了延迟队列,而它们都是设置的延时0
秒,那么在下一次事件循环的时候显然第二个定时器会优先拿出来调用。
promise 的链式调用
对于链式调用这一块,我想小狮子们应该或多或少的听说过,或许你听说过返回普通值,或者返回 promise
对象啥的,但应该对于这个链式调用理解的还是很模糊,接下来我们在这一章节一起来探讨一下:
首先,还是由浅入深,看一下简单例子:
let p1 = new Promise((resolve, reject) => { |
显然改变了 promise
状态为 resolve
成功态,会输出 1
。
上面代码应该很容易就看出来了,下面我们对上文代码进行新增,尝试进行链式调用,如下:
let p1 = new Promise((resolve, reject) => { |
输出结果如下:
1 |
这里来解释一下,首先我们想一想,为什么能进行链式调用呢?第一个 then
是因为 promise
状态变为了成功态,就可以执行回调,那么第二个 then
为什么能执行呢?带着这个问题,我们不妨打印一下第一个 then
的返回结果:
console.log(p1.then(res => console.log(res))) // Promise { <pending> } |
发现,执行 then
回调之后返回的也是 promise
,那么为什么能进行链式调用也就解释的清了。如果我们接着执行 then
方法,那么 p1.then
会包装返回 Promise.resolve(undefined)
,因为 console.log
函数默认返回值就是 undefined
,因此第二个 then
回调打印的其实就是 undefined+1
,显然结果就是 NaN
。
对于第三个 then
回调,得到的是第二个 then
包装返回的Promise.resolve(undefined)
,而 console.log
函数默认返回值就是 undefined
,因此第三个 then
回调打印的其实就是 undefined+2
,显然结果就是 NaN
。
我们继续修改一下代码,看看返回 promise
对象这种情况,如下:
let p1 = new Promise((resolve, reject) => { |
打印结果如下,这里就不解释了,可以参考上文讲述哈~
error:2 |
接着,我们看看如下代码,直接看它们区别,这对于你理解链式调用会有一定帮助。
let p1 = new Promise((resolve, reject) => { |
对于第一种情况,它都是看的 p1
的状态,为 resolve
,那么执行结果都一样。
对于第二种情况它会有三个 promise
状态,并且是链式调用的,那么下一个回调需要上一个回调的返回值。
promise 的固化 | 多层嵌套
let p1 = new Promise((resolve, reject) => { |
打印结果如下:
1 |
解释一波,对于中间那两行 .then()
,可能有的小狮子们会疑惑这到底是有啥用,我的回答是确实没用,会默认忽略掉!
而 promise
固化即一旦状态发生变化后就不能再更改。
promise 状态依赖
直接看下面这段代码,看看会输出什么?
let p1 = new Promise((resolve, reject) => { |
输出结果为3s
之后打印 error:400
,或许你会疑惑,我 p2
明明是resolve
状态,为什么会走到catch
那里捕获错误呢,这里要解释一下了:
当
promise
状态存在依赖时,它的状态与自身无关了,由依赖来决定,对于上述代码,就是 p2 的状态由依赖 p1 来决定,而p1是reject
,所以会走catch
。
Promise.all | Promise.race
有了上文
promise
的介绍,这两个方法就比较简单了,下文就直接给出例子啦~
Promise.all 使用
传递一个异步请求数组,只有当请求状态全都是成功态,才能调用成功回调函数,同时,它会受 reject
的影响,只要有 reject
,那么就不会调用 then
的回调。
let p1 = new Promise((resolve, reject) => { |
输出结果如下:
[ 10, 20, 30 ] |
Promise.race使用
let p1 = new Promise((resolve, reject) => { |
输出结果是 10
,Promise.race
传参也是一个 promise
相关的数组,但是它不会受 reject
的影响,谁跑的最快就返回谁。
async | await
如果要细说的话,那么本文篇幅就会很多了,但本文文末会提供参考文献,或者大家留下关于 async、await 写的不错的文章链接。
async await和generator的写法很像,就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await
但async 函数对 Generator 函数做了改进:
1、内置执行器:Generator函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器.也就是说,async 函数的执行,与普通函数一模一样。
2、更好的语义:async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
3、更广的适用性: co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
async 函数是非常新的语法功能,新到都不属于 ES6,而是属于 ES7。目前,它仍处于提案阶段,但是转码器 Babel 和 regenerator 都已经支持,转码后就能使用。
Promise 自测面试题(由易到难 | 满分100分)
第一题(5分)
Promise.resolve() |
查看答案
输出结果如下:
then:Error: error!!! |
第一题热身题,应该不成问题,还是解释一下: Promise
是 resolve
成功状态,那么就会执行第一个then
回调,返回值是 Error
对象,那么就会封装一个 Promise.resolve(参数为Error对象)
,然后又执行第二个 then
回调,那么打印结果如上所示。
第二题(5分)
let promise = new Promise((resolve, reject) => { |
查看答案
输出结果如下:
then:success1 |
本题主要考察 promise
的固化,一旦状态改变就不会再改变了!
第三题(10分)
let promise = new Promise((resolve, reject) => { |
查看答案
输出结果如下:
1 |
本题与事件循环相关,Promise
构造函数内部的执行器函数内部属于同步代码,.then
注册的回调函数属于微任务,那么会先输出同步代码 1
,遇到 resolve()
并不会阻止后面同步代码的执行,因为并没有 return
语句。然后将微任务加入微任务队列,之后打印同步代码 2
,之后继续先打印同步代码 4
,最后取出微任务队列中的任务元素,打印 3
,因此打印结果为 1 2 4 3
。
第四题(10分)
let p1 = new Promise((resolve, reject) => { |
查看答案
输出结果如下:
42 |
解释: p1
是返回的 reject
状态的 promise
,那么就会走 catch
,首先就会打印 42
,然后遇到 return
语句,返回的是普通值,那么就会封装成 Promise.resolve(43)
,那么就会执行后面的 then
回调,打印 43
。
第五题(10分)
let p1 = new Promise((resolve, reject) => { |
查看答案
输出结果如下:
42 |
解释一下,打印 42
我想应该不用解释了,我们注意 p1
执行 .then
回调时返回的是 p2
,而 p2 是失败的 promise
状态,那么就不会像上一题一样进行 promise.resolve()
的封装了,直接返回失败的状态,那么就只会执行 then
回调的第二个 err
那条路了。
第六题(10分)
setTimeout(() => { |
查看答案
输出结果如下:
promise2 |
这题也是考察事件循环相关,首先遇到 setTimeout
,加入宏任务队列,然后遇到 Promise.resolve().then
微任务,加入微任务队列,此时主线程没有同步代码可执行,先拿出微任务队列中的人物执行,先执行同步代码 promise2
,然后遇到 setTimeout
,加入宏任务队列。此时微任务执行完毕,取出宏任务队列中的任务,依次执行即可,打印输出结果。
第七题(10分)
Promise.resolve() |
查看答案
输出结果如下:
1 |
这道题很容易做错,你可能会想着打印出 1 2 3
,但是最外层的两个 then
属于同一级别关系,因此会先加入外层第一个 then
,然后再加入外层第二个 then
。此时主线程没有同步代码,于是取微任务队列中的任务,先执行输出 1
,然后又添加了一个输出 2 的微任务,然后取出微任务队列头部的任务,输出 3
,最后取出最后一个任务,输出 2
。
第八题(10分)
async function async1() { |
查看答案
输出结果如下:
async2 end |
这题考察了 async/await
,在执行 async1
函数时,遇到 await async2();
这段代码,而 async2
函数是个同步函数,直接输出 async2 end
,然后因为是 await
,返回的是 promise
对象,返回值是 async2()
执行的结果,即默认的 undefined
,之后的代码属于微任务,加入微任务队列,此时再走同步代码,输出 10
,之后,主线程已经执行完毕,然后去找微任务队列,取出之前加入的微任务,输出 async1 end
。
下面,我们还可以对上述代码进行一个变形,如下代码所示:
async function async1() { |
查看答案
输出结果如下:
async2 end |
这里要注意的就是对于 await
返回 reject
状态,必须要用 try / catch
进行捕获错误,不然就会报错!
第九题(15分)
下面这道题是特别特别经典的一道题了!
async function async1() { |
查看答案
总体思路就是:先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务
- 从上往下执行代码,先执行同步代码,输出
script start
- 遇到
setTimeout
,现把setTimeout
的代码放到宏任务队列中 - 执行
async1()
,输出async1 start
, 然后执行async2()
, 输出async2
,把async2()
后面的代码console.log('async1 end')
放到微任务队列中 - 接着往下执行,输出
promise1
,把 .then() 放到微任务队列中;注意 Promise 本身是同步的立即执行函数,.then是异步执行函数 - 接着往下执行, 输出
script end
。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码 - 依次执行微任务中的代码,依次输出
async1 end
、promise2
, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出setTimeout
最后结果如下:
script start |
第十题(15分)
let a; |
查看答案
输出结果如下:
promise1 |
这道题是一道综合性比较强的题,但是如果理解了前面9道题,这道题应该问题也不是很大的,现在来解释一波:
首先, new Promise
里面是同步代码,会优先打印 promise1
。然后注册了三个 then
的回调,加入微任务队列。
之后来到下一个 new Promise
,此时的 a
还没有接收到任务返回值,那么就是默认值 undefined
。
然后遇到 await b
,然而 b 是一个promise 的实例, 已经在之前通过 new Promise 执行了,是三个微任务,之后先去找找看还有没有同步代码,于是找到了输出 end
。
此时主线程同步代码已经执行完毕,去找微任务,依次打印 promise2 promise3 promise4
。
而 await b
也是返回 promise
对象,即封装好的 Promise.resolve(undefined)
。然而没有进行返回和接受。
之后执行输出 a
的那行代码,此时主线程同步代码已经执行完了,那么 a
也会返回一个 Promise
,但是状态没有发生变化,因此打印的是 Promise { <pending> }
。
然后执行输出 after1
。
然后遇到 await a
,而此时 a
还是 pending
,因此后面回调代码不会执行,这也是这道题的小坑,很容易跳进去!
到此,自测面试题就结束了,满分100分,你得到多少分了呢?来评论区留下你的分数吧,你也可以提出疑问,让大伙一起解决~
本文参考
建议大家伙先观看小夏老师的视频再来阅读本文~
最后
文章产出不易,还望各位小伙伴们支持一波!
往期精选:
leetcode-javascript:LeetCode 力扣的 JavaScript 解题仓库,前端刷题路线(思维导图)
小伙伴们可以在Issues中提交自己的解题代码,🤝 欢迎Contributing,可打卡刷题,Give a ⭐️ if this project helped you!
访问超逸の博客,方便小伙伴阅读玩耍~
学如逆水行舟,不进则退 |