JS是单线程的,确切的说JS只有一个主线程,但是浏览器的一个tab页是有多线程的,当然所有任务只在主线程执行,其中js的主线程和渲染线程是互斥的。
JS为什么是单线程:(多线程会造成dom操作的冲突)
- js是一门单线程的语言,最初是为了在浏览器中运行而设计的。
- js主要用途是与用户互动以及操作DOM。如果js同时有两个线程,同时对同一个dom进行操作,一个添加,一个删除,这时就有可能造成dom渲染的冲突。为了避免这种问题,js被设计成一门单线程语言。
为什么要采用事件循环机制?
为了解决单线程运行效率和阻塞问题,协调(事件)用户交互(user interaction),脚本(script),渲染(rendering),网络请求(networking)等,提高程序的并发性。JavaScript用到了计算机系统的一种运行机制,这种机制就叫做事件循环(Event Loop)
事件循环机制:
含义: Event Loop事件循环,本质上是做调度的,其实就是处理JS代码的执行顺序,负责协调(事件),用户交互(user interaction),脚本(script)执行,渲染(rendering),网络请求(networking)、收集和处理事件以及执行队列中的子任务等。目前JS主要有两个运行环境,浏览器和Node环境。浏览器的事件循环又分为同步任务和异步任务。
同步任务
含义:是在主线程调用后会立即得到结果的任务。需要在主线程上排队执行,只有一个任务执行完,才能执行下一个任务。同步任务的执行会阻塞后续任务的执行,因此如果某个同步任务执行时间过长,会导致后续任务等待时间过长,影响用户体验。
异步任务
含义:指不会进入主线程、而是会进入“任务队列”的任务。这些任务不会立即执行,而是会被放置在任务队列中等待。只有当主线程中的所有同步任务执行完毕后,系统才会读取任务队列中的任务,并将异步任务放入主线程执行。异步任务的执行不会阻塞后续任务的执行,因此可以提高程序的并发性和响应性。
宏任务与微任务:
异步任务又被分为 宏任务(macrotask)与 微任务 (microtask),不同任务源的任务会依次进入自身对应的队列中,然后等待 事件循环(Event Loop) 将它们依次压入执行栈中执行。每轮事件循环会执行一个宏任务,清空所有的微任务。
宏任务(macrotask): 宏任务是事件循环执行JS的基本单位。一个JS脚本(script)就是一个宏任务。除此之外,I/O操作、定时器、事件点击等都会创建一个宏任务。运行完宏任务后,浏览器可以继续其他调度,如重新渲染页面的UI或执行垃圾回收。
script(整体代码)、setTimeout、setInterval、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)
微任务(microtask): 是更小的任务,微任务更新应用程序的状态,但必须是在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的UI重绘,UI重绘会使应用程序的状态不连续。
Promise、 MutaionObserver、process.nextTick(Node.js环境)
注:在最新的W3C标准中,事件循环不单单只划分为两种队列了,但是浏览器中一定是有一个微任务队列的,而且微任务在所有异步任务中的优先级是最高的。
主线程执行遇到网络请求的例子:
- 主线程执行JavaScript代码,遇到网络请求(如fetch())。
- 浏览器将网络请求交给网络线程处理。
- 网络线程独立于主线程执行网络请求。
- 网络请求完成后,网络线程将结果放到异步队列里等待主线程空闲再处理。
- 主线程在执行完当前的同步任务后,才会读取任务队列中的任务。
- 事件循环会监听主线程是否空闲,然后从异步队列取出任务执行。
- 首先检查并执行微任务队列中的所有任务。
- 微任务队列清空后,事件循环继续检查并执行宏任务队列中的任务。
- 如果在执行宏任务的过程中又有新的微任务被添加到微任务队列中,那么这些微任务会在当前宏任务执行完毕后、下一个宏任务执行之前被处理。
事件循环流程(Event Loop):
1、主线程开始执行一段JS脚本时,会先执行代码中的同步任务,执行过程中,当遇到异步任务时,会根据异步任务的类型将其放置到对应的异步任务队列中;
2、 如果是宏任务,加入到宏任务队列中,如果是微任务,加入到微任务队列中;
3、 事件循环会监听主线程是否空闲,然后从异步队列取出任务执行。同步代码执行完,执行栈空闲,检查微任务队列中是否有可执行任务,如果有,依次执行所有微任务队列中的任务,将依次执行完微任务队列中的所有微任务。
4、 在微任务队列清空后,浏览器会检查是否需要渲染UI。如果需要,GUI线程会接管渲染任务,进行渲染;
5、 UI渲染完后,JS线程接着接管,事件循环会再次查看宏任务队列是否有可执行的任务,如果有,取出队列中最前面的那个宏任务,加入到执行栈中开始执行。然后重复 以上 步骤(宏任务 > 所有微任务 > 宏任务)。直到所有异步任务执行完毕。
事件循环流程.png
推荐文章:https://segmentfault.com/a/1190000021295911
Promise
Promise 是异步编程的一种解决方案,采用链式调用,可以有效的解决回调地狱的问题。
Promise对象代表一个异步操作,有三种状态:
- Pending(进行中)
- Resolved(已完成,又称Fulfilled)
- Rejected(已失败)
Promise状态是不可逆的,一旦状态改变,就不会再变。Promise对象的状态改变,只有两种可能:- 从 Pending => Resolved
- 从 Pending => Rejected
pending 状态不会触发 then 和 catch
fulfilled 状态会触发后续的 then 函数的成功回调
rejected 状态会触发后续的 catch 回调和then函数的失败回调then 正常返回 fulfilled,里面有抛出错误时返回 rejected;
catch 正常返回 fulfilled,里面有抛出错误时返回 rejected。
async与await
什么是async和await,以及它们是如何工作的?
- async和await是JavaScript中用于处理异步操作的关键字。
- async用于声明一个异步函数,而await只能在async函数内部使用,用于暂停异步函数的执行并等待Promise的完成或拒绝,并返回Promise的结果值。这样可以让我们以同步的方式编写异步代码,使代码更加简洁和易于理解。
async函数返回的是什么?
async函数总是返回一个Promise。async函数默认返回一个 被Promise.resolve()包装的一个Promise对象。如果async函数抛出错误,那么返回的Promise将会被拒绝(reject),否则的话都是履行(fulfilled)。
await后面可以跟任何值吗?
await后面只能跟返回Promise的对象。如果await后面跟的值不是一个Promise,它会被转换成用Promise.resolve()包裹的一个已解决的Promise。
async/await优缺点
优点:
- 代码可读性更高:async/await 使异步代码可以进行同步编程,减少了回调地狱(Callback Hell),使得代码更加清晰和易于理解。
- 易于调试和测试:由于 async/await 使得异步代码看起来像同步代码,因此更容易进行调试和测试。
缺点:
- 滥用 await 可能会导致性能问题,因为 await会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。
async/await 如何捕获异常?
async function fn(){
try{
let a = await Promise.reject('error')
}catch(err){
console.log(err)
}
}
async/await执行顺序
async隐式返回 Promise 作为结果的函数。await后面等待的函数执行完毕时,await后面剩下的代码会产生一个微任务(Promise.then是微任务)放到微任务队列。然后直接跳出await所在的async函数(此时将会保留async函数的上下文),执行async函数后面的代码。等到所有的同步任务执行完后,才会去执行异步队列里的任务。
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// 答案: script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
从事件循环的角度来看60Hz 刷新率
现代大多数显示器的刷新率为 60Hz,即每秒刷新 60 次。这意味着浏览器在理想状态下会每隔 16.67ms(1000ms ÷ 60)尝试渲染一次内容。
避免掉帧的核心是减少
主线程
的阻塞时间。以下是主要策略:
- 避免长时间的同步操作,将任务拆分为多个小任务,通过异步机制(如 setTimeout、Promise、requestIdleCallback)分散执行。
- 将耗时任务移到后台线程,如使用 Web Worker。
- 动画逻辑与刷新频率同步,使用 requestAnimationFrame。
- 合理调度任务优先级,避免重要任务被低优先级任务阻塞。