Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

面试官:JS中的异步编程与Promise #31

Open
linwu-hi opened this issue Jul 30, 2023 · 0 comments
Open

面试官:JS中的异步编程与Promise #31

linwu-hi opened this issue Jul 30, 2023 · 0 comments

Comments

@linwu-hi
Copy link
Owner

linwu-hi commented Jul 30, 2023

JS中的异步编程与Promise

一、JavaScript的异步编步机制

在了解JavaScript的异步机制之前,我们首先需要理解JavaScript是一种单线程语言。单线程就意味着所有的任务需要按照顺序一次执行,如果前一个任务没有完成,后一个任务就无法开始。这个特性在执行大量或耗时任务时可能会导致阻塞或者界面卡死,这显然是不可取的。

为了解决这个问题,JavaScript引入了异步编程的机制。简单地说,异步就是你现在发出了一个“命令”,但是并不等待这个“命令”完成,而是继续执行下一个“命令”。只有在“听到”之前的那个“命令”完成了的消息时,才会回过头来处理这个“命令”的结果。这就是所谓的异步编程。

二、事件循环(Event Loop)和任务队列(Task Queue)

这种异步的机制是如何实现的呢?关键在于事件循环(Event Loop)和任务队列(Task Queue)。

事件循环是 JavaScript 内部的一个处理过程,系统会在此处不断地循环等待,检查任务队列中是否有任务,如果有,就处理它。

而任务队列,就是一个存储待处理任务的队列,当我们使用 setTimeout、setInterval、ajax等API时,实际上是向任务队列中添加了一个任务。

当主线程空闲时(也就是同步任务都执行完毕),便会去看任务队列里有没有任务,如果有,便将其取出执行;没有的话,则继续等待。

这个模型可以简单地用下面的代码表示:

while (true) {
  let task = taskQueue.pop();
  execute(task);
}

三、宏任务和微任务

在任务队列中,任务被分为两类:宏任务(MacroTask)和微任务(MicroTask)。两者的区别在于,宏任务在下一轮事件循环开始时执行,微任务在本轮事件循环结束时执行。这意味着微任务的优先级高于宏任务。

常见的宏任务有:script全文(可以看作一种宏任务)、setTimeout、setInterval、setImmediate(Node.js 环境)、I/O、UI渲染。

常见的微任务有:Promise、process.nextTick(Node.js环境)、MutationObserver(html5新特性)。

事件循环的顺序,决定了 JavaScript 代码的执行顺序。过程如下:

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

image

代码示例如下:

console.log('script start');  // 宏任务

setTimeout(function() {
  console.log('setTimeout');  // 宏任务
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');    // 微任务
}).then(function() {
  console.log('promise2');    // 微任务
});

console.log('script end');    // 宏任务

输出顺序为:script start -> script end -> promise1 -> promise2 -> setTimeout。这是因为JavaScript执行机制决定了微任务比宏任务优先执行。

四、requestAnimationFrame

requestAnimationFrame是一个优化动画效果的函数,也有它在事件循环中的位置。

requestAnimationFrame 的调用是有频率限制的,在大多数浏览器里,这个频率是60Hz,也就是说,每一次刷新间隔为1000/60≈16.7ms。requestAnimationFrame 的执行时机是在下一次重绘之前,而不是立即执行。

requestAnimationFrame 的优点是由系统来决定回调函数的执行时机。如果系统忙到一定程度,可能会两次“刷新”之间多次执行回调函数,这时就可以省略掉一些回调函数的执行。这种机制可以有效节省 CPU 开销,提高系统的性能。

requestAnimationFrame 的位置在事件循环中的具体位置是视浏览器的实现而定,但一般来说,它在宏任务执行完,渲染之前,这使得其可以获取到最新的布局和样式信息。

五、Promise的发展

Promise 对象代表一个异步操作的最终完成(或失败)及其结果值。一个 Promise 处于以下状态之一:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

一个 promise 必须处于一种状态:fulfilled、rejected 或 pending。一个 promise 的状态在 settle 之后就不能再改变。

Promise起初是由社区提出并实现的,最早的版本是由 Kris Kowal 提出的 Q 库,后来被 ES6 正式接受,并成为了浏览器的原生对象。

Promise 主要解决了两类问题:

  • 异步操作的一致性问题:无论异步操作是同步完成还是异步完成,使用 Promise 对象的 then 方法都可以以同样的方式进行处理。
  • 回调地狱问题:回调地狱指的是多层嵌套的回调函数,导致代码难以维护和理解。Promise 可以通过链式调用的方式,解决回调地狱问题。

我们可以通过下面的代码示例来看一下 Promise 是如何工作的:

let promise = new Promise(function(resolve, reject) {
  // 异步处理
  // 处理结束后、调用resolve 或 reject
});

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

Promise 的状态一旦改变,就会一直保持那个状态,不会再次改变。这个特性可以让我们有序地处理异步操作的结果,避免出现复杂的状态判断。

参考资料

  1. MDN文档 - 使用 Promises
  2. MDN文档 - Window.requestAnimationFrame()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant