目录
1. 从输入url到渲染页面,中间经历了什么?
2. vue中的v-if和v-show有什么区别
3. 什么是Css中的回流(重排)与重绘
4. 介绍一下let、const、var的区别
5. 箭头函数和普通函数有什么区别
6. Css中常用的水平垂直居中解决方案有哪些
7. 什么是BFC
怎样触发BFC
BFC的特点和用途
8. localStorage,sessionStorage,cookies的区别
9. 常用的跨域解决方案有哪些
10. Js中的单线程和事件循环
11. Vue2与Vue3中的双向数据绑定
12. React Hook为什么不能放到条件语句中?
13. React有哪些常用的hooks?
14. Promise 是什么?有什么用途?
14.1. 为什么要使用 Promise?
14.2. 什么是 Promise?
14.3. 如何使用 Promise?
14.4. Promise 的三种状态
14.5. 使用 .then() 处理异步结果
14.6. 链式调用
14.7. 错误处理
14.8. finally() 方法
14.9. 什么是 async/await?
14.10. 总结
15. 浏览器中的事件循环 (Event Loop)
15.1. 同步 (synchronous) 与异步 (asynchronous)
15.2. 事件循环 (Event loop) 的组成 - 执行栈和任务队列
15.3. 事件循环 (Event loop) 的步骤
15.4. 宏任务 (Macro Task) 与微任务 (Micro Task)
1. 从输入url到渲染页面,中间经历了什么?
- 浏览器的地址栏输入URL并按下回车。
- 浏览器查找当前URL是否存在缓存,并比较缓存是否过期。
- DNS解析URL对应的IP。
- 根据IP建立TCP连接(三次握手)。
- HTTP发起请求。
- 服务器处理请求,浏览器接收HTTP响应。
- 渲染页面,构建DOM树。
- 关闭TCP连接(四次挥手)
从输入URL到最终页面渲染的过程涉及多个步骤,包括域名解析、建立连接、服务器响应、页面下载和页面渲染等。
1.域名解析:当用户在浏览器中输入URL时,浏览器会首先解析该URL,获取其中的域名,并向DNS服务器发送解析请求,获取该域名对应的服务器IP地址。
2.建立连接:浏览器根据获取的IP地址,向服务器发起连接请求。如果连接成功,浏览器将与服务器建立TCP连接。
3.服务器响应:一旦建立了连接,浏览器会向服务器发送HTTP请求,请求特定页面的数据。服务器接收到请求后,会根据请求的内容和服务器上的资源,生成相应的HTTP
响应,并将其发送回给浏览器。
4.页面下载:浏览器接收到来自服务器的HTTP响应后,会解析响应头和响应体,从中提取页面所需的资源(如HTML、CSS、JavaScript、图片等),并将这些资源下载到本地的缓存中。
5.页面渲染:页面所需的资源下载完毕后,浏览器会根据HTML结构和CSS样式对页面进行解析和渲染,JavaScript代码将被执行以处理页面交互和动态效果。构建DOM树,最终
,页面被渲染出来,并展示给用户。
总的来说,从输入URL到最终页面渲染,涉及了域名解析、建立连接、服务器响应、页面下载和页面渲染等多个步骤,这些步骤共同构成了用户访问网页的完整过程。
三次握手:
客户端向服务器发送一个SYN包(同步请求)以建立连接。
服务器接收到SYN包后,返回一个ACK包(确认)以表示接收到了客户端的连接请求,并发送自己的SYN包以建立连接。
客户端接收到服务器的ACK包和SYN包后,返回一个ACK包以确认建立连接。
四次挥手:
客户端向服务器发送一个FIN包(关闭连接请求)以表示客户端不再发送数据。
服务器接收到客户端的FIN包后,返回一个ACK包以确认收到了关闭连接请求。
服务器完成在发送数据后向客户端发送一个FIN包以关闭连接。
客户端接收到服务器的FIN包后,返回一个ACK包以确认关闭连接。此时连接关闭。
2. vue中的v-if和v-show有什么区别
v-if和v-show都是在Vue中用来控制元素显示与隐藏的指令,但是它们之间有一些区别:
v-if是真正的条件渲染,即如果条件为false,则该元素不会被渲染到DOM中;而v-show仅仅是通过CSS的display属性来控制元素的显示和隐藏,元素始终会被渲染到DOM中。
当页面中的元素频繁切换显示与隐藏时,使用v-show会比v-if性能更好,因为v-show只是切换CSS的display属性,而v-if需要频繁地添加和移除DOM元素,会引起重排。
因此,如果元素的显示与隐藏频繁变化,可以使用v-show来提升性能,如果元素的显示与隐藏是由条件决定的,并且不频繁变化,可以使用v-if来实现条件渲染。
3. 什么是Css中的回流(重排)与重绘
- 回流(重排)指的是当页面中的元素发生布局或几何属性发生变化时,浏览器需要重新计算这些元素的位置和大小,然后重新构建页面的渲染树,这个过程称为回流。由于需要重新计算布局,回流的代价很大,会对页面的性能产生负面影响。
- 重绘指的是当页面中的元素样式发生改变时,浏览器会重新绘制这些元素的外观,但不会改变它们在页面中的位置和大小。重绘的代价相对较小,但仍然会对页面性能产生一定的影响.
在Vue中,通过条件渲染指令v-if和v-else-if来动态控制元素的显示和隐藏。可以影响页面的重排。当 v-if 的条件从 false 变为 true 时,被包裹的元素会从 DOM 中移除,而当条件从 true 变为 false 时,被包裹的元素会重新插入到 DOM 中去。这种操作会导致页面发生重排。因此,如果需要避免页面重排,可以考虑使用 v-show 指令,它在条件变化时只是控制元素的 display 样式,不会影响到 DOM 结构。
4. 介绍一下let、const、var的区别
在JavaScript中,let、const和var都是用来声明变量的关键字,它们之间有一些重要的区别。
- var:
使用var声明的变量属于函数作用域或全局作用域,而不是块级作用域。这意味着在if语句、循环或函数内部声明的var变量可以在外部访问。
var声明的变量可以被重复声明,而不会产生错误。
var变量可以被提升,即在代码执行过程中变量声明会被提升到函数或全局作用域的顶部。
- let:
使用let声明的变量属于块级作用域,只在声明的块内部有效。这意味着if语句、循环或函数内部声明的let变量在外部是不可访问的。
let声明的变量不可以被重复声明,如果重复声明同一个变量会产生错误。
- const:
使用const声明的变量也属于块级作用域,同样只在声明的块内部有效。
const声明的变量必须被初始化,并且不能被重新赋值。这意味着一旦const变量被赋值,就不能再被修改。
const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了
总的来说,let和const相对于var是更加安全和可控的变量声明方式,能够避免一些常见的问题和错误。而const则更为严格,要求变量在声明时必须被初始化,并且不能被重新赋值。因此在开发中,推荐优先使用let和const来声明变量,避免使用var。
5. 箭头函数和普通函数有什么区别
- 写法不同:箭头函数使用箭头(=>)来定义,适用于匿名函数场景,而普通函数使用 function 关键字定义。
- this 的处理方式不同:在箭头函数中,this 的值与外层作用域的 this 绑定。而在普通函数中,this 的值由调用该函数的方式决定。
- 箭头函数没有 arguments 对象:箭头函数中没有自己的 arguments 对象,它的参数只能通过参数列表来传递。
- 箭头函数不能用作构造函数:由于箭头函数中没有自己的 this 值,因此不能用作构造函数来创建对象实例。
- 箭头函数没有prototype
箭头函数和普通函数的主要区别在于它们的语法和作用域。
语法:箭头函数使用箭头符号(=>)来定义函数,而普通函数使用关键字function来定义函数。
作用域:箭头函数没有自己的this,它会捕获其所在上下文的this值。而普通函数有自己的this,它的值在函数被调用时由调用方式决定。
参数:箭头函数只能包含一个表达式,如果需要多行语句或者语句块,需要使用大括号来定义函数体。而普通函数可以包含任意数量的语句和语句块。
总的来说,箭头函数更简洁,适合于单一的表达式或者语句,而普通函数更灵活,可以包含更多的逻辑和控制结构。选择使用哪种函数取决于具体的需求和使用场景。
6. Css中常用的水平垂直居中解决方案有哪些
- 使用Flexbox布局
.container {
display: flex;
align-items: center;
justify-content: center;
}
- 使用Grid布局
.container {
display: grid;
place-items: center;
}
- 使用绝对定位和transform属性
.container {
position: relative;
}
.item {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
- 使用表格布局
.container {
display: table;
}
.item {
display: table-cell;
text-align: center;
vertical-align: middle;
}
- 使用CSS网格布局
.container {
display: grid;
justify-items: center;
align-items: center;
}
这些方法都可以实现水平和垂直居中,选择适合自己项目的解决方案即可。
7. 什么是BFC
BFC(Block Formatting Context)是 CSS 中一个很重要的概念。它是指一个块级容器,其中的元素按照特定规则布局和渲染,同时也影响着其内部和外部元素的布局。
BFC 全称:Block Formatting Context, 名为 “块级格式化上下文”。
W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。
简单来说就是,BFC是一个完全独立的空间(布局环境),让空间里的子元素不会影响到外面的布局。那么怎么使用BFC呢,BFC可以看做是一个CSS元素属性
怎样触发BFC
这里简单列举几个触发BFC使用的CSS属性
- 根元素(HTML) :浏览器的元素所产生的盒子本身就是BFC区域 (了解即可,用不上)
- 设置float属性
- 设置overflow属性(属性值不为visible即可)
- 设置position属性(属性值需要是absolute或fixed)
- 设置display属性值为inline-block、flow-root、flex
- 多列容器(column-count)
- 表格元素(table thead tbody tfoot tr th td caption)
- column-span为all的元素(表格第一行横跨所有列)
BFC的特点和用途
用途:
- 解决浮动和定位元素引起的布局问题。
- 实现自适应布局和响应式设计。
- 改善文本排版和可读性。
特点:
- BFC就是一个块级元素,块级元素会在垂直方向一个接一个的排列
- BFC就是页面中的一个隔离的独立容器,容器里的标签不会影响到外部标签
- 垂直方向的距离由margin决定, 属于同一个BFC的两个相邻的标签外边距会发生重叠
- BFC 可以包含浮动元素,并确保它们不影响其他元素的布局。
8. localStorage,sessionStorage,cookies的区别
localStorage,sessionStorage和cookies都是在客户端存储数据的方式。
- localStorage:
存储容量:一般来说,localStorage的存储容量是比较大的,可以存储几MB的数据。
持久性:localStorage中的数据是持久性的,即使用户关闭浏览器或者重启计算机,数据也会保留。
作用域:localStorage中的数据是在同一个域名下共享的。
- sessionStorage:
存储容量:sessionStorage的存储容量一般比较小,通常是5MB左右。
持久性:sessionStorage中的数据是会话性的,即当用户关闭标签页或者浏览器时,数据会被清除。
作用域:sessionStorage中的数据也是在同一个域名下共享的。
- Cookies:
存储容量:cookies的存储容量一般比较小,通常是几KB的数据。
持久性:cookies中的数据可以设置过期时间,可以是会话性的,也可以是持久性的。
作用域:cookies中的数据可以在同一个域名下共享,同时也可以通过设置路径来限制作用域。
区别:
1、存储空间:cookie存储空间最小、只有4kb、但是http请求中可以携带cookie,loacalstorage、sessionstorage存储空间5m或更大
2、有效期:cookie不设置时间、关闭游览器销毁,sessionstorage关闭游览器(窗口或者标签页)销毁,loacalstorage不手动清除一直保留
3、作用域:sessionStorage不在不同游览器窗口(标签页)共享、即使同源,cookie、localstorage在所有同源窗口之间共享
9. 常用的跨域解决方案有哪些
- JSONP (JSON with Padding):利用script标签的src属性不受同源策略限制的特性,通过动态创建script标签,向其他域名请求数据并传递回调函数,实现跨域请求。
- CORS (跨域资源共享):在服务器端设置响应头,允许指定的源或所有源的请求访问资源,实现跨域访问。
- 代理服务器:在自己的域下设置一个代理服务器,然后在代理服务器上请求其他域的资源,再将结果返回给客户端。这样客户端就不会遇到跨域问题了。
- postMessage:通过window.postMessage方法在不同窗口间进行跨域通信,实现数据传递。
- 服务器端转发:在同源服务器上设置一个接口,将前端的跨域请求转发到其他域名的真正数据接口,再将数据返回给前端。
- WebSocket:WebSocket 是一种 HTML5 协议,它使得浏览器和服务器之间可以建立持久化的连接,可以直接使用 Socket 进行通信,避免了浏览器的跨域限制。
- Nginx反向代理:利用Nginx的反向代理功能,在同一域下转发请求到目标服务器,从而实现跨域请求。
- 使用跨域资源共享的JSON注入:在请求另一个域的资源时,在请求中加上callback参数,并且服务器返回的数据是JSON格式,并用callback参数指定的函数名包裹返回的数据。
10. Js中的单线程和事件循环
- Js是单线程,但是浏览器是多线程。
- Js中采用了事件循环(Event Loop)来执行异步任务。
- 所以,事件循环是一种异步编程模型,事件循环会不断地从任务队列(Task Queue)中取出待处理的任务并执行,直到任务队列为空为止。任务可以分为两类:宏任务(Macro Task)和微任务(Micro Task)。
- 微任务会优先于宏任务执行
JS中是单线程的,意思是说JS在同一时间只能执行一段代码,不能同时执行多段代码。
但是JS是基于事件驱动的,它包含一个事件循环,这个事件循环可以让JS在执行代码的同时处理异步操作和事件。
当JS执行一段代码时,如果遇到异步操作(比如网络请求、定时器等),它会将这个操作放入事件队列中,然后继续执行下一段代码。当当前代码执行完毕后,JS会去事件队列中查看是否有待处理的事件,如果有,则立即执行这些事件对应的回调函数。
这样,JS就能在单线程的情况下实现异步操作和事件处理。事件循环的机制保证了JS能够高效地处理异步操作和事件,同时保持单线程的特性。
11. Vue2与Vue3中的双向数据绑定
在Vue2中,双向数据绑定是通过v-model指令实现的。v-model可以在表单元素上创建双向数据绑定,使输入的值与Vue实例中的数据属性相互影响,是指当数据发生变化时,Vue会自动更新视图中依赖此数据的部分,保持视图与数据的同步。Vue通过使用Object.defineProperty或者Proxy来追踪数据的变化,当数据被修改时,Vue会自动触发相关的视图更新。
而在Vue3中,双向数据绑定的实现方式有所改变。Vue3中引入了新的API,即使用 v-model 指令和 model 选项来创建双向数据绑定。v-model 指令用于在组件中创建双向数据绑定,而 model 选项用于在自定义组件中配置双向绑定的行为。
双向数据绑定就是:数据劫持 + 发布订阅模式(观察者模式)
在Vue中,响应式数据的观察者是Watcher对象,它会观察数据的变化并更新相关的视图。当响应式数据发生变化时,Watcher会接收到通知并执行更新视图的操作。接收者指的是视图中使用响应式数据的地方,它们会被Watcher更新以反映数据的变化。
总之,Vue的响应式数据通过观察者模式来实现,观察者负责监视数据的变化,接收者则是使用响应式数据的地方,它们会被观察者更新以反映数据的变化。
12. React Hook为什么不能放到条件语句中?
React Hook不能放到条件语句中的主要原因是,React Hook必须按照相同的顺序被调用,并且必须在函数的顶层作用域中调用。如果将React Hook放到条件语句中,它可能会在组件渲染过程中被跳过或者多次调用,导致组件状态的不一致性和不可预测行为。因此,React Hook只能被放在函数组件的最顶层作用域中调用,而不能放在循环、条件语句或嵌套函数中。这样能够确保React Hook在每次渲染时以相同的顺序被调用,从而保证组件的状态一致性和可预测性。
13. React有哪些常用的hooks?
- useState:该 Hook 用于在函数组件中添加一个状态管理器。通过 useState,可以创建一个状态变量及其更新函数,并在组件内使用该变量来保存和更新组件的状态。
- useEffect:该 Hook 用于在组件渲染完成后执行一些副作用操作(例如订阅数据、更新 DOM 等)。通过 useEffect,可以在组件加载、更新和卸载时设置和清理副作用操作,并且可以在副作用操作之间共享状态。
- useContext:该 Hook 用于在组件之间共享一些全局的状态或函数,以避免通过多层嵌套的 Props 传递进行数据传输。通过 useContext,可以让组件在全局状态或函数的上下文中运行,并让它们能够方便地读取或更新全局状态或函数。
- useReducer:该 Hook 用于在组件中使用一种“状态容器”模式,以避免通过多层 Props 传递或 Context 共享进行状态管理。通过 useReducer,可以创建一个状态容器及其更新函数,并在组件内使用该容器来保存和更新组件的状态。
- useMemo:该 Hook 用于在组件渲染完成后缓存一些计算结果,以避免因为重复计算导致的性能问题。通过 useMemo,可以创建一个缓存变量,并在组件内使用该变量来保存计算结果并缓存。
- useCallback:该 Hook 用于在组件渲染完成后,将一些函数进行缓存,以避免因函数重复创建导致的性能问题。通过 useCallback,可以创建一个缓存函数,并在组件内使用该函数来代替重复创建的函数。
- useRef:该 Hook 用于在组件渲染完成后创建一个引用,以便在组件多次渲染时能够保留上一次渲染中的值。通过 useRef,可以创建一个引用变量,并在组件内使用该变量来保存一些持久化的数据。
- useImperativeHandle:该 Hook 用于在组件中实现一些自定义的 Ref 对象,并且要求将一些组件内部的方法或状态暴露给父组件使用。通过 useImperativeHandle,可以创建一个自定义的 Ref 对象,并在组件内指定一些公开的方法或属性。
- useLayoutEffect:该 Hook 与 useEffect 类似,但它会在浏览器渲染更新之前同步执行副作用操作,以确保 React 组件与浏览器同步更新。通常情况下,应该使用 useEffect,但在需要直接操作 DOM 元素或进行测量布局界面时,应当使用 useLayoutEffect。
- useDebugValue:该 Hook 可以帮助开发者在调试工具中显示额外的信息,以便更好地理解 Hook 的使用和行为。通常情况下,这个 Hook 只用于调试过程中,而不是实际的应用程序代码中。
14. Promise 是什么?有什么用途?
14.1. 为什么要使用 Promise?
在了解 Promise 之前,先来看一下为什么需要它的出现。
JavaScript 是单线程非阻塞,异步的解释性脚本语言,异步操作(如文件操作、数据库操作、AJAX 请求、定时器等)是不可避免的。异步操作允许 JavaScript 在执行耗时任务时,不必阻塞主线程,而是可以继续执行其他代码,任务完成后再通知程序。
在 ES6 之前,通常使用回调函数(callback)来处理异步操作,但这种写法在处理多重嵌套时,会导致“回调地狱”(callback hell)现象,代码难以维护。比如下面的代码:
callback(() => {
console.log("Hello!");
callback(() => {
console.log("Hello!");
callback(() => {
console.log("Hello!");
callback(() => {
console.log("Hello!");
}, 200);
}, 200);
}, 200);
}, 200);
为了避免回调地狱,Promise 被引入来改善代码的可读性和可维护性。
14.2. 什么是 Promise?
Promise 是一种用于处理异步操作的对象,它代表一个尚未完成的操作,并允许在操作完成后处理成功或失败的结果。Promise 主要解决了异步编程中代码嵌套过深和错误处理困难的问题。
14.3. 如何使用 Promise?
Promise 是一个构造函数,可以通过 new
关键字来创建。它接受一个函数作为参数,这个函数被称为执行器(executor),并会立即执行。
new Promise((resolve, reject) => {
console.log("executor 立即执行");
});
执行器函数有两个参数:resolve
和 reject
。resolve
用于表示操作成功,reject
用于表示操作失败。比如:
function requestData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url === "example.com") {
resolve("Success: Data fetched!");
} else {
reject("Error: Invalid URL");
}
}, 3000);
});
}
// 请求成功
requestData("example.com").then((res) => {
console.log(res); // Success: Data fetched!
});
// 请求失败
requestData("wrong-url.com").catch((error) => console.log(error)); // Error: Invalid URL
14.4. Promise 的三种状态
一个 Promise 对象有三种状态:
- pending:初始状态,表示操作尚未完成。
- fulfilled:操作成功,调用了
resolve
。 - rejected:操作失败,调用了
reject
。
14.5. 使用 .then()
处理异步结果
then()
方法用于注册回调函数,在 Promise 完成后执行。它接受两个参数:第一个是成功时的回调,第二个是失败时的回调。
requestData("example.com").then(
(res) => {
console.log(res);
},
(error) => {
console.log(error);
}
);
14.6. 链式调用
then()
可以链式调用,多个异步操作可以通过这种方式串联起来,避免回调地狱。例如:
requestData("example.com")
.then((res) => {
console.log(res);
return 1;
})
.then((res) => {
console.log(res); // 1
return 2;
})
.then((res) => {
console.log(res); // 2
});
14.7. 错误处理
Promise 的另一个优点是可以统一处理错误。通过 .catch()
方法,可以捕获异步操作中的错误并处理。
fetch("https://example.com/data")
.then((response) => response.json())
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error("Error:", error);
});
14.8. finally()
方法
finally()
方法用于在 Promise 的状态无论是成功还是失败时,都要执行的代码。比如关闭加载动画等。
fetch("https://example.com/data")
.then((response) => response.json())
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error("Error:", error);
})
.finally(() => {
console.log("操作结束,关闭加载动画");
});
14.9. 什么是 async/await?
async/await
是基于 Promise 的语法糖,使得异步代码看起来更像是同步代码。async
关键字标记的函数会返回一个 Promise,而 await
用于等待 Promise 的完成。
async function fetchData() {
const res = await fetch("https://example.com/data");
const data = await res.json();
console.log(data);
}
fetchData();
通过 async/await
,可以让异步代码更简洁,同时也更容易处理错误和调试。
14.10. 总结
- Promise 是用于处理异步操作的对象,解决了回调地狱问题。
- 它有三种状态:
pending
、fulfilled
和rejected
。 - Promise 支持链式调用,可以通过
.then()
、.catch()
和.finally()
来处理异步结果和错误。 - async/await 是 Promise 的语法糖,使异步代码更直观。
多个Promise并发
Promise.all
适用于同时对下文有依赖关系的多个请求。
会等待所有的 Promise 都会履行后返回结果,如果有一个 Promise 被拒绝,就立刻返回拒绝的错误。
const arr = [
Promise.resolve(1),
Promise.resolve(2),
new Promise(resolve => {
setTimeout(() => {
resolve(3);
}, 2000)
})
];
console.time('1');
Promise.all(arr).then(res => {
console.timeEnd('1'); // 2001 ms
console.log('res', res); // [1, 2, 3]
});
Promise.any
有一个 Promise 被履行就返回结果,只有所有的 Promise 都被拒绝才返回失败。
const arr = [
Promise.resolve(1),
Promise.reject(2),
Promise.reject(3),
];
Promise.any(arr).then(res => {
console.log('res', res); // 1
}).catch(err => {
console.log('err', err);
});
Promise.allSettled
返回的值都会处于履行状态,通过 status 和 value 去分别处理。
const arr = [
Promise.resolve(1),
Promise.reject(2),
Promise.reject(3),
];
Promise.allSettled(arr).then(res => {
console.log('res', res);
}).catch(err => {
console.log('err', err);
});
Promise.race
永远取首先确定状态的 Promise 的值,履行就走 then,失败就走 catch。
const arr = [
Promise.resolve(1),
Promise.reject(2),
new Promise(resolve => {
setTimeout(() => {
resolve(3);
}, 2000)
})
];
Promise.race(arr).then(res => {
console.log('res', res); // 1
}).catch(err => {
console.log('err', err);
});
一个比较有用的地方就是,race 可以用来终止耗时的操作
const promise1 = () => new Promise((resolve, reject) => {
setTimeout(() => {
reject('time out')
}, 5000)
});
const promise2 = () => new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 6000)
});
Promise.race([promise1(), promise2()]).then(res => {
console.log('res', res);
}).catch(err => {
console.log('err', err); // time out
});
15. 浏览器中的事件循环 (Event Loop)
15.1. 同步 (synchronous) 与异步 (asynchronous)
在讨论事件循环前,我们需要先了解同步与异步的概念。JavaScript 是单线程的编程语言,代码按照顺序一行一行执行,称为同步 (synchronous)。然而,这种执行方式可能带来问题。比如,如果一段代码需要等待外部数据(如从服务器获取数据),这可能会导致页面长时间无法响应,给用户带来很差的体验。因此,JavaScript 引入了异步 (asynchronous) 概念。
异步代码不会阻塞主线程,主线程可以继续执行其他操作,直到异步任务完成后再处理它。正是通过事件循环的机制,JavaScript 才能够解决单线程的局限性,使耗时操作不会阻塞主线程。
15.2. 事件循环 (Event loop) 的组成 - 执行栈和任务队列
事件循环本身并不存在于 JavaScript 内部,而是由 JavaScript 的执行环境(浏览器或 Node.js)实现的。它包括以下几个概念:
- 堆 (Heap):用于存储对象的数据结构。
- 栈 (Stack):后进先出(LIFO)数据结构。函数调用时会被推入栈顶,执行完后移出栈。
- 队列 (Queue):先进先出(FIFO)数据结构。等待执行的任务会进入队列,等到栈空时再从队列取任务执行。
- 事件循环 (Event loop):不断检查栈是否为空,若为空,则从队列取任务放入栈中执行。
事件循环的堆(Heap)、栈(Stack)和队列(Queue)
15.3. 事件循环 (Event loop) 的步骤
事件循环的执行过程可以总结为以下几步:
- 所有任务在主线程上执行,形成一个执行栈。
- 如果遇到异步任务(如
setTimeout
),执行环境会调用相关 API 处理,完成后再将任务放入任务队列中。 - 一旦执行栈中的所有同步任务完成,事件循环会从任务队列中取出第一个任务放入栈中执行。
- 事件循环会持续这个过程,直到所有任务完成。
15.4. 宏任务 (Macro Task) 与微任务 (Micro Task)
JavaScript 的异步任务可以分为宏任务 (Macro Task) 和微任务 (Micro Task),它们的执行顺序不同。如果不区分这两类任务,代码的执行顺序可能会出乎意料。
例如,以下代码的输出顺序是什么?
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
Promise.resolve()
.then(function () {
console.log(3);
})
.then(function () {
console.log(4);
});
如果仅考虑同步和异步,可能会认为输出顺序是 1234
;但实际上正确答案是 1342
。这是因为 Promise
任务属于微任务,而 setTimeout
属于宏任务。在一次事件循环中,宏任务只执行一个,之后会先执行微任务,因此 Promise
中的任务会先执行。
常见的宏任务和微任务如下:
- 宏任务:
script
(整体代码)、setTimeout
、setInterval
、I/O、事件、postMessage
、MessageChannel
、setImmediate
(Node.js)。 - 微任务:
Promise.then
、MutationObserver
、process.nextTick
(Node.js)。
执行顺序如下:
- 执行一次宏任务(最开始是整个
script
,因此先执行console.log(1)
)。 - 遇到宏任务时,放入宏任务队列。
- 遇到微任务时,放入微任务队列。
- 执行栈空时,先检查微任务队列,执行所有微任务。
- 执行浏览器渲染,接着开始下一个宏任务。