面试题记录

html/css 相关

HTML 语义化

语义化 HTML 意味着使用能够表达内容意义的标签,而不是仅仅用 <div><span>。例如:
<header>、<nav>、<main>、<article>、<section>、<footer> 等标签清晰地划分了页面各部分,便于搜索引擎抓取、屏幕阅读器解析以及后期维护。
优点:增强可访问性,提升 SEO,同时代码更具可读性和可维护性。

元素布局与居中

// Flex 布局
.container {
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center;     /* 垂直居中 */
}
// 绝对定位
.element {
    position: absolute;
    left: 0; right: 0; top: 0; bottom: 0;
    margin: auto;
}
// 绝对定位与 transform
.element {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

display 与 visibility 的区别

display: none;:元素完全从页面中移除,不占据任何空间。
visibility: hidden;:元素不可见,但仍占据原有空间。

清除浮动

使用空标签:在浮动元素后添加一个 <div style="clear: both;"></div>;但这种方法会增加不必要的标记。
伪元素 clearfix:

.clearfix::after {
  content: "";
  display: table;
  clear: both;
}

这种方法不改变 HTML 结构,较为推荐。
设置父元素 overflow:如 overflow: hidden; 或 overflow: auto;,使父容器包裹浮动子元素,但可能会影响溢出内容的显示。
各方法各有优缺点,选择时应结合实际情况。

性能优化相关

浏览器渲染与性能优化

浏览器渲染流程大致可以分为以下步骤:

  • 解析 HTML 生成 DOM 树

    浏览器读取 HTML 文档,并利用 HTML 解析器构建出一个层次化的 DOM 树;
  • 解析 CSS 生成 CSSOM 树

    同时,CSS 文件和内联样式被解析成 CSSOM 树,记录每个选择器对应的样式;
  • 构建渲染树(Render Tree)

    将 DOM 树与 CSSOM 树合并,只包含需要绘制的节点(例如 display: none 的节点会被过滤);
  • 布局/回流(Reflow)

    根据渲染树计算每个节点的几何信息(位置、尺寸等);
  • 绘制/重绘(Paint)

    将布局信息转换为实际像素,在屏幕上绘制内容;
  • 合成(Composite)

    如果存在多个图层,浏览器会对各图层进行合成渲染。

在实际项目中,常用的优化策略包括:

  • 减少DOM操作:通过批量更新、虚拟DOM(如React/Vue)和文档片段(DocumentFragment)降低频繁的回流重绘;
  • 使用 CSS3 动画与 GPU 加速:利用 transform 和 opacity 替代 top/left 等属性,减少回流;
  • 代码分割与懒加载:对非首屏内容采用异步加载、按需加载或虚拟列表技术(virtual scrolling);
  • 服务端渲染(SSR)与预渲染:提高首屏加载速度,并通过 CDN 缓存静态资源来减少网络延迟。

单页应用(SPA)的架构设计与性能优化

对于大型SPA项目,通常采取以下措施:

代码分割与懒加载:利用 Webpack 的动态 import() 将路由组件、第三方库分离为独立的代码块,实现按需加载;

缓存策略:通过 Service Worker 实现离线缓存,同时利用 HTTP 缓存(强缓存、协商缓存)减少重复请求;

首屏优化:采用服务端渲染(SSR)或预渲染,确保首屏渲染时间尽可能短;
异步数据加载与骨架屏:在数据尚未加载完毕时展示骨架屏或占位符,提升用户体验。

前端架构与组件化开发

面对复杂的业务需求,倾向于构建模块化、组件化且易于维护的前端架构。思路主要包括:

模块化与组件化:采用 React 或 Vue 构建独立且复用性高的组件,每个组件封装独立业务逻辑和状态;

状态管理:根据项目规模选择合适的状态管理方案,如使用 Redux、MobX 或 Vuex,集中管理全局状态,同时保持局部组件状态的独立;

目录结构和分层设计:通过清晰的目录结构划分视图层、逻辑层与数据层,便于多人协作与代码复用;

设计模式和接口契约:在团队中推广使用设计模式(如 MVC、Flux)和明确的 API 文档,以降低耦合度和便于后期扩展。

这种架构不仅提高了开发效率,还在长期维护中证明了其良好的可扩展性与稳定性。

构建工具与工程化实践

主要配置与实践包括:
Loader 配置:使用 Babel-loader 转换 ES6+ 代码,并通过 cacheDirectory 缓存编译结果;

Tree Shaking 与代码分割:启用 ES6 模块特性以便 Webpack 能够进行 Tree Shaking,同时利用动态 import 进行路由级代码分割;

生产环境优化:利用 UglifyJS/terser 插件压缩代码、配置 long-term caching(例如给文件名添加 hash 值),以及使用 DllPlugin 提前打包第三方库;

开发体验:使用 webpack-dev-server 提供热更新、代理跨域请求,并结合 ESLint 等工具确保代码质量。

通过这些工程化实践,我能够大幅提升打包效率和运行性能,同时保证开发体验和代码维护性。

前端安全性

常见的前端安全问题主要包括 XSS(跨站脚本攻击)和 CSRF(跨站请求伪造)。在项目中采取了以下防范措施:

防止 XSS:对用户输入严格进行过滤和转义,利用成熟的库(如 DOMPurify)进行清洗,同时配置 Content Security Policy(CSP)限制外部脚本加载;

防范 CSRF:在关键请求中加入 CSRF Token(服务器下发并验证),并在 Cookie 中设置 HttpOnly 与 SameSite 属性;

点击劫持防护:通过设置 HTTP 头 X-Frame-Options 来防止页面被嵌入到 iframe 中;

其它措施:定期安全扫描、代码审查以及采用 HTTPS 加密传输,确保整体安全性。
通过以上综合手段,我能够有效降低前端安全风险,保护用户数据和系统安全。

JS 相关

数组去重

Array.from(new Set(list))
list.filter((e, i) => list.indexOf(e, i + 1) < 0)

防抖截流

const debounce = (cb, time) => {
    let timer
    return (...args) => {
        clearTimeout(timer)
        timer = setTimeout(() => {
            cb.apply(this, args)
        }, time)
    }
}

// 防抖示例:输入框联想搜索,只有停止输入 500ms 后才发送请求
const debouncedSearch = debounce(() => console.log('搜索请求'), 500);

const throttle = (cb, time) => {
    let timestamp = Date.now()
    return (...args) => {
        if (Date.now() - timestamp >= time) {
            cb.apply(this, args)
            timestamp = Date.now()
        }
    }
}
// 节流示例:监听滚动事件,每 200ms 执行一次回调
const throttledScroll = throttle(() => console.log('滚动处理'), 200);

链式调用

class LazyMan {
    constructor(name) {
        this.tasks = [];
        // 添加问候任务
        this.tasks.push(() => {
            console.log(`Hi! This is ${name}!`);
            this.next();
        });
        // 开始任务调度(延迟启动,确保链式调用已完成)
        setTimeout(() => this.next(), 0);
    }

    next() {
        const task = this.tasks.shift();
        if (task) task();
    }

    sleep(time) {
        this.tasks.push(() => {
            setTimeout(() => {
                console.log(`Wake up after ${time}`);
                this.next();
            }, time * 1000);
        });
        return this;
    }

    sleepFirst(time) {
        this.tasks.unshift(() => {
            setTimeout(() => {
                console.log(`Wake up after ${time}`);
                this.next();
            }, time * 1000);
        });
        return this;
    }

    eat(food) {
        this.tasks.push(() => {
            console.log(`Eat ${food}~`);
            this.next();
        });
        return this;
    }
}

new LazyMan("Hank").sleepFirst(2).eat("supper");
// 输出顺序:
// (等待2秒)
// Wake up after 2
// Hi! This is Hank!
// Eat supper~

实现链式求和函数(sum)

function sum(num) {
    let total = num;

    function inner(next) {
        total += next;
        return inner;
    }

    // 通过 valueOf 实现隐式转换
    inner.valueOf = function () {
        return total;
    };

    // 同时重写 toString 方便调试和显示
    inner.toString = function () {
        return total.toString();
    };

    return inner;
}

// 测试示例:
console.log(sum(1)(2)(3) + 0); // 输出 6
console.log(`Sum is ${sum(4)(5)(6)}`); // 输出 "Sum is 15"

实现 Promise.all

function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument must be an array'));
    }

    const result = [];
    let count = 0;
    const len = promises.length;

    // 若数组为空,直接 resolve 空数组
    if (len === 0) {
      return resolve(result);
    }

    promises.forEach((p, index) => {
      // 将非 Promise 值包装为 Promise.resolve
      Promise.resolve(p)
        .then(value => {
          result[index] = value;
          count++;
          if (count === len) {
            resolve(result);
          }
        })
        .catch(err => {
          // 一旦有一个 reject,则整体 reject
          reject(err);
        });
    });
  });
}

深拷贝

最简单的办法是使用 JSON.parse(JSON.stringify(obj))(但这种方法不适用于含有函数、Date、RegExp 等特殊对象);

function deepClone(obj, hash = new WeakMap()) {
    // 如果不是对象(包括 null),直接返回(基本类型)
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    // 处理 Date 对象
    if (obj instanceof Date) {
        return new Date(obj);
    }

    // 处理 RegExp 对象
    if (obj instanceof RegExp) {
        return new RegExp(obj.source, obj.flags);
    }

    // 如果已拷贝过,直接返回(处理循环引用)
    if (hash.has(obj)) {
        return hash.get(obj);
    }

    // 根据数组或对象创建新的实例
    const cloneObj = Array.isArray(obj) ? [] : {};
    // 将当前对象记录在 WeakMap 中
    hash.set(obj, cloneObj);

    // 遍历对象的自有属性进行拷贝
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            cloneObj[key] = deepClone(obj[key], hash);
        }
    }
    // 处理 Symbol 属性
    const symbols = Object.getOwnPropertySymbols(obj);
    symbols.forEach(sym => {
        cloneObj[sym] = deepClone(obj[sym], hash);
    });

    return cloneObj;
}

使用 WeakMap 而不是 Map 的主要原因是:

避免内存泄露:
WeakMap 对其键持有弱引用,不会阻止垃圾回收。深拷贝函数中用于存储已拷贝的对象时,使用 WeakMap 可以确保在对象不再被其他引用使用时,它们能被回收;而 Map 中的键引用是强引用,可能导致内存泄露。

键必须是对象:
在深拷贝过程中,我们只关心对象之间的引用关系。WeakMap 要求其键必须是对象,这正好符合深拷贝中记录循环引用的需求,同时也避免了误用基本数据类型作为键。

总之,WeakMap 更适合在深拷贝中用来存储源对象与其拷贝之间的映射,从而判断循环引用,同时又能在不再需要时自动释放内存。

拍平数组

function flattenDeep(arr) {
    if (!Array.isArray(arr)) return [arr]; // 如果传入的不是数组,则直接返回包含该值的一维数组

    let result = [];
    for (let item of arr) {
        if (Array.isArray(item)) {
            result = result.concat(flattenDeep(item)); // 如果 item 是数组,则递归扁平化,并连接结果
        } else {
            result.push(item); // 否则直接 push 到结果中
        }
    }
    return result;
}

// 测试示例
console.log(flattenDeep([1, [2, [3, 4], 5], 6])); // [1, 2, 3, 4, 5, 6]

实现 Memoize 函数

function memoize(fn) {
    const cache = new Map(); // 使用 Map 缓存结果

    return function (...args) {
        // 使用 JSON.stringify 作为 key(注意:如果参数顺序或对象顺序不一致可能有问题)
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            return cache.get(key);
        }
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

// 测试示例
function complexCalculation(a, b) {
    console.log('Computing...', a, b);
    return a + b;
}
const memoizedCalc = memoize(complexCalculation);
console.log(memoizedCalc(2, 3)); // 第一次调用:计算并输出 5
console.log(memoizedCalc(2, 3)); // 第二次调用:直接返回缓存结果 5,无计算日志

闭包作用、缺陷

作用:封装内部变量,防止全局变量污染

缺陷:内存泄漏

this 的指向

全局上下文:在浏览器中,全局作用域下的 this 指向 window。
函数调用:在普通函数调用中,this 的值取决于调用方式(严格模式下未绑定为 undefined,非严格模式下为 window);
对象方法:方法调用时,this 通常指向调用该方法的对象;
箭头函数:箭头函数没有自己的 this,它继承自外层词法作用域。
显式绑定:使用 call、apply 或 bind 可以手动指定 this 的指向。

事件循环(Event Loop)

JavaScript 是单线程语言,通过事件循环机制处理异步任务。
宏任务(如整体代码、setTimeout、setInterval)与微任务(如 Promise 的 .then()、MutationObserver)之间有严格的执行顺序:当前宏任务结束后,会先执行所有微任务,再开始下一个宏任务。

浏览器原理 & 性能优化

  1. 输入与 URL 解析阶段
  • 用户在地址栏输入内容后,浏览器首先判断输入的内容是一个合法的 URL 还是搜索关键字。
  • 如果是 URL,浏览器会进行格式补全(例如自动添加 “http://” 或 “https://”)。
  1. 缓存检查阶段
  • 在真正发起网络请求前,浏览器会先检查本地缓存(包括内存缓存、磁盘缓存以及 Service Worker 缓存等)。
  • 如果缓存命中且资源仍处于有效期(强缓存生效),浏览器直接使用缓存;否则进入协商缓存阶段,再决定是否需要重新下载资源。
  1. DNS 解析阶段
  • 若缓存未命中,则浏览器需要将 URL 中的域名解析为对应的 IP 地址。
  • 这一过程会依次查询浏览器自身的 DNS 缓存、操作系统的 DNS 缓存、路由器缓存和 ISP 的 DNS 服务器。
  1. TCP 连接阶段
  • 获得目标服务器的 IP 地址后,浏览器发起 TCP 连接,与服务器进行三次握手,确保连接的可靠性。
  1. TLS/SSL 握手阶段(适用于 HTTPS)
  • 如果使用 HTTPS,则在 TCP 连接建立后进行 TLS 握手,协商加密算法和密钥,建立安全通道。
  1. HTTP 请求发送阶段
  • 连接建立后,浏览器构造 HTTP 请求(包含方法、请求头、可能的请求体和缓存验证字段如 If-Modified-Since/If-None-Match),并发送给服务器。
  1. 服务器处理阶段
  • 服务器接收到请求后,根据业务逻辑处理请求,并生成 HTTP 响应。
  • 响应中包含状态码(如 200、304 等)、响应头(如 Cache-Control、Last-Modified、ETag 等)以及响应体(即资源内容)。
  1. HTTP 响应接收阶段
  • 浏览器接收服务器返回的响应:
  • 如果返回 304(Not Modified),说明资源未发生变化,浏览器会继续使用本地缓存。
  • 如果返回 200,则下载最新的资源,并根据响应头更新本地缓存。
  1. 文档传递与进程间通信(IPC)阶段
  • 在多进程架构(如 Chrome)中,网络进程将响应数据通过 IPC 传递给浏览器进程,再由浏览器进程传给渲染进程。
  1. HTML 解析与 DOM 树构建阶段
  • 渲染进程开始解析 HTML 字节流,通过分词器(Tokenizer)将代码拆分为标记(Token),并构建 DOM 树(文档对象模型),表示页面结构。
  1. CSS 解析与 CSSOM 构建阶段
  • 浏览器解析 CSS 文件,构建 CSSOM(CSS 对象模型),描述所有 CSS 规则和样式信息。
  1. 渲染树构建阶段
  • 浏览器将 DOM 树和 CSSOM 合并,生成渲染树(Render Tree),这棵树只包含页面中实际需要展示的节点及其样式。
  1. 布局(Reflow)阶段
  • 浏览器根据渲染树计算每个节点的几何信息(位置、尺寸等),确定各元素在页面上的实际布局。
  1. 绘制(Paint)阶段
  • 浏览器遍历布局树,将各元素绘制成像素信息,生成最终页面的位图。
  1. 合成(Composite)阶段
  • 如果页面有多个层(例如有 CSS 动画、滚动效果等),浏览器将各个层合成,最终输出到屏幕上。
  1. JavaScript 执行与交互阶段
  • 最后,页面中的 JavaScript 脚本开始执行,绑定事件、处理异步任务,进一步增强页面交互性和动态效果。
  1. 页面加载过程
  • 用户在地址栏输入 URL。
  • 浏览器检查缓存,若无则进行 DNS 解析获取服务器 IP。
  • 建立 TCP 连接(完成三次握手)。
  • 发送 HTTP 请求,服务器返回响应数据。
  • 浏览器解析 HTML,构建 DOM 树;同时解析 CSS 生成 CSSOM。
  • 构建渲染树(Render Tree),计算布局(Reflow),然后绘制(Repaint)。
  • 执行 JavaScript,并根据需要进行后续的重排重绘。
  1. 重排与重绘
  • 重排(Reflow/Layout):当元素的几何尺寸或位置发生改变时,浏览器重新计算元素位置,开销较大。
  • 重绘(Repaint):当元素的外观(如颜色)变化但尺寸不变时,仅重新绘制,开销较小。
  • 优化方法:减少 DOM 操作、合并多次修改、使用 CSS3 变换(如 translate、opacity)代替会引起重排的属性变化。
  1. 资源加载优化
  • 文件压缩和合并:压缩 HTML、CSS、JavaScript 文件,合并多个文件减少 HTTP 请求。
  • 使用 CDN:将静态资源部署在内容分发网络上,加速用户访问。
  • 懒加载和异步加载:对图片和非关键脚本延迟加载,减少初始加载时间。
  • 浏览器缓存:合理设置 HTTP 缓存头(如 Cache-Control、ETag)来利用缓存,减少重复请求。
  1. 缓存机制
  • 浏览器根据服务器响应头信息(例如 Cache-Control、Expires、ETag)判断资源是否有效,从而决定是否从缓存中读取。
  • 合理配置缓存可以大大提升用户体验,减轻服务器压力。

常见的内存泄漏原因

  • 全局变量滥用:

    将大量对象或数据挂载到全局作用域,导致这些对象长时间无法被回收。
  • DOM引用泄漏:

    当 JavaScript 对象不正确地持有 DOM 元素的引用,即使该元素已经从页面中移除,内存中依然保留着它,造成内存泄漏。
  • 闭包问题:

    闭包容易捕获外部变量,如果不小心,可能会让一些本该释放的对象持续存在于内存中。
  • 定时器和事件监听器未清理:

    使用 setInterval、setTimeout 或绑定事件后未及时清除,导致相关的对象无法被回收。
  • 缓存未及时清理:

    数据缓存策略不当,导致缓存中的数据不断累积,长时间占用内存。

解决策略:

  • 及时清理定时器和事件监听器: 在组件或页面销毁时,确保调用 clearInterval、clearTimeout,以及移除所有事件监听器。
  • 解除不必要的引用: 当 DOM 元素不再需要时,确保将其引用置为 null 或从对象中删除引用。
  • 优化闭包: 避免在闭包中引用大对象或不再需要的变量,尽可能减少闭包的作用范围。
  • 合理管理全局变量: 尽量避免将数据挂载在全局,采用局部作用域或模块化的方式组织代码。
  • 缓存管理: 定期清理或限制缓存数据的大小,确保不会因为缓存策略不当而导致内存占用过高。

React相关问题

React Fiber 与调度机制

React Fiber 是 React 16 引入的全新重构架构,目的是改善动画、布局、手势等任务的响应性。传统的同步渲染有可能导致长任务阻塞用户交互,而 Fiber 允许将渲染过程拆分成许多小任务,从而可以中断、延续执行,提高响应性。
内部原理:
Fiber 将组件树转化为一系列“工作单元”(Fiber 节点),每个节点代表一个组件的更新任务。
React 调度器会为不同任务分配不同优先级(例如用户输入、动画、数据更新),使得高优先级任务可以打断低优先级任务。
当一个任务被中断后,React 会保存当前状态,稍后恢复工作,直到所有工作单元完成后进入 Commit 阶段,将更新应用到真实 DOM。
Concurrent Mode:
结合 Concurrent Mode,React 可以在渲染期间允许更细粒度的中断和任务调度,从而使界面保持流畅。

Diff 算法与 Reconciliation 过程

Diff 算法:
React 通过对比新旧虚拟 DOM 树来计算最小变更量。其核心策略包括:
层级对比(Tree Diff): 仅比较同一层级的节点,不处理跨层级的节点移动。
元素对比(Element Diff): 利用每个元素的 key 来标识同一位置的节点,如果 key 相同,则 React 尽可能复用该节点;否则认为是全新节点,直接替换。
Reconciliation 过程:
当组件状态或 props 改变时,React 会生成新的虚拟 DOM 树。
React 对比新旧树(利用 diff 算法),找出需要更新的部分。
最后进入 Commit 阶段,根据差异对真实 DOM 进行最小化更新,从而大大降低更新成本。

Hooks 的实现原理与自定义 Hook

Hooks 原理:
Hooks(如 useState、useEffect)让函数组件拥有状态和副作用能力,其内部依赖于调用顺序不变的规则(Hook 调用必须在组件最顶层),React 内部会维护一个 Hook 链表来保存每个 Hook 的状态。
自定义 Hook:
自定义 Hook 就是一个 JavaScript 函数,它可以调用其他 Hook,并封装特定的业务逻辑,使得多个组件可以复用这一逻辑。
例如,一个简单的状态管理 Hook:

import { useState } from 'react';

function useStatefulHook(initialValue) {
  const [value, setValue] = useState(initialValue);
  const setValueWithCallback = newValue => {
    setValue(newValue);
    // 可在此执行额外操作,比如日志记录
  };
  return [value, setValueWithCallback];
}
export default useStatefulHook;

优点:
降低了组件之间逻辑复用的门槛,无需使用 HOC 或 Render Props 即可共享状态和副作用逻辑。

类组件生命周期替换

// componentDidMount
useEffect(() => {
  console.log('组件挂载完成');
}, []);

// componentDidUpdate
useEffect(() => {
  // 每次 count 更新后执行
  console.log(`count 更新为 ${count}`);
}, [count]);

// componentWillUnmount
useEffect(() => {
  return () => {
    console.log('组件将卸载');
  };
}, []);

// 数据缓存
let ref = useRef(0);
function handleClick() {
  ref.current = ref.current + 1;
  alert('你点击了 ' + ref.current + ' 次!');
}

组件性能优化

防止不必要的渲染:
React.memo(函数组件):对组件进行浅比较,只有当 props 发生变化时才重新渲染。
PureComponent(类组件):自动实现 shouldComponentUpdate 进行浅比较。
useMemo & useCallback: 分别缓存计算结果和函数引用,避免因每次渲染创建新引用。
性能排查:
使用 React Profiler 或浏览器的性能分析工具来检测渲染瓶颈。
对于列表渲染,确保使用稳定且唯一的 key 以防止不必要的重排。
其他建议:
避免在 render 中直接定义函数或内联对象,尽量在组件外部或使用 useCallback 缓存。

setState 的异步行为与批量更新

异步与批量更新:
在 React 的合成事件和生命周期方法(如 componentDidMount、componentDidUpdate)中,setState 会被批量处理,因此表现为异步更新。
而在原生事件(例如 DOM 事件监听器)和 setTimeout 中,setState 通常是同步执行的。
工作原理:
React 会将多个 setState 调用合并成一次更新,保证在一次渲染周期内只进行一次 DOM 更新,从而提高性能。
如果需要在更新后获取最新 state,可使用 setState 的第二个参数(回调函数)。

this.setState({ count: this.state.count + 1 }, () => {
  console.log('更新后的值', this.state.count);
});

错误边界 (Error Boundaries)

定义与作用:
错误边界是用来捕获其子组件渲染过程中、生命周期方法或构造函数中抛出的错误,从而防止整个组件树崩溃。
实现方式:
通过在类组件中实现静态方法 static getDerivedStateFromError(error)(用于更新状态以显示备用 UI)和实例方法 componentDidCatch(error, info)(用于记录错误日志)。

class ErrorBoundary extends React.Component {
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    console.error(error, errorInfo); // 可以记录错误信息到日志服务
  }
  render() {...}
}

应用场景:
在关键业务组件周围使用错误边界,防止整个页面因个别组件错误而崩溃。

服务端渲染(SSR)与数据预取

SSR 基本流程:
在服务器上使用 ReactDOMServer(如 renderToString 或 renderToStaticMarkup)将 React 组件渲染成 HTML 字符串,然后发送给客户端。
这样可以提高首屏加载速度和 SEO 效果。
数据预取与同步:
SSR 中通常需要在渲染前完成数据预取(例如通过调用 API),将数据注入组件,然后在客户端完成 hydration(状态接管)。
解决数据不一致问题的方法有:在 HTML 中嵌入预取数据,客户端启动时读取该数据,从而避免重复请求。
框架支持:
如 Next.js 等框架封装了 SSR、数据预取及客户端状态同步的流程,开发者可以更专注于业务逻辑。

受控组件与非受控组件

受控组件:
其输入状态完全由 React 组件的 state 控制,表单数据通过 props 传递并通过 setState 更新。
优点:状态集中管理、易于验证和调试;缺点:代码较繁琐,可能导致频繁重渲染。
非受控组件:
依赖于 DOM 自身的状态(使用 ref 访问),更接近传统 HTML 表单操作。
优点:实现简单,对简单表单场景适用;缺点:难以集中管理和验证数据。

高阶组件 (HOC) 与 Render Props

高阶组件 (HOC):
是一个接受组件作为参数并返回增强组件的函数,主要用于逻辑复用。例如封装权限验证或日志记录。
优点:清晰地将公共逻辑抽离出来;缺点:可能导致嵌套层级过深(wrapper hell)。
Render Props 模式:
通过将一个函数作为 prop 传递给组件,允许在渲染时动态决定 UI 的输出,从而实现逻辑共享。
优点:灵活性高;缺点:同样可能导致嵌套结构复杂。
Hooks 的出现:
现在大部分复用逻辑可以用自定义 Hook 解决,从而降低嵌套深度,提高代码可读性。

状态管理架构设计

思路:
在大型项目中,状态管理需要解决多个组件间数据共享、数据同步和全局状态控制问题。
常用方案:
Redux: 单一 store、纯 reducer、严格单向数据流,便于调试和时间旅行。
MobX: 基于响应式编程,状态变化自动触发组件更新,语法更简洁但调试难度可能较高。
Context API 与 Hooks: 对于局部状态共享非常适合,可避免 props drilling,但不适合非常频繁更新的场景。
设计注意:
将复杂状态逻辑拆分到多个模块,结合中间件(如 redux-thunk 或 redux-saga)处理异步操作。
通过“容器组件”与“展示组件”分离来降低耦合。

Suspense 与代码拆分

Suspense 机制:
结合 React.lazy 可以实现组件级别的懒加载,在组件尚未加载完成时显示一个备用的 loading UI。
语法示例:

const LazyComponent = React.lazy(() => import('./LazyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

代码拆分:
通过动态 import,将应用拆分为多个 chunk,从而减少初始加载体积,提高页面响应速度。
注意:Suspense 在错误处理和 SSR 上有一定局限性,需要额外的解决方案(例如 Next.js 提供更完善的支持)。

Refs 与组件实例访问

用途:
Refs 允许直接访问 DOM 节点或组件实例,常用于第三方库集成、动画处理、元素尺寸测量、管理焦点等场景。
实现方式:
在类组件中,可通过 React.createRef() 创建 ref,并在组件挂载后通过 this.myRef.current 获取 DOM;
在函数组件中,则结合 useRef 钩子使用。如果需要将 ref 转发给子组件,则使用 forwardRef。
注意事项:
避免过度依赖 refs,尽量保持声明式编程风格。
尽量不要在 render 中直接操作 DOM,以免破坏 React 的更新流程。

Vue 相关问题

Vue 响应式原理

在 Vue 2 中,Vue 会在实例化阶段遍历 data 对象,对每个属性通过 Object.defineProperty 添加 getter 和 setter。当组件在渲染期间访问数据时,getter 会将当前正在执行的 Watcher(如渲染 Watcher、计算属性 Watcher、用户自定义 Watcher)收集到对应属性的依赖中;而当数据发生变化时,setter 会触发依赖通知(通过 Dep.notify()),从而使得相关的 Watcher 重新计算并更新视图。

Vue 3 则采用了 JavaScript 的 Proxy,通过代理整个对象来捕捉对任意属性的读写操作,从而实现更全面、更高效的响应式,同时也能解决 Vue 2 无法检测新增属性或数组索引变更的问题。

虚拟 DOM 与 Diff 算法

Vue 将模板编译为渲染函数,渲染函数生成一个轻量的 JavaScript 对象树,即虚拟 DOM。当数据更新时,Vue 会重新生成新的虚拟 DOM 树,并使用 Diff 算法对比新旧虚拟 DOM 的差异。该算法利用节点的标识(如 key)和树结构的局部性,快速确定最小化的 DOM 变更(如新增、删除或修改节点),最后通过 patch 操作将这些差异映射到真实 DOM 上,避免了直接操作 DOM 带来的性能开销。

大型应用性能优化

代码分割与懒加载:使用 Webpack 等工具对应用进行代码分割,仅在需要时加载对应模块或路由组件。

  • 虚拟滚动:对于长列表等数据量巨大的场景,采用虚拟滚动技术仅渲染可视区域的 DOM。
  • 缓存与 keep-alive:对频繁切换的组件使用 keep-alive 缓存组件状态,减少重复渲染。
  • 优化依赖与计算属性:合理使用 computed 缓存,避免不必要的 watcher 触发;对大数据集合使用节流或防抖策略。
  • 减少重排与重绘:通过合并 DOM 更新、使用 CSS3 动画(如 transform、opacity)代替 top/left 等属性,降低浏览器重排代价。
  • 组件组织与复用
  • 在大型项目中,通常采用单文件组件(.vue)来封装模板、逻辑和样式;利用 mixins 或 Vue 3 的 Composition API 将可复用逻辑提取出来;同时通过局部或全局组件注册管理组件层级结构;对于公用功能,还可以通过插件机制扩展 Vue 实例,保证代码的模块化和可维护性。

状态管理与 Vuex 应用

Vuex 提供了集中式的状态管理,将应用状态存储在一个全局的 Store 中,通过 mutations 同步更新状态,actions 处理异步操作,并结合 getters 提供派生状态。在实际项目中,我会使用模块化(modules)方式将状态按业务领域拆分,确保各模块之间的隔离与独立。同时,还会注意避免直接修改状态(必须通过 commit),并对异步操作合理使用 thunk 来保证数据更新的可追踪性和可测试性。

服务端渲染(SSR)

服务端渲染通过在服务器端预先渲染 Vue 组件,生成 HTML 后发送给客户端,从而提高首屏渲染速度和 SEO 效果。常用方案包括使用 Nuxt.js 或 Vue 官方的 vue-server-renderer。需要注意的点有:确保代码中没有直接操作 DOM;处理异步数据获取与预取;在客户端进行“hydration”以绑定事件;同时考虑缓存策略以提升性能。

项目迁移与升级

将 Vue 2 项目迁移到 Vue 3 时,首先应使用官方的迁移工具和兼容性构建,以逐步适配新特性。主要挑战包括:

重构部分使用 Object.defineProperty 的响应式实现为 Proxy;
处理第三方库兼容性问题;

  • 逐步引入 Composition API 以替换部分 Options API 逻辑;
  • 严格测试各模块以确保无破坏性变更,最终实现平滑升级。
  • TypeScript 在 Vue 项目中的应用
  • TypeScript 为 Vue 项目带来静态类型检查,提升代码可维护性和可读性。通过 Vue CLI 或 Vite 创建 TypeScript 项目,可以在组件中为 props、data、computed、methods 等添加类型定义。挑战在于需要正确配置 tsconfig.json、处理 Vue 特有的类型(例如 Vue.extend、this 的类型推导),以及一些模板内类型声明问题,但总体能大大降低运行时错误风险,并增强 IDE 的代码补全和重构能力。

异步更新与 nextTick

Vue 在数据变更后不会立即更新 DOM,而是将所有变更缓冲到异步更新队列中,等到当前同步任务执行完毕后,再统一刷新 DOM,从而提高性能。nextTick 提供了一个回调,在 DOM 更新完成后执行。它内部通常利用 Promise.then、MutationObserver 或 setImmediate 来安排微任务或宏任务,以确保在更新队列刷新后调用回调,这对于需要在数据更新后立即操作 DOM 的场景非常有用。

组件间通信与架构设计

  • 父子组件:通过 props 向子组件传递数据,通过事件($emit)让子组件通知父组件;
  • 跨级通信:使用 provide/inject 机制,可以让祖先组件向后代传递数据而不需要层层传递;
  • 兄弟组件:可使用全局事件总线(EventBus),但在大型项目中推荐使用 Vuex(或 Pinia)集中管理状态;
  • 架构考量:选择通信方式时需考虑数据的流向、更新频率以及模块之间的耦合度。比如,使用 Vuex 可以让数据更新变得可预测和可追踪,但可能增加初期架构复杂性;而在简单场景下,直接使用 props 和 emit 更为直观。选择最合适的方案,往往取决于项目规模和团队的开发习惯。

其他问题

js原生数据类型有哪些

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它属于 JavaScript 语言的原生数据类型之一,其他数据类型是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。

跨页面传参

url,localStoreage/sessionStoreage, cookie

http有几种请求

post、get、put、delete、option、head

.map文件原理

sourcemap映射行、列、文件路径,用Base64 VLQ进行编码

vue

获取dom: this.$ref
生命周期及使用场景
不用vuex跨组件传参
父子组件传参、修改:props,$emit
全局/局部组件注册: Vue.component
指令有什么
v-if/v-show区别: 渲染成本、生命周期
路由有几种: hash/history

posted @   _NKi  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示