【前端面试基础】(三)异步(含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
相关推荐
m0_748247552 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255022 小时前
前端常用算法集合
前端·算法
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203983 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2343 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1234 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~4 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语5 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport5 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg5 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全