前端面试题6--JS基础--异步进阶

95 阅读3分钟

event loop (事件循环/事件轮询)

  • JS是单线程运行的
  • 异步要基于回调来实现
  • event loop 就是异步回调的实现原理 JS如何执行?
  • 从前到后,一行一行执行
  • 如果某一行执行报错,则停止下面代码的执行
  • 先把同步代码执行完,再执行异步
console.log('Hi')
setTimeout(function cb1() {
    console.log('cb1') // cb 即 callback
})
console.log('Bye')
// 'Hi'
// 'Bye'
// 'cb1'
  • Call Stack 调用栈
  • Web APIs 浏览器api(es之外的)
  • Event Loop 事件轮询/事件循环
  • Callback Queue 回调函数队列

1.jpg

2.jpg

3.jpg 总结event loop过程1

  • 同步代码,一行一行放在Call Stack 执行
  • 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
  • 时机到了,就移动到 Callbanck Queue 总结event loop过程2
  • 如 Call Stack 为空(即同步代码执行完)Event Loop 开始工作
  • 轮询查找 Callback Queue,如有则移动到 Call Stack 执行
  • 然后继续轮询查找(永动机一样) DOM 事件和event loop
  • JS是单线程的
  • 异步(setTimeout,ajax 等)使用回调,基于event loop
  • DOM 事件也使用回调,基于 event loop(DOM 事件不是异步,立马执行,页面触发)

5.jpg

Promise

  • 三种状态:pending resolved rejected
  • 状态的表现和变化:pending —> resolved 或 pending —> rejected,变化不可逆
  • then 和 catch 对状态的影响:pending 状态,不会触发 then 和 catch;resolved 状态,会触发后续的 then 回调函数;rejected 状态,会触发后续的 catch 回调函数 Promise.resolve()、Promise.reject()

    then 和 catch 改变状态:
  • then 正常返回 resolved,里面有报错则返回rejected
  • catch 正常返回 resolved,里面有报错则返回rejected
const p3 = Promise.reject('my error').catch(err => {
    console.error(err)
})
console.log('p3', p3) // resolved 注意!触发then回调

p3.then(() => { console.log('100') })   // 可打印100

const p4 = Promise.reject('my error').catch(err => {
    throw new Error('catch error')
})
console.log('p4', p4) // rejected 触发 catch 回调

p4.then(() => { console.log('200') })      // 不可打印
.catch(() => { console.log('some err') })  // 可打印  resolved状态
// 第一题
Promise.resolve().then(() => {
    console.log(1)       // 1
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)       // 3
}) // resolved

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

// 第三题 
Promise.resolve().then(() => { // rejected 触发 catch 回调
    console.log(1)       // 1
    throw new Error('error')
}).catch(() => {         // resolved 触发 then 回调
    console.log(2)       // 2
}).catch(() => {
    console.log(3)       // 注意这里是catch
})

async/await

  • 异步回调 callback hell
  • Promise then catch 链式调用,但也是基于回调函数 async/await 是同步语法,彻底消灭回调函数
// 加载图片
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.scr = src
    })
    return p
}
(async function () { // 同步写法 执行异步代码
    const img1 = await loadImg(src1)
    console.log(img1.width)
    const img2 = await loadImg(src2)
    console.log(img2.width)
})
// 另一种写法
async function loadImg1() {
    const img1 = await loadImg(src1)
    return img1
}
async function loadImg2() {
    const img2 = await loadImg(src2)
    return img2
}
(async function () {
    const img1 = await loadImg1()
    console.log(img1.width)
    const img2 = await loadImg2()
    console.log(img2.width)
})

async/await 和 Promise 的关系

  • async/await 是消灭异步回调的终极武器
  • 但和Promise并不互斥
  • 反而,两者相辅相成
  • 执行async 函数,返回的是Promise 对象
  • await 相当于 Promise 的 then
  • try...catch 可捕获异常,代替了Promise 的 catch
// 第一例
async function fn1() {
    return 100              // 相当于 return Promise.resolve(100)
}
const res1 = fn1()          // 执行async函数,返回的是一个 Promise对象
console.log('res1', res1)   // Promise对象
res1.then(data => {
    console.log('data', data) // 100
})
// 第二例
!(async function() {
    const p1 = Promise.resolve(300)
    const data = await p1    // await 相当于 Promise的then
    console.log('data', data)
})()
// 第三例
!(async function() {
    const data = await 400     // await Promise.resolve(400)
    console.log('data', data)  // 400
})()
// 第四例
!(async function() {
    const data = await fn1()
    console.log('data', data)  // 100
})()
// 第五例,需要用try..catch捕获
!(async function() {
    const p4 = Promise.reject('err')    // rejected状态
    const res = await p4 // await -> then,因为上一步是rejected状态,所以不会执行
    console.log(res)  // 不会打印
})()

try..catch

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

异步的本质

  • async/await 是消灭异步回调的终极武器
  • JS还是单线程,还得是有异步,还得是基于 event loop
  • async/await只是一个语法糖,但这颗糖真香!
// 函数定义完不会立即执行
async function async1 () {
    console.log('async1 start') // 2 重要
    await async2() // 先执行async2,再进行await操作
    // await 的后面,都可以看作是 callbank 里的内容,即异步
    console.log('async1 end')   // 5
    // 相当于Promise.resolve().then(() => {console.log('async1 end')})
}
async function async2 () {
    console.log('async2') // 3 重要
}
console.log('script start')   // 1
async1() // 执行函数,立即执行里面内容,还没到异步
console.log('script end')     // 4 同步代码已执行完,去执行异步

for...of

  • for...in(以及forEach for)是常规的同步遍历
  • for...of常用于异步的遍历
function muti (num) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(num*num)
        },1000)
    })
}
const nums = [1, 2, 3]
// 同步循环
nums.forEach(async (i) => {
    const res = await muti(i)
    console.log(res)
}) // 一次性打印出来了,一瞬间执行了三次
// 异步循环
!(async function(){
    for (let i of nums) {
        const res = await muti(i)
        console.log(res)
    }
})()


宏任务 macroTask 和微任务 microTask

什么是宏任务,什么是微任务:

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

event loop 和 DOM 渲染:

  • 再次回归一遍 event loop 的过程
  • JS 是单线程的,而且和 DOM 渲染共用一个线程
  • JS 执行的时候,得留一些时机供 DOM 渲染
  • 每次 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 渲染,便于查看效果

6.jpg 微任务和宏任务的区别:

  • 宏任务:DOM 渲染后触发,如 setTimeout
  • 微任务:DOM 渲染前触发,如 Promise 7.jpg
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container').append($p1)
               .append($p2)
               .append($p3)
// 微任务:DOM 渲染前触发
Promise.resolve().then(() => {
    console.log('length1', $('#container').children().length)
    alert('Promise then') // DOM 渲染了吗---no
})
// 宏任务:DOM 渲染后触发
setTimeout(() => {
    console.log('length2', $('#container').children().length)
    alert('setTimeout') // DOM 渲染了吗---yes
})

从 event loop 解释,为何微任务执行更早: 为什么?

  • 微任务是ES6语法规定的
  • 宏任务是由浏览器规定的
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 () {      // 宏任务 setTimeout
    console.log('setTimeout') // 8
}, 0)
async1()
// 初始化 promise 时,传入的函数会立刻被执行
new Promise(function () {
    console.log('promise1')   // 4
    resolve()
}).then (function () {        // 微任务
    console.log('promise2')   // 7
})
console.log('script end')     // 5 
// 同步代码执行完毕(event loop -call stack 被清空)
// 执行微任务
// (尝试触发 DOM 渲染)
// 触发 Event Loop
// 执行宏任务