前端常见面试题之异步(event loop, promise, async/await, 宏任务/微任务)

文章目录

  • 一、js如何执行
  • 二、event loop过程
  • 三、Promise
    • 1. 三种状态
    • 2. then和catch函数返回的状态
    • 3. 看几道题
  • 四、async/await
    • 1. 基本用法
    • 2. async/await和Promise有什么关系
    • 3. 看个题
  • 五、宏任务macroTask和微任务microTask

一、js如何执行

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

示例:

console.log('开始');
setTimeout(function() {
  console.log('异步操作');
}, 2000);
console.log('结束');

输出结果为:

开始
结束
异步操作

可以看到,在执行异步操作的过程中,主线程不会等待异步操作结束,而是继续往下执行后续的代码,当满足条件时触发异步操作的回调函数。

异步代码不会阻塞后续代码的执行,它会在后台运行,并在特定条件满足时触发回调函数。常见的异步操作包括定时器事件监听AJAX 请求等

需要注意的是,JavaScript 采用事件循环(event loop)机制来处理异步代码的执行顺序。当主线程执行完同步代码后,会检查任务队列中是否还有异步任务,如果有,则按照优先级依次执行。当所有异步任务执行完毕后,事件循环会等待,直到有新的异步任务被添加到队列中才会继续执行。这就是 JavaScript 中异步执行的原理。

二、event loop过程

前端常见面试题之异步(event loop, promise, async/await, 宏任务/微任务)_第1张图片

  • 同步代码,一行一行放到Call Stack执行
  • 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
  • 时机到了,就移动到Callback Queue
  • 如果Call Stack 为空(即同步代码执行完)Event Loop 开始工作
  • 轮询查找Callback Queue,如有则移动到Call Stack 执行
  • 然后继续轮询查找(永动机)

三、Promise

在JavaScript中,Promise是一种用于处理异步操作的对象。它可以表示一个尚未完成但最终会完成的操作,并提供了一种处理该操作结果的方式。

Promise有三种状态:pending(进行中)、resolved(已成功)和rejected(已失败)。最初处于pending状态,可以转换为resolved或rejected状态。一旦状态转变,就不会再改变。

Promise的主要用途是解决回调地狱(callback hell)问题,即多个异步操作依赖于前一个异步操作的结果,导致代码嵌套层级非常深。

以下是一个使用Promise的简单示例:

function getData() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const data = 'Hello, World!';
      if (data) {
        resolve(data); // 将操作状态改为resolved,并将数据传递给下一个then方法
      } else {
        reject('Data not found'); // 将操作状态改为rejected,并传递错误信息给下一个catch方法
      }
    }, 2000);
  });
}

getData()
  .then(function(result) {
    console.log(result); // 打印数据:Hello, World!
    return result.length;
  })
  .then(function(length) {
    console.log(length); // 打印长度:13
  })
  .catch(function(error) {
    console.log(error); // 如果出错,则打印错误信息
  });

在上面的代码中,通过getData函数返回一个Promise对象。在Promise的构造函数中,通过setTimeout模拟一个异步操作,并在一段时间后模拟完成操作。在操作完成时,使用resolve方法将状态改为resolved,并将数据传递给第一个then方法。如果出现错误,则使用reject方法将状态改为rejected,并将错误信息传递给catch方法。

通过链式调用then方法,可以在每个then方法中依次处理操作结果。如果其中一个then方法返回了新的Promise对象,它将作为下一个then方法的输入。

最后,通过catch方法捕获任何错误,并在控制台中打印错误信息。

使用Promise可以更清晰地表达异步操作的逻辑,避免了回调地狱的问题,从而提高代码的可读性和可维护性。

1. 三种状态

  1. Pending:初始状态,表示promise的操作还未完成。不会触发then和catch
  2. Resolved:表示promise的操作已经成功完成。会触发后续的then函数
  3. Rejected:表示promise的操作失败或发生错误。会触发后续的catch函数

举例:

pending状态:创建一个新的Promise实例时,初始状态是pending。例如:

const promise = new Promise((resolve, reject) => {
    // do something
});

Resolved状态:当Promise实例成功地执行了resolve函数时,状态变为Resolved。例如:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Success');
    }, 2000);
});

promise.then((result) => {
    console.log(result);  // 输出:Success
});

rejected状态:当Promise实例执行了reject函数或者发生了错误时,状态变为rejected。例如:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(new Error('Something went wrong'));
    }, 2000);
});

promise.catch((error) => {
    console.error(error);  // 输出:Error: Something went wrong
});

2. then和catch函数返回的状态

promise的then函数在正常情况下会返回一个新的promise对象,它的状态由then函数中的回调函数的返回值决定。如果回调函数返回一个普通的值,那么新的promise对象会变为已解决状态,并且其值就是回调函数的返回值。如果回调函数返回的是一个promise对象,那么新的promise对象的状态将跟随该promise对象的状态。catch函数也一样。

需要注意的是,如果then函数或catch函数中的回调函数本身抛出了异常,则新的promise对象的状态会变为已拒绝,并且该异常会被传递给下一个catch函数

const promise = new Promise((resolve, reject) => {
  resolve('Hello');
});

promise.then((result) => {
  throw new Error('Something went wrong');
}).catch((error) => {
  console.error(error.message); // 输出:Something went wrong
});

上面这个示例会输出‘Something went wrong’,那么问题来了,在catch后面再加上一个catch回调,加上的这个catch会执行吗?

const promise = new Promise((resolve, reject) => {
  resolve('Hello');
});

promise.then((result) => {
  throw new Error('Something went wrong');
}).catch((error) => {
  console.error(error.message); // 输出:Something went wrong
}).catch(() => {
  console.error('会不会执行呢?');
})

答案是第二个catch不会执行,控制台依然只打印
Something went wrong
前端常见面试题之异步(event loop, promise, async/await, 宏任务/微任务)_第2张图片
这是因为第一个catch函数返回的状态是resolved,不会触发后续的catch函数。

3. 看几道题

示例1

 Promise.reject().then(()=>{
          console.log('1')
      }).catch(()=>{
          console.log('2')
      }).then(()=>{
          console.log('3')
      })

执行这段代码会打印出以下内容:

2
3

这是因为Promise.reject()返回一个立即被拒绝的Promise对象。在第一个.then()中,由于Promise对象被拒绝,所以不会执行回调函数,然后会进入.catch(),打印出’2’。接着,第一个.catch()返回一个Promise对象,然后进入下一个.then(),打印出’3’。
注意:由于Promise.reject()直接触发了catch()方法,因此then()方法中的回调函数是不会执行的。

示例2

        Promise.resolve().then(() => {
          console.log("1");
          throw new Error("error1");
        }).catch(() => {
          console.log("2");
        }).then(() => {
          console.log("3");
        });

这段代码的执行完打印的顺序是:

1
2
3

首先,使用Promise.resolve()返回一个resolved状态的promise,然后执行.then()方法中的回调函数。在该回调函数中,首先打印"1",然后抛出一个错误"error1"。

接着,使用.catch()方法捕捉上一步中的错误。由于之前抛出了错误,所以会跳转到.catch()方法中的回调函数。在这个回调函数中,打印"2"。

最后,使用.then()方法执行回调函数,并打印"3"。

总结起来,即先执行.then()中的回调函数,再执行.catch()中的回调函数,最后执行下一个.then()中的回调函数。在使用Promise链式调用时,错误会被捕获并传递到接下来的.catch()中,不会中断链式调用。

示例3


      Promise.resolve().then(()=>{
          console.log('1')
          throw new Error('error1')
      }).catch(()=>{
          console.log('2')
      }).catch(()=>{
          console.log('3')
      })

执行完打印的顺序是:

1
2

原因是:首先,Promise.resolve()是一个已经完成的promise,所以then()方法会立即执行,并打印出1。然后,在then()方法的回调函数中,因为有一个throw语句,所以会抛出一个错误。接下来,catch()方法捕获到错误并执行相应的回调函数,打印出2。由于没有再出现错误,所以第二个catch()方法不会被执行。

四、async/await

1. 基本用法

async/await是JavaScript中处理异步操作的一种方式。它使得编写异步代码更加简洁和易于理解,同时解决了一些常见的异步编程问题。

使用async/await可以将异步操作的代码写成同步的风格,避免了回调地狱和链式调用的复杂性。它利用ES7(ECMAScript 2017)中引入的async函数和await表达式,使得异步代码的执行流程更加清晰。

举个例子,假设有一个需要异步获取数据的函数fetchData:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Data fetched!');
    }, 2000);
  });
}

在使用传统的Promise方法时,我们需要通过.then()来处理异步操作的结果:

fetchData()
  .then(data => {
    console.log(data);
    // 执行其他操作...
  })
  .catch(error => {
    console.error(error);
  });

使用async/await,可以将上述代码改成如下形式:

async function fetchData() {
  try {
    const data = await fetchData();
    console.log(data);
    // 执行其他操作...
  } catch (error) {
    console.error(error);
  }
}

在async函数内部,可以使用await关键字暂停函数的执行,等待Promise对象的状态变为resolved后再继续执行。通过使用try-catch语句,也能够轻松地捕获和处理异步操作可能抛出的错误。

总结一下,async/await主要作用有:

  1. 简化异步操作的代码结构,使其更易读和维护。
  2. 解决了回调地狱(callback hell)和链式调用的问题。
  3. 更容易捕获和处理异步操作的错误。

需要注意的是,使用async/await的代码需要在支持ES7的运行时环境中运行,或者通过Babel等工具进行转译。

2. async/await和Promise有什么关系

async/await 是 Promise 的一种语法糖,它建立在 Promise 的基础上,使异步的操作更加直观和易读。

通过使用 async 关键字来修饰一个函数,这个函数会自动返回一个 Promise 对象。在这个函数内部,可以使用 await 关键字来等待一个 Promise 对象的完成,并返回 Promise 的结果。

下面是一个使用 async/await 和 Promise 的示例:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 2000);
  });
}

async function displayData() {
  try {
    console.log("Fetching data...");
    const result = await fetchData();
    console.log(result);
    console.log("Data displayed");
  } catch (error) {
    console.log(error);
  }
}

displayData();

在上面的例子中,fetchData 函数返回一个 Promise 对象,并在 2 秒后通过 resolve 方法将结果设为 “Data fetched”。
displayData 函数使用了 async 关键字来标记为异步函数,并通过 await 等待 fetchData 函数的完成。
当 fetchData 函数完成后,返回的结果会赋值给 result 变量,并输出结果到控制台。
在 try-catch 块中,如果 Promise 被 reject,则会捕获到错误,并输出到控制台。

async/await 消除了 Promise 的嵌套回调,使异步代码的写法更加简洁和易读。同时,它可以结合其他 Promise 方法,如 Promise.all、Promise.race 等,来更好地管理和处理异步操作。

总结

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

3. 看个题

        async function async1() {
            console.log('1')
            await async2()
            console.log('2')
        }
        async function async2() {
            console.log('3')
        }
        console.log('4')
        async1()
        console.log('5')

执行的顺序是:

4
1
3
5
2

这其实主要是await async2() 这里,只要是await后面的就相当于是异步的回调,只不过是用同步的写法来表示异步而已,如下:
前端常见面试题之异步(event loop, promise, async/await, 宏任务/微任务)_第3张图片

五、宏任务macroTask和微任务microTask

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

微任务是dom渲染前触发,宏任务是dom渲染后触发

看个例子

        // 事件循环:每次循环称为tick,每次tick的任务队列称为task queue
        // 1. 执行全局script
        // 2. 执行微任务
        // 3. 执行宏任务
        // 4. 渲染UI
        // 5. 执行下一个tick
        // 微任务是dom渲染前触发,宏任务是dom渲染后触发
        // 事件循环的顺序:先执行同步代码,再执行微任务,再执行宏任务
        console.log('1')
        setTimeout(() => {
            console.log('2')
            Promise.resolve().then(() => {
                console.log('3')
            })
        }, 0)

        Promise.resolve().then(() => {
            console.log('4')
            setTimeout(() => {
                console.log('5')
            }, 0)
        })
        console.log('6') // 1 6 4 2 3 5

这段代码的执行顺序如下:

  1. 执行 console.log('1'),打印出 1
  2. 执行 setTimeout(),将回调函数放入宏任务队列,并设置延迟为0秒
  3. 执行第一个 Promise.resolve().then(),将回调函数放入微任务队列
  4. 执行 console.log('6'),打印出 6
  5. 执行第一个微任务队列中的回调函数,打印出 4
  6. 执行第二个 setTimeout(),将回调函数放入宏任务队列,并设置延迟为0秒
  7. 执行宏任务队列中的回调函数,打印出 2
  8. 执行第二个微任务队列中的回调函数,打印出 3
  9. 执行第二个宏任务 setTimeout(),打印出 5

你可能感兴趣的:(面试题,前端,promise,event,loop,async/await,宏任务/微任务)