如何准备一个高级前端工程师的面试

109 阅读14分钟

前言

我是个工作了快 4 年的 Node.js 工程师,说实话 Node.js 对我挺有缘分的,这个故事又要从 2020 年的暑假开始说起,一晃五年过去了。但是其实我从两年前开始就非常迷茫,觉得自己是应该继续往后端发展呢,还是往前端发展,毕竟 JavaScript 是一个前端语言,并且随着 AI 时代的到来,全栈工程师应该是未来的趋势,因此我对我过去的四年做一个项目以及知识的总结。

应该所具备的能力

前端

Node.js

事件循环

提起 Node.js ,我觉得最应该关注的就是它的事件循环机制。因为它实现了 如何在单线程中实现 高并发、高吞吐量、非阻塞的异步编程模型? 换句话说,它解决了: 在只有一个线程的前提下,如何让程序同时处理多个任务(特别是 I/O 操作)而不会阻塞其他任务的执行。

而 Node.js 的事件循环机制做了什么?

👉 1. 异步 I/O

把“慢”的任务(如文件读写、网络请求)交给底层(libuv / 线程池 / 操作系统)。

当前线程继续执行其他任务,不等待慢任务的返回结果。

👉 2. 事件注册 + 回调机制

慢任务完成后,将对应的“结果处理函数”注册到事件队列中,等主线程空闲了再回调执行。

本质上是“先做别的事,完成了再告诉我”。

👉 3. 事件循环调度机制

主线程通过事件循环,在各个阶段依次从队列中取出任务执行。

保证了 任务之间的有序性 和 系统整体响应能力。

执行阶段

事件循环按阶段执行,每个阶段都有一个 FIFO 队列 存放回调任务:

┌───────────────────────────────┐
│           timers              │  -> setTimeout/setInterval
├───────────────────────────────┤
│       pending callbacks       │  -> 某些系统操作的回调
├───────────────────────────────┤
│          idle, prepare        │  -> libuv内部
├───────────────────────────────┤
│           poll                │  -> I/O事件回调执行
├───────────────────────────────┤
│           check               │  -> setImmediate
├───────────────────────────────┤
│           close callbacks     │  -> 如 socket.on('close')
└───────────────────────────────┘
  • timers:执行由 setTimeout()setInterval() 安排的回调。
  • pending callbacks:执行某些系统操作(如 TCP 错误类型)的回调。
  • idle, prepare:libuv 内部使用,无需关注。
  • poll:
    • 处理 I/O 事件。
    • 如果无 I/O、队列为空,进入 check 阶段。
    • 如果 I/O 队列不为空,执行回调。
  • check: 执行 setImmediate() 注册的回调。
  • close callbacks: 处理如 socket.destroy() 的关闭事件。
微任务机制(Microtasks)

在每个 事件循环阶段结束前,Node.js 会处理两类微任务队列:

  1. process.nextTick() 队列(优先级更高
  2. Promise.then/catch/finally 的微任务队列(基于 V8 的微任务)执行顺序:
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));

输出顺序:

nextTick
promise

如何还想延伸更多细节:事件循环 (例如事件循环有什么特点?事件循环有什么缺点)

垃圾回收

React 框架

过去面试几家大厂前端工程师岗位,我发现面试官特别喜欢问你使用的是 React 哪个版本。我当时非常不理解为什么都这么喜欢 React 的版本。我记得我在 2022 年写 React 的时候,已经 useEffect 写法了,我的当初使用的是 React 16 版本,具体不知道是 16.x 多少了。在我是校招生的时候,我看前端面经,很喜欢问 React 的生命周期是什么,例如 :componentDidMount()shouldComponentUpdate()等一些钩子。但是当时并没有使用到,取而待之的就是各种 useXxx()这样的 hook 方法。

Hooks

React 是在 16.8 版本 中正式引入了 Hooks, 这个版本发布的时间是:2019 年 2 月 6 日

老版本 React 的生命周期:🧬React 生命周期(Class 组件)

对比常见 Hook:🪝 Hooks 阶段(函数组件)

我的理解: Hooks 把生命周期“函数化”了,粒度更细、更灵活

那如何实现自定义的 Hook? 我们说的 “自定义 Hook”(custom hook)是 React 中对函数的一种特殊命名方式,它来源于 React Hook 的使用规范与代码复用的需求。

“自定义 Hook”这个名字的来源,是因为你自己定义了一个使用其他 Hook 的函数。React 官方规定:

“只要函数名以 use 开头,并在其内部调用了其他 Hook(比如 useStateuseEffect),它就是一个自定义 Hook。”

它不是 React 内置的,而是你自己“定义”的 Hook 逻辑

自定义 Hook 的意义是什么?

  • 抽离组件逻辑(例如:多个组件都需要定时器、滚动监听等功能)。
  • 复用性高(不用重复粘贴相同的 useEffect / useState 逻辑)。
  • 可组合性强(多个 Hook 组合使用,逻辑清晰)。
  • 让组件更干净、更关注 UI 展示

如何定义一个自定义 Hook?

定义 Hook 本质上就是一个普通的 JS 函数,但必须:

  1. use 开头
  2. 可以使用其他 Hook(如 useStateuseEffect

几个示例:

  • 封装一个监听窗口大小的 Hook :
import { useState, useEffect } from 'react';

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// 使用它
function MyComponent() {
  const { width, height } = useWindowSize();

  return (
    <div>
      当前窗口尺寸:{width} x {height}
    </div>
  );
}
  • 防抖 Hook(如搜索输入) :
import { useState, useEffect } from 'react';

export function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

const [text, setText] = useState('');
const debouncedText = useDebounce(text, 500);

// 用 debouncedText 去触发接口请求
function SearchComponent() {
    const [text, setText] = useState('');
    const debouncedText = useDebounce(text, 500);

    useEffect(() => {
        if (debouncedText) {
            // 在这里调用接口
            fetchData(debouncedText);
        }
    }, [debouncedText]);

    const fetchData = async (query) => {
        try {
            const response = await fetch(`https://api.example.com/search?query=${query}`);
            const data = await response.json();
            console.log(data);
            // 处理获取到的数据
        } catch (error) {
            console.error('请求失败:', error);
        }
    };

    return (
        <input
            type="text"
            value={text}
            onChange={(e) => setText(e.target.value)}
            placeholder="输入搜索内容..."
        />
    );
}
  • useFetch:封装异步请求
import { useEffect, useState } from 'react';

export function useFetch<T = any>(url: string, options?: RequestInit) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    if (!url) return;

    setLoading(true);
    fetch(url, options)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

const { data, loading, error } = useFetch('/api/user');
Fiber

React Fiber 是在 React 16 中正式发布的。那么为什么 React 会引入 Fiber?

没有 Fiber 的 React 时期的主要性能问题

在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象。

针对这一个问题,React 团队从框架层面对 web 页面的运行机制做了优化,就是我们所说的 Fiber,得到很好的效果。

React 的 render 初衷是:

将声明式 UI(JSX/ReactElement)映射成实际的 DOM 操作,并在状态变化时只进行最小化的 DOM 更新。

目标是提供一个:

  • 高效(相比手动操作 DOM)
  • 声明式(让开发者只关注“UI 长什么样”)
  • 自动 diff(避免不必要的 DOM 操作) 的 UI 渲染机制

🧱 React 15 及以前版本:没有 Fiber 时的渲染逻辑(Stack Reconciler)

旧版本是 递归式同步遍历更新整棵组件树,核心逻辑过程如下:

🔂 1. 初始化:JSX 转换成 ReactElement
ReactDOM.render(<App />, document.getElementById('root'));
  • JSX 会被 Babel 转换成 React.createElement(type, props, children)
  • 最终形成一个嵌套结构的 ReactElement 树(描述 UI 的数据结构)
🌳 2. 构建组件实例树 + 生成 DOM(初次挂载)
  • 递归遍历 ReactElement:
    • 如果是函数组件:调用函数,拿到子元素;
    • 如果是类组件:实例化类,调用 render(),拿到子元素;
    • 如果是原生标签:直接生成对应 DOM 节点;
  • 然后递归地挂载子节点,最终把整棵 DOM 插入页面。

这个过程类似:

function mount(element) {
  if (typeof element.type === 'function') {
    const childElement = element.type(element.props);
    return mount(childElement);
  } else {
    const dom = document.createElement(element.type);
    // 设置属性 + 递归 appendChild
    return dom;
  }
}
🔁 3. 组件更新(状态或 props 变化时)

当组件调用 setState() 时:

  • React 会从该组件重新开始向下递归对比旧的 ReactElement 树和新的 ReactElement 树
  • 对比逻辑是通过 reconciler.diff() 实现的:
    • tag 相同、key 相同 → 复用节点,更新属性
    • 否则认为是新的节点 → 卸载旧的、挂载新的
  • 最终生成最小化的 DOM 操作 patch 应用到页面上

这是经典的 stack reconciler 实现,即:

使用函数递归调用栈完成协调(reconciliation)过程

✅ 总结一下

旧版本 React render 的目的:

  • 将 JSX → 虚拟 DOM(ReactElement)→ 真 DOM
  • 最小化 DOM 操作(diff 算法)
  • 提供 declarative(声明式) UI 渲染模型

实现方式(React 15 及以前):

  • 使用递归同步遍历整个组件树
  • 不能中断渲染过程
  • 使用 Stack Reconciler 栈调和
  • 性能和交互体验受限

Fiber 的出现是为了解决:

  • 渲染中断性
  • 高优先级任务调度
  • 并发渲染能力
什么是 Fiber

Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:

Fiber 是一个执行单元,每次执行完一个执行单元,React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去。

Fiber 关键特性:

  • 增量渲染
    • Fiber 把一次完整的渲染拆成“小任务”,均匀到每一帧里面去执行。
  • 暂停,终止,复用渲染任务
  • 不同更新的优先级
  • 并发方面新的基础能力
React Fiber 运行流程图

画板

React Fiber 渲染调度流程图

  • 🔁 渲染阶段(render phase):构建 Fiber 树,可以被中断。
  • 🚦 提交阶段(commit phase):更新 DOM,不可中断。
  • 🧠 增量渲染的核心是 “每执行一段,看是否有更高优先级任务”,用 requestIdleCallback、MessageChannel 或 scheduler 调度控制。
React Fiber 节点结构示意图

🧠 结构说明

  • child:指向第一个子节点
  • sibling:指向下一个兄弟节点(链表形式遍历子节点)
  • return:指向父节点(方便回溯)
  • alternate:指向当前节点的“另一个版本”(双缓存结构:current vs workInProgress

这个结构让 React 在渲染时可以:

  • 遍历整个组件树
  • 构建新的 Fiber 树同时保留旧的
  • 快速对比差异并提交最小更新
🧱 FiberNode 数据结构(简化版源码)
type FiberTag = number; // FunctionComponent, ClassComponent, HostComponent 等
type Flags = number;    // 用于表示副作用,比如 Placement, Update

class FiberNode {
  // 基础标识
  tag: FiberTag;
  key: null | string;
  type: any;
  elementType: any;
  stateNode: any;

  // 构建树结构(核心三叉链表结构)
  return: FiberNode | null;   // 父节点
  child: FiberNode | null;    // 第一个子节点
  sibling: FiberNode | null;  // 下一个兄弟节点
  index: number;

  // 双缓存机制
  alternate: FiberNode | null; // current 与 workInProgress 相互指向

  // 状态相关
  pendingProps: any; // 本次更新传入的 props
  memoizedProps: any; // 上次渲染完的 props
  memoizedState: any; // 上次渲染完的 state
  updateQueue: any; // 更新队列(setState 就存在这里)

  // 副作用标志
  flags: Flags;
  subtreeFlags: Flags;

  // DOM 或组件引用
  ref: null | (((handle: any) => void) | { current: any });

  constructor(tag, pendingProps, key) {
    this.tag = tag;
    this.key = key;
    this.pendingProps = pendingProps;
    this.memoizedProps = null;
    this.memoizedState = null;
    this.updateQueue = null;

    this.return = null;
    this.child = null;
    this.sibling = null;
    this.index = 0;

    this.stateNode = null;
    this.type = null;
    this.elementType = null;
    this.alternate = null;

    this.flags = 0;
    this.subtreeFlags = 0;
    this.ref = null;
  }
}

Next.js 框架 - SSR

Next.js 中的 SSR、SSG、ISR、CSR 有什么区别?你在项目中如何选择?

✅ SSR(Server-side Rendering):

  • 页面在请求时在服务端生成 HTML
  • 实现方式:getServerSideProps
  • 优点:数据实时,适合登录态/搜索页
  • 缺点:每次请求都会触发渲染,性能开销大

✅ SSG(Static Site Generation):

  • 页面在构建时就静态生成
  • 实现方式:getStaticProps
  • 优点:性能极高,适合博客/产品页
  • 缺点:构建时固定数据,缺乏实时性

✅ ISR(Incremental Static Regeneration):

  • 静态生成 + 增量更新,结合了 SSR 和 SSG 的优点
  • getStaticProps + revalidate
  • 优点:支持缓存+定时刷新,兼顾性能与实时性
  • 典型场景:内容平台、新闻网站

✅ CSR(Client-side Rendering):

  • 在浏览器中用 JS 获取并渲染数据
  • 实现方式:useEffect 中调用 fetch
  • 优点:无需服务端,交互灵活
  • 缺点:不利于 SEO,首屏渲染慢

三、使用选择建议(结合场景):

场景推荐方式理由
详情页 / 营销页SSG 或 ISR静态渲染快,SEO 友好
登录态页面(如个人中心)SSR需要实时性与权限判断
列表页 / 搜索页SSR 或 CSR数据实时变化
新闻类平台 / 内容 CMSISR可设置定期重建页面
SPA 应用 / 后台系统CSR不需要 SEO,操作性强
SSR 服务端渲染的底层逻辑

Next.js 实现服务端渲染(SSR, Server-Side Rendering)的核心原理,是在 Node.js 环境中运行 React 组件生成 HTML,再把生成的 HTML 返回给客户端,从而实现首屏加速与 SEO 友好等效果。下面我们来深入解析一下底层的实现原理:

整体流程

  • 浏览器请求页面(如 /about)
  • Next.js 路由系统匹配到对应的页面组件(如 **pages/about.tsx**
  • 在服务器端调用 **getServerSideProps**(如果存在)获取数据
  • 使用 React 的服务端渲染 API(如 **renderToString****renderToPipeableStream**)将组件转为 HTML 字符串
  • 将 HTML 返回给浏览器,浏览器解析 HTML 并加载相关 JS
  • 前端进行 hydration (注水),将静态 HTML 变为可交互的 React 应用

相对于 SSG 不同的方式

函数名执行频率执行时机运行环境典型用途
**getServerSideProps**每次请求都会执行请求页面时服务端登录后页面、权限控制、实时内容
**getStaticProps**只在构建时执行一次**next build**服务端博客/产品页等静态内容
🌟 Next.js 构建流程(next build

当执行 next build 时,整个构建流程大致可以拆解为以下几个阶段:

  1. 初始化阶段(Preflight)
  • 读取配置文件(next.config.js
  • 检查必要文件是否存在(如 pages/_app.jspages/index.js
  • 分析 TypeScript 配置(如使用 .ts/.tsx 文件)
  • 初始化 Babel 和 Webpack 配置
  • 加载插件和扩展(如 next-transpile-modules
  1. 编译阶段(Build)

📄 页面编译

  • 使用 Webpack + Babelpages/app/components/ 目录的文件进行打包编译
  • 根据 getStaticProps / getServerSideProps 判断页面渲染方式(SSG、SSR、ISR)

🚀 编译结果

  • 输出服务端代码(server
  • 输出客户端代码(static/chunks/pages
  • 使用 react-server-dom-webpack 编译 React Server Components(仅适用于 App Router)
  1. 优化阶段(优化输出产物)
  • 静态资源优化(例如 CSS、JS 分包,Tree-Shaking,压缩)
  • Image 优化(预生成优化版本)
  • 预渲染(Pre-render)
    • SSG 页面会被渲染为 HTML + JSON
    • ISR 页面会生成静态 HTML,但带有 revalidate 配置
    • SSR 页面只保留入口模块,运行时动态渲染
  1. 产物生成阶段(Output)
  • 输出 .next 目录,包含以下内容:
    • .next/static:静态资源
    • .next/server:服务端 SSR 渲染代码
    • .next/cache:缓存数据
    • .next/build-manifest.json.next/prerender-manifest.json:构建元信息
  1. 分析阶段(可选)
  • 使用 next build && next exportnext build && next analyze 查看构建分析报告
  • @next/bundle-analyzer 插件可用于可视化 bundle 大小

Webpack 工程化

核心原理与机制
  1. Webpack 构建流程原理
  • 流程六阶段
    1. 初始化参数(配置文件解析)
    2. 创建 Compiler 对象
    3. 调用 run 启动编译
    4. 确定入口(entry)
    5. 递归编译模块(loader 处理,构建 AST)
    6. 输出 assets(plugins 修改最终资源)

实战应用:自定义构建插件,比如统计构建耗时、输出构建依赖图。

  1. Module Resolution 模块解析机制
  • 默认查找路径:node_modules
  • 扩展字段:resolve.extensions, resolve.alias, resolve.mainFields
  • 支持三种模块:
    • CommonJS(require)
    • ES Module(import/export)
    • AMD(较少用)

实战应用:通过 alias 优化业务代码模块路径引用、支持 monorepo 结构。

  1. Loader 机制
  • 作用:模块预处理,转化文件内容(如 .ts -> .js)
  • 执行顺序:从右到左(或从下到上)
  • Loader 分类:
    • preLoaders(前置处理器)
    • normal loaders
    • postLoaders(后置处理器)

实战应用:编写自定义 loader,例如 markdown 转 html、组件按需加载。

  1. Plugin 机制
  • 作用:用于构建过程中扩展 webpack 功能(钩子系统)
  • 插件通过 compiler.hookscompilation.hooks 插入处理逻辑

实战应用:打包进度条(使用 ProgressPlugin)、自定义插件实现构建时间日志、资源输出分析

性能优化策略
  1. Tree Shaking 原理
  • 依赖 ESModule 的静态结构特性(import/export)
  • 只移除未被使用的 export 成员
  • 配合:
    • sideEffects: false(package.json)
    • usedExports: true(webpack 配置)

实战应用:减少无效代码输出,压缩包体积

  1. Code Splitting(代码分割)
  • 实现方式:
    • 入口分割(多个 entry)
    • 动态导入:import()
    • SplitChunksPlugin(提取公共代码)

实战应用:拆分页面路由模块,首屏加载更快、将 react, lodash 等大包分离成独立 chunk,利用缓存

  1. 缓存优化
  • contenthash: 文件变更才更新文件名
  • cache-loader, babel-loader 的缓存
  • 持久化缓存:cache: { type: 'filesystem' }

实战应用:提高二次构建速度,适用于大型项目多模块打包

4.并行与多进程加速

  • 使用 thread-loaderparallel-webpack
  • 开启 TerserPlugin 多线程压缩:parallel: true
  • 文件缓存配合使用

实战应用:优化构建耗时,CI/CD 构建显著提速

实战配置能力(可组合 + 可扩展)
  1. 多环境配置(重要)- 可用于灰度发布
  • 使用 webpack-merge 合并基础配置 + dev/prod 配置
  • 动态注入环境变量(DefinePlugin
new webpack.DefinePlugin({
  'process.env.API_BASE': JSON.stringify(process.env.API_BASE),
});
  1. 开发体验提升
  • 热更新(HMR)原理:webpack-dev-server + HMR 插件
  • 使用 SourceMap 定位错误(devtool 配置详解)
  • 使用 eslint-loader + stylelint 保证代码规范
  1. 项目架构支持
  • 多页面应用支持(MPA)
  • 微前端支持(Module Federation)
  • SSR 场景优化(可用于自研 SSR 框架或优化 Next.js)

性能优化

后端

nginx

k8s

mysql