Skip to content
本页目录

JS异步进阶

JS异步编程是整个JS知识中的重点之重 所以在此章节中我会重点讲解JS异步在实际应用中使用场景。

主要内容有:

  • event-loop
  • promise进阶
  • async/await
  • 宏任务 & 微任务

1. 先看题目

  • 描述 event loop 运行机制(可画图)
  • Promise 哪几种状态,如何变化?
  • 宏任务和微任务的区别
  • 场景题:Promise catch 连接 then
  • 场景题:Promise 和 setTimeout 顺序
  • 场景题:各类异步执行顺序问题
  • 手写 Promise

Promise catch 连接 then

js
// 第一题
Promise.resolve().then(() => {
    console.log(1)
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})

// 第二题
Promise.resolve().then(() => {
    console.log(1)
    throw new Error('erro1')
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})

// 第三题
Promise.resolve().then(() => {
    console.log(1)
    throw new Error('erro1')
}).catch(() => {
    console.log(2)
}).catch(() => { // 注意这里是 catch
    console.log(3)
})

async/await 语法问题

js
async function fn() {
    return 100
}
(async function () {
    const a = fn()           
    const b = await fn()
})()
js
(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 顺序

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

执行顺序问题

js
async function async1 () {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

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

console.log('script start')

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

async1()

new Promise (function (resolve) {
  console.log('promise1')
  resolve()
}).then (function () {
  console.log('promise2')
})

console.log('script end')

2. event-loop

简单介绍一下 什么是event-loop: 事件循环/事件轮询

众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),

核心: JS是单线程语言

但是在我们写代码时又会出现很多异步代码 如: setTimeout DOM事件 ajax请求

核心: 异步要基于回调来实现

那么event-loop本质就是异步回调的实现原理

JS如何执行?

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

示例

我们来查看下面这段代码来看看是如何运行的以及结果是怎样的

js
console.log('hi1')

setTimeout(() => {
  console.log('hi2')
}, 1000);

console.log('hi3')

通过画图讲解代码执行过程

执行过程

过程1

  • 同步代码,一行一行 放入call stack执行
  • 遇到异步,会先 记录 等待时机执行(定时器、网络请求)
  • 时机一到,自动将异步移动至callback Queue

过程2

  • call stack为空(即同步代码执行完成) Event Loop机制开始工作
  • 轮询查询 callback Queue , 如有则移动至 call stack执行
  • 如何进行轮询查找 (永动机一样)

DOM事件和Event Loop

html
<button id="btn1">提交</button>

<script>
console.log('Hi')

$('#btn1').click(function (e) {
    console.log('button clicked')
})

console.log('Bye')
</script>

查看这段代码 DOM事件代码是如何执行的? 也是被event loop调用的 本质还是回调

3. Promise

重点讲解内容:

  1. 三种状态
  2. 状态和 then catch
  3. 常用 API

先回顾一下 Promise 的基本使用

js
// 加载图片
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
        }
    )
    return p
}
const url = 'https://zeng.pub/cat.svg'
loadImg(url).then(img => {
    console.log(img.width)
    return img
}).then(img => {
    console.log(img.height)
}).catch(ex => console.error(ex))

Promise解决了什么问题? 解决了回调嵌套问题 - 回调地狱

三种状态

三种状态 pending resolved rejected

(画图表示转换关系,以及转换不可逆)

js
// 刚定义时,状态默认为 pending
const p1 = new Promise((resolve, reject) => {

})

// 执行 resolve() 后,状态变成 resolved
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve()
    })
})

// 执行 reject() 后,状态变成 rejected
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject()
    })
})
js
// 直接返回一个 resolved 状态
Promise.resolve(100)
// 直接返回一个 rejected 状态
Promise.reject('some error')

状态和 then catch

状态变化会触发 then catch —— 这些比较好理解,就不再代码演示了

  • pending 不会触发任何 then catch 回调
  • 状态变为 resolved 会触发后续的 then 回调
  • 状态变为 rejected 会触发后续的 catch 回调

then catch 会继续返回 Promise ,此时可能会发生状态变化!!!

js
// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
    return 100
})

// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
    throw new Error('err')
})
js
// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
    console.error('catch some error')
})


// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
    console.error('catch some error')
    throw new Error('err')
})

看一个综合的例子,即那几个面试题 试试看

js
// 第一题
Promise.resolve().then(() => {
    console.log(1)
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})


// 第二题
Promise.resolve().then(() => {
    console.log(1)
    throw new Error('erro1')
}).catch(() => { 
    console.log(2)
}).then(() => {
    console.log(3)
})


// 第三题
Promise.resolve().then(() => {
    console.log(1)
    throw new Error('erro1')
}).catch(() => { 
    console.log(2)
}).catch(() => {
    console.log(3)
})

常用 API

Promise.all 执行所有Promise函数当所有状态不为pending时返回结果then

Promise.race 执行所有Promise函数当任意一个状态不为pending是返回结果then

4. async/await

异步代码处理史:

  • 异步回调 callback hell
  • Promise then catch 链式调用,但也是基于回调函数
  • async/await是同步语法, 彻底消灭回调函数

语法介绍

用同步的方式,编写异步。

js
function loadImg(src) {
    const promise = new Promise((resolve, reject) => {
        const img = document.createElement('img')
        img.onload = () => {
            resolve(img)
        }
        img.onerror = () => {
            reject(new Error(`图片加载失败 ${src}`))
        }
        img.src = src
    })
    return promise
}

async function loadImg1() {
    const src1 = 'https://zeng.pub/cat.svg'
    const img1 = await loadImg(src1)
    return img1
}

async function loadImg2() {
    const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
    const img2 = await loadImg(src2)
    return img2
}



(async function () {
    // 注意:await 必须放在 async 函数中,否则会报错
    try {
        // 加载第一张图片
        const img1 = await loadImg1()
        console.log(img1)
        // 加载第二张图片
        const img2 = await loadImg2()
        console.log(img2)
    } catch (ex) {
        console.error(ex)
    }
})()

和 Promise 的关系

  • async 函数返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)
js
async function fn2() {
    return new Promise(() => {})
}
console.log( fn2() )

async function fn1() {
    return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
  • await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
  • await 后续跟非 Promise 对象:会直接返回
js
(async function () {
    const p1 = new Promise(() => {})
    await p1
    console.log('p1') // 不会执行
})()

(async function () {
    const p2 = Promise.resolve(100)
    const res = await p2
    console.log(res) // 100
})()

(async function () {
    const res = await 100
    console.log(res) // 100
})()

(async function () {
    const p3 = Promise.reject('some err')
    const res = await p3
    console.log(res) // 不会执行
})()
  • try...catch 捕获 rejected 状态
js
(async function () {
    const p4 = Promise.reject('some err')
    try {
        const res = await p4
        console.log(res)
    } catch (ex) {
        console.error(ex)
    }
})()

总结来看:

  • async 封装 Promise
  • await 处理 Promise 成功
  • try...catch 处理 Promise 失败

异步本质

await 是同步写法,但本质还是异步调用。

核心: 只是语法糖 不改代码运行本质

js
async function async1 () {
  console.log('async1 start')
  await async2()
  console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
}

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

console.log('script start')
async1()
console.log('script end')

即,只要遇到了 await ,后面的代码都相当于放在 callback 里。 最终还是需要 EventLoop

5. 宏任务与微任务

宏任务: macroTask 微任务: microTask

什么是宏任务? 什么是微任务?

宏任务:setTimeout setInterval DOM 事件

微任务:Promise(对于前端来说)

微任务比宏任务执行的更早 先记住~~~

下面查看一段代码:

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

event loop 和 DOM 渲染

再次回顾 event loop 的过程

  • JS是单线程的, 而且和DOM渲染公用一个线程
  • JS执行时,得留一些时机供DOM渲染
js
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
            .append($p1)
            .append($p2)
            .append($p3)

console.log('length',  $('#container').children().length )

代码有执行 并且也有结果 但是DOM是没有渲染的

那么DOM是什么时候渲染的?

  • 每一次 call stack 结束,都会触发 DOM 渲染(不一定非得渲染,就是给一次 DOM 渲染的机会!!!)
  • 然后再进行 event loop

以下列代码为例:

js
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
            .append($p1)
            .append($p2)
            .append($p3)

console.log('length',  $('#container').children().length )

// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')

// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预
// 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
setTimeout(function () {
    alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了')
})

宏任务和微任务的区别

  • 宏任务:DOM 渲染后再触发
  • 微任务:DOM 渲染前会触发

以下列代码为例:

js
// 修改 DOM
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
    .append($p1)
    .append($p2)
    .append($p3)

// // 微任务:渲染之前执行(DOM 结构已更新)
// Promise.resolve().then(() => {
//     const length = $('#container').children().length
//     alert(`micro task ${length}`)
// })

// 宏任务:渲染之后执行(DOM 结构已更新)
setTimeout(() => {
    const length = $('#container').children().length
    alert(`macro task ${length}`)
})

为何微任务执行更早

微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。

宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。

简单总结:微任务是ES标准会被 eventloop 优先执行

6. 问题解答

简答题

  1. 描述 event loop 运行机制(可画图)

    自行回顾event loop的过程 以及与DOM渲染关系 宏任务和微任务eventloop不同的处理

  2. 宏任务和微任务的区别

    宏任务: setTimeout, setInterval, Ajax, DOM事件

    微任务: Promise, async/await

    微任务执行时机比宏任务早

  3. Promise的三种状态,如何变化

    • pending resolved rejected
    • pending -> resolved或rejected -> resolved
    • 变化不可逆

场景题

  1. Promise catch 连接 then
  2. async/await 语法问题
  3. Promise 和 setTimeout 顺序
  4. 执行顺序问题

具体代码演示

手写 Promise 【可选】

考察点: 对promise的理解、编程能力

实现: 1. 初始化&异步调用 2. then catch链式调用 3. API: .resolve .reject .all .race

具体代码演示效果

7. 总结

本章节主要介绍目前热门JS异步面试题:

eventloop promise进阶 async/await 微任务/宏任务

  • eventloop: 执行过程、与DOM事件的关系、与DOM渲染的时机
  • Promise进阶: 三种状态、then与catch调用时机
  • async/await: 解决了什么问题、与Promise的关系
  • 微任务/宏任务: 微宏任务有哪些、为什么微任务触发时机更早,与DOM渲染的关系