2025年01月13日字节(本地生活)前端面试
目录
- 自我介绍
- 项目介绍(拷问)
- js事件循环
- 浏览器如何解析html文件
- 输入 url 到页面呈现的过程
- http与https,https为什么安全,证书是如何获取的?
- require引入与import的引入的区别
- vite 打包工具相比 webpack 优势
- 手写 promise
- 手写发布订阅模式
3. js事件循环
以下是对 JavaScript 事件循环的更深入解释:
基本概念
- 单线程执行模型:JavaScript 是单线程的,即在同一时间内只能执行一个任务。这意味着 JavaScript 代码按顺序执行,不会出现多个任务同时执行的情况。但为了处理异步操作,JavaScript 引入了事件循环机制,使它可以在等待某些操作完成时继续执行其他代码。
核心组件
- 执行栈(Call Stack):
- 执行栈是一个后进先出(LIFO)的数据结构,用于存储当前正在执行的函数调用。
- 当一个函数被调用时,它会被压入执行栈;当函数执行完成,它会从栈中弹出。
- 例如:
function first() {
second();
}
function second() {
third();
}
function third() {
console.log('Hello, World!');
}
first();
- 调用 `first()` 时,`first` 函数会被压入执行栈;`first` 函数调用 `second()`,`second` 函数会被压入执行栈;`second` 函数调用 `third()`,`third` 函数会被压入执行栈;`third` 函数执行并打印 `Hello, World!`,然后 `third` 函数从栈中弹出,接着 `second` 函数弹出,最后 `first` 函数弹出。
- 任务队列(Task Queue):
- 任务队列存储着等待执行的任务,主要是异步操作的回调函数。
- 任务队列可以分为宏任务队列(Macrotask Queue)和微任务队列(Microtask Queue)。
宏任务与微任务
-
宏任务(Macrotasks):
- 常见的宏任务包括
setTimeout
、setInterval
、setImmediate
(Node.js)、I/O 操作、UI 渲染等。 - 宏任务的执行顺序是一个接一个的,即执行完一个宏任务后,才会开始执行下一个宏任务。
- 例如,
setTimeout
函数会将其回调函数添加到宏任务队列中,当达到设定的延迟时间后,该回调函数会等待被执行。
- 常见的宏任务包括
-
微任务(Microtasks):
- 常见的微任务包括
Promise.then()
、Promise.catch()
、process.nextTick
(Node.js)、queueMicrotask
等。 - 微任务的优先级高于宏任务。在当前宏任务执行结束后,会优先执行微任务队列中的所有微任务,直到微任务队列为空。
- 例如,
Promise.resolve().then()
会将其回调函数添加到微任务队列中,该回调函数会在当前宏任务完成后立即执行,而不是等待下一个宏任务。
- 常见的微任务包括
事件循环的执行流程
- 检查执行栈是否为空。
- 如果执行栈不为空,继续执行栈中的函数调用。
- 如果执行栈为空,进入下一步。
- 检查微任务队列是否为空。
- 如果微任务队列不为空,按顺序依次执行微任务队列中的任务,直到微任务队列为空。
- 如果微任务队列也为空,进入下一步。
- 从宏任务队列中取出一个任务,将其添加到执行栈中并执行。
- 重复上述步骤。
示例代码及详细解释
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => {
console.log('Promise inside Timeout 1');
});
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('Timeout inside Promise 1');
}, 0);
});
console.log('End');
- 代码执行顺序如下:
- 首先,
console.log('Start')
是同步代码,直接执行,输出Start
。 setTimeout(() => {...}, 0)
是宏任务,其回调函数被添加到宏任务队列中。Promise.resolve().then(() => {...})
是微任务,其回调函数被添加到微任务队列中。console.log('End')
是同步代码,直接执行,输出End
。- 此时执行栈为空,检查微任务队列,发现
Promise.resolve().then(() => {...})
的回调函数,执行该微任务,输出Promise 1
,并将另一个setTimeout
回调添加到宏任务队列。 - 微任务队列已空,从宏任务队列中取出
setTimeout(() => {...})
的回调函数,执行该宏任务,输出Timeout 1
,同时将内部的Promise.then()
微任务添加到微任务队列。 - 再次检查微任务队列,执行内部的
Promise.then()
微任务,输出Promise inside Timeout 1
。 - 最后,执行之前添加到宏任务队列的
setTimeout(() => {...})
回调函数,输出Timeout inside Promise 1
。
- 首先,
事件循环的重要性和应用场景
-
重要性:
- 事件循环使 JavaScript 能够高效处理异步操作,避免因等待某些操作(如网络请求、文件读取等)而阻塞代码执行,保证程序的流畅性。
- 理解事件循环有助于避免一些常见的异步编程错误,如竞态条件、回调地狱等。
-
应用场景:
- 网络请求:当使用
fetch
或XMLHttpRequest
进行网络请求时,请求完成后的回调函数会被添加到任务队列中,等待执行。 - 用户交互:点击事件、输入事件等用户交互的处理函数会被添加到任务队列中,在用户触发事件后等待执行。
- 定时器操作:使用
setTimeout
、setInterval
等定时器,其回调函数会在设定的时间后添加到任务队列中。
- 网络请求:当使用
在面试中,可以这样回答:“JavaScript 事件循环是一种处理异步操作的机制,它基于单线程执行模型。核心组件包括执行栈和任务队列,任务队列又分为宏任务队列和微任务队列。宏任务如 setTimeout
、setInterval
等,微任务如 Promise.then()
等。事件循环的执行流程是先检查执行栈是否为空,若为空,检查微任务队列,若微任务队列不为空,执行微任务直到为空,再从宏任务队列取一个任务执行,不断重复这个过程。这一机制使 JavaScript 可以在等待异步操作时继续执行其他代码,避免阻塞,同时保证了执行顺序。例如在处理网络请求、用户交互和定时器操作等场景中,事件循环能确保这些异步操作的回调函数在适当的时间得到执行,同时避免因等待而影响程序的流畅性。”
通过这样的详细解释和示例,可以清晰地阐述 JavaScript 事件循环的概念、流程、重要性和应用场景,让面试官了解你对该知识点的深入理解和掌握程度。
4. 浏览器如何解析html文件
以下是浏览器解析 HTML 文件的详细过程:
1. 字节流转换
- 浏览器接收到 HTML 文件时,它是以字节流的形式存在的。首先,这些字节流会被转换为字符,根据文件的编码方式(如 UTF-8、UTF-16 等)将字节解码为字符。
2. 词法分析(标记化)
- 浏览器会对字符进行词法分析,将字符流转换为一系列的标记(Tokens)。
- 例如,将
<html>
标记为StartTag:html
,将</html>
标记为EndTag:html
,将文本内容标记为CharData
等。 - 常见的标记类型包括:
- StartTag:表示 HTML 元素的开始标签,如
<div>
。 - EndTag:表示 HTML 元素的结束标签,如
</div>
。 - CharData:表示元素内的文本内容,如
This is a div
。 - Comment:表示 HTML 中的注释,如
<!-- This is a comment -->
。
- StartTag:表示 HTML 元素的开始标签,如
3. 语法分析(构建 DOM 树)
- 浏览器将生成的标记进行语法分析,构建 DOM(Document Object Model)树。
- 这个过程遵循 HTML 语法规则:
- 当遇到
StartTag
时,会创建一个新的 DOM 节点,并将其添加到 DOM 树中。 - 对于嵌套的元素,会将子元素添加到相应父元素的子节点列表中。
- 例如,对于
<html><body><h1>Hello</h1></body></html>
:- 首先遇到
<html>
,创建html
节点作为根节点。 - 接着遇到
<body>
,创建body
节点并添加到html
节点的子节点列表。 - 然后遇到
<h1>
,创建h1
节点并添加到body
节点的子节点列表。 - 对于
Hello
的CharData
,将其作为h1
节点的文本内容。 - 最后遇到
</h1>
和</body>
、</html>
,完成节点的闭合和树的构建。
- 首先遇到
- 当遇到
4. 预加载扫描
- 在构建 DOM 树的同时,浏览器会进行预加载扫描。
- 会查找一些外部资源的引用,如
<link rel="stylesheet" href="style.css">
中的style.css
和<script src="script.js"></script>
中的script.js
。 - 这些资源会被添加到预加载队列中,以便后续快速加载。
5. 处理脚本和样式
- 对于
<script>
元素:- 如果没有
async
或defer
属性,会暂停 DOM 树的构建,下载并执行脚本。 - 如果有
async
属性,脚本会异步下载,下载完成后立即执行,可能会中断 DOM 树的构建。 - 如果有
defer
属性,脚本会异步下载,在 DOM 树构建完成后,按照脚本出现的顺序执行。
- 如果没有
- 对于
<link rel="stylesheet">
元素:- 会下载 CSS 文件,开始解析 CSS,构建 CSSOM(CSS Object Model)树。
6. 构建 CSSOM 树
- 浏览器解析 CSS 文件或内联 CSS,将 CSS 规则转换为 CSSOM 树。
- CSSOM 树的结构与 DOM 树类似,但存储的是 CSS 的样式信息。
- 例如,对于
body { font-size: 16px; }
,会在 CSSOM 树的body
节点添加font-size
属性,值为16px
。
7. 构建渲染树(Render Tree)
- 结合 DOM 树和 CSSOM 树,构建渲染树。
- 渲染树只包含可见元素,对于
display: none
的元素不会包含在渲染树中。 - 对于每个可见元素,会根据 DOM 节点和 CSSOM 中的样式信息,计算出最终的样式。
8. 布局(Layout)
- 浏览器会对渲染树进行布局计算,确定每个元素在屏幕上的位置和大小。
- 这涉及计算元素的盒模型(Margin、Border、Padding、Content),根据父元素和视口的大小进行布局。
9. 绘制(Paint)
- 最后,浏览器将渲染树中的元素绘制到屏幕上。
- 绘制过程会将每个元素的样式和位置信息转换为屏幕上的像素,使用浏览器的绘制引擎将元素绘制出来。
在面试中可以这样回答:“浏览器解析 HTML 文件时,首先将字节流转换为字符,然后进行词法分析,将字符转换为标记,如 StartTag
、EndTag
等。接着进行语法分析,构建 DOM 树,根据标记创建相应的 DOM 节点并添加到树中。同时进行预加载扫描,查找外部资源引用。对于脚本和样式,会根据不同的属性进行不同的处理,暂停或异步执行脚本,解析 CSS 并构建 CSSOM 树。之后将 DOM 树和 CSSOM 树结合构建渲染树,对渲染树进行布局计算,确定元素的位置和大小,最后将渲染树绘制到屏幕上。这个过程是一个逐步构建和处理的过程,涉及多个子过程,确保页面的正确显示和用户体验。”
通过这样的回答,可以向面试官展示你对浏览器解析 HTML 文件的深入理解,以及对整个页面渲染流程的掌握。你可以根据自己的实际情况,进一步详细解释每个步骤的细节,如 DOM 树构建的算法、CSSOM 树的细节、布局的计算方法等,展示你对前端开发的深入知识。
5. 输入 url 到页面呈现的过程
以下是从输入 URL 到页面呈现的详细过程:
1. 用户输入 URL
- 用户在浏览器的地址栏中输入一个 URL(Uniform Resource Locator),例如
https://www.example.com
。
2. URL 解析
- 浏览器会对输入的 URL 进行解析,将其拆分成几个部分:
- 协议:如
https
,确定使用的通信协议。 - 域名:如
www.example.com
,用于定位服务器。 - 端口:如果没有指定,对于
https
协议默认是 443,对于http
协议默认是 80。 - 路径:如
/index.html
,表示请求的资源路径。 - 查询参数:如
?param1=value1¶m2=value2
,提供额外的信息。 - 片段标识符:如
#section1
,用于定位页面内的特定部分。
- 协议:如
3. DNS 查询
- 浏览器会首先检查本地 DNS 缓存,看是否已经解析过该域名。
- 如果没有,浏览器会向操作系统的 DNS 解析器发送请求,操作系统会先查看本地的 hosts 文件。
- 如果还没有,请求会发送到 DNS 服务器,DNS 服务器会根据域名的层次结构进行解析,从根域名服务器到顶级域名服务器,再到权威域名服务器,最终找到对应的 IP 地址。
4. 建立 TCP 连接
- 浏览器使用获取到的 IP 地址,与服务器建立 TCP 连接。
- 对于 HTTPS,还需要进行 TLS 握手,包括:
- 客户端向服务器发送
ClientHello
消息,包含支持的加密算法、随机数等。 - 服务器向客户端发送
ServerHello
消息,选择的加密算法、证书等。 - 客户端验证证书,生成一个随机的预主密钥,使用服务器的公钥加密并发送给服务器。
- 服务器使用私钥解密得到预主密钥,双方根据预主密钥和之前交换的随机数生成会话密钥。
- 客户端向服务器发送
5. 发送 HTTP 请求
- 一旦 TCP 连接建立(包括 TLS 握手完成),浏览器会向服务器发送 HTTP 请求。
- 请求包括请求行(请求方法、请求路径、HTTP 协议版本)、请求头(包含各种信息,如
User-Agent
、Accept
等)和请求体(如果有)。
6. 服务器处理请求
- 服务器收到请求后,会根据请求的信息进行处理:
- 可能会调用后端程序(如 PHP、Node.js、Java 等),处理动态内容。
- 或者直接从文件系统中读取静态文件(如 HTML、CSS、JavaScript、图片等)。
7. 服务器响应请求
- 服务器会发送 HTTP 响应,包括:
- 响应行(HTTP 协议版本、状态码、状态消息)。
- 响应头(包含内容类型、内容长度、缓存信息等)。
- 响应体(实际的数据内容)。
8. 浏览器接收和解析响应
- 浏览器收到响应后,会检查状态码:
- 对于
200 OK
等成功状态码,会继续处理响应。 - 对于
404 Not Found
等错误状态码,会显示相应的错误页面。
- 对于
- 浏览器会根据响应头中的
Content-Type
解析响应体:- 对于 HTML,会开始解析 HTML 结构,构建 DOM 树。
- 对于 CSS,会解析 CSS 规则,构建 CSSOM 树。
- 对于 JavaScript,会执行 JavaScript 代码,可能会修改 DOM 或 CSSOM。
9. 渲染页面
- 浏览器会将 DOM 树和 CSSOM 树结合起来,构建渲染树(Render Tree)。
- 计算渲染树中每个节点的布局(Layout),确定元素的位置和大小。
- 最后将渲染树绘制(Paint)到屏幕上,显示页面。
10. 后续操作
- 浏览器会继续加载其他资源,如图片、CSS 文件、JavaScript 文件等,根据需要进行缓存操作。
- 还会处理页面中的脚本,如监听用户事件、执行异步请求等。
在面试中可以这样回答:“当用户输入 URL 时,浏览器会对其进行解析,然后进行 DNS 查询找到服务器的 IP 地址。对于 HTTPS 请求,会进行 TLS 握手确保通信安全。接着建立 TCP 连接,发送 HTTP 请求。服务器收到请求后进行处理,然后发送 HTTP 响应。浏览器收到响应后,根据状态码和响应头处理响应内容,对于 HTML 会构建 DOM 树,对于 CSS 会构建 CSSOM 树,然后结合两者构建渲染树,进行布局和绘制操作,最终将页面呈现出来。后续还会继续加载其他资源并进行缓存操作,处理页面中的脚本等。这个过程涉及多个步骤,包括网络、安全、解析和渲染等多个方面,确保页面的正确呈现和用户的良好体验。”
这样的回答可以让面试官看到你对整个从输入 URL 到页面呈现的完整流程的理解,同时也可以根据需要进一步细化每个步骤的解释,比如详细说明 TCP 连接的三次握手、TLS 握手的详细过程等,展现你对网络和前端开发的深入知识。
6. http与https,https为什么安全,证书是如何获取的?
以下是关于 HTTP 与 HTTPS 的区别,以及 HTTPS 为什么安全和证书获取方式的详细解释:
HTTP 与 HTTPS 的区别
- HTTP (HyperText Transfer Protocol):
- 是一种明文传输协议,数据在网络上以明文形式传输,容易被窃取和篡改。
- 端口通常是 80。
- 适用于对安全性要求不高的场景,但对于涉及敏感信息(如用户密码、信用卡信息等)的传输不推荐使用。
- 例如,当你访问一个普通的网站时,可能使用的就是 HTTP 协议,数据在传输过程中,网络中的第三方可以轻松截获和查看传输的内容。
- HTTPS (HyperText Transfer Protocol Secure):
- 是 HTTP 的安全版本,在 HTTP 的基础上加入了 SSL/TLS 协议,对数据进行加密传输,确保数据的安全性和完整性。
- 端口通常是 443。
- 广泛应用于需要安全传输的场景,如网上银行、电子商务网站、登录页面等。
HTTPS 为什么安全
- 加密机制:
- 对称加密:在 HTTPS 中,首先使用对称加密算法对传输的数据进行加密和解密,对称加密使用相同的密钥进行加解密,速度快。但对称加密的密钥需要在客户端和服务器之间传输,如何安全传输这个密钥是一个问题。
- 非对称加密:为了解决对称加密密钥的传输问题,使用非对称加密算法。服务器有一对公钥和私钥,公钥可以公开,私钥只有服务器持有。客户端向服务器请求公钥,然后使用公钥对对称加密的密钥进行加密,服务器收到后用私钥解密得到对称加密的密钥,这样就可以安全地传输对称加密的密钥。
- 混合加密:结合对称加密和非对称加密的优点,使用非对称加密传输对称加密的密钥,使用对称加密传输实际的数据,保证了数据传输的安全性和性能。
- 身份验证:
- 服务器需要使用证书来证明自己的身份,证书是由受信任的证书颁发机构 (CA, Certificate Authority) 颁发的,包含服务器的公钥、服务器信息、证书有效期等。
- 当客户端连接服务器时,服务器会将证书发送给客户端,客户端会验证证书的有效性,包括证书是否由受信任的 CA 颁发、证书是否过期、证书的域名是否与服务器的域名匹配等。
- 这种机制防止了中间人攻击,因为只有合法的服务器才能拥有受信任的证书。
- 数据完整性:
- 使用消息认证码 (MAC) 或数字签名来确保数据在传输过程中未被篡改。
- 服务器和客户端在传输数据时会对数据进行签名或生成 MAC,接收方收到数据后会验证签名或 MAC,确保数据没有被篡改。
证书的获取方式
- 证书颁发机构 (CA) 颁发:
- 生成证书签名请求 (CSR):
- 首先,服务器管理员会在服务器上生成一对公钥和私钥,然后使用私钥生成 CSR,CSR 包含服务器的公钥、组织信息、域名等。
- 例如,在 Linux 服务器上可以使用
openssl
命令生成 CSR:
- 生成证书签名请求 (CSR):
openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr
- 这里生成了一个 2048 位的 RSA 私钥 `server.key` 和 CSR 文件 `server.csr`。
2. **向 CA 提交 CSR**:
- 将 CSR 文件提交给 CA,CA 会对服务器的身份进行验证,验证方式可能包括验证域名所有权、组织身份等。
- 不同的 CA 有不同的验证流程,对于一些高级别的证书(如 EV SSL 证书),验证过程会更严格。
3. **CA 颁发证书**:
- 如果验证通过,CA 会使用自己的私钥对 CSR 进行签名,生成证书,将证书颁发给服务器管理员。
- 证书包含服务器的公钥、服务器信息、CA 的签名等。
4. **安装证书**:
- 服务器管理员将证书和私钥安装到服务器上,通常会配置服务器软件(如 Apache 或 Nginx),使其支持 HTTPS。
- 例如,在 Nginx 中:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /path/to/server.crt;
ssl_certificate_key /path/to/server.key;
...
}
- 自签名证书:
- 对于开发和测试环境,可以使用自签名证书,但这种证书不受信任,会在浏览器中显示安全警告。
- 生成自签名证书的步骤如下:
- 使用
openssl
生成自签名证书:
- 使用
openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365
2. 将生成的 `server.crt` 和 `server.key` 安装到服务器上,配置服务器软件支持 HTTPS。
在面试中可以这样回答:“HTTPS 比 HTTP 更安全,因为它使用了 SSL/TLS 协议,结合了对称加密和非对称加密,使用非对称加密传输对称加密的密钥,使用对称加密传输数据,确保数据的安全性。同时,通过证书进行身份验证,防止中间人攻击。证书可以从受信任的 CA 机构获取,首先服务器生成 CSR 并提交给 CA,CA 验证后颁发证书;也可以使用自签名证书,但在生产环境中不推荐,因为不受信任。对于 CA 颁发的证书,服务器需要向 CA 提供 CSR,CA 会验证服务器的身份,包括域名所有权等,验证通过后颁发证书,然后服务器将证书和私钥安装到服务器上,配置服务器软件使用 HTTPS 协议。”
总之,HTTPS 通过加密、身份验证和数据完整性保证,为网络传输提供了更高的安全性,而证书的获取和正确使用是实现 HTTPS 的重要环节,需要根据不同的应用场景和需求选择合适的证书获取方式。
7. require引入与import的引入的区别
以下是 require
和 import
引入的区别:
1. 语法和规范
- require:
- 是 CommonJS 规范的一部分,主要用于 Node.js 环境。
- 语法:
const module = require('module_name');
- 它是一个函数调用,接受一个字符串参数,表示要引入的模块名称或路径,可以是内置模块、第三方模块或自定义模块。
- 例如:
const fs = require('fs');
const express = require('express');
const myModule = require('./myModule');
- import:
- 是 ES6 模块规范的一部分,主要用于现代 JavaScript 环境,包括浏览器和 Node.js(使用
.mjs
文件或在package.json
中设置"type": "module"
)。 - 语法:
- 是 ES6 模块规范的一部分,主要用于现代 JavaScript 环境,包括浏览器和 Node.js(使用
import module from 'module_name';
import { namedExport } from 'module_name';
import * as moduleName from 'module_name';
- 它是一个静态声明,不能在代码的任意位置使用,只能在文件的顶层,并且需要使用大括号 `{}` 来引入具名导出,使用 `* as` 来引入命名空间。
- 例如:
import React from 'react';
import { useState } from 'react';
import * as ReactDOM from 'react-dom';
2. 静态分析与动态加载
- require:
- 可以在代码的任何位置使用,包括条件语句和函数内部,因此支持动态加载模块。
- 例如:
if (condition) {
const module = require('module_name');
}
function loadModule() {
const module = require('module_name');
}
- 这种动态加载能力在某些场景下很有用,比如根据运行时的条件加载不同的模块。
- import:
- 是静态声明,只能在文件的顶层使用,不支持在条件语句或函数内部使用。
- 不过,ES 模块支持动态导入,可以使用
import()
函数实现动态导入:
if (condition) {
import('module_name').then(module => {
// 使用导入的模块
});
}
3. 模块导出的处理
- require:
- 在 CommonJS 中,导出模块通常使用
module.exports
或exports
:
- 在 CommonJS 中,导出模块通常使用
// myModule.js
module.exports = {
function1: () => {},
function2: () => {}
};
- 或者:
// myModule.js
exports.function1 = () => {};
exports.function2 = () => {};
- 引入时,可以直接获得导出的对象或函数:
const myModule = require('./myModule');
myModule.function1();
-
import:
- 在 ES 模块中,使用
export
关键字进行导出:
- 在 ES 模块中,使用
// myModule.js
export function function1() {}
export function function2() {}
- 或者:
// myModule.js
const function1 = () => {};
const function2 = () => {};
export { function1, function2 };
- 引入时,可以根据导出的方式进行相应的导入:
import { function1, function2 } from './myModule';
function1();
4. 加载机制
- require:
- 在 Node.js 中,
require
会同步加载模块,这意味着当执行到require
语句时,会阻塞代码的执行,直到该模块加载完成。 - 对于一些依赖较多的模块,可能会导致较长的等待时间,影响性能。
- 在 Node.js 中,
- import:
- ES 模块的
import
语句会在编译阶段进行静态分析,在运行时异步加载模块,不会阻塞代码的执行。 - 这对于提高性能和代码的并行加载有一定帮助,尤其是在浏览器环境中。
- ES 模块的
5. 性能
- require:
- 由于同步加载,在加载多个模块时,可能会出现性能问题,特别是在复杂的 Node.js 应用中。
- 不过,Node.js 对
require
有缓存机制,多次require
同一个模块时,只会加载一次,后续直接从缓存中获取。
- import:
- 由于是静态分析和异步加载,更有利于性能优化,尤其是在现代浏览器环境中,浏览器可以更好地利用资源的并行加载。
6. 执行顺序
- require:
- 模块的执行顺序是根据
require
的调用顺序和模块的依赖关系,一般来说,会先执行被依赖的模块,再执行依赖模块。 - 例如,如果
moduleA
依赖moduleB
,会先加载并执行moduleB
,再执行moduleA
。
- 模块的执行顺序是根据
- import:
- 模块的导入和执行顺序也是根据依赖关系,但由于是静态分析,在编译阶段就确定了导入顺序,相对更加清晰和可预测。
在面试中,可以这样回答:“require
是 CommonJS 规范下的模块引入方式,主要在 Node.js 中使用,它是一个函数调用,可以在代码的任何位置使用,支持动态加载,并且同步加载模块,使用 module.exports
或 exports
进行导出。而 import
是 ES6 模块引入方式,适用于现代 JavaScript 环境,它是静态声明,只能在文件顶层使用,不支持在条件语句或函数内部使用,但可以使用 import()
实现动态导入,它使用 export
关键字导出,异步加载模块,在性能和静态分析方面有一定优势,尤其在浏览器和现代 JavaScript 开发中表现更好。”
总之,了解 require
和 import
的区别对于选择合适的模块引入方式和开发环境至关重要,根据不同的开发场景和需求,可以选择更合适的方式。在 Node.js 中,如果是老项目或需要动态加载模块,可以使用 require
;在现代前端开发或使用 ES6 及以上的项目中,使用 import
可以更好地利用新的特性和性能优势。
8. vite 打包工具相比 webpack 优势
1. 开发时的启动速度
- Vite:
- 利用现代浏览器原生支持的 ES 模块(ESM),在开发模式下不进行打包操作,而是通过服务器根据请求的文件进行即时编译和转换。这使得开发服务器的启动几乎是瞬间的,无需像 Webpack 那样等待整个项目的构建完成。
- 例如,当你在浏览器中请求一个模块时,Vite 会将该模块及其依赖进行即时编译和传输,避免了 Webpack 中打包整个项目的时间开销。
- Webpack:
- 在开发模式下,会将整个项目打包成一个或多个 bundle 文件,随着项目规模的增大,启动开发服务器的时间会显著增加,因为它需要分析整个项目的依赖树,对每个模块进行处理和打包。
2. 热模块替换(HMR)性能
- Vite:
- 由于采用基于 ESM 的 HMR,只需要对修改的模块进行重新编译和更新,更新速度非常快。并且由于 Vite 是按需编译,不会因为项目规模的增长而显著影响 HMR 的性能。
- 当你修改了一个文件时,Vite 能快速将其编译并更新到浏览器中,提高开发效率。
- Webpack:
- 虽然也支持 HMR,但由于其需要对整个打包的 bundle 进行更新,在处理复杂项目时,HMR 速度会受到影响,并且可能会导致整个页面的刷新,影响开发体验。
3. 构建性能
- Vite:
- 在生产环境中,Vite 利用 Rollup 进行构建,利用 Rollup 的优势,可以生成更优化的构建结果。同时,Vite 的预配置已经考虑了许多优化,减少了手动配置的需要。
- 对于大型项目,Vite 可以利用其按需编译的特点,在一定程度上提高构建速度,尤其是在处理大量模块时。
- Webpack:
- 需要进行大量的配置和优化才能达到较好的构建性能,对于复杂的项目,配置的复杂性会进一步增加,并且需要使用一些插件来优化构建过程,如
terser-webpack-plugin
等。
- 需要进行大量的配置和优化才能达到较好的构建性能,对于复杂的项目,配置的复杂性会进一步增加,并且需要使用一些插件来优化构建过程,如
4. 配置复杂度
- Vite:
- 开箱即用,对于许多常见的前端项目,Vite 的默认配置就可以满足需求,并且配置文件通常比 Webpack 简洁。
- 例如,使用 Vite 开发 Vue 或 React 项目,只需要少量的配置就可以启动开发服务器和进行生产构建。
- Webpack:
- 功能强大,但配置相对复杂,对于新手来说,要达到理想的开发和构建效果,需要学习大量的配置选项和插件。
5. 对现代前端框架的支持
- Vite:
- 对 Vue 3 有很好的原生支持,其开发体验流畅,并且也支持 React 等其他现代框架,其开发服务器和构建过程可以很好地与这些框架的开发流程相结合。
- 在 Vue 3 项目中,Vite 可以更好地利用 Vue 3 的特性,提供更好的开发体验。
- Webpack:
- 可以通过各种插件支持 Vue 3、React 等,但需要更多的配置和插件的配合,而且不同插件之间的兼容性和性能需要开发者自己平衡。
6. 插件生态
- Vite:
- 虽然起步相对较晚,但 Vite 的插件生态正在迅速发展,已经有许多高质量的插件可以满足不同的需求,并且 Vite 的插件 API 相对简洁。
- 可以方便地开发和使用插件来扩展 Vite 的功能,如对 CSS 预处理器的支持、对 TypeScript 的支持等。
- Webpack:
- 拥有庞大的插件生态,几乎可以满足任何需求,但由于其插件 API 的复杂性,开发和使用插件的门槛相对较高。
7. 类型支持
- Vite:
- 对于 TypeScript 的支持较为友好,在开发和构建过程中可以很好地处理 TypeScript 文件,不需要复杂的额外配置。
- 可以在 Vite 项目中轻松地使用 TypeScript 进行开发,而不会增加太多的配置负担。
- Webpack:
- 需要配置
ts-loader
或babel-loader
来处理 TypeScript 文件,并且需要仔细调整配置以确保类型检查和构建的顺利进行。
- 需要配置
综上所述,Vite 在开发时的启动速度、HMR 性能、配置的简洁性、对现代前端框架的支持、插件生态的易用性以及对 TypeScript 的支持等方面都有一定的优势,尤其是对于开发体验和开发效率有更高要求的项目,Vite 是一个很好的选择。然而,Webpack 仍然是一个强大的工具,对于一些复杂的、需要高度定制化的项目,Webpack 的丰富插件和强大的配置能力可以更好地满足需求。在选择时,可以根据项目的具体情况和团队的经验来决定使用哪种工具。
在面试中回答这个问题时,可以结合实际的项目经验,例如:“在我之前的项目中,使用 Vite 开发一个 Vue 3 项目,开发服务器的启动速度非常快,几乎是瞬间完成,而之前使用 Webpack 时,启动时间会随着项目规模的增加而显著增加。而且 Vite 的 HMR 性能很好,修改代码后可以立即看到效果,无需长时间等待,相比之下,Webpack 的 HMR 有时会出现整个页面刷新的情况,影响开发体验。Vite 的配置也更加简洁,对于 TypeScript 的处理也很方便,而在使用 Webpack 时,需要更多的配置来处理 TypeScript 模块和实现类似的开发体验。不过,如果是一个需要高度定制化的大型项目,Webpack 可以通过其丰富的插件和复杂的配置来满足需求,但这也需要更多的时间和精力去配置和维护。”
通过这样的回答,可以向面试官展示你对两种打包工具的深入了解和在实际项目中的应用经验。
9. 手写 promise
好的,下面是一个简单的手写 Promise 实现:
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
if (this.state === 'fulfilled') {
onFulfilled(this.value);
}
if (this.state === 'rejected') {
onRejected(this.reason);
}
if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => {
onFulfilled(this.value);
});
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
});
}
}
}
这个简单的实现包括了 Promise 的基本功能:创建 Promise 对象、执行器函数、resolve
和 reject
方法、then
方法以及状态管理。请注意,这个实现没有处理异步操作和链式调用,这些是 Promise 完整实现的一部分。
下面是一个更完整的 Promise 实现,包括异步处理和链式调用:
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
setTimeout(() => {
this.onFulfilledCallbacks.forEach(fn => fn());
}, 0);
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
setTimeout(() => {
this.onRejectedCallbacks.forEach(fn => fn());
}, 0);
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
return new MyPromise((resolve) => {
resolve(value);
});
}
static reject(reason) {
return new MyPromise((resolve, reject) => {
reject(reason);
});
}
static all(promises) {
return new MyPromise((resolve, reject) => {
let resolvedCount = 0;
const result = [];
promises.forEach((promise, index) => {
promise.then(
value => {
result[index] = value;
resolvedCount++;
if (resolvedCount === promises.length) {
resolve(result);
}
},
reason => {
reject(reason);
}
);
});
});
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
promise.then(
value => {
resolve(value);
},
reason => {
reject(reason);
}
);
});
});
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called;
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then;
if (typeof then === 'function') {
then.call(
x,
y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
r => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
这个实现包括了以下功能:
- 异步处理:使用
setTimeout
来确保then
方法中的回调在下一个事件循环中执行。 - 链式调用:
then
方法返回一个新的 Promise 对象。 - 错误处理:
catch
方法用于捕获错误。 - 静态方法:
resolve
、reject
、all
和race
。 - 处理返回值为 Promise 的情况:
resolvePromise
函数用于处理then
方法返回的值。
10. 手写发布订阅模式
class EventEmitter {
constructor() {
// 存储事件和对应的回调函数
this.events = new Map();
}
// 订阅事件
on(eventName, callback) {
let handlers = this.events.get(eventName);
if (!handlers) {
handlers = new Set();
this.events.set(eventName, handlers);
}
handlers.add(callback);
// 返回取消订阅的函数
return () => {
this.off(eventName, callback);
};
}
// 取消订阅
off(eventName, callback) {
const handlers = this.events.get(eventName);
if (handlers) {
if (callback) {
handlers.delete(callback);
}
// 如果没有回调函数,则删除整个事件
if (!callback || handlers.size === 0) {
this.events.delete(eventName);
}
}
}
// 发布事件
emit(eventName, ...args) {
const handlers = this.events.get(eventName);
if (handlers) {
handlers.forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error('Error in event handler:', error);
}
});
}
}
// 只订阅一次
once(eventName, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(eventName, wrapper);
};
return this.on(eventName, wrapper);
}
}
使用示例:
// 创建实例
const eventBus = new EventEmitter();
// 订阅事件
const unsubscribe = eventBus.on('userLogin', (user) => {
console.log('User logged in:', user);
});
// 订阅一次性事件
eventBus.once('notification', (message) => {
console.log('Notification:', message);
});
// 发布事件
eventBus.emit('userLogin', { id: 1, name: 'John' });
eventBus.emit('notification', 'Welcome!');
// 取消订阅
unsubscribe();
// 或者
eventBus.off('userLogin');
完整功能:
- 功能完整:
- 支持事件订阅 (on)
- 支持取消订阅 (off)
- 支持事件发布 (emit)
- 支持一次性订阅 (once)
- 健壮性考虑:
- 使用 Map 存储事件,Set 存储回调函数,避免重复订阅
- 回调函数执行时使用 try-catch 捕获异常
- 支持多个参数传递
- 使用便利:
- 支持链式调用
- 提供取消订阅的便捷方法
- 支持批量取消某个事件的所有订阅
- 内存管理:
- 自动清理没有订阅者的事件
- once 方法会在执行后自动取消订阅
这个实现适合在大多数前端项目中使用,可以作为全局事件总线或组件间通信的解决方案。