70. 渐进增强与优雅降级的区别是什么?
渐进增强:核心思路是首先针对低版本浏览器构建页面,确保最基本的功能可以正常运行。在此基础上,再针对支持高级特性的现代浏览器,逐步增强页面的视觉效果和交互体验。
优雅降级:则恰恰相反。它的起点是基于高级浏览器构建完整、优秀的功能和体验。然后再为低版本或老旧的浏览器提供兼容性处理,虽然体验会有所降低,但核心功能应保证可用。
71. 为什么利用多个域名存储网站资源更有效?
这主要有以下四个方面的好处:
- 节省主域名连接数,提升响应速度:浏览器对同一域名的并发请求数有限制。将静态资源(如图片、JS、CSS)分散到不同的域名下,可以突破这个限制,并行下载更多资源,从而加快页面整体加载速度。
- 突破浏览器并发限制:正如第一点所述,这是提高加载效率的关键技术手段。
- 方便CDN缓存与服务器分离:将动态请求和静态请求分别放在不同的服务器或域名下,管理起来更加清晰,也便于为静态资源配置独立的、高效的CDN缓存策略。
- 避免不必要的安全问题:例如,将用户上传的内容(可能包含恶意脚本)放在独立的域名下,可以防止其窃取主站域名下的Cookie等敏感信息,实现安全隔离。
72. 如何优化大型电商网站的大量图片加载?
面对图片多、加载慢的挑战,可以针对不同场景采用不同的优化策略:
- 图片懒加载:对于超出首屏可视区域的大量图片,这是首选方案。监听页面滚动事件,当图片即将进入可视区域时,再动态加载其
src。这能显著减少初始页面的请求数和数据量。
- 图片预加载:对于像幻灯片、相册这类需要连续查看的图片,可以在展示当前图片时,默默加载其前一张和后一张图片,这样用户在切换时就能获得无缝的体验。
- CSS Sprite(雪碧图):将页面中许多小图标、Logo、背景图等合并到一张大图中,通过
background-position来定位显示。这能将大量小图片的HTTP请求合并为少数几个,减少请求开销。
- 使用渐进式图片或缩略图:对于大图,可以先加载一个经过高度压缩的模糊缩略图,让页面快速呈现布局,然后再逐步加载完整清晰的原图,提升用户感知速度。
73. 从输入URL到页面加载完成,发生了什么?
这个过程通常被称为“关键渲染路径”,可以概括为以下几个核心步骤:
- DNS解析:浏览器首先解析域名,查找对应的IP地址。它会依次检查浏览器缓存、系统缓存、路由器缓存、ISP DNS缓存,如果都没有,则发起递归查询。
- 建立TCP连接:浏览器获得IP后,与目标服务器通过TCP三次握手建立可靠的连接。
- 发送HTTP请求:连接建立后,浏览器向服务器发送一个HTTP GET请求,请求指定的资源。
- 服务器处理并响应:服务器接收到请求,处理并找到资源,然后通过同一个TCP连接发回HTTP响应(包含状态码、响应头和响应体)。
- 关闭TCP连接:对于HTTP/1.0或非持久连接,数据传送完毕后会通过四次挥手关闭TCP连接。HTTP/1.1默认使用持久连接。
- 浏览器解析渲染:浏览器接收到响应数据后,开始解析HTML构建DOM树,解析CSS构建CSSOM树,合并成渲染树,计算布局,最后绘制像素到屏幕上。同时会并行解析并执行JavaScript。
74. 前端如何进行登录身份的判断?
一个典型的基于Token的前端登录验证流程如下:
- 用户提交登录信息,前端发送请求到后端。
- 后端验证成功后,生成一个Token并返回给前端。
- 前端将收到的Token存入
localStorage或sessionStorage中。
- 后续所有需要身份验证的API请求,前端都在请求头(如
Authorization)中携带此Token。
- 后端中间件拦截请求,验证Token的有效性和过期时间。
- 如果Token过期,后端返回特定的状态码(如
401)。
- 前端接收到过期状态后,自动清除本地存储的Token,并跳转回登录页面。
75. 电商项目跟其它项目有什么不同?
电商项目的核心差异在于其强交易属性。一切功能都围绕“商品展示 -> 购物决策 -> 支付成交”这个核心链路展开。因此,它对支付环节的安全性、稳定性和用户体验要求极高,对商品搜索、推荐算法、库存管理、订单系统的复杂度也远超内容型或工具型项目。
而其他类型的网站,如新闻门户、企业官网、后台管理系统等,则更侧重于内容的组织、展示、管理以及特定业务流程的实现。
76. 实践题:线上故障分析与解决思路
面对集中上线后出现的各种问题(502、登录失败、重复提交、白屏),我的分析如下:
- 502错误与登录失败:这通常是服务器在高并发下不堪重负的表现。上午8点学生集中访问,瞬间流量激增,可能导致后端应用服务(如PHP-FPM)的
worker进程全部被占用,或数据库连接池耗尽。解决方案包括:横向扩容服务器、优化数据库查询、引入缓存(如Redis)、对非核心功能进行降级或限流。
- 提交数据反复:这常由前端防重复提交机制缺失或后端并发控制不当引起。用户可能因网络延迟多次点击提交按钮。解决方案是:前端点击后禁用按钮或显示加载态;后端接口设计成幂等的,或使用数据库唯一约束、分布式锁来防止数据重复写入。
- 白屏现象:这是典型的单页面应用(SPA)首屏加载问题。巨大的JavaScript包在下载、解析、执行完成前,页面是空的。优化方案有多样选择,各有优劣:
- SSR服务端渲染:在服务端生成完整HTML,有效解决白屏和SEO问题,但增加了服务器压力和开发复杂度。
- 预渲染 (Prerendering):构建时针对特定路由生成静态HTML,适用于变化不频繁的页面。
- 骨架屏 (Skeleton Screen):在内容加载前先展示页面的大致结构,提升用户等待时的感知体验,实现成本相对较低。
77. 你在项目开发中遇到过哪些挑战?
挑战一:Vue移动端APP的列表缓存优化
在开发一个菜谱APP时,左侧品类切换会导致右侧列表频繁重新请求接口,页面闪烁,体验差。我通过Vuex实现了缓存:以品类ID为key,将列表数据对象存储在Vuex中。切换时,先检查缓存是否存在,存在则直接使用,否则再请求。关键在于,当退出页面或在特定时机(如使用keep-alive时的deactivated钩子)需要手动清空缓存,以确保下次进入时数据能更新。
挑战二:百万级关键词的高亮性能瓶颈
需求是为文本中大量用户配置的关键词添加高亮。最初简单的遍历匹配在数据量达百万级时导致页面卡死。我通过将关键词列表构建成字典树 (Trie Tree) 来进行优化。字典树能在O(n)时间复杂度内完成单次文本的扫描匹配,将原本数十分钟的渲染时间缩短到1秒以内,性能提升巨大。
挑战三:封装满足个性化需求的小程序组件
需求是一个带动效的Tab切换组件,而现有UI库组件样式和交互固化。我选择对小程序原生swiper和自定义导航栏进行二次封装。使用flex布局实现动态头部,利用slot插槽注入不同内容,并通过wx.getSystemInfo动态计算swiper高度。这个过程让我深入理解了原生组件的特性与限制。
挑战四:应对“难用”的第三方库或API
在小程序项目中使用新的音频API时,官方文档模糊,坑点很多。我的解决路径是:1)仔细阅读文档,从字里行间推断;2)查看官方示例和社区讨论;3)若无法解决,将问题抽象成一个最小可复现的Demo,去技术社区(如云栈社区)提问或向有经验的开发者请教,最终定位到API的正确使用方式。
78. 项目研发流程中前端扮演什么角色?
前端开发常常自嘲是“团队核心废物”,但这恰恰说明了其核心枢纽的角色。前端是用户与产品、设计与技术、业务与数据的交汇点。
- 与UI沟通:不只是被动实现设计稿,更要主动沟通技术可行性。例如,对于图表组件,可以建议UI基于ECharts的默认样式进行设计,避免天马行空导致无法实现。
- 与后端沟通:联调前必须吃透业务逻辑,明确数据格式和边界。清晰界定前后端职责,避免因需求不清被“踢皮球”。
- 与产品经理沟通:需要将产品的大方向需求,拆解成技术上无歧义的详细逻辑。例如,“用户提交作品审核”这个需求,就必须追问:提交后能否编辑?审核不通过是否可再次提交?是否需要版本记录?
- 与测试沟通:需要耐心复现和定位Bug,明确是前端逻辑问题、后端数据问题还是需求理解偏差。
因此,一个优秀的前端开发者,必须是优秀的沟通者、翻译者和协调者。
79. 哪些项目可以继续优化?为何没做?
我曾负责一个Vue动态官网项目,后期客户要求做SEO。当时采取的应急方案是在index.html里写死了<meta keywords>和<meta description>标签。虽然单个网站生效了,但该项目对应多个官网,导致其他网站的元数据被污染,这显然是不科学的。
理想的优化方案是采用服务端渲染。但这在项目后期重构成本极高,几乎等于重写。因此,在人力物力有限的情况下,折中方案是:除了简单的Meta标签,还应生成XML Sitemap、使用语义化HTML标签、合理规划内容结构等,这些都是对搜索引擎更友好的做法。
80. 平时写项目总结吗?总结哪些内容?
是的,我会坚持做项目总结,主要记录以下几类内容:
- 技术难点与解决方案:记录遇到的问题、排查思路和最终解法。例如,在Taro小程序项目中封装滚动筛选组件时,遇到
scroll-into-view的ID不能以数字开头、样式必须严格按文档设置等“坑”,都会详细记下。
- 项目规范与架构:总结本次项目在代码规范、目录结构、打包配置、UI组件使用等方面的实践。
- 业务逻辑与接口:梳理自己负责模块的业务流程、联调的接口及其数据格式。
- 完成的需求清单:简明扼要地列出自己实现的功能点,便于汇报和复盘。
81. 请绘制登录场景的业务流程图。
以React后台管理系统为例,一个典型的登录与鉴权流程如下:
- 用户访问登录页,输入账号密码。
- 前端校验格式后,调用登录接口。
- 后端验证成功,返回Token及用户角色/权限信息。
- 前端存储Token(如Redux或localStorage),并根据权限信息动态生成路由菜单。
- 用户跳转至主页,后续每次请求在Header中携带Token。
- 后端中间件验证Token,并检查请求路径是否在该用户角色权限内。
- 前端通过路由守卫,在每次路由跳转前检查目标路由是否在用户权限列表中,无权限则拦截。
82. 解决History模式下页面刷新404问题
这是Vue Router或React Router在history模式下部署到服务器的经典问题。当你在地址栏直接输入www.abc.com/layout并回车时,这个请求会直接发给服务器,而服务器上并没有名为layout的真实文件或路由,因此返回404。
解决方法:需要配置服务器,将所有非静态文件的请求,都重定向到应用的入口文件(如index.html)。以Nginx为例:
location / {
try_files $uri $uri/ /index.html;
}
这样,服务器找不到/layout这个文件时,就会返回index.html,然后由前端路由库(Vue Router/React Router)来解析/layout路径并渲染正确的组件。
83. 项目中的接口、文档、代码与发布流程
- 接口定义:应由后端主导。后端根据业务逻辑和数据模型设计接口草案,然后前后端一起评审,前端从使用角度提出建议(如字段命名、数据格式、是否冗余)。核心在于充分沟通和熟悉业务。
- 文档管理:公司文档(如需求文档、设计稿、API文档)通常使用在线协作工具(如Confluence、语雀、飞书文档)进行集中管理,确保信息同步。
- 代码上传与发布:
- 开发者从
master或main分支拉取特性分支开发。
- 开发完成后,提交Pull Request或Merge Request。
- 同事进行代码评审(Code Review)。
- 评审通过后,合并到开发或测试分支,触发CI/CD流水线进行自动化构建和测试。
- 测试通过后,由运维或通过自动化流程将代码部署到预发布/生产环境。整个过程使用Git进行版本控制。
84. 初级、中级与高级前端工程师的区别
- 初级工程师:核心是“实现”。能熟练使用HTML、CSS、JavaScript完成静态页面和基本交互,了解响应式布局,会使用jQuery、Bootstrap等库,掌握Ajax与后端通信。主要任务是按照设计稿和需求,准确无误地实现功能。
- 中级工程师:核心是“工程化与解决方案”。熟练掌握至少一门主流框架(Vue、React、Angular)及其生态(路由、状态管理)。具备前端工程化能力(Webpack/Vite配置、代码规范、性能优化)。能独立负责一个业务模块的开发,并考虑组件的复用性和可维护性。
- 高级工程师:核心是“架构、赋能与深度”。能从架构层面设计复杂的前端应用(微前端、模块化、状态体系)。对浏览器原理、网络协议、编译原理等底层知识有深入理解。能主导技术选型、性能瓶颈攻关和复杂问题解决。同时,关注团队提效(搭建工具链、沉淀组件库)、业务赋能(技术驱动业务创新)以及跨部门协调推动。
85. 使用ECharts与Highcharts遇到的问题及解决
在React项目中集成ECharts时,需要注意几点:
- 初始化时机:ECharts实例化是DOM操作,必须在组件挂载完成后进行。在React函数组件中,应将其放在
useEffect钩子中执行。
- 容器与样式:必须为图表容器
<div>设置明确的宽高,否则图表无法显示。
- 数据动态更新:通常结合
useEffect处理。一个useEffect负责在组件挂载时初始化图表实例;另一个useEffect在依赖数据变化时,调用实例的setOption方法更新图表。
- 性能优化:对于频繁更新的图表,可以使用
useRef来持久化图表实例,避免重复初始化。大量图表时,应抽象出统一的options生成器来管理复杂的配置逻辑。
import React, { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
function ChartComponent({ data }) {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
// 初始化图表
chartInstance.current = echarts.init(chartRef.current);
// 组件卸载时销毁实例,防止内存泄漏
return () => {
chartInstance.current?.dispose();
};
}, []);
useEffect(() => {
// 数据变化时更新图表
if (chartInstance.current && data) {
const options = generateOptions(data); // 你的options生成函数
chartInstance.current.setOption(options);
}
}, [data]);
return <div ref={chartRef} style={{ width: '100%', height: '400px' }} />;
}
86. Git工作流简介
以下是基于功能分支的一个常见Git工作流示例:
- 克隆项目:
git clone <repository-url>
- 基于测试分支创建本地开发分支:
git checkout -b dev origin/test # 拉取远程test分支并创建本地dev分支
- 创建功能分支进行开发:
git checkout -b feature-xxx
- 开发与提交:
git add .
git commit -m "feat: add xxx feature"
- 推送到远程并申请合并:将
feature-xxx分支推送到远程仓库,然后创建Pull Request (PR) 或 Merge Request (MR),请求合并到test分支。
- 代码评审与测试:团队进行Code Review,评审通过后合并到
test分支,触发自动化测试和部署到测试环境。
- 上线:测试通过后,再次创建PR,将
test分支合并到main/master主分支,并打上版本标签,部署到生产环境。
87. 最近项目中你负责什么?
(此题为开放回答,需结合自身经历。示例结构如下:)
在最近的XXX后台管理系统中,我主要负责用户权限模块和数据分析报表模块。
- 用户权限模块:我设计了基于角色(RBAC)的动态路由和菜单权限方案。前端通过接口获取用户权限列表,利用路由守卫过滤生成可访问的路由表,并递归渲染出侧边栏菜单。同时,封装了权限指令,用于控制页面内按钮级别的显示与隐藏。
- 数据分析报表模块:我使用ECharts封装了通用的图表组件,并抽象了
options配置生成器,使业务方只需关注数据格式。针对大数据量的表格,我实现了虚拟滚动和前端分页,确保页面流畅。同时,与后端协作设计了报表数据的缓存策略,提升了多次查询的响应速度。
88. 如何判断开发环境与生产环境?
在Node.js和Webpack构建体系中,通常通过环境变量NODE_ENV来区分。
- 使用
cross-env跨平台地设置环境变量(在package.json中):
{
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve --config webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.prod.js"
}
}
- 在Webpack配置文件中读取该变量:
const isProduction = process.env.NODE_ENV === 'production';
- 根据
isProduction变量,决定是否启用代码压缩、提取CSS、生成Source Map等优化行为。
89. Vue如何实现未登录重定向?
通过路由守卫实现。首先在路由定义中为需要认证的路由添加元信息:
// router.js
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true } // 标记该路由需要认证
}
];
然后在全局前置守卫中进行判断:
// main.js 或 router.js
import router from './router';
router.beforeEach((to, from, next) => {
// 1. 判断目标路由是否需要认证
if (to.matched.some(record => record.meta.requiresAuth)) {
// 2. 检查用户是否已登录(例如检查本地是否有token)
const isAuthenticated = localStorage.getItem('token'); // 仅为示例
if (!isAuthenticated) {
// 未登录,重定向到登录页
next({ path: '/login', query: { redirect: to.fullPath } });
} else {
// 已登录,放行
next();
}
} else {
// 不需要认证的路由,直接放行
next();
}
});
90. Vue项目常见优化点
1. 打包体积与首屏加载优化
2. 代码层面优化
- 合理使用
computed和watch:computed用于依赖其他数据的计算,有缓存;watch用于数据变化需要执行异步或复杂操作时。
v-if与v-show:频繁切换用v-show,运行时条件很少改变用v-if。
v-for的key与避免和v-if共用:为v-for提供唯一的key,且不要在同一节点上使用v-if(v-for优先级更高)。
- 图片压缩:使用
image-webpack-loader等工具在构建时压缩图片。
- 避免内存泄漏:及时在
beforeUnmount/unmounted生命周期中清除定时器、事件监听器、第三方库实例。
3. 运行时优化
- 组件懒加载:对于复杂组件,使用
defineAsyncComponent进行异步加载。
- 长列表性能:使用
vue-virtual-scroller等库实现虚拟滚动。
91. JavaScript异步解决方案有哪些?
-
回调函数 (Callback):最原始的方式,但容易导致“回调地狱”。
fs.readFile('file.txt', 'utf8', function(err, data) {
if (err) throw err;
console.log(data);
});
-
Promise:ES6引入,提供了.then()和.catch()的链式调用,解决了回调地狱,但链式调用仍然繁琐。
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
-
Generator + Co:ES6引入,可以暂停执行函数,需要配合执行器(如co库)。
function* gen() {
let result = yield fetch('/api/data');
console.log(result);
}
// 需要co(gen())这样的执行器来运行
-
Async/Await:ES2017引入,Generator的语法糖,是当前最优雅的解决方案。它以同步的方式写异步代码。
async function getData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
92. 移动端点击300ms延迟及解决方案
原因:早期移动端浏览器为了区分“单击”和“双击缩放”操作,在点击后会等待约300ms,看用户是否会进行第二次点击。
解决方案:
- 禁用缩放:通过
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">禁止用户缩放,浏览器就不再需要等待判断双击。但牺牲了可访问性。
- 使用FastClick库:原理是在
touchend事件发生时,立即模拟一个click事件并阻止300ms后真正的浏览器click事件。这是最通用的解决方案。
- CSS属性
touch-action: manipulation:现代浏览器支持,告诉浏览器该元素上的触摸操作只用于滚动和持续缩放,可以消除点击延迟。
93. 如何实现函数的柯里化?如 add(1)(2)(3)
柯里化是将一个多参数函数转化为一系列单参数函数的过程。实现一个通用的add函数如下:
方法一:参数聚合
function add(...args1) {
let allArgs = [...args1];
function fn(...args2) {
allArgs = [...allArgs, ...args2];
return fn; // 返回函数本身,支持继续调用
}
fn.valueOf = function() { // 或 fn.toString
return allArgs.reduce((sum, cur) => sum + cur, 0);
};
return fn;
}
// 利用隐式类型转换触发 valueOf
console.log(add(1)(2)(3) + 0); // 6
console.log(add(1, 2)(3) + 0); // 6
方法二:利用闭包保存参数,固定参数长度
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
94. 什么是反柯里化 (Uncurrying)?
反柯里化与柯里化相反,它扩大了一个方法的使用范围。让一个对象可以使用原本不属于它的方法。
一个简单的实现示例:
Function.prototype.uncurrying = function() {
const self = this; // 这里的this是要被“借用的”方法,比如 Array.prototype.push
return function(...args) {
// args[0] 是调用对象,其余是参数
return self.apply(args[0], args.slice(1));
};
};
// 使用
const push = Array.prototype.push.uncurrying();
const obj = { length: 0 };
push(obj, 'first', 'second');
console.log(obj); // {0: 'first', 1: 'second', length: 2}
这样,普通的对象obj就可以“借用”数组的push方法了。
95. 如何避免回调地狱?
除了上一问提到的Promise和Async/Await,还有:
-
Promise化 (Promisify):将基于回调的API封装成返回Promise的函数。
const fs = require('fs').promises; // Node.js v10+
// 或手动封装
function readFilePromise(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
-
使用Async/Await:这是目前最清晰、最易读的解决方案,它能以近乎同步的代码结构处理异步流程,从根本上避免了嵌套。
96. 常见的内存泄漏场景有哪些?
- 意外的全局变量:在函数内未使用
var、let、const声明变量,或给window的属性赋值。
function leak() {
leakedVar = 'I am global'; // 糟糕!成了全局变量
window.tempData = hugeData; // 同样糟糕
}
- 被遗忘的定时器或回调:设置了
setInterval或事件监听器,但在组件销毁时未清除。
// Vue/React 组件中
mounted() {
this.timer = setInterval(() => {...}, 1000);
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() { // 必须清理!
clearInterval(this.timer);
window.removeEventListener('resize', this.handleResize);
}
- 脱离DOM的引用:在JavaScript中保存了对某个DOM元素的引用,即使该元素已从页面上移除,也无法被垃圾回收。
const elements = { button: document.getElementById('myButton') };
// 即使从DOM中移除了 #myButton,elements.button 仍然引用着它
- 闭包:如果闭包中引用了外部变量,且该闭包被长期持有(如挂在全局),那么外部变量也不会被释放。
97. 常见的浏览器兼容性问题及封装
以下是一些经典兼容性问题的处理代码片段:
// 1. 获取滚动条位置
function getScrollTop() {
return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
}
// 2. 事件对象与目标元素
function getEventTarget(event) {
event = event || window.event;
return event.target || event.srcElement;
}
// 3. 阻止事件冒泡
function stopPropagation(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true; // IE
}
}
// 4. 阻止默认行为
function preventDefault(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false; // IE
}
}
// 5. 添加事件监听 (简易版)
function addEvent(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + type, handler);
} else {
element['on' + type] = handler;
}
}
// 6. 获取计算样式
function getComputedStyle(element, prop) {
if (window.getComputedStyle) {
return window.getComputedStyle(element, null)[prop];
} else {
return element.currentStyle[prop]; // IE
}
}
98. 如何在离开A页面时暂停其定时器?
在Vue或React等SPA中,当组件销毁时,必须清理其创建的资源。
Vue 选项式API:
export default {
data() {
return {
timer: null
};
},
mounted() {
this.timer = setInterval(() => {
console.log('tick');
}, 1000);
},
beforeUnmount() { // 或 beforeDestroy (Vue 2)
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
};
Vue 组合式API / React:
// Vue 3 with Composition API
import { onMounted, onUnmounted } from 'vue';
setup() {
let timer = null;
onMounted(() => {
timer = setInterval(() => { console.log('tick'); }, 1000);
});
onUnmounted(() => {
clearInterval(timer);
});
}
// React with Hooks
import React, { useEffect } from 'react';
function ComponentA() {
useEffect(() => {
const timer = setInterval(() => { console.log('tick'); }, 1000);
// 清理函数会在组件卸载时执行
return () => clearInterval(timer);
}, []);
return <div>Component A</div>;
}
99. 什么是深拷贝?项目哪里用到了?
浅拷贝只复制对象的第一层属性。如果属性是引用类型(如对象、数组),拷贝的是引用地址,新旧对象会共享这个引用。
深拷贝会递归复制对象的所有层级,创建一个完全独立的新对象,新旧对象互不影响。
项目应用场景:
- 状态管理:在Vuex或Redux的reducer/mutation中,需要返回一个新的状态对象。如果直接修改原状态中的嵌套对象,可能会导致视图不更新(因为引用没变)或难以追踪变化。这时需要对状态的一部分进行深拷贝后再修改。
// 一个不严谨但常用的快速深拷贝方法(仅适用于可序列化数据)
const newState = JSON.parse(JSON.stringify(oldState));
newState.nestedObj.property = 'new value';
return newState;
- 表单编辑:编辑一个复杂对象时,通常需要先深拷贝一份原始数据作为表单的临时数据。这样,用户取消编辑时,可以直接丢弃临时数据,而不会污染原始数据。
100. Swiper插件数据动态加载后不动怎么办?
问题根源:Swiper在DOM元素加载完成前就初始化了,此时动态数据还未渲染到DOM中,Swiper无法正确计算轮播项的数量和尺寸。
解决方案:
-
在Swiper配置中启用观察器(推荐):
new Swiper('.swiper-container', {
// ... 其他配置
observer: true, // 修改swiper自己或子元素时,自动初始化
observeParents: true, // 修改swiper的父元素时,自动初始化
});
-
在数据更新、DOM渲染完成后初始化Swiper:
- 在Vue中,使用
this.$nextTick()确保DOM已更新。
mounted() {
this.fetchData().then(() => {
this.$nextTick(() => {
this.initSwiper();
});
});
}
- 在React中,使用
useEffect并在依赖数组中放入数据变量。
useEffect(() => {
if (data.length > 0) {
initSwiper();
}
}, [data]); // data更新后触发
101. 常见的内存泄漏场景(补充)
- 缓存不当:使用普通对象或Map做缓存,且无清除策略,缓存会无限增长。
- 考虑使用
WeakMap或WeakSet,其键名是弱引用,不影响垃圾回收。
- 为缓存设置大小限制或过期时间。
- 事件监听器未移除(重复强调):不仅是DOM事件,还包括自定义事件、第三方库的事件订阅等。
- 循环引用:两个或多个对象相互引用,即使在脱离执行环境后也无法被回收(现代垃圾回收算法能处理大部分简单循环引用,但复杂情况仍需注意)。
102. 如何分批插入大量DOM以避免页面卡顿?
一次性插入数万个DOM节点会阻塞主线程,导致页面长时间无响应。解决方案是分片插入,将任务拆分成多个小任务,在浏览器的空闲时段执行。
// 假设要插入 total 条数据
const total = 100000;
const batchSize = 20; // 每批插入数量
const container = document.getElementById('list');
function insertItemsBatch(startIndex, count) {
const fragment = document.createDocumentFragment(); // 使用文档片段减少回流
for (let i = 0; i < count && startIndex + i < total; i++) {
const li = document.createElement('li');
li.textContent = `Item ${startIndex + i}`;
fragment.appendChild(li);
}
container.appendChild(fragment);
// 如果还有数据,计划下一批插入
if (startIndex + count < total) {
// 使用 requestAnimationFrame 或 setTimeout 将任务放入任务队列
requestAnimationFrame(() => {
insertItemsBatch(startIndex + count, batchSize);
});
// 或者使用 setTimeout(fn, 0)
}
}
// 开始插入
insertItemsBatch(0, batchSize);
关键点:
DocumentFragment:在内存中操作DOM节点,最后一次性地添加到真实DOM,减少重排重绘次数。
- 分片 (Batch):将大任务拆分成小任务。
requestAnimationFrame:在下一次浏览器重绘之前执行,比setTimeout更高效,能保证在流畅的动画周期中执行,避免卡顿。