【前端面试基础】(三)异步(含9个场景题)

同步和异步的区别是什么?

  • 异步基于 JS 是单线程语言,只能同时做一件事
  • JS 和 DOM 渲染共用同一个线程,因为 JS 可以修改 DOM 结构
  • 异步不会阻塞代码执行
  • 同步会阻塞代码执行

手写Promise加载一张图片

javascript 复制代码
function loadImg(src) {
    return 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;
    })
}

Promise演示

javascript 复制代码
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))

const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'

loadImg(url1).then(img1 => {
    console.log(img1.width)
    return img1 // 普通对象
}).then(img1 => {
    console.log(img1.height)
    return loadImg(url2) // promise 实例
}).then(img2 => {
    console.log(img2.width)
    return img2
}).then(img2 => {
    console.log(img2.height)
}).catch(ex => console.error(ex))

异步应用场景

  • 网络请求,如 ajax 图片加载
  • 定时任务,如 setTimeout

请描述event loop(事件循环/事件轮询)的机制,可画图

我们知道:

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

Event loop的执行过程

  • 同步代码,一行一行放在 Call Stack 执行,执行完之后,会将其清空
  • 遇到异步,会先"记录下",等待时机(定时、网络请求等)
  • 时机到了,就移到 Callback Queue
  • 如果 Call Stack 为空(即同步代码执行完)
  • 执行当前微任务队列中的微任务(宏任务和微任务区别后面有讲到)
  • 尝试DOM渲染(如果DOM结构改变)
  • Event Loop开始工作
  • 轮询查找 Callback Queue,如果有则移动到 Call Stack 执行
  • 然后继续轮询查找(永动机一样)

什么是宏任务和微任务,两者有什么区别?

Promise有哪三种状态?如何变化?

三种状态

  • pending 过程中
  • resolved(fulfilled) 成功
  • rejected 失败
  • 状态变化不可逆,一旦改变一次,则无法再发生改变。

状态的表现

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

then和catch改变状态

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

javascript 复制代码
const p1 = Promise.resolve().then(() => {
    return 100
})
console.log('p1', p1); //resolved 会触发后续 then 回调

p1.then(() => {
    console.log('123');
})
const p2 = Promise.resolve().then(() => {
    throw new Error('then error')
})

console.log('p2', p2); //rejected 会触发后续 catch 回调

p2.then(() => {
    console.log('456');
})

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

javascript 复制代码
const p3 = Promise.reject('my error').catch(err => {
    console.log(err);
})
console.log('p3', p3); // resolved 注意!!!! 触发 then回调
p3.then(() => {
    console.log(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');
})

如何改变?

  • 执行resolve()由pedding改变为resolved

  • 执行reject()由pedding改变为rejected

  • 遇到错误抛出由pedding改变为rejected

  • Promise.then()方法中

    • return非Promise对象,返回成功的Promise成功的值为return的值(pendding => resolved
    • return一个Promise对象,则返回的Promise状态和return的Promise一样,值也一样。
    • 抛出错误,返回失败的Promise,值为抛出错误内容(pending => rejected
  • Promise.catch()方法实际上是语法糖,相当于Promise.then(空, err=>{}),因此catch返回的Promise状态和then()一样

  • Promise.all()方法中

    • 如果传入的Promise数组状态都为成功,则返回成功的Promise,值为所有Promise成功值组成的数组。
    • 如果传入的Promise数组只要有一个失败,则返回失败的Promise,值为第一个状态变为失败的Promise的值。
  • Promise.race()方法中,取决于传入Promise数组中第一个改变状态的Promise,

    • 这个Promise如果成功,则race()返回成功的Promise。
    • 如果失败,则race()返回失败的Promise
    • 成功或失败的值和这个改变状态的Promise相同。

async/await和Promise 的关系

  • 执行async函数,返回的是Promise对象
  • await 相当于Promise的then
  • try ... catch可捕获异常,代替了Promise的catch

async/await

  • 由异步回调的 callback hell(回调地狱) 引出了Promise

  • 但Promise的then和catch的链式调用,依然是基于回调函数的

  • async/await 是同步语法,彻底消灭回调函数

  • async函数返回一个Promise对象,返回的规则同then()方法:

    • return 非Promise值,返回成功的值,成功的值为return 的值
    • return Promise值,返回的Promise状态由return的Promise决定,成功或失败的值相同
    • 抛出错误,返回失败的Promise,值为抛出的错误
javascript 复制代码
async function fn1() {
    // return 100    //相当于 return Promise.resolve(100)
    return Promise.resolve(200)
}

const res1 = await fn1() //执行async 函数,返回的是一个Promise对象
// console.log('res1', res1); //打印结果:res1  --> Promise {<fulfilled>: 100}
res1.then((data) => {
    console.log('data', data);  //打印结果:  data 200
})
  • await返回值的情况根据其后面语句执行结果决定:

    • 后面的语句执行结果如果是成功的Promise,await语句返回其成功的值(类似Promise.then()方法)
    • 后面的语句执行结果如果是失败的Promise,则await语句抛出错误,可以使用try...catch...捕获
    • 后面的语句执行结果如果是非Promise值,则await语句直接返回该值
  • async函数调用时,是同步执行

  • await 语句以下的所有代码,都可以看作是一个callback回调里的内容,即异步,微任务

  • async/await 只是一个语法糖,异步的本质还是回调函数

  • async/await是消灭异步回调的终极武器

  • JS还是单线程,还得是有异步,还得基于event loop

javascript 复制代码
//匿名函数
!(async function () {
    const p1 = Promise.resolve(300)
    const data = await p1 //await 相当于 Promise then
    console.log('data1', data);  //打印结果:  data 300
})()


!(async function () {
    const data = await 400 //相当于 await Promise.resolve(400)
    console.log('data2', data);  //打印结果:  data 400
})()

!(async function () {
    const p2 = Promise.reject('err')  //rejected状态
    try {
        const data = await p2 //await 相当于 Promise then
        console.log('data3', data);  //打印结果:  data 300
    } catch (error) {
        console.log(error);  //try ...catch 相当于 Promise catch
    }
})()



!(async function () {
    const p4 = Promise.reject('err1')  //rejected状态
    const res = await p4  //await 相当于 Promise then,由于p4是rejected状态,所以不会执行这句代码,也就没有打印结果
    console.log(res);
})()

场景题

做完之后,可以右滑,查看注释的答案。

1. setTimeout

javascript 复制代码
console.log(1);
setTimeout(() => {
    console.log(2);
}, 1000);
console.log(3);
setTimeout(() => {
    console.log(4);
}, 0);
console.log(5);
                                                        // 1 3 5 4 2

2. Promise (1)

javascript 复制代码
Promise.resolve().then(() => {
    console.log(1);
}).catch(() => {
    console.log(2);
}).then(() => {
    console.log(3);
})
                                                        // 1 3

3. Promise (2)

javascript 复制代码
Promise.resolve().then(() => { // then正常返回resolved,里面有报错则返回rejected
    console.log(1);
    throw new Error('erro1')
}).catch(() => {  // catch 正常返回resolved,里面有报错则返回rejected
    console.log(2);
}).then(() => {
    console.log(3);
})
                                                        // 1 2 3

4. Promise (3)

javascript 复制代码
Promise.resolve().then(() => {
    console.log(1);
    throw new Error('erro1')
}).catch(() => {
    console.log(2);
}).catch(() => {
    console.log(3);
})
                                                        // 1 2 

5. 宏任务和微任务

javascript 复制代码
console.log(100)
setTimeout(() => {
    console.log(200);
});
Promise.resolve().then(() => {
    console.log(300);
})
console.log(400);
                                                            // 100 400 300 200

6. async/await (1)

javascript 复制代码
(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');
})()
                                                                        // start
                                                                        // a: 100
                                                                        // b: 200
                                                                        // 报错

7. async/await (2)

javascript 复制代码
async function async1 () {
    console.log('async1 start')
    await async2()  //undefined
    //await 的后面,都可以看做是callback 里的内容,即异步
    //类似。event loop,setTimeout(cb1)
    //setTimeout(function(){   console.log('async1 end')})
    //Promise.resolve().then(()=>{ console.log('async1 end'})  //微任务/宏任务  
    console.log('async1 end') 
}
​
async function async2 () {
    console.log('async2')
}
​
console.log('script start')
async1()
console.log('script end')
//同步代码已经执行完,开始执行event loop
                                                                    // script start
                                                                    // async1 start
                                                                    // async2
                                                                    // script end
                                                                    // async1 end

8.async/await (3)

javascript 复制代码
async function async1() {
    console.log('async1 start')  
    await async2()
​//下面三行是异步回调callback 的内容
    console.log('async1 end') 
    await async3()
    ​//下面一行是异步回调callback 的内容
    console.log('async1 end 2') 
}
​
async function async2() {
    console.log('async2')  
}
​
async function async3() {
    console.log('async3')   
}
​
​
console.log('script start')  
async1()
console.log('script end')  
//同步代码已经执行完,开始执行event loop
                                                                    // script start
                                                                    // async1 start
                                                                    // async2
                                                                    // script end
                                                                    // async1 end
                                                                    // async3
                                                                    // async1 end 2

9. 综合题

javascript 复制代码
async function async1 () {
    console.log('async1 start');
    await async2();
    //await 后面的都做完回调内容----微任务
    console.log('async1 end');
}
​
async function async2() {
    console.log('async2');
}
​
console.log('script start');
​
setTimeout(() => {//宏任务
    console.log('setTimeout');
}, 0);
​
async1();
​
//初始化Promise时,传入的函数会立刻被执行
new Promise (function (resolve) {
    console.log('promise1');
    resolve()
}).then(function() {//微任务
    console.log('promise2');
})
​
console.log('script end');
//同步代码执行完毕(相当于event loop--call stack被清空)
//执行微任务
//尝试触发DOM渲染
//触发event loop,执行宏任务
                                                                    // script start
                                                                    // async1 start
                                                                    // async2
                                                                    // promise1
                                                                    // script end
                                                                    // async1 end
                                                                    // promise2
                                                                    // setTimeout

for ... of

  • for... in(以及forEach for)是常规的同步遍历
  • for ... of 常用于异步的遍历
javascript 复制代码
function muti(num) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(num * num)
        }, 1000)
    })
}


const nums = [1, 2, 3]

// 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
// nums.forEach(async (i) => {
//     const res = await muti(i)
//     console.log(res);
// })

// 使用 for...of ,可以让计算挨个串行执行
!(async function () {
    for (let i of nums) {
        const res = await muti(i)
        console.log(res);
    }
})()

5. 什么是宏任务,什么是微任务?

异步任务分为两种,一种宏任务,一种微任务,分别位于两个任务队列,微任务的执行实际比宏任务要早。至于为什么,先了解以下event loop 和 DOM 渲染的关系。

宏任务有哪些?微任务有哪些?

微任务的执行实际比宏任务要早。

宏任务:

  • setTimeout、
  • setInterval、
  • Ajax、
  • DOM事件、
  • setImmediate(Node.js 环境)、
  • I/O 操作、
  • UI 渲染

微任务:

  • Promise 回调函数(.then().catch().finally()),
  • async/await、
  • process.nextTick(Node.js 环境)
javascript 复制代码
console.log(100)
//宏任务
setTimeout(() => {
    console.log(200)
})
//微任务
Promise.resolve().then(() => {
    console.log(300)
})
console.log(400)
// 100 400 300 200

event loop 和 DOM 渲染 的关系

  • 每次 Call Stack 清空(即每次轮询结束),即同步任务执行完成
  • 都是 DOM 重新渲染的机会,都会先尝试DOM渲染,如果DOM结构有改变则重新渲染
  • 然后再去触发下一次 Event Loop

宏任务和微任务的区别,执行时机与DOM渲染的关系

  • 宏任务:DOM 渲染后触发,如 setTimeout
  • 微任务:DOM 渲染前触发,如 Promise
  • 根本区别:为任务是 ES6 语法规定的,宏任务是由浏览器规定的

可以通过alert阻断代码执行,来验证微任务、宏任务与DOM渲染的关系:

xml 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DOM渲染示例</title>
  </head>
  <body>
    <div id="container"></div>

    <script>
      // 创建三个<p>元素并将它们添加到#container中
      const p1 = document.createElement("p");
      p1.textContent = "一段文字";

      const p2 = document.createElement("p");
      p2.textContent = "一段文字";

      const p3 = document.createElement("p");
      p3.textContent = "一段文字";

      const container = document.getElementById("container");
      container.appendChild(p1);
      container.appendChild(p2);
      container.appendChild(p3);

      console.log("length", container.children.length);

      alert("本次 call stack 结束,DOM 结构已更新,但尚未触发渲染");

      // 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预

      // 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
      setTimeout(function () {
        alert(
          "setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了"
        );
      });
    </script>
  </body>
</html>
xml 复制代码
<div id="container"></div>
<script>
    const div = document.getElementById('container');
    div.innerHTML = `<p>一段文字</p>
<p>一段文字</p>
<p>一段文字</p>`
​
    Promise.resolve().then(() => {
        console.log(111);
        alert('执行微任务,此时可以看到页面没有发生渲染');
    })   
​
    setTimeout(() => {
        console.log(222);
        alert('执行宏任务,此时可以看到页面DOM结构已经重新渲染');
    }, 0);
</script>

从event loop解释,为何微任务执行更早

执行Promise时,会等待时机进入微任务队列,但不会经过Web APIs,因为Promise时ES6规范,不是W3C规范

  • 微任务是ES66语法规定的
  • 宏任务是浏览器规定的

综上

执行顺序:

  • 1、Call Stack清空
  • 执行当前的微任务
  • 尝试DOM渲染
  • 触发Event Loop
相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax