web 面试高频考点 —— JavaScript 篇(二)【JS 异步进阶】Event Loop、then 和 catch、async/await、宏任务微任务、手撕 Promise 源码

系列文章目录



JS 异步进阶

什么是 event loop(事件循环/事件轮询)

  • JS 是单线程运行的
  • 异步要基于回调来实现
  • event loop 就是异步回调的实现原理

JS 如何执行?

  • 从前到后,一行一行执行
  • 如果某一行执行报错,则停止下面代码的执行
  • 先把同步代码执行完,再执行异步

event loop 执行过程

event loop 过程 1

  • 同步代码,一行一行放在 Call Stack 执行
  • 遇到异步,会先记录下来,等待时机(定时、网络请求等)
  • 时机到了,就移动到 Callback Queue

event loop 过程 2

  • 如果 Call Stack 为空(即同步代码执行完)Event loop 开始工作
  • 轮询查找 Callback Queue,如有则移动到 Call Stack 执行
  • 然后继续轮询查找(永动机一样)

图解代码执行过程:

    console.log('Hi')
    setTimeout(function cb1() {
    
    
        console.log('cb1')
    }, 2000)
    console.log('Bye')

图片出处:https://coding.imooc.com/lesson/400.html#mid=35171

请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

DOM 事件与 event loop

  • JS 是单线程
  • 异步(setTimeout,ajax等)使用回调,基于 event loop
  • DOM 事件也使用回调,基于 event loop

Promise 三种状态

三种状态

  • pending resolve rejected
  • pending => resolve 或 pending => rejected
  • 变化不可逆

状态的表现

  • pending 状态,不会触发 then 和 catch
  • resolved 状态,会触发后续的 then 回调函数
  • rejected 状态,会触发后续的 catch 回调函数

成功的回调:只执行 then,不会执行 catch

	// 输出结果:data 100
    const p1 = Promise.resolve('100')
    p1.then(data => {
    
    
        console.log('data', data)
    }).catch(err => {
    
    
        console.log('err', err)
    })

失败的回调:只执行 catch,不执行 then

	// 输出结果:err2 -100
    const p2 = Promise.reject('-100')
    p2.then(data => {
    
    
        console.log('data2', data)
    }).catch(err => {
    
    
        console.log('err2', err)
    })

then 和 catch 改变状态

  • then 正常返回 resolved,里面有报错则返回 rejected
  • catch 正常返回 resolved,里面有报错则返回 rejected

第一种情况:

    const p1 = Promise.resolve().then(() => {
    
    
        return 100
    })
    console.log('p1', p1);
    p1.then(() => {
    
    
        console.log('200')
    })

    const p2 = Promise.resolve().then(() => {
    
    
        throw new Error('then error')
    })
    console.log('p2', p2)
    p2.then(() => {
    
    
        console.log('300')
    }).catch(err => {
    
    
        console.log('error', err)
    })

在这里插入图片描述

第二种情况:

	const p3 = Promise.reject('my error').catch(err => {
    
    
        console.log(err)
    })
    console.log('p3', p3)
    p3.then(() => {
    
    
        console.log(100)
    })

    const p4 = Promise.reject('my error').catch(err => {
    
    
        throw new Error('catch err')
    })
    console.log('p4', p4)
    p4.then(() => {
    
    
        console.log(200)
    }).catch(() => {
    
    
        console.log('some err')
    })

在这里插入图片描述

Promise 关于 then 和 catch 的面试题

示例 1 :resolve 成功输出 1 后又是一个成功的回调(fulfilled),之后执行 then,不会执行 catch

	// 输出结果:1 3
	Promise.resolve().then(() => {
    
    
        console.log(1)
    }).catch(() => {
    
    
        console.log(2)
    }).then(() => {
    
    
        console.log(3)
    })

示例 2:resolve 成功输出 1 后报错异常(rejected),之后执行 catch 输出 2 后(fulfilled),执行 then

	// 输出结果:1 2 3 
    Promise.resolve().then(() => {
    
    
        console.log(1)
        throw new Error('error1')
    }).catch(() => {
    
    
        console.log(2)
    }).then(() => {
    
    
        console.log(3)
    })

示例 3:resolve 成功输出 1 后报错异常(rejected),之后执行 catch 输出 2 后(fulfilled),之后就结束了

	// 输出:1 2
    Promise.resolve().then(() => {
    
    
        console.log(1)
        throw new Error('error1')
    }).catch(() => {
    
    
        console.log(2)
    }).catch(() => {
    
    
        console.log(3)
    })

async 和 await 基本使用

  • 使用 await,必须有 async 包裹
  • ! 的作用是防止此前代码结尾不加分号
    // 加载图片
    function loadImg(src) {
    
    
        const p = new Promise((resolve, reject) => {
    
    
            const img = document.createElement('img')
            img.onload = () => {
    
    
                resolve(img)
            }
            img.onerror = () => {
    
    
                const err = new Error(`图片加载失败${
      
      src}`)
                reject(err)
            }
            img.src = src
            document.body.appendChild(img)
        })
        return p
    }
    
    const src = 'xxx.png'
    
    !(async function() {
    
    
        const img = await loadImg(src)
        console.log(img.width, img.height) // 100 110
    })()

async/await 和 Promise 的关系

  • async/awiat 是消灭异步回调的终极武器
  • 但和 Promise 并不互斥
  • 反而,两者相辅相成
  • 执行 async 函数,返回的是 Promise 对象
  • await 相当于 Promise 的 then
  • try…catch 可捕获异常,代替了 Promise 的 catch

示例 1:执行 async 函数,返回的是一个 Promise 对象

    async function fn1(){
    
    
        // return 100 //相当于 return Promise.resolve(100)
        return Promise.resolve(200)
    }
    const res1 = fn1() // 执行 async 函数,返回的是一个 Promise 对象
    console.log('res1', res1);
    res1.then(data => {
    
    
        console.log('data', data) // 200
    })

在这里插入图片描述

示例 2:

    async function fn() {
    
    
        return Promise.resolve(200)
    }
    !(async function() {
    
    
        const p1 = Promise.resolve(300)
        const data = await p1 // await 相当于 Promise 的 then
        console.log('data', data)
    })()
    !(async function() {
    
    
        const data1 = await 400 // await Promise.resolve(400)
        console.log('data1', data1)
    })()
    !(async function() {
    
    
        const data2 = await fn()
        console.log('data2', data2);
    })()

在这里插入图片描述

示例 3:try…catch 异常捕获

    !(async function() {
    
    
        const p4 = Promise.reject('error1')
        try {
    
    
            const res = await p4
            console.log(res)
        } catch (err) {
    
    
            console.error(err) // try...catch 相当于 promise catch
        }
    })()

在这里插入图片描述
示例 4:

    !(async function() {
    
    
        const p5 = Promise.reject('err') // rejected 状态
        const res = await p5 // await -> then
        console.log('res', res) // 不会执行
    })()

在这里插入图片描述

异步的本质

  • 异步的本质是回调函数
  • async/await 是消灭异步回调的终极武器
  • JS 是单线程,还得是有异步,还得是基于 event loop
  • async/await 是一个很好的语法糖

示例:await 后面的内容是 异步的

    async function async1() {
    
    
        console.log('async1 start') // 2
        await async2() // undefined
        // await 的后面,都可以看作是 callback 里的内容,即异步
        // 类似,event loop,setTimeout()
        console.log('async1 end') // 5
    }
    async function async2() {
    
    
        console.log('async2') // 3
    }
    console.log('script start') // 1
    async1()
    console.log('script end') // 4 

输出顺序:

在这里插入图片描述

示例 2:注——await 后面的都是异步内容,等主线程执行完才会触发 event loop 机制执行

	async function async1() {
    
    
        console.log('async1 start') // 2
        await async2() // undefined

        // 下面三行都是异步回调 callback 的内容
        console.log('async1 end') // 5
        await async3()
        console.log('async1 end 2') // 7
    }
    async function async2() {
    
    
        console.log('async2') // 3
    }
    async function async3() {
    
    
        console.log('async3') // 6
    }
    console.log('script start') // 1
    async1()
    console.log('script end') // 4
    // 同步代码执行完,event loop

在这里插入图片描述

宏任务与微任务

  • 宏任务:setTimeout、setInterval、Ajax、DOM事件
  • 微任务:Promise、async/await
  • 微任务执行时机比宏任务要早

Event Loop 和 DOM 渲染

  • 每次 Call Stack 清空(即每次轮询结束)
  • 都是 DOM 重新渲染的机会,DOM 结构如有改变则重新渲染
  • 然后再去触发下一次 Event Loop

图片出处:https://coding.imooc.com/lesson/400.html#mid=35181
在这里插入图片描述

宏任务和微任务的区别

  • 宏任务:DOM 渲染后触发,如 setTimeout
  • 微任务:DOM 渲染前触发,如 Promise

宏任务和微任务的根本区别

  • 在 micro task queue 里执行当前的微任务
  • 微任务比宏任务的执行时机更早
  • 微任务发生在 DOM 渲染之前
  • 宏任务发生在 DOM 渲染之后

在这里插入图片描述

JS 异步面试题

什么是宏任务和微任务、两者的区别

  • 宏任务:setTimeout,setInterval,Ajax,DOM 事件
  • 微任务:Promise,async/await
  • 微任务执行时机比宏任务要早

场景题 — async、await 语法

  • async 修饰的函数返回的是 promise 实例
  • a 没有 await 修饰,输出的是 promise 实例
  • b 有 await 修饰,相当于 then ,输出的是 100

示例 1:

    async function fn() {
    
    
        return 100
    }
    (async function() {
    
    
        const a = fn()
        const b = await fn()
        console.log(a)
        console.log(b)
    })()

在这里插入图片描述

示例 2:失败后面的代码都不会执行

    (async function() {
    
    
        console.log('start')
        const a = await 100
        console.log('a', a)
        const b = await Promise.resolve(200)
        console.log('b', b)
        const c = await Promise.reject(300)
        console.log('c', c)
        console.log('end')
    })()

在这里插入图片描述
在这里插入图片描述

promise 和 setTimeout 的顺序问题

  • 先执行主线程的任务
  • 再执行微任务
  • 再执行宏任务

示例:

    console.log(100)
    setTimeout(() => {
    
    
        console.log(200)
    })
    Promise.resolve().then(() => {
    
    
        console.log(300)
    })
    console.log(400)

在这里插入图片描述

async/await 的顺序问题

  • 主线程 => 微任务 => 宏任务
  • 初始化 promise 时,传入的函数会立刻被执行(主线程任务)
    async function async1 () {
    
    
        console.log('async1 start') // 2
        await async2()
        // await 后面的都作为回调内容 —— 微任务
        console.log('async1 end') // 6
    }

    async function async2 () {
    
    
        console.log('async2') // 3
    }

    console.log('script start') // 1

    setTimeout(function () {
    
     // 宏任务
        console.log('setTimeout') // 8
    }, 0)

    async1()

    // 初始化 promise 时,传入的函数会立刻被执行
    new Promise (function (resolve) {
    
    
        console.log('promise1') // 4
        resolve()
    }).then(function () {
    
     // 微任务
        console.log('promise2') // 7
    })

    console.log('script end') // 5
    // 同步代码执行完毕(event loop —— call stack 被清空)
    // 执行微任务
    // 尝试触发 DOM 渲染
    // 触发 Event Loop,执行宏任务

在这里插入图片描述

手写 Promise 源码

  • 有难度有深度
    /**
     * @description MyPromise
     * @author 前端杂货铺
     */
    class MyPromise {
    
    
        state = 'pending' // 状态,'pending' 'fulfilled' 'rejected'
        value = undefined // 成功后的值
        reason = undefined // 失败后的原因

        resolveCallbacks = [] // pending 状态下,存储成功的回调
        rejectCallbacks = [] // pending 状态下,存储失败的回调

        constructor(fn) {
    
    
            const resolveHandler = (value) => {
    
    
                if (this.state === 'pending') {
    
    
                    this.state = 'fulfilled'
                    this.value = value
                    this.resolveCallbacks.forEach(fn => fn(this.value))
                }
            }

            const rejectHandler = (reason) => {
    
    
                if (this.state === 'pending') {
    
    
                    this.state = 'rejected'
                    this.reason = reason
                    this.rejectCallbacks.forEach(fn => fn(this.reason))
                }
            }

            try {
    
    
                fn(resolveHandler, rejectHandler)
            } catch (err) {
    
    
                rejectHandler(err)
            }
        }

        then(fn1, fn2) {
    
    
            fn1 = typeof fn1 === 'function' ? fn1 : (v) => v
            fn2 = typeof fn2 === 'function' ? fn2 : (e) => e

            if (this.state === 'pending') {
    
    
                const p1 = new MyPromise((resolve, reject) => {
    
    
                    this.resolveCallbacks.push(() => {
    
    
                        try {
    
    
                            const newValue = fn1(this.value)
                            resolve(newValue)
                        } catch (err) {
    
    
                            reject(err)
                        }
                    })

                    this.rejectCallbacks.push(() => {
    
    
                        try {
    
    
                            const newReason = fn2(this.reason)
                            reject(newReason)
                        } catch (err) {
    
    
                            reject(err)
                        }
                    })
                })
                return p1
            }

            if (this.state === 'fulfilled') {
    
    
                const p1 = new MyPromise((resolve, reject) => {
    
    
                    try {
    
    
                        const newValue = fn1(this.value)
                        resolve(newValue)
                    } catch (err) {
    
    
                        reject(err)
                    }
                })
                return p1
            }

            if (this.state === 'rejected') {
    
    
                const p1 = new MyPromise((resolve, reject) => {
    
    
                    try {
    
    
                        const newReason = fn2(this.reason)
                        reject(newReason)
                    } catch (err) {
    
    
                        reject(err)
                    }
                })
                return p1
            }
        }

        // 就是 then 的一个语法糖,简单模式
        catch (fn) {
    
    
            return this.then(null, fn)
        }
    }

    MyPromise.resolve = function (value) {
    
    
        return new MyPromise((resolve, reject) => resolve(value))
    }
    MyPromise.reject = function (reason) {
    
    
        return new MyPromise((resolve, reject) => reject(reason))
    }

    MyPromise.all = function (promiseList = []) {
    
    
        const p1 = new MyPromise((resolve, reject) => {
    
    
            const result = [] // 存储 promiseList 所有的结果
            const length = promiseList.length
            let resolvedCount = 0

            promiseList.forEach(p => {
    
    
                p.then(data => {
    
    
                    result.push(data)

                    // resolvedCount 必须在 then 里面做 ++
                    // 不能用 index
                    resolvedCount++
                    if (resolvedCount === length) {
    
    
                        // 已经遍历到了最后一个 promise
                        resolve(result)
                    }
                }).catch(err => {
    
    
                    reject(err)
                })
            })
        })
        return p1
    }

    MyPromise.race = function (promiseList = []) {
    
    
        let resolved = false // 标记
        const p1 = new Promise((resolve, reject) => {
    
    
            promiseList.forEach(p => {
    
    
                p.then(data => {
    
    
                    if (!resolved) {
    
    
                        resolve(data)
                        resolved = true
                    }
                }).catch((err) => {
    
    
                    reject(err)
                })
            })
        })
        return p1
    }

不积跬步无以至千里 不积小流无以成江海

点个专注不迷路,持续更新中…

猜你喜欢

转载自blog.csdn.net/qq_45902692/article/details/126123773