找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3072

积分

0

好友

395

主题
发表于 昨天 16:32 | 查看: 4| 回复: 0

70. 渐进增强与优雅降级的区别是什么?

渐进增强:核心思路是首先针对低版本浏览器构建页面,确保最基本的功能可以正常运行。在此基础上,再针对支持高级特性的现代浏览器,逐步增强页面的视觉效果和交互体验。

优雅降级:则恰恰相反。它的起点是基于高级浏览器构建完整、优秀的功能和体验。然后再为低版本或老旧的浏览器提供兼容性处理,虽然体验会有所降低,但核心功能应保证可用。

71. 为什么利用多个域名存储网站资源更有效?

这主要有以下四个方面的好处:

  1. 节省主域名连接数,提升响应速度:浏览器对同一域名的并发请求数有限制。将静态资源(如图片、JS、CSS)分散到不同的域名下,可以突破这个限制,并行下载更多资源,从而加快页面整体加载速度。
  2. 突破浏览器并发限制:正如第一点所述,这是提高加载效率的关键技术手段。
  3. 方便CDN缓存与服务器分离:将动态请求和静态请求分别放在不同的服务器或域名下,管理起来更加清晰,也便于为静态资源配置独立的、高效的CDN缓存策略。
  4. 避免不必要的安全问题:例如,将用户上传的内容(可能包含恶意脚本)放在独立的域名下,可以防止其窃取主站域名下的Cookie等敏感信息,实现安全隔离。

72. 如何优化大型电商网站的大量图片加载?

面对图片多、加载慢的挑战,可以针对不同场景采用不同的优化策略:

  1. 图片懒加载:对于超出首屏可视区域的大量图片,这是首选方案。监听页面滚动事件,当图片即将进入可视区域时,再动态加载其src。这能显著减少初始页面的请求数和数据量。
  2. 图片预加载:对于像幻灯片、相册这类需要连续查看的图片,可以在展示当前图片时,默默加载其前一张和后一张图片,这样用户在切换时就能获得无缝的体验。
  3. CSS Sprite(雪碧图):将页面中许多小图标、Logo、背景图等合并到一张大图中,通过background-position来定位显示。这能将大量小图片的HTTP请求合并为少数几个,减少请求开销。
  4. 使用渐进式图片或缩略图:对于大图,可以先加载一个经过高度压缩的模糊缩略图,让页面快速呈现布局,然后再逐步加载完整清晰的原图,提升用户感知速度。

73. 从输入URL到页面加载完成,发生了什么?

这个过程通常被称为“关键渲染路径”,可以概括为以下几个核心步骤:

  1. DNS解析:浏览器首先解析域名,查找对应的IP地址。它会依次检查浏览器缓存、系统缓存、路由器缓存、ISP DNS缓存,如果都没有,则发起递归查询。
  2. 建立TCP连接:浏览器获得IP后,与目标服务器通过TCP三次握手建立可靠的连接。
  3. 发送HTTP请求:连接建立后,浏览器向服务器发送一个HTTP GET请求,请求指定的资源。
  4. 服务器处理并响应:服务器接收到请求,处理并找到资源,然后通过同一个TCP连接发回HTTP响应(包含状态码、响应头和响应体)。
  5. 关闭TCP连接:对于HTTP/1.0或非持久连接,数据传送完毕后会通过四次挥手关闭TCP连接。HTTP/1.1默认使用持久连接。
  6. 浏览器解析渲染:浏览器接收到响应数据后,开始解析HTML构建DOM树,解析CSS构建CSSOM树,合并成渲染树,计算布局,最后绘制像素到屏幕上。同时会并行解析并执行JavaScript

74. 前端如何进行登录身份的判断?

一个典型的基于Token的前端登录验证流程如下:

  1. 用户提交登录信息,前端发送请求到后端。
  2. 后端验证成功后,生成一个Token并返回给前端。
  3. 前端将收到的Token存入localStoragesessionStorage中。
  4. 后续所有需要身份验证的API请求,前端都在请求头(如Authorization)中携带此Token。
  5. 后端中间件拦截请求,验证Token的有效性和过期时间。
  6. 如果Token过期,后端返回特定的状态码(如401)。
  7. 前端接收到过期状态后,自动清除本地存储的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. 平时写项目总结吗?总结哪些内容?

是的,我会坚持做项目总结,主要记录以下几类内容:

  1. 技术难点与解决方案:记录遇到的问题、排查思路和最终解法。例如,在Taro小程序项目中封装滚动筛选组件时,遇到scroll-into-view的ID不能以数字开头、样式必须严格按文档设置等“坑”,都会详细记下。
  2. 项目规范与架构:总结本次项目在代码规范、目录结构、打包配置、UI组件使用等方面的实践。
  3. 业务逻辑与接口:梳理自己负责模块的业务流程、联调的接口及其数据格式。
  4. 完成的需求清单:简明扼要地列出自己实现的功能点,便于汇报和复盘。

81. 请绘制登录场景的业务流程图。

以React后台管理系统为例,一个典型的登录与鉴权流程如下:

  1. 用户访问登录页,输入账号密码。
  2. 前端校验格式后,调用登录接口。
  3. 后端验证成功,返回Token及用户角色/权限信息。
  4. 前端存储Token(如Redux或localStorage),并根据权限信息动态生成路由菜单。
  5. 用户跳转至主页,后续每次请求在Header中携带Token。
  6. 后端中间件验证Token,并检查请求路径是否在该用户角色权限内。
  7. 前端通过路由守卫,在每次路由跳转前检查目标路由是否在用户权限列表中,无权限则拦截。

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、语雀、飞书文档)进行集中管理,确保信息同步。
  • 代码上传与发布
    1. 开发者从mastermain分支拉取特性分支开发。
    2. 开发完成后,提交Pull Request或Merge Request。
    3. 同事进行代码评审(Code Review)。
    4. 评审通过后,合并到开发或测试分支,触发CI/CD流水线进行自动化构建和测试。
    5. 测试通过后,由运维或通过自动化流程将代码部署到预发布/生产环境。整个过程使用Git进行版本控制。

84. 初级、中级与高级前端工程师的区别

  • 初级工程师:核心是“实现”。能熟练使用HTML、CSS、JavaScript完成静态页面和基本交互,了解响应式布局,会使用jQuery、Bootstrap等库,掌握Ajax与后端通信。主要任务是按照设计稿和需求,准确无误地实现功能。
  • 中级工程师:核心是“工程化与解决方案”。熟练掌握至少一门主流框架(Vue、React、Angular)及其生态(路由、状态管理)。具备前端工程化能力(Webpack/Vite配置、代码规范、性能优化)。能独立负责一个业务模块的开发,并考虑组件的复用性和可维护性。
  • 高级工程师:核心是“架构、赋能与深度”。能从架构层面设计复杂的前端应用(微前端、模块化、状态体系)。对浏览器原理、网络协议、编译原理等底层知识有深入理解。能主导技术选型、性能瓶颈攻关和复杂问题解决。同时,关注团队提效(搭建工具链、沉淀组件库)、业务赋能(技术驱动业务创新)以及跨部门协调推动。

85. 使用ECharts与Highcharts遇到的问题及解决

在React项目中集成ECharts时,需要注意几点:

  1. 初始化时机:ECharts实例化是DOM操作,必须在组件挂载完成后进行。在React函数组件中,应将其放在useEffect钩子中执行。
  2. 容器与样式:必须为图表容器<div>设置明确的宽高,否则图表无法显示。
  3. 数据动态更新:通常结合useEffect处理。一个useEffect负责在组件挂载时初始化图表实例;另一个useEffect在依赖数据变化时,调用实例的setOption方法更新图表。
  4. 性能优化:对于频繁更新的图表,可以使用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工作流示例:

  1. 克隆项目git clone <repository-url>
  2. 基于测试分支创建本地开发分支
    git checkout -b dev origin/test  # 拉取远程test分支并创建本地dev分支
  3. 创建功能分支进行开发
    git checkout -b feature-xxx
  4. 开发与提交
    git add .
    git commit -m "feat: add xxx feature"
  5. 推送到远程并申请合并:将feature-xxx分支推送到远程仓库,然后创建Pull Request (PR) 或 Merge Request (MR),请求合并到test分支。
  6. 代码评审与测试:团队进行Code Review,评审通过后合并到test分支,触发自动化测试和部署到测试环境。
  7. 上线:测试通过后,再次创建PR,将test分支合并到main/master主分支,并打上版本标签,部署到生产环境。

87. 最近项目中你负责什么?

(此题为开放回答,需结合自身经历。示例结构如下:)

在最近的XXX后台管理系统中,我主要负责用户权限模块和数据分析报表模块

  • 用户权限模块:我设计了基于角色(RBAC)的动态路由和菜单权限方案。前端通过接口获取用户权限列表,利用路由守卫过滤生成可访问的路由表,并递归渲染出侧边栏菜单。同时,封装了权限指令,用于控制页面内按钮级别的显示与隐藏。
  • 数据分析报表模块:我使用ECharts封装了通用的图表组件,并抽象了options配置生成器,使业务方只需关注数据格式。针对大数据量的表格,我实现了虚拟滚动和前端分页,确保页面流畅。同时,与后端协作设计了报表数据的缓存策略,提升了多次查询的响应速度。

88. 如何判断开发环境与生产环境?

在Node.js和Webpack构建体系中,通常通过环境变量NODE_ENV来区分。

  1. 使用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"
      }
    }
  2. 在Webpack配置文件中读取该变量:
    const isProduction = process.env.NODE_ENV === 'production';
  3. 根据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. 打包体积与首屏加载优化

  • 路由懒加载:利用动态import()语法分割代码。
    { path: '/about', component: () => import('./views/About.vue') }
  • 开启Gzip压缩:使用compression-webpack-plugin在构建时生成.gz文件,服务器开启Gzip支持。
  • 使用CDN:将vuevue-routerelement-ui等稳定依赖通过externals排除,改用CDN引入,减小项目体积。

2. 代码层面优化

  • 合理使用computedwatchcomputed用于依赖其他数据的计算,有缓存;watch用于数据变化需要执行异步或复杂操作时。
  • v-ifv-show:频繁切换用v-show,运行时条件很少改变用v-if
  • v-forkey与避免和v-if共用:为v-for提供唯一的key,且不要在同一节点上使用v-ifv-for优先级更高)。
  • 图片压缩:使用image-webpack-loader等工具在构建时压缩图片。
  • 避免内存泄漏:及时在beforeUnmount/unmounted生命周期中清除定时器、事件监听器、第三方库实例。

3. 运行时优化

  • 组件懒加载:对于复杂组件,使用defineAsyncComponent进行异步加载。
  • 长列表性能:使用vue-virtual-scroller等库实现虚拟滚动。

91. JavaScript异步解决方案有哪些?

  1. 回调函数 (Callback):最原始的方式,但容易导致“回调地狱”。

    fs.readFile('file.txt', 'utf8', function(err, data) {
      if (err) throw err;
      console.log(data);
    });
  2. Promise:ES6引入,提供了.then().catch()的链式调用,解决了回调地狱,但链式调用仍然繁琐。

    fetch('/api/data')
      .then(response => response.json())
      .then(data => console.log(data))
      .catch(error => console.error(error));
  3. Generator + Co:ES6引入,可以暂停执行函数,需要配合执行器(如co库)。

    function* gen() {
      let result = yield fetch('/api/data');
      console.log(result);
    }
    // 需要co(gen())这样的执行器来运行
  4. 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,看用户是否会进行第二次点击。

解决方案

  1. 禁用缩放:通过<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">禁止用户缩放,浏览器就不再需要等待判断双击。但牺牲了可访问性。
  2. 使用FastClick库:原理是在touchend事件发生时,立即模拟一个click事件并阻止300ms后真正的浏览器click事件。这是最通用的解决方案。
  3. 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,还有:

  1. 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);
        });
      });
    }
  2. 使用Async/Await:这是目前最清晰、最易读的解决方案,它能以近乎同步的代码结构处理异步流程,从根本上避免了嵌套。

96. 常见的内存泄漏场景有哪些?

  1. 意外的全局变量:在函数内未使用varletconst声明变量,或给window的属性赋值。
    function leak() {
      leakedVar = 'I am global'; // 糟糕!成了全局变量
      window.tempData = hugeData; // 同样糟糕
    }
  2. 被遗忘的定时器或回调:设置了setInterval或事件监听器,但在组件销毁时未清除。
    // Vue/React 组件中
    mounted() {
      this.timer = setInterval(() => {...}, 1000);
      window.addEventListener('resize', this.handleResize);
    },
    beforeUnmount() { // 必须清理!
      clearInterval(this.timer);
      window.removeEventListener('resize', this.handleResize);
    }
  3. 脱离DOM的引用:在JavaScript中保存了对某个DOM元素的引用,即使该元素已从页面上移除,也无法被垃圾回收。
    const elements = { button: document.getElementById('myButton') };
    // 即使从DOM中移除了 #myButton,elements.button 仍然引用着它
  4. 闭包:如果闭包中引用了外部变量,且该闭包被长期持有(如挂在全局),那么外部变量也不会被释放。

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. 什么是深拷贝?项目哪里用到了?

浅拷贝只复制对象的第一层属性。如果属性是引用类型(如对象、数组),拷贝的是引用地址,新旧对象会共享这个引用。

深拷贝会递归复制对象的所有层级,创建一个完全独立的新对象,新旧对象互不影响。

项目应用场景

  1. 状态管理:在Vuex或Redux的reducer/mutation中,需要返回一个新的状态对象。如果直接修改原状态中的嵌套对象,可能会导致视图不更新(因为引用没变)或难以追踪变化。这时需要对状态的一部分进行深拷贝后再修改。
    // 一个不严谨但常用的快速深拷贝方法(仅适用于可序列化数据)
    const newState = JSON.parse(JSON.stringify(oldState));
    newState.nestedObj.property = 'new value';
    return newState;
  2. 表单编辑:编辑一个复杂对象时,通常需要先深拷贝一份原始数据作为表单的临时数据。这样,用户取消编辑时,可以直接丢弃临时数据,而不会污染原始数据。

100. Swiper插件数据动态加载后不动怎么办?

问题根源:Swiper在DOM元素加载完成前就初始化了,此时动态数据还未渲染到DOM中,Swiper无法正确计算轮播项的数量和尺寸。

解决方案:

  1. 在Swiper配置中启用观察器(推荐):

    new Swiper('.swiper-container', {
      // ... 其他配置
      observer: true, // 修改swiper自己或子元素时,自动初始化
      observeParents: true, // 修改swiper的父元素时,自动初始化
    });
  2. 在数据更新、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. 常见的内存泄漏场景(补充)

  1. 缓存不当:使用普通对象或Map做缓存,且无清除策略,缓存会无限增长。
    • 考虑使用WeakMapWeakSet,其键名是弱引用,不影响垃圾回收。
    • 为缓存设置大小限制或过期时间。
  2. 事件监听器未移除(重复强调):不仅是DOM事件,还包括自定义事件、第三方库的事件订阅等。
  3. 循环引用:两个或多个对象相互引用,即使在脱离执行环境后也无法被回收(现代垃圾回收算法能处理大部分简单循环引用,但复杂情况仍需注意)。

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更高效,能保证在流畅的动画周期中执行,避免卡顿。



上一篇:基于SPICE的ESD仿真:IEC 61000-4-2标准模型与防护方案评估指南
下一篇:Lottie动画在uniapp微信小程序中的实践:避坑指南与组件封装
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-7 05:31 , Processed in 0.416163 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表