LMX
博客精选作品支持关于
Command Palette

Search for a command to run...

博客

中级前端面试整理-中篇

LMX2024年4月16日18 分钟阅读#面试总结

总结了一些中级前端面试中常见的知识点和问题,涵盖事件循环、Webpack和Vite的按需加载、HTTP缓存控制策略、Vue2和Vue3的区别等内容。

事件循环

JS的事件循环是一种机制,它允许 JavaScript 在处理用户输入、网络请求和其他异步事件时保持响应。

它是一个不断循环的过程,它执行以下步骤:

  1. 检查事件队列:事件队列是一个存储待处理事件的队列。如果队列中存在事件,则将其取出并执行相应的事件处理程序。
  2. 执行微任务:微任务是 JavaScript 代码中的一小段代码,它会在当前执行上下文结束之前执行。例如,Promise 的 .then() 方法中的代码就是微任务。
  3. 执行宏任务:宏任务是 JavaScript 代码中的一段代码,它会在当前执行上下文结束后执行。例如,setTimeout() 函数中的代码就是宏任务。
  4. 渲染页面:如果在事件循环的任何阶段进行了任何更改,则浏览器会重新渲染页面以反映这些更改。

JS中的事件循环(EventLoop)是实现异步编程的核心机制之一,它确保了JS在单线程环境下能够处理异步任务,保持了代码的执行顺序,并且避免了阻塞。

事件循环机制

事件循环是JS运行时环境的一部分,它负责管理执行栈(Execution Stack)和任务队列(Task Queue),事件循环的基本流程如下:

  1. 执行栈(Execution Stack): JavaScript 代码的执行过程中,会形成一个执行栈,也称为调用栈。主线程执行同步任务时,将任务按顺序放入执行栈中执行。
  2. 任务队列(Task Queue): 异步任务完成后,会被放入任务队列中等待执行。任务队列分为宏任务队列(Macrotask Queue)和微任务队列(Microtask Queue)两种。
  3. 事件循环(Event Loop): 主线程执行完当前的执行栈后,会去检查微任务队列中是否有任务,如果有,则依次执行微任务队列中的所有任务。然后,主线程会从宏任务队列中取出一个任务放入执行栈中执行。这个过程不断重复,形成了事件循环。

其实可以理解为:

检查事件队列 --> 执行微任务 --> 执行宏任务 --> 渲染页面

宏任务和微任务

  • 宏任务:包括整体的代码、setTimeOut、setInterval、I/O、UI渲染都等,他们会被放到宏任务队列中等待执行。
  • 微任务:包括Promise的回调函数,MutationObserver的回调函数等,他们会被放到微任务的队列中等待执行。

微任务的执行优先级高于宏任务。这样说可能不是很严谨,准确的说:

当执行栈中的任务执行完成后,在执行下一个宏任务之前,会首先检查微任务队列是否有任务需要执行,如果有,则会优先执行微任务队列中的所有任务。只有在所有微任务执行完毕后,才会执行下一个宏任务。

举例说明:

<!DOCTYPE html>
<html lang="en">
  <head>
     
    <meta charset="UTF-8" />
     
    <title>微任务和宏任务的优先级</title>
  </head>
  <body>
     <button onclick="handleClick()">点击我</button> ​  
    <script>
          function handleClick() {
            console.log('点击了按钮');
      ​
            setTimeout(() => {
              console.log('计时器');
            }, 0);
      ​
            Promise.resolve().then(() => {
              console.log('Promise');
            });
          }
        
    </script>
  </body>
</html>

当用户单击按钮时,会触发一个 click 事件。浏览器会执行相应的 click 事件处理程序,该处理程序会将 "点击了按钮" 输出到控制台。

然后,事件处理程序会调用 setTimeout() 函数,该函数会在 0 毫秒后执行一个回调函数。

接下来,事件处理程序会调用 Promise.resolve() 方法,该方法会创建一个新的 Promise 对象并将其解析为成功状态。

由于微任务的优先级高于宏任务,因此 Promise 的 .then() 方法中的代码会在 setTimeout() 函数中的回调函数之前执行,并将 "Promise" 输出到控制台。

最后,在所有微任务执行之后,会再执行所有宏任务。因此,setTimeout() 函数中的回调函数会在所有微任务执行之后执行,并将 "计时器" 输出到控制台。

需要注意的是,微任务和宏任务的概念只在浏览器环境中存在。在 Node.js 环境中,事件循环的执行顺序有所不同。

Webpack和Vite的按需加载

Webpack 和 Vite 都支持代码的按需加载(Code Splitting)和延迟加载(Lazy Loading)功能,但它们的实现方式有所不同。

Webpack的按需加载

Webpack主要使用动态导入(dynamic import)和代码分离(code splitting)策略来实现按需加载。

动态导入

在代码中使用'import()'语法动态引入模块,Webpack会自动的将引入的模块单独打包为一个chunk文件。

import("path/to/module").then((module) => {
  // ...
});

代码分离

Webpack 还提供了一些配置选项来优化代码分割,例如通过 optimization.splitChunks 选项自动分离第三方库文件,或者使用 import() 手动分离代码。

Vite的按需加载

Vite 利用了原生 ES 模块的动态导入特性,并在开发环境使用浏览器原生的导入语法实现按需加载,而在生产环境则借助 Rollup 实现代码分割。

ES模块动态导入

Vite 利用浏览器原生的 import() 语法实现模块的动态导入,从而实现按需加载。

高效的缓存

Vite 在开发模式下提供了高效的模块缓存机制,利用浏览器缓存来避免不必要的模块重新加载。源码部分使用协商缓存,依赖部分使用强缓存的策略,实现了高效的HMR(动态模块热替换)。

预构建依赖

Vite 会预构建第三方依赖,提高了动态导入的效率。

总结

Webpack 和 Vite 都支持按需加载,但实现方式和侧重点有所不同。Webpack 依赖于它的打包机制和import()语法来实现代码分割和懒加载,适用于需要复杂配置和优化的大型项目。而Vite 利用现代浏览器的ES模块导入特性,提供更快的开发体验和高效的按需加载,适合现代Web项目,特别是在开发环境下。在生产构建时,Vite 通过Rollup进行优化,实现高效的代码分割和加载。

HTTP缓存控制策略

HTTP缓存控制策略是指客户端和服务器端协同工作,在客户端缓存中存储资源副本,以减少重复请求和提高性能的技术。Http的缓存策略一般有两种,一种是强制缓存,一种是协商缓存。

强制缓存

由服务器端控制,强制客户端缓存资源。

强制缓存是指服务器端通过设置HTTP响应头中的Cache-Control和Expires字段来强制客户端缓存资源。

Cache-Control

Cache-Control 是一种更细粒度的缓存控制机制,它可以指定以下属性:

  • max-age:指定资源的缓存时间,单位为秒。
  • s-maxage:指定资源在共享缓存中的缓存时间,单位为秒。
  • public:指定资源可以被所有缓存代理缓存。
  • private:指定资源只能被客户端缓存。
  • no-cache:指定资源不能被缓存。
  • no-store:指定资源不能被缓存,并且每次请求都必须从服务器端获取。

示例:

Cache-Control: max-age=3600, public

Expires

Expires 指定资源的过期时间,单位为 GMT 时间。

Expires: Thu, 01 Dec 2023 16:00:00 GMT

协商缓存

由客户端和服务器端协商,共同决定是否缓存资源。

协商缓存的流程如下:

  1. 客户端向服务器端发送请求。
  2. 服务器端检查资源是否在缓存中。
  3. 如果资源在缓存中,则服务器端会检查资源是否过期。
  4. 如果资源没有过期,则服务器端会将资源返回给客户端,并在响应头中设置 Etag 字段。
  5. 如果资源过期,则服务器端会将资源返回给客户端,并在响应头中设置 Last-Modified 字段。
  6. 客户端收到响应后,会检查 Etag 或 Last-Modified 字段。
  7. 如果 Etag 或 Last-Modified 字段与客户端缓存中的资源匹配,则客户端会使用缓存中的资源。
  8. 如果 Etag 或 Last-Modified 字段与客户端缓存中的资源不匹配,则客户端会向服务器端发送新的请求。

Etag

Etag 是一个唯一的字符串,用于标识资源的版本。

Last-Modified

Last-Modified 是资源的最后修改时间。

示例:

GET /index.html HTTP/1.1
​
If-None-Match: "1234567890"

缓存策略选择

强制缓存适用于以下场景:

  • 资源经常被访问。
  • 资源的内容不会频繁更改。

协商缓存适用于以下场景:

  • 资源的内容可能经常更改。
  • 需要精确控制缓存的资源。

以下是一些缓存策略的最佳实践:

  • 使用 Cache-Control 而不是 Expires 来控制缓存。
  • 为静态资源设置较长的缓存时间。
  • 为动态资源设置较短的缓存时间。
  • 使用 Etag 或 Last-Modified 来进行协商缓存。
  • 定期更新缓存中的资源。

Vue2的data和Vue3的Ref区别

Vue2和Vue3的ref都是用来存储响应性数据的,但是他们之间存在以下差异性。

使用方式

  • Vue2: data 选项用于在组件中定义响应式数据。
  • Vue3: ref 函数用于创建响应式变量。

作用域

  • Vue2: data 选项中的数据在组件实例中是全局可见的。
  • Vue3: ref 函数创建的变量在当前作用域内可见。

响应性

  • Vue2: data 选项中的数据默认是响应式的。
  • Vue3: ref 函数创建的变量需要使用 .value 访问才能触发响应式更新。

类型

  • Vue2: data 选项中的数据可以是任何类型。
  • Vue3: ref 函数创建的变量可以是任何类型,但建议使用基本类型或简单对象,以避免性能问题。

默认值

  • Vue2: data 选项中的数据可以设置默认值。
  • Vue3: ref 函数创建的变量需要显式传入默认值。

更新

  • Vue2: data 选项中的数据可以通过 this.data.xxx 方式直接更新。
  • Vue3: ref 函数创建的变量可以通过 .value 方式更新。

销毁

  • Vue2: data 选项中的数据在组件销毁时会被销毁。
  • Vue3: ref 函数创建的变量需要手动销毁,否则会造成内存泄漏。

Vue3中watch和watchEffect的区别

监听方式

  • watch: 可以监听单个或多个响应式数据,并指定回调函数在数据变化时执行。
  • watchEffect: 监听回调函数内部使用的所有响应式数据,并在数据变化时重新执行回调函数。

回调函数参数

  • watch: 回调函数会收到两个参数,分别是新值和旧值。
  • watchEffect: 回调函数不会收到任何参数。

执行时机

  • watch: 组件首次渲染时不会执行回调函数,只有在数据变化时才会执行。
  • watchEffect: 组件首次渲染时会立即执行回调函数,并在数据变化时再次执行

性能

  • watch: 仅在监听的数据变化时才会执行回调函数,因此性能开销较小。
  • watchEffect: 会在任何依赖的数据变化时都重新执行回调函数,因此性能开销相对较大。

使用场景

  • watch: 适用于需要精确控制监听范围的情况,以及需要访问新旧值的情况。
  • watchEffect: 适用于不需要明确定义监视的数据,以及只需根据数据变化自动执行操作的情况。

总结

  • watch 和 watchEffect 都是 Vue 3 中用来监听响应式数据变化的函数。
  • watch 更适合需要精确控制监听范围和需要访问新旧值的情况。
  • watchEffect 更适合不需要明确定义监视的数据和只需根据数据变化自动执行操作的情况。

WebSocket 的使用

WebSocket 是一种在客户端和服务器之间进行全双工通信的协议。它可以用于各种实时通信应用,例如聊天、游戏和数据流。

WebSocket 的使用方式如下:

  1. 在客户端和服务器端建立 WebSocket 连接。
  2. 在客户端和服务器端发送和接收数据。
  3. 关闭 WebSocket 连接。

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

客户端代码:

const socket = new WebSocket('ws://localhost:8080');
​
socket.onopen = () => {
  console.log('WebSocket connection opened');
};
​
socket.onmessage = (event) => {
  console.log('Received message:', event.data);
};
​
socket.onclose = () => {
  console.log('WebSocket connection closed');
};
​
socket.send('Hello, world!');

服务器端代码:

import asyncio
import websockets
​
async def handler(websocket, path):
  while True:
    message = await websocket.recv()
    await websocket.send(message)
​
async def main():
  async with websockets.serve(handler, 'localhost', 8080):
    await asyncio.Future()  # run forever
​
if __name__ == '__main__':
  asyncio.run(main())

不用 WebSocket 做实时通信,还有以下几种方式可以做:

  • HTTP 轮询:客户端定期向服务器发送 HTTP 请求,以获取更新。
  • 长轮询:客户端向服务器发送 HTTP 请求,并保持连接打开,直到服务器有更新数据。
  • SSE (Server-Sent Events) :服务器向客户端发送连续的事件流。
  • Comet:一种结合了 HTTP 轮询和长轮询的技术。

选择哪种方式取决于具体的应用需求。

  • WebSocket 是实时通信的最佳选择,因为它具有低延迟和高性能。
  • HTTP 轮询是一种简单易行的选择,但延迟较高。
  • 长轮询可以降低延迟,但会增加服务器负载。
  • SSE 和 Comet 是介于 HTTP 轮询和长轮询之间的选择。

以下是一些不用 WebSocket 做实时通信的示例:

  • HTTP 轮询:
function poll() {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', '/api/updates');
  xhr.onload = () => {
    if (xhr.status === 200) {
      const data = JSON.parse(xhr.responseText);
      // 更新 UI
    }
  };
  xhr.send();
}
​
setInterval(poll, 1000);
  • 长轮询:
function poll() {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', '/api/updates');
  xhr.onload = () => {
    if (xhr.status === 200) {
      const data = JSON.parse(xhr.responseText);
      // 更新 UI
      poll();
    }
  };
  xhr.send();
}
​
poll();
  • SSE:
<script>
const source = new EventSource('/api/updates');
​
source.onmessage = (event) => {
  const data = JSON.parse(event.data);
  // 更新 UI
};
</script>
  • Comet:
function poll() {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', '/api/updates');
  xhr.onload = () => {
    if (xhr.status === 200) {
      const data = JSON.parse(xhr.responseText);
      // 更新 UI
      if (data.more) {
        poll();
      }
    }
  };
  xhr.send();
}
​
poll();

Vue2项目升级Vue3的优点

性能提升

  • 更快的虚拟DOM: Vue 3引入了一个重写的虚拟DOM算法,相比于Vue 2,它在渲染速度和内存消耗上都有显著提升。
  • Tree Shaking支持: Vue 3的设计允许通过Tree Shaking去除未使用的代码,这意味着最终的打包文件可以更小,加载时间更短。
  • Fragment, Teleport, Suspense: 这些新特性提供了更多的灵活性,在一些情况下可以提升性能或改善用户体验。

提升的组合式API

  • 更好的逻辑复用和代码组织: Composition API使得逻辑复用变得更简单,有助于在复杂组件中保持代码的组织和清晰度,特别是对于大型项目或应用来说。
  • 响应式系统的改进: Vue 3的响应式系统基于Proxy实现,不仅性能更好,而且可以监听到更多类型的变化(例如数组索引和对象属性的添加/删除)。

类型支持和TypeScript集成

  • 更好的TypeScript集成: Vue 3从一开始就考虑了对TypeScript的支持,提供了更好的类型推断和集成体验。这使得开发大型项目时,能够利用TypeScript带来的类型安全性和开发效率。

新的组件特性

  • Composition API: 除了提供更好的代码组织能力外,还允许更灵活地使用Vue的响应式系统。
  • Teleport组件: 允许将子组件模板的部分内容“传送”到DOM的其他位置,这对于处理模态框、提示框等非常有用。
  • Suspense组件: 提供了一种新的方式来处理异步组件的加载状态,让这一过程的用户体验更加流畅。

自定义指令API

Vue 3对自定义指令API进行了改进,使其使用起来更简单直观。

模块化

Vue 3的模块化程度更高,让开发者可以根据需要导入特定的功能,这有助于减少应用体积。

升级带来的挑战

尽管Vue 3带来了很多改进,但升级过程中也可能遇到一些挑战,比如需要重新学习新的API、迁移现有的代码以及确保第三方库/插件的兼容性等。

评估是否需要升级到Vue3

项目需求

  • 性能需求:如果项目对性能有高要求,考虑到Vue 3提供了更优化的渲染性能和更小的打包体积,升级可能是一个好的选择。
  • 功能需求:Vue 3引入了多项新功能,如Composition API、Teleport、Suspense等。如果这些新特性对于满足项目的特定需求特别有帮助,那么升级可能是合适的。

团队技能和学习曲线

  • 对新技术的适应能力:升级到Vue 3可能需要团队成员学习新的概念和API,如Composition API。团队需要评估自身的学习能力和对新技术的适应速度。
  • TypeScript使用:Vue 3提供了更好的TypeScript支持。如果团队已经在使用或打算使用TypeScript,那么升级到Vue 3会是一个顺理成章的选择。

生态系统和第三方库的支持

  • 插件和库的兼容性:确保项目依赖的第三方库和插件与Vue 3兼容。虽然许多库已经更新以支持Vue 3,但仍有一些可能尚未更新。
  • 工具和生态系统的成熟度:虽然Vue 3的生态系统正在快速成长,但需要评估所需的开发和调试工具是否已经足够成熟和稳定。

项目规模和复杂度

  • 新项目还是现有项目:对于新项目,直接使用Vue 3可能是一个不错的选择,因为可以从一开始就利用其提供的所有新特性和改进。对于现有项目,需要评估迁移的复杂度和成本。
  • 项目的规模和寿命:对于大型或长期项目,投资于升级到Vue 3可能会带来长远的收益,尤其是考虑到性能优化和未来的维护性。

时间和成本

  • 升级成本:评估升级过程中的时间和资源成本,包括学习新API、重构现有代码、测试和修复升级相关的bug等。
  • 长期维护成本:考虑到Vue 3将是未来发展的主要方向,长期来看,升级可能有助于减少维护成本和确保项目的持续更新。

前端设计模式

先声明这里只是粗略的介绍前端设计模式,真是要展开说的话,可能要很多了!

前端设计模式是指用于解决前端开发中常见问题的通用解决方案。它们可以帮助开发人员提高代码的可维护性、可读性和可复用性。

前端设计模式可以分为三大类:

  • 创建型模式:用于创建对象的模式。
  • 结构型模式:用于组织对象的模式。
  • 行为型模式:用于定义对象之间通信方式的模式。

创建型模式

创建型模式用于创建对象的模式。它们可以帮助开发人员更灵活地创建对象,并解耦对象的创建过程。

常见创建型模式包括:

  • 单例模式:确保一个类只有一个实例。
  • 工厂方法模式:创建一个对象的工厂类。
  • 抽象工厂模式:创建一个创建多个类型对象的工厂类。
  • 建造者模式:将一个复杂对象的创建过程分解为多个步骤。
  • 原型模式:通过克隆现有对象来创建新对象。

结构型模式

结构型模式用于组织对象的模式。它们可以帮助开发人员将对象组合成更大的结构,并提高代码的可维护性。

常见结构型模式包括:

  • 适配器模式:将一个不兼容的接口转换为兼容的接口。
  • 桥接模式:将对象的接口和实现解耦。
  • 组合模式:将对象组合成树形结构。
  • 装饰器模式:在不改变对象本身的基础上为对象添加功能。
  • 外观模式:为复杂系统的接口提供简单易用的外观。

行为型模式

行为型模式用于定义对象之间通信方式的模式。它们可以帮助开发人员提高代码的可扩展性和可复用性。

常见行为型模式包括:

  • 策略模式:封装算法或行为,并允许客户选择不同的算法或行为。
  • 观察者模式:定义了一种一对多依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会得到通知。
  • 发布订阅模式:一种观察者模式的变体,它允许发布者发布消息,订阅者可以订阅这些消息。
  • 责任链模式:将多个处理请求的对象串联起来,并逐个处理请求。
  • 模版方法模式:定义一个操作的骨架,允许子类在不改变骨架结构的情况下重定义其中的一部分步骤。
  • 迭代器模式:为遍历集合中的元素提供一种方法。

前端设计模式的应用

前端设计模式可以应用于各种前端开发场景,例如:

  • 组件开发:可以使用设计模式来构建可维护和可复用的组件。
  • 数据管理:可以使用设计模式来管理和操作数据。
  • 用户交互:可以使用设计模式来设计用户交互界面。
  • 性能优化:可以使用设计模式来优化前端性能。

评论交流

支持作者
上一篇

面试管:来,说说数组都有哪些方法,就问基础

下一篇

面试管:来,小伙子, 说一下Set和Map的理解

LMX BlogWeb · TypeScript · Vue · Node.js
创作者
@LMXZQQ
版本
暂无
日期
2026-10-30
文章
29
部署平台
Nginx
模板源码
GitHub
许可证
MIT License
字体
Geist
技术栈
  • next@16.4.3
  • react@19.3.1
  • @base-ui/react@1.8.0
  • shadcn@4.12.0
  • tailwindcss@4.3.3
访问统计
  • OpenPanel
  • Google Analytics
供智能体阅读
  • llms.txt
  • index.md
灵感来源
  1. Tailwind CSS
  2. shadcn/ui
  3. Vercel
  4. Apple
  5. Evil Charts
  6. Devouring Details
  7. Skiper UI
  8. Making Software
  9. shadcncraft
维护者
静水流深 / LMX
1602
Command Palette

Search for a command to run...