【前端面试】JavaScript-异步原理:单线程模型、消息队列、setTimeout、事件循环、Promise及实现、await/async、面试题

参考文章:
[1]JavaScript 单线程模型 - W3Cschool
[2]并发模型与事件循环 - MDN
[3]JavaScript中的事件循环与消息队列
[4]一篇搞定(Js异步、事件循环与消息队列、微任务与宏任务)- 知乎
[5]终于明白的JS——消息队列与事件循环 - CSDN
[6]Promise - MDN
[7]then - MDN
[8]catch - MDN
[9]Promise实现原理(附源码)- 掘金
[10]xieranmaya/Promise3 - Github
[11]async函数 - MDN
[12]使用Promise的优缺点及一些常见的问题 - CSDN
[13]await - MDN

JavaScript单线程模型(并发模型)[1]

单线程模型指的是,JavaScript只在一个线程上运行。也就是说,JavaScript同时只能执行一个任务,其他任务都必须在后面排队等待。
注意,JavaScript只在一个线程上运行,不代表JavaScript引擎只有一个线程。 事实上,JavaScript引擎有多个线程,单个脚本只能在一个线程上运行,其他线程都是在后台配合。
JavaScript从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。
尽管HTML5为了利用多核CPU的计算能力从而提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。因此,这个新标准并没有改变JavaScript单线程的本质。
在计算机的世界中,IO和计算从来不应该是一个冲突的事(可以参考一下操作系统的处理)。JavaScript语言的设计者意识到,CPU完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是JavaScript内部采用的Event Loop机制。

小结

由于JavaScript只在一个线程上运行,导致了JavaScript的单线程特性。尽管HTML5提出了Web Worker标准,但并没有依然改变JavaScript的这一执行方式。为了提高在IO过程中的CPU的利用率,JavaScript设计者采用了事件循环机制完成了对JavaScript异步的实现。

消息队列[1]

JavaScript运行时,除了一个运行线程,引擎还提供一个消息队列(message queue),里面是各种需要当前程序处理的消息。新的消息进入队列的时候,会自动排在队列的尾端。
运行线程只要发现消息队列不为空,就会取出排在第一位的那个消息,执行它对应的回调函数。等到执行完,再取出排在第二位的消息,不断循环,直到消息队列变空为止。
每条消息与一个回调函数相联系,也就是说,程序只要收到这条消息,就会执行对应的函数。 另一方面,进入消息队列的消息,必须有对应的回调函数。否则这个消息就会遗失,不会进入消息队列。举例来说,鼠标点击就会产生一条消息,报告click事件发生了。如果没有回调函数,这个消息就遗失了。如果有回调函数,这个消息进入消息队列。等到程序收到这个消息,就会执行click事件的回调函数。
另一种情况是setTimeout会在指定时间向消息队列添加一条消息。如果消息队列之中,此时没有其他消息,这条消息会立即得到处理;否则,这条消息会不得不等到其他消息处理完,才会得到处理。因此,setTimeout指定的执行时间,只是一个最早可能发生的时间,并不能保证一定会在那个时间发生。
一旦当前执行栈空了,消息队列就会取出排在第一位的那条消息,传入程序。程序开始执行对应的回调函数,等到执行完,再处理下一条消息。

JavaScript运行时模型[2]

在这里插入图片描述

  1. (函数执行)栈:函数调用形成了一个由若干帧(Frame)组成的栈(Stack)。
  2. 堆:对象(Object)被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。
  3. (消息)队列:一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。
  4. 注意:在JavaScript中,基本类型数据(Number,String,Undefined,Null,Boolean)放在栈中,引用类型数据(Function,Object,Array)。这里只针对解释器的实现原理
事件触发线程与引擎线程[3]

消息队列的维护是由事件触发线程控制的。 事件触发线程同样是浏览器渲染引擎提供的,它会维护一个消息队列。S引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等…),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应的 回调函数 加入到消息队列中,消息队列中的回调函数等待被执行。
同时,引擎线程会维护一个 执行栈,同步代码会依次加入执行栈然后执行,结束会退出执行栈。如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。

多个运行时模型[2]

一个 web worker 或者一个跨域的 iframe 都有自己的栈、堆和消息队列。两个不同的运行时只能通过 postMessage 方法进行通信。如果另一个运行时侦听 message 事件,则此方法会向该运行时添加消息。

小结

根据上面的描述,JavaScript解释器底层通过消息队列的形式首先实现了“异步”,每个消息队列的元素包含消息和回调函数,在开发者角度就是事件循环机制。因此,可以简单理解成:JavaScript最原始的异步实现就是“回调”当然,通过轮询(类似操作系统的时间片)的方式也可以实现异步和并发,只是JavaScript没有使用这种方式

setTimeout函数

setTimeout会在指定时间向消息队列添加一条消息。如果消息队列之中,此时没有其他消息,这条消息会立即得到处理;否则,这条消息会不得不等到其他消息处理完,才会得到处理。因此,setTimeout指定的执行时间,只是一个最早可能发生的时间,并不能保证一定会在那个时间发生。

setTimeout被阻塞
const getTime = () => new Date().getTime() // 获取时间戳
const block = (number, cb) => {
    const start = getTime()
    while (getTime() - start < number);
    cb()
} // 同步阻塞

console.log("程序开始时间", getTime())
const timer = setTimeout(() => {
    console.log('定时器执行完毕', getTime())
}, 1000)
block(3000, () => { console.log('阻塞函数执行完毕 ', getTime()) })
// > 程序开始时间,1652886340559
// > 阻塞函数执行完毕 ,1652886343559
// > 定时器执行完毕,1652886343570

可以看到程序开始执行后,JavaScriptx执行线程被一段阻塞代码阻塞3000秒,定时器没有按照预想的程序执行1秒后立刻执行。

setTimeout零延迟

显式指定延迟为0

const getTime = () => new Date().getTime() // 获取时间戳

console.log("程序开始时间", getTime())
const timer = setTimeout(()=>{
    console.log('零延迟的定时器执行完毕', getTime())
},0)
console.log('定时器发起后执行的一段代码执行时间',getTime())
// > 程序开始时间,1652887712873
// > 定时器发起后执行的一段代码执行时间,1652887712873
// > 零延迟的定时器执行完毕,1652887712881

隐式指定延迟为0

const getTime = () => new Date().getTime() // 获取时间戳

console.log("程序开始时间", getTime())
const timer = setTimeout(()=>{
    console.log('零延迟的定时器执行完毕', getTime())
}) // 注意这里没有指定延迟,setTimeout将默认以0为延迟执行
console.log('定时器发起后执行的一段代码执行时间',getTime())
// > 程序开始时间,1652887712873
// > 定时器发起后执行的一段代码执行时间,1652887712873
// > 零延迟的定时器执行完毕,1652887712881

可见,零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数。

setTimeout传参

setTimeout可以支持两种参数

  1. 参数形式:第一个参数指定回调函数名,第二个参数指定延迟时间,后面的所有参数传入指定的回调函数。
  2. 回调形式:第一个参数指定回调函数,第二个参数指定延迟时间
const add = function () {
    let result = 0
    for (let i = 0; i < arguments.length; i++)result = result + arguments[i]
    console.log('add执行结果为 ',result)
    return result
}

const timer1 = setTimeout(add, 500, 1, 2, 3, 4, 5)
const timer2 = setTimeout(()=>{
    add(1,2,3,4)
},1000)
// > add执行结果为 ,15
// > add执行结果为 ,10
setTimeout最小延迟问题

在这里插入图片描述

setTimeout小结

通过上面两个例子,都印证了上面描述的:setTimeout指定的执行时间,只是一个最早可能发生的时间,并不能保证一定会在那个时间发生。

Event Loop

所谓Event Loop机制,指的是一种内部循环,即事件循环,用来一轮又一轮地处理消息队列之中的消息,即执行对应的回调函数。
一个宏任务(macro-task或Task)可能会产生一个或多个微任务(micro-task或Job),当执行完当前宏任务产生的微任务后,当前宏任务才算结束。

执行机制
  1. 执行一个宏任务(函数执行栈中没有就从消息队列中获取
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  4. 开始下一个宏任务
    下面两个图是对事件循环的宏观描述和微观描述:
宏观视图

宏观视图

微观视图

微观视图

宏任务与微任务

一般的,我们把宿主(浏览器、Node环境)发起的任务称为宏任务(如setTimeout和整体代码),把JavaScript引擎发起的任务称为微观任务(如Promise、await)。

常见的宏任务与微任务[4][5]

下面的表格根据第四篇文章和第五篇文章,取了浏览器和Node.js两个环境支持的宏任务和微任务交集。
宏任务中,浏览器还支持requestAnimationFrame和UI rendering,Node.js还支持setImmediate。
微任务应该还有一个await/async,但这两个只是ES7的一个语法糖,本质上也是Promise对象的这几个方法。

任务类型代码
宏任务整体代码,setTimeout,setInterval,I/O
微任务Promise.then catch finally

Promise对象

描述[6]

一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知的值。 它让异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。 这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

一个 Promise 必然处于以下几种状态之一:

  1. 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
  2. 已兑现(fulfilled): 意味着操作成功完成。
  3. 已拒绝(rejected): 意味着操作失败。
状态转换图示[6]

在这里插入图片描述

特点[12]

优点
  1. Promise和其then方法,提供了链式调用的代码风格,有效避免了回调地狱的出,现,代码可读性更高。
  2. 基于then和catch方法,可以更好地进行异常处理。
缺点
  1. Promise 无法取消
  2. 如果不使用then和catch进行异常处理,Promise内部地错误将不能被外部感知。
  3. pending状态时无法知道代码执行处于哪个阶段(刚刚开始还是即将完成)

实例方法[7][8]

ES6中Promise的实例提供了then、catch两个方法来对结果进行处理,有些类似异常捕获机制。不过finally机制不在ES6标准中,而是在后续的标准中,这里只是提一下。

1. promise.then(onFufilled[,onRejected])

参数:

  1. 当 Promise 变成接受状态(fulfilled)时调用的函数。该函数有一个参数,即接受的最终结果(the fulfillment value)。如果该参数不是函数,则会在内部被替换为 (x) => x,即原样返回 promise 最终结果的函数。
    Promise.resolve('第1个参数')
    .then('第2个参数')
    .then(res=>console.log(res))
    // > 第1个参数
    
    之所以返回这样的结果是因为then传递了一个非函数参数,被自动替换成了 (x) => x,直接返回了上一步的结果promise。
  2. 当 Promise 变成拒绝状态(rejected)时调用的函数。该函数有一个参数,即拒绝的原因(rejection reason)。 如果该参数不是函数,则会在内部被替换为一个 “Thrower” 函数 (it throws an error it received as argument)。

实例:

  1. 返回了一个值,那么 then 返回的 Promise 将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。即使return new Error,也会被认为是返回了一个值,如果希望抛出异常,只能使用throw关键字。

    const promise = Promise.resolve('第1个参数')
    const then1 = promise.then((res)=>{
    console.log('then1 ',res)
    return '第2个参数'
    })
    const then2 = then1.then((res)=>{
    console.log('then2 ',res)
    })
    // > then1 ,第1个参数
    // > then2 ,第2个参数
    
  2. 没有返回任何值,那么 then 返回的 Promise 将会成为接受状态,并且该接受状态的回调函数的参数值为 undefined。

    const promise = Promise.resolve('第1个参数')
    const then1 = promise.then((res)=>{
    console.log('then1 ',res)
    })
    const then2 = then1.then((res)=>{
    console.log('then2 ',res)
    })
    // > then1 ,第1个参数
    // > then2 ,undefined
    
  3. 抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。

    const promise = Promise.resolve('第1个参数')
    const thenResult = promise.then(res => {
    	throw new Error('then报错')
    })
    thenResult.then(res => {}, reason => {
    	console.log('reason is typeof Error ',reason instanceof Error)
    	console.log('rejected ',reason.message)
    })
    // > reason is typeof Error ,true
    // > rejected ,then报错
    
  4. 返回一个已经是接受状态的 Promise,那么 then 返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。

    const promise = Promise.resolve('第1个参数')
    const thenResult = promise.then(res => {
    	return Promise.resolve('第2个参数')
    })
    thenResult.then(res => {
    	console.log('fulfilled ',res)
    })
    // > fulfilled ,第2个参数
    
  5. 返回一个已经是拒绝状态的 Promise,那么 then 返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。

    const promise = Promise.resolve('第1个参数')
    const thenResult = promise.then(res => {
    	return Promise.reject('错误')
    })
    thenResult.then(res => {},reason=>{
    	console.log(reason)
    })
    // > 错误
    
  6. 返回一个未定状态(pending)的 Promise,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。

    const getTime = ()=> new Date().getTime()
    
    console.log('程序开始执行',getTime())
    const promise = new Promise(resolve=>{
    	setTimeout(()=>{
        	resolve('第1个参数')
        	console.log('定时器发起',getTime())
     	},2000)
    })
    const thenResult = promise.then(res => {
    	console.log('中间结果',res,getTime())
    	return res
    })
    thenResult.then(res => {
    	console.log('获取到then返回',res,getTime())
    })
    // > 程序开始执行,1652926588773
    // > 定时器发起,1652926590776
    // > 中间结果,第1个参数,1652926590776
    // > 获取到then返回,第1个参数,1652926590776
    
2. promise.catch(onRejected)

catch() 方法返回一个Promise ,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。 (事实上, calling obj.catch(onRejected) 内部calls obj.then(undefined, onRejected))

	// 使用catch捕获
	const promise1 = Promise.reject('error')
	promise1.then(res => {
    	console.log('此处onFulfilled未生效 ', res)
	}).catch(error => {
    	console.log('catch catch rejected ', error)
	})
	// > catch catch rejected ,error

	// 使用then捕获异常
	promise1.then(res => { }, error => {
    	console.log('then catch rejected', error)
	}).catch(error => {
    	console.log('此处catch未生效')
	})
	// > then catch rejected,error

	// 使用catch的冒泡性质
	promise1.then(res => res)
    .then(res => res)
    .then(res => res)
    .then(res => res)
    .catch(error => {
        console.log('catch的冒泡性质1',error)
    })
	// > catch的冒泡性质1,error

	// 使用两个catch分段捕获异常
	promise1.then(res => res)
    .catch(error => {
        console.log('catch的冒泡性质2',error)
    })
    .then(res => {
        // 注意不要return new Error
        throw new Error('catch后的错误')
    })
    .then(res => console.log(res))
    .then(res => res)
    .catch(error => {
        console.log('catch的冒泡性质3',error.message)
    })
	// > catch的冒泡性质2,error
	// > catch的冒泡性质3,catch后的错误

注意,上面打印结果在控制台上表现为,也可以当作一个面试题来看看:

then catch rejected,error
catch catch rejected ,error
catch的冒泡性质2,error
catch的冒泡性质1,error
catch的冒泡性质3,catch后的错误

这是由于事件循环导致的,这样理解:

  1. 执行程序,然后发现代码都是promise1.then,因此全部挂起,宏任务执行完毕,开始执行微任务。当前队列(队首开始)恰好是第1、2、3、4代码段的promise1.then。
    4个队列元素依次被取出,第1个promise1.then没有被执行并向微任务队列添加了一个catch代码,第2个promise1.then由于同时添加了异常处理,因此先输出 (1)then catch rejected,error,并追加一个catch,第3、4段代码同样执行以后在微任务队列追加了catch。这一轮执行以后微任务队列恰好变成了第1、2、4代码段的链式调用中的第一个catch和第3段代码的then。
  2. 继续取出上面队列的4个catch,第1段代码的catch输出**(2)catch catch rejected ,error**;第2段代码的catch因为then中已经捕获了异常,不会执行;第3段代码的then继续链式传递了res结果;第4段代码的catch输出**(3)catch的冒泡性质2,error**。此时,只剩下第3段代码和第4段代码还在进行微任务的排队,微任务队列:第3段代码的第3个then和第4段代码的第2个then。
  3. 继续执行,这里可以直接看剩余的微任务数量,发现都剩下3个微任务,两段代码的微任务交替入队,因为第3段代码先执行,最后以“微弱优势胜出“,先输出 (4)catch的冒泡性质1,error,再输出 (5)catch的冒泡性质3,catch后的错误
3. promise.finally(onFinally)

finally() 方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在then()和catch()中各写一次的情况。
注意,onFinally回调不接收任何参数,类似提供了一个集中插入代码的位置:
在这里插入图片描述

静态方法[6]

对于静态方法,我的理解是JavaScript设计者通过Promise的静态方法,提供了一些适应多种场景的方法(例如1到4),以及提供了更为简便的写法(5和6)。

1. Promise.all(iterable)

这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。

const getTime = () => new Date().getTime()
const promise1 = new Promise((resolve, reject) => {
    console.log('promise1 inited',getTime())
    setTimeout(() => {
        console.log('promise1 resolved',getTime())
        resolve(1)
    }, 1000)
})
const promise2 = new Promise((resolve, reject) => {
    console.log('promise2 inited',getTime())
    setTimeout(() => {
        console.log('promise2 resolved',getTime())
        resolve(2)
    }, 2000)
})
const promise3 = new Promise((resolve, reject) => {
    console.log('promise3 inited',getTime())
    setTimeout(() => {
        console.log('promise3 resolved',getTime())
        resolve(3)
    }, 3000)
})
Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log('all promises done',getTime(),res)
})
// > promise1 inited,1652930504867
// > promise2 inited,1652930504867
// > promise3 inited,1652930504867
// > promise1 resolved,1652930505868
// > promise2 resolved,1652930506878
// > promise3 resolved,1652930507874
// > all promises done,1652930507874,[1,2,3]
2. Promise.allSettled(iterable)

等到所有promises都已敲定(settled)(每个promise都已兑现(fulfilled)或已拒绝(rejected)),返回一个promise,该promise在所有promise完成后完成。并带有一个对象数组,每个对象对应每个promise的结果。

const getTime = () => new Date().getTime()
const promise1 = new Promise((resolve, reject) => {
    console.log('promise1 inited',getTime())
    setTimeout(() => {
        console.log('promise1 rejected',getTime())
        reject(1)
    }, 1000)
})
const promise2 = new Promise((resolve, reject) => {
    console.log('promise2 inited',getTime())
    setTimeout(() => {
        console.log('promise2 rejected',getTime())
        reject(2)
    }, 2000)
})
const promise3 = new Promise((resolve, reject) => {
    console.log('promise3 inited',getTime())
    setTimeout(() => {
        console.log('promise3 resolved',getTime())
        resolve(3)
    }, 3000)
})
Promise.allSettled([promise1,promise2,promise3]).then(res=>{
    console.log('all promises done',getTime(),res)
})
// > promise1 inited,1652932850845
// > promise2 inited,1652932850845
// > promise3 inited,1652932850845
// > promise1 rejected,1652932851851
// > promise2 rejected,1652932852853
// > promise3 resolved,1652932853846
// > all promises done,1652932853846,[{"status":"rejected","reason":1},{"status":"rejected","reason":2},{"status":"fulfilled","value":3}]
3. Promise.any(iterable)

接收一个Promise对象的集合,当其中的一个 promise 成功,就返回那个成功的promise的值

const getTime = () => new Date().getTime()
const promise1 = new Promise((resolve, reject) => {
    console.log('promise1 inited',getTime())
    setTimeout(() => {
        console.log('promise1 rejected',getTime())
        reject(1)
    }, 1000)
})
const promise2 = new Promise((resolve, reject) => {
    console.log('promise2 inited',getTime())
    setTimeout(() => {
        console.log('promise2 rejected',getTime())
        reject(2)
    }, 2000)
})
const promise3 = new Promise((resolve, reject) => {
    console.log('promise3 inited',getTime())
    setTimeout(() => {
        console.log('promise3 resolved',getTime())
        resolve(3)
    }, 3000)
})
Promise.any([promise1,promise2,promise3]).then(res=>{
    console.log('some promise done',getTime(),res)
})
// > promise1 inited,1652933104689
// > promise2 inited,1652933104689
// > promise3 inited,1652933104689
// > promise1 rejected,1652933105697
// > promise2 rejected,1652933106704
// > promise3 resolved,1652933107694
// > some promise done,1652933107694,3
4. Promise.race(iterable)

当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。

const getTime = () => new Date().getTime()
const promise1 = new Promise((resolve, reject) => {
    console.log('promise1 inited',getTime())
    setTimeout(() => {
        console.log('promise1 rejected',getTime())
        reject(1)
    }, 1000)
})
const promise2 = new Promise((resolve, reject) => {
    console.log('promise2 inited',getTime())
    setTimeout(() => {
        console.log('promise2 rejected',getTime())
        reject(2)
    }, 2000)
})
const promise3 = new Promise((resolve, reject) => {
    console.log('promise3 inited',getTime())
    setTimeout(() => {
        console.log('promise3 resolved',getTime())
        resolve(3)
    }, 3000)
})
Promise.race([promise1,promise2,promise3]).then(res=>{
    console.log('some promise done',getTime(),res)
}).catch(error=>{
    console.log('some promise error',getTime(),error)
})
// > promise1 inited,1652934779911
// > promise2 inited,1652934779911
// > promise3 inited,1652934779911
// > promise1 rejected,1652934780915
// > some promise error,1652934780915,1
// > promise2 rejected,1652934781912
// > promise3 resolved,1652934782925
5. Promise.reject(reason)

返回一个带有拒绝原因的Promise对象。

const promise = Promise.reject([1,2,3])
promise.then(undefined, (error) => { console.log(error) })
// > [1,2,3]
6. Promise.resolve(value)

返回一个以给定值解析后的Promise 对象。如果这个值是一个 promise ,那么将返回这个 promise ;如果这个值是thenable(即带有"then" 方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。

Promise.resolve(new Error('error')).then(res=>console.log(res.message))
// > error
Promise.resolve([1,2,3]).then(res=>console.log(res))
// > [1,2,3]

对Promise的实现

Promise构造器

暂时没有找到合适的教程,有兴趣了解可以查看参考文章[9][10],但这两个的实现都或多或少存在一些问题。

Promise静态方法实现

代码(最好结合下面的思路一起看):

class MyPromise {
    static reject(value) {
        return new Promise(resolve => resolve(value))
    }
    static reject(reason) {
        return new Promise((resovle, reject) => reject(reason))
    }
    static race(iterable) {
        return new Promise((resolve, reject) => {
            if (!Array.isArray(iterable)) return reject(new Error('arguments must be an array'))
            iterable.forEach(promise => promise.then(res => resolve(res), reason => reject(reason)))
        })
    }
    static all(iterable) {
        return new Promise((resolve, reject) => {
            if (!Array.isArray(iterable))
                return reject(new Error('arguments must be an array'))
            let result = new Array(iterable.length)
            let counter = 0
            iterable.forEach((promise, index) => {
                promise.then(res => {
                    counter++
                    result[index] = res
                    if (counter === result.length)
                        return resolve(result)
                }, reason => {
                    return reject(reason)
                })
            })
        })
    }
    static allSettled(iterable) {
        return new Promise((resolve, reject) => {
            if (!Array.isArray(iterable))
                return reject(new Error('arguments must be an array'))
            let result = new Array(iterable.length)
            let counter = 0
            iterable.forEach((promise, index) => {
                promise.then(res => {
                    counter++
                    result[index] = { status: 'FULFILLED', value: res }
                    if (counter === result.length)
                        return resolve(result)
                }, reason => {
                    counter++
                    result[index] = { status: 'REJECTED', reason }
                    if (counter === result.length)
                        return resolve(result)
                })
            })

        })
    }
    static any(iterable) {
        return new Promise((resolve, reject) => {
            if (!Array.isArray(iterable))
                return reject(new Error('arguments must be an array'))
            // let result = new Array(iterable.length)
            let counter = 0
            iterable.forEach((promise,index)=>{
                promise.then(res=>{
                    resolve(res)
                },reason=>{ 
                    counter++
                    if(counter === iterable.length)
                        return reject(new Error('no promise fulfilled'))
                })
            })
        })
    }
}

实现思路:

  1. resolve和reject只需要通过Promise构造器即可实现,这个实现没有什么困难。
  2. race包装了一个父promise,提供resolve和reject方法,对传入的promise数组进行遍历并添加then方法。在then的回调函数中,无论哪一个promise被解析或失效,都将调用父promise的resolve或reject方法并传入具体的值,这也就意味着任何一个最先完成promise将作为整个数组的返回。
  3. all包装了一个父promise,提供resolve和reject方法,对传入的promise数组进行遍历并添加then方法。在then的回调函数中,任意一个promise失效,立刻调用父promise的reject方法(有些类似短路的作用);当一个promise被解析,立刻将辅助计数器+1、将promise结果放入辅助数组中,然后校验当前是否所有的promise已经被解析,若是则调用父promise的resolve方法并传入辅助数组,否则不操作。
  4. allSettled包装了一个父promise,提供resolve和reject方法,对传入的promise数组进行遍历并添加then方法。在then的回调函数中,无论是否promise被解析或失效,都将辅助计数器+1、并将当前的结果(包含具体值和结束状态)放入辅助数组中,然后判断是否所有的promise都已经完成,若是则调用父promise的resolve方法传入辅助数组,否则不操作。
  5. any包装了一个父promise,提供resolve和reject方法,对传入的promise数组进行遍历并添加then方法。在then的回调函数中,任何一个被解析的promise都将立刻调用父promise的resolve方法;当promise失效时,首先将辅助计数器+1,然后判断当前是否所有的promise都失效了,若都失效了则调用父promise的reject方法,否则不操作。
静态方法实现小结
  1. 需要借用Promise构造器来包装一下。
  2. 当需要返回整个数组结果(比如all、allSettled),就要使用一个辅助数组
  3. 当需要判断所有的promise完成情况(比如all、allSettled、any),就要使用辅助计数器
  4. 2和3都利用了闭包的性质

async/await[11][13]

这一块其实没有太多要理解的,主要是代码风格。

async函数

async函数是使用async关键字声明的函数。 async函数是AsyncFunction构造函数的实例, 并且其中允许使用await关键字。async和await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise[11]。

async function foo() {
   return 1
}

等价于

function foo() {
   return Promise.resolve(1)
}
await操作符

await 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。

使用async/await风格改写链式调用
function getProcessedData(url) {
  return downloadData(url) // 返回一个 promise 对象
    .catch(e => {
      return downloadFallbackData(url)  // 返回一个 promise 对象
    })
    .then(v => {
      return processDataInWorker(v); // 返回一个 promise 对象
    });
}

等价于

async function getProcessedData(url) {
  let v;
  try {
    v = await downloadData(url);
  } catch (e) {
    v = await downloadFallbackData(url);
  }
  return processDataInWorker(v);
}

一道综合题

试着分析一下下面代码的输出过程:

async function async1() { 				
  console.log(1)						
  await async2()						
  console.log(2)						
  return await 3						
}										

async function async2() {
  console.log(4)
}

setTimeout(function() {
  console.log(5)
},0)

async1().then(v=>{console.log(v)})

new Promise(function(resolve){
  console.log(6)
  resolve();
  console.log(7)
}).then(function(){
  console.log(8)
})
console.log(9)
//1 4 6 7 9 2 8 3 5

我的理解:

  1. setTimeout,直接加入下一个宏任务队列
  2. 进入async1函数,输出1
  3. async1函数中执行await async2,加入微任务队列(微任务队列:await async2)
  4. 进入async2函数,输出4,返回undefined,async1函数被阻塞,挂起。
  5. 跳回整体代码,new Promise中输出6和7,中间的resolve只对Promise紧挨着的then起效果,返回undefined。Promise后的then加入微任务队列(微任务队列:await async2、Promise.then),然后挂起。
  6. 回到整体代码输出9,当前宏任务执行完毕
  7. 取出微任务队列队首:await async2后阻塞的代码,输出2,再次遇到返回3,并被promise包裹,作为微任务加入微任务队列(微任务队列:Promise.then、await 3)
  8. 取出微任务队列队首:Promise.then,继续执行,输出8
  9. 取出微任务队列队首:await 3,继续执行,async1后的then,入队,再次取出队首,输出3,微任务结束。
  10. 执行下一宏任务,setTimeout,输出5

其余关于执行顺序的面试题可以查看:JavaScript 高级深入浅出:async、await 与事件循环

写在最后

这篇文章是在我整理笔记时顺便写的,许多地方参考了他人和组织的观点,并且在整理过程中难免会有一些疏漏,希望和大家共同学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值