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

简介: 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

fd76a88fb3bd45ab8b7c6a946ef7ea54.png

f17baf04dd03467ca49415ba4d6a22ca.png

136ad4c4d03c43b9a0875cd2db59a947.png

423a3a6329074aa18de09df711c2e78c.png

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)
    })

468664c1939a4c5f8e92ec3089b4a10e.png

第二种情况:

  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')
    })

a73c062a89084760b2c8f5f7a10d35a7.png

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
    })

826ad17ac436490baa01b1bc61d922c1.png

示例 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);
    })()

c6e082c4e00f45849954aa04b3d5127f.png

示例 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
        }
    })()

51b5b45ca3224c39a605121f15c0a0e0.png

示例 4:

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

e887f010446c47639c54b37043af3cd4.png

异步的本质

  • 异步的本质是回调函数
  • 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 

输出顺序:

5fd44faf7d5c43119346186b65462c5c.png

示例 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

936f05e9b5fd4559b657453788dd234c.png

宏任务与微任务

  • 宏任务: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

9d40f121fffe49b89bde9c7bea762a9f.png

宏任务和微任务的区别

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

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

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

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)
    })()

b2dc0b51c9fa4a1ba42a9d95e5cd98a6.png

示例 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')
    })()

e631fe81c3f841f9adae28b025491ddf.png

e65f5d072f324870acbf7912983dfc6c.png

promise 和 setTimeout 的顺序问题

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

示例:

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

370490a8e03743169f05b340b1c13430.png

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,执行宏任务

0a1574c362d0411f989fe7ce0d1f1e4d.png

手写 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
    }

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

相关文章
|
2月前
|
JavaScript 前端开发 API
详解队列在前端的应用,深剖JS中的事件循环Eventloop,再了解微任务和宏任务
该文章详细讲解了队列数据结构在前端开发中的应用,并深入探讨了JavaScript的事件循环机制,区分了宏任务和微任务的执行顺序及其对前端性能的影响。
|
1月前
|
人工智能 JavaScript 前端开发
使用Node.js模拟执行JavaScript
使用Node.js模拟执行JavaScript
|
1月前
|
JavaScript 前端开发
电话号码正则表达式 代码 javascript+html,JS正则表达式判断11位手机号码
电话号码正则表达式 代码 javascript+html,JS正则表达式判断11位手机号码
103 1
|
1月前
|
存储
经典面试题:写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个 复制 #define MIN(a,b) ((a)<=(b)?(a):(b))
你的宏定义已非常接近标准。以下是改进后的 `MIN` 宏定义,支持多种数据类型并避免副作用:
|
1月前
|
Web App开发 JavaScript 前端开发
Node.js:JavaScript世界的全能工具
Node.js:JavaScript世界的全能工具
|
1月前
|
JSON JavaScript 前端开发
使用JavaScript和Node.js构建简单的RESTful API服务器
【10月更文挑战第12天】使用JavaScript和Node.js构建简单的RESTful API服务器
17 0
|
1月前
|
JavaScript 前端开发 调度
在JavaScript中异步任务里的微任务和宏任务的特点和生命周期
在JavaScript中异步任务里的微任务和宏任务的特点和生命周期
42 0
|
1月前
|
Java 调度 Android开发
Android面试题之Kotlin中async 和 await实现并发的原理和面试总结
本文首发于公众号“AntDream”,详细解析了Kotlin协程中`async`与`await`的原理及其非阻塞特性,并提供了相关面试题及答案。协程作为轻量级线程,由Kotlin运行时库管理,`async`用于启动协程并返回`Deferred`对象,`await`则用于等待该对象完成并获取结果。文章还探讨了协程与传统线程的区别,并展示了如何取消协程任务及正确释放资源。
24 0
|
2月前
|
前端开发 JavaScript API
JavaScript 的宏任务和微任务有什么区别
【9月更文挑战第6天】JavaScript 的宏任务和微任务有什么区别
64 4
|
1月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
107 3