一、理解修改dom和重排重绘的过程

display:none和visibility:hidden的区别。visibility:hidde还在渲染树中,只要在渲染树中,修改就会消耗性能。
javascript
// 消耗性能,不建议
for(let i=0; i<100; i++){
let div = document.createElement('div');
container.appendChild(div); // 每次 append → 一次重排
}
// 批量操作dom,建议
let nodes = [];
for(let i=0; i<100; i++){
let div = document.createElement('div');
nodes.push(div); // 先放进数组
}
container.append(...nodes); // ✅ 一次性 append
transform/opacity(仅触发合成,不触发重排重绘),浏览器会把他们提升为GPU图层,不占用cpu,不干扰主线程。可使用will-change: transform给高频动效元素开启GPU加速,但不可滥用。
二、懒加载和虚拟列表
1. 懒加载的使用IntersectionObserver
- 代码监听所有非首屏元素,由浏览器原生高效监控,不卡主线程;
- 元素进入视口才渲染 DOM,渲染后取消监听;
- 渲染后滚出视口,完全不处理,DOM 保留,无任何性能损耗;
- 这是最优懒加载方案:首屏渲染树最小,后续无冗余操作。
javascript
// 1. 创建一个「视口监听器」:专门盯着元素是否进入屏幕
const observer = new IntersectionObserver((entries) => {
// entries = 所有被监听的元素的状态集合
entries.forEach(entry => {
// 2. 核心判断:元素【进入视口】了吗?
if (entry.isIntersecting) {
const el = entry.target; // 获取当前这个元素
// 3. ✨ 关键:进入视口 → 才渲染真实 DOM(首屏不渲染,优化首屏)
el.innerHTML = `<div class="real-dom">${el.dataset.content}</div>`;
// 4. ✨ 最关键:加载完成 → 立刻取消监听!
// 从此这个元素和监听器彻底没关系,滚来滚去都不会再触发任何逻辑
observer.unobserve(el);
}
// ⚠️ 没有写 else!所以:元素【滚出视口】→ 啥也不做!DOM 保留!
});
});
// 5. 给【所有非首屏占位元素】都开启监听
document.querySelectorAll('.lazy-placeholder').forEach(el => {
observer.observe(el);
});
2. 虚拟列表和懒加载对比
- 只渲染30-50 条,渲染极少的dom = 虚拟列表
- 不管数据多少,页面 DOM 永远只有 30-50 个
- 比普通懒加载更强,性能天花板
- 普通懒加载:DOM 只增不减 、
- 虚拟列表:DOM 有增有减,数量永远不变
3. 虚拟列表和懒加载的实际使用
例1:电商商品详情页(最经典)
顶部:商品图、价格、购买按钮(首屏必须加载)
中间:详情介绍(首屏必须加载)
底部:评价列表、推荐商品、售后服务、相关好物(非首屏)
👉 这些底部模块,必须用懒加载! 它们不是长列表 每个模块结构完全不同 虚拟列表用不了 首屏不渲染,减少渲染树节点 滚动到底部再加载 这就是懒加载的主场!
例2:知乎 / 掘金文章页
顶部:文章标题、内容(首屏)
底部:评论区、相关文章、热门推荐(非首屏)
👉 评论区和推荐模块 → 懒加载 不是列表,虚拟列表用不了!
例3:B 站首页
顶部:导航、banner(首屏)
下方:各种分区、直播、推荐(非首屏)
👉 下方所有大模块 → 懒加载
例4:你的个人主页
头像、昵称(首屏)
下方:作品、动态、收藏、设置(非首屏)
👉 全部 → 懒加载
说明一点:也可采用分页。但分页多用于后台管理系统、传统工具页。像电商商品列表、社交平台信息、移动端长列表多采用虚拟列表。
三、promise
all:一个失败 → 立刻 catch,但其他继续执行,结果扔掉;若都成功,则返回[数据1, 数据2, 数据3]
race:一个完成 → 立刻返回,但其他继续执行,结果扔掉;
allSettled:全部等完,一个都不放过,返回[
{ status: 'fulfilled', value: 数据1 },
{ status: 'rejected', value: 数据2 },
{ status: 'fulfilled', value: 数据3 }
],不会进入catch;
javascript
// 整体捕获
async function getAll() {
try {
let [res1, res2] = await Promise.all([p1(), p2()]);
} catch (err) {
// 任何一个失败,都会进 catch;已经开始的异步:会继续执行,不会停止,只是结果被抛弃了
console.log("接口失败");
}
}```
// 单独捕获,一个失败不影响其他
async function getAll() {
try {
let [res1, res2] = await Promise.all([
p1().catch(err => null), // 失败返回 null
p2().catch(err => null)
]);
} catch (err) {
// 这里不会触发,因为错误都被内部 catch 了
}
}
四、箭头函数
1.为什么要有箭头函数
- 为了固定this:让this不乱跑
- 简化代码:少些function、return、{},写法更优雅
例如:
javascript
const obj = {
name: "小明",
getData: function() {
// 这里的 this 指向 obj
console.log(this.name); // 小明
setTimeout(function() {
// 这里的 this 指向 window (因为定时器是window调用的)
console.log(this.name); // undefined!
}, 1000);
}
};
// 以前为了保住 this,必须写丑陋的代码:
let _this = this; // 缓存 this
setTimeout(function() {
console.log(_this.name);
});
// 箭头函数没有自己的 this,它的 this 是继承来的,从父作用域继承,永远不变!
const obj = {
name: "小明",
getData: function() {
setTimeout(() => {
// 这里的 this 直接继承外层 getData 的 this
// 稳稳指向 obj ,永远不跑偏!
console.log(this.name); // 小明
});
}
};
五、样式适配
最常用的vw和rem,现在主流使用vw。
安装npm install postcss-px-to-viewport,在项目根目录新建文件:postcss.config.js。这样在代码中写px,运行后自动会转为vw
javascript
module.exports = {
plugins: {
"postcss-px-to-viewport": {
viewportWidth: 375, // 设计图宽度(最常用)
viewportUnit: "vw", // 转换成 vw
unitPrecision: 5, // 保留5位小数
},
},
};
安装npm install postcss-pxtorem,在项目根目录新建文件:postcss.config.js。这样在代码中写px,运行后自动会转为rem
javascript
document.documentElement.style.fontSize = clientWidth / 10 + 'px';
javascript
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 37.5, // 1rem = 37.5px
propList: ['*'], // 全部转换
}
}
}
750的图需先/2,2倍图,高倍图。uniapp除外。
ui给的图都是物理像素,代码中计算的都是逻辑像素
六、vue2框架和vue3框架的区别
1、开发语法:Options API vs Composition API(最大区别)
2、响应式原理(底层核心差异)
- Vue2:基于 Object.defineProperty()
缺陷:无法监听对象新增 / 删除属性、数组下标修改 /长度修改,必须用set/set/set/delete 兜底。 - Vue3:基于 ES6 的 Proxy 代理
优势:直接监听整个对象 /数组,支持所有增删改查操作,无需$set,性能更高。
3、生命周期函数
- setup 执行时机早于 Vue2 的 beforeCreate/created,这两个钩子直接废弃;
- 其余生命周期加前缀 on:
beforeMount → onBeforeMount
mounted → onMounted
beforeDestroy →onBeforeUnmount
destroyed → onUnmounted
新增调试钩子:onRenderTracked/onRenderTriggered。
4、模板根节点
- Vue2:模板必须有且仅有一个根节点,所有内容要套一个div
- Vue3:支持多根节点(Fragment片段),无需多余根标签,代码更简洁。
5、TypeScript 支持
- Vue2:原生 TS 支持极差,需要依赖第三方库,类型推导弱;
- Vue3:全程用 TS 重写,原生完美支持TS,自动类型推导,无需额外配置
6、打包体积
- Vue2:全局引入 API,未使用的功能也会被打包,体积偏大;
- Vue3:支持 Tree-Shaking(摇树优化),未使用的 API(如transition)会被自动剔除,打包体积更小(最小仅 10KB 左右)。
7、性能优化
- 编译时优化:静态节点提升、事件缓存、按需更新;
- 响应式性能:Proxy 比 defineProperty 效率更高;
- 内存占用更低,大型应用流畅度远超 Vue2。
8、新增重磅特性
- Teleport(传送门):把组件渲染到任意 DOM 节点(专治弹窗、遮罩层级问题);
- Suspense:异步组件加载时展示占位符;
- emits 选项:强制声明自定义事件,代码更规范;
- 组合式函数:替代 Vue2 的 mixin(解决命名冲突、数据来源不清晰问题)。
9、全局 API 变更
javascript
// 全局单例
import Vue from 'vue'
Vue.use(xxx)
// 只引入了createApp,可以创建无数个独立实例
import { createApp } from 'vue'
const app = createApp(App)
app.use(xxx)
七、虚拟dom
1、根据template和setup里的初始数据,两者结合在内存中画出第一份虚拟dom,vue拿着这个虚拟 DOM(JS 对象),命令浏览器创建对应的页面元素(挂载),浏览器收到命令,全新创建真实 DOM。把这份虚拟dom存起来当旧dom。
2、真实的dom是浏览器节点,不是js对象。
3、数据改变后,vue生成新的虚拟dom树,和旧的虚拟树进行对比,Vue只保留一份最新的虚拟 DOM,旧的会被自动清理(垃圾回收),永远只存最新的当对比草稿;
| 名称 | 本质是什么 | 谁生成的 | 存在哪里 | 类型 |
|---|---|---|---|---|
| 虚拟 DOM | 纯JS 对象 | Vue 生成 | JS 内存 | 代码 / 对象 |
| 真实 DOM | 浏览器原生节点 | 浏览器生成 | 浏览器渲染引擎 | 页面元素 / 硬件渲染 |
beforeCreate 之前:初始化组件空壳
beforeCreate:钩子执行
created 之前:初始化 data、methods 等
created:钩子执行(此时:有数据,无虚拟 DOM)
✨ 关键阶段:created 执行完 → vue内部编译 template+数据 → 生成虚拟 DOM
beforeMount:钩子执行(此时:虚拟 DOM 已生成,无真实 DOM)
beforeMount 之后:虚拟 DOM → 生成真实 DOM 并挂载到页面
mounted:钩子执行(真实 DOM 渲染完成)
八、webpack和vite的区别
| Webpack | Vite | |
|---|---|---|
| 开发模式 | 全量打包:把所有代码打包成 bundle,再启动 | 不打包:用浏览器原生 ESM,按需加载 |
| 启动速度 | 项目越大越慢 | 无论项目多大,秒启动 |
| 热更新 | 改代码→重新打包→更新,大项目卡顿 | 改代码→只更新单个文件,毫秒级更新 |
| 生产环境 | 打包构建 | 用 Rollup 打包(和 Webpack 结果一致) |
| 配置 | 复杂,需要配 loader、plugin | 开箱即用,0 配置跑 Vue 项目 |
| 适配 | 所有前端项目(Vue2/3/React) | 主打Vue3、现代框架 |
九、组件中的name
1、支持递归组件
2、配合keep-alive缓存
十、大屏
1、大屏适配
scale适配就是不对页面内部元素做响应式,而是把整个大屏页面按设计稿比例写死宽高,再通过 CSS 的 transform: scale() 把整个页面等比缩放到当前屏幕,保证在任何分辨率下布局、比例、文字、图表都不变形、不挤压、不错位。
javascript
①以设计稿(如 1920×1080)为基准,页面宽高写死。
②计算当前屏幕宽高与设计稿的缩放比例:
scaleX = 当前宽 / 设计稿宽
scaleY = 当前高 / 设计稿高
最终取最小值 scale = min(scaleX, scaleY)
③用 transform: scale(scale值) 对整个页面居中等比缩放。
④监听窗口 resize,实时重新计算缩放。
2、原生js大屏写scale的话有什么缺点?
-
文字 / 图表在极端屏幕下会模糊
解决:用 devicePixelRatio 初始化 ECharts,提高渲染清晰度。
-
缩放后图表事件坐标偏移
解决:确保 transform-origin: left top,并居中定位。
-
浏览器缩放会影响布局
解决:resize 里做防抖,重新计算 scale。
3、性能
| 问题 | 解决 | |
|---|---|---|
| 图表太多、同时渲染 → 首屏巨卡 | 一个屏 10~20 个图表,一起初始化,CPU 瞬间拉满 | 用 延时渲染 / 队列渲染 错开时间;不在可视区的图表先不渲染;用 v-if 控制,不要全部一上来就渲染 |
| 数据量太大(几千、上万条)→ 直接卡死 | 折线图、曲线图点太多,ECharts 扛不住 | 后端做 数据抽样 / 降采样;前端用 dataZoom 只渲染可视区;关闭 animation 动画 |
| 定时器不清理 → 内存越来越大,越跑越卡 | 大屏常用定时刷新,页面销毁定时器还在跑 | onUnmounted 里必须 clearInterval;一个页面只维护一个定时器,不要一堆 |
| 图表实例不销毁 → 内存泄漏 | 切换页面 / 组件,ECharts 实例还在内存里 | chart.dispose() ;chart = null |
| 特效、3D、地图太耗性能 | 发光、阴影、渐变、3D 柱状图、地图下钻 | 能关就关:shadowBlur、opacity 过度地图禁用海量点标注;3D 尽量少用 |
| 频繁 setOption → 不停重绘 | 一更新数据就 setOption,图表疯狂重渲染 | 合并更新,不要频繁改;使用setOption(newOption, { notMerge: false, lazyUpdate: true }) |
| 全屏 scale + 高分辨率屏幕 → 渲染压力大 | 4K 屏、8K 屏、拼接屏,像素点巨多 | ECharts 初始化加 devicePixelRatio 优化:echarts.init(dom, null, { devicePixelRatio: 1 }) |
| 大量图表一起 resize → 掉帧、卡顿 | 窗口变化,所有图表同时重绘 | 防抖;只对当前可见图表做 resize |
| CSS 动画 + JS 动画 + 图表动画 → 性能打架 | 页面流光、滚动、呼吸灯 + ECharts 动画一起跑 | 优先用 CSS3 硬件加速(transform /opacity);禁用不必要的 ECharts 动画;大量动效少用 js 去改 DOM |
| 接口请求频繁 → 页面一直抖动 | 1s 一次刷新,接口一直请求、一直渲染 | 延长刷新间隔(3s、5s、10s);接口做缓存;数据不变就不更新图表 |
- 延时渲染/队列渲染:延时渲染是会创建定时器,比如间隔30~50ms,到点就渲染一个,适合图标少,5个以内的时候。队列渲染是,前一个执行完后一个再执行,严格串行,适合图标多的时候。
- 数据抽样 / 降采样:把大量数据 "压缩变少",只保留趋势,不影响看图,让 ECharts 不卡顿。采样方式可以用均匀抽样或LTTB算法。
- echarts中提升性能的属性
javascript
chart.setOption(newOption, {
notMerge: false, // 合并更新,不清空重画
lazyUpdate: true // 延迟更新,合并多次操作
})
4、数据分片
javascript
<template>
<div id="chart" style="width:600px;height:400px"></div>
</template>
<script setup>
import * as echarts from 'echarts'
import { onMounted } from 'vue'
let chart = null
// 假设有 10000 条超大数据
const bigData = Array.from({ length: 10000 }, (_, i) => ({
name: '数据' + i,
value: Math.random() * 1000
}))
// 分片配置
const pageSize = 200 // 每片200条
let current = 0 // 当前渲染到第几片
onMounted(() => {
chart = echarts.init(document.getElementById('chart'))
startLoadSlice()
})
// 🔥 核心:分片加载函数
function startLoadSlice() {
// 1. 取出当前这一小片数据
const sliceData = bigData.slice(current, current + pageSize)
// 2. 追加渲染到图表
chart.setOption({
xAxis: { data: chart.getOption()?.xAxis?.[0]?.data || [] },
series: [{
data: [
...(chart.getOption()?.series?.[0]?.data || []),
...sliceData
]
}]
})
// 3. 下标前进
current += pageSize
// 4. 还没渲染完,继续渲染下一片
if (current < bigData.length) {
// 用 setTimeout 让出主线程,页面不卡
setTimeout(startLoadSlice, 50)
}
}
</script>
注意和空闲加载区分requestIdleCallback,空闲加载适用于不是必须立刻渲染完成时
javascript
let bigData = [...] // 1万条数据
let pageSize = 100
let current = 0
function loadSlice() {
// 取一小片
let slice = bigData.slice(current, current + pageSize)
// 渲染这一片
chart.setOption(...)
current += pageSize
// ✅ 关键:等浏览器下一次空闲,再渲染下一片
if (current < bigData.length) {
requestIdleCallback(loadSlice)
}
}
// 开始
requestIdleCallback(loadSlice)
十一、webpack分包
javascript
// 没分包
import Home from './views/Home.vue'
import About from './views/About.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
javascript
// 分包
const routes = [
{
path: '/',
component: () => import('./views/Home.vue') // 🔥 这就分包了
},
{
path: '/about',
component: () => import('./views/About.vue') // 🔥 这也分包了
}
]
十一、websocket
javascript
class Socket {
constructor(url) {
this.url = url
this.ws = null
this.lock = false // 防止重复连接
this.heartTimer = null // 心跳定时器
this.reconnectTimer = null // 重连定时器
this.connect() // 一进来就连接
}
// 1. 创建连接
connect() {
if (this.lock) return
this.ws = new WebSocket(this.url)
// 连接成功
this.ws.onopen = () => {
console.log('连接成功')
this.lock = false
this.startHeart() // 开启心跳
}
// 收到后端推送的消息
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('收到数据:', data)
// 在这里更新页面/图表
}
// 关闭 → 重连
this.ws.onclose = () => {
console.log('断开连接')
this.lock = false
this.reconnect() // 自动重连
}
// 报错 → 重连
this.ws.onerror = () => {
console.log('连接异常')
this.lock = false
this.reconnect()
}
}
// 2. 心跳(保活)
startHeart() {
this.heartTimer = setInterval(() => {
if (this.ws.readyState === 1) {
this.ws.send(JSON.stringify({ type: 'heartbeat' }))
}
}, 30000) // 30秒一次心跳
}
// 3. 自动重连
reconnect() {
if (this.lock) return
this.lock = true
clearInterval(this.heartTimer)
clearTimeout(this.reconnectTimer)
// 3秒后重试
this.reconnectTimer = setTimeout(() => {
console.log('重试连接...')
this.connect()
}, 3000)
}
}
// 使用
const ws = new Socket('ws://xxx.xxx.xxx:8080')