乐趣区

关于前端:异步进阶面试回顾

event loop(事件循环)

  • 概念

    • js 是单线程运行的
    • 异步要基于回调来实现
    • event loop 就是异步回调的实现原理
    • DOM 事件也应用回调, 基于 event loop
  • 过程

    • 同步代码, 一行一行放在 Call Stack 执行
    • 遇到异步会先记录下, 期待机会 (定时, 网络申请等)
    • 机会到了就挪动到 Callback Queue
    • 如 Call Stack 为空 (即同步代码执行完) Event Loop 开始工作
    • 轮询查找 Callback Queue, 如有则挪动到 Call Stack 执行
    • 而后持续轮询查找

Promise

  • 三种状态
  • 状态和 then catch
  • 罕用 API

先回顾一下 Promise 的根本应用

// 加载图片
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://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
loadImg(url).then(img => {console.log(img.width)
    return img
}).then(img => {console.log(img.height)
}).catch(ex => console.error(ex))

三种状态

三种状态 pending resolved rejected

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

// 刚定义时,状态默认为 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()
    })
})

状态和 then catch

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

  • pending 不会触发任何 then catch 回调
  • 状态变为 resolved 会触发后续的 then 回调
  • 状态变为 rejected 会触发后续的 catch 回调
// 间接返回一个 resolved 状态
Promise.resolve(100)
// 间接返回一个 rejected 状态
Promise.reject('some error')
// then() 个别失常返回 resolved 状态的 promise
Promise.resolve().then(() => {return 100})

// then() 里抛出谬误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {throw new Error('err')
})

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

看几个面试题

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

// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
    console.log(1)
    throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
    console.log(2)
}).then(() => {console.log(3)
}) //1 2 3

// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
    console.log(1)
    throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
    console.log(2)
}).catch(() => {console.log(3)
}) //1 2

async/await

  • Promise 的 then,catch 链式调用, 也是基于回调函数
  • 用同步的形式编写异步, 彻底毁灭回调函数
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 = 'http://www.imooc.com/static/img/index/logo_new.png'
    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,则主动封装一下)
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 对象:会间接返回
(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 状态
(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 是同步写法,但实质还是异步调用。

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 里。

for…of

// 定时算乘法
function multi(num) {return new Promise((resolve) => {setTimeout(() => {resolve(num * num)
        }, 1000)
    })
}

// // 应用 forEach,是 1s 之后打印出所有后果,即 3 个值是一起被计算出来的
// function test1 () {//     const nums = [1, 2, 3];
//     nums.forEach(async x => {//         const res = await multi(x);
//         console.log(res);
//     })
// }
// test1();

// 应用 for...of,能够让计算挨个串行执行
async function test2 () {const nums = [1, 2, 3];
    for (let x of nums) {
        // 在 for...of 循环体的外部,遇到 await 会挨个串行计算
        const res = await multi(x)
        console.log(res)
    }
}
test2()

宏工作和微工作

  • 宏工作:setTimeout setInterval DOM 事件
  • 微工作:Promise(对于前端来说)
  • 微工作比宏工作执行的更早
console.log(100)
setTimeout(() => {console.log(200)
})
Promise.resolve().then(() => {console.log(300)
})
console.log(400)
// 100 400 300 200

event loop 和 DOM 渲染

再次回顾 event loop 的过程

  • 每一次 call stack 完结,都会触发 DOM 渲染(不肯定非得渲染,就是给一次 DOM 渲染的机会!!!)
  • 而后再进行 event loop
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('本次 call stack 完结,DOM 构造已更新,但尚未触发渲染')
//(alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看成果)// 到此,即本次 call stack 完结后(同步工作都执行完了),浏览器会主动触发渲染,不必代码干涉

// 另外,依照 event loop 触发 DOM 渲染机会,setTimeout 时 alert,就能看到 DOM 渲染后的后果了
setTimeout(function () {alert('setTimeout 是在下一次 Call Stack,就能看到 DOM 渲染进去的后果了')
})

宏工作和微工作的区别

  • 宏工作:DOM 渲染后再触发
  • 微工作:DOM 渲染前会触发
// 批改 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)干涉解决。
退出移动版