Skip to content

Vue

首屏优化

浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容。

优化策略

  1. 减少资源数量、压缩资源:减少网络请求次数和文件大小,加快页面加载速度。例如压缩图片大小,使用 Gzip 压缩 HTML、JS、CSS,合并 CSS、JS 来减少资源数量。

  2. 路由懒加载:首屏仅加载必要组件,非核心路由延迟加载。

  3. 按需引用 UI 框架:只加载当前页面实际需要的组件和样式。

SPA

指的是整个应用只有一个 HTML 页面,页面在首次加载后不会再整体刷新,而是通过 JavaScript 动态更新页面内容来完成交互。

在 SPA 中:

  • 页面结构(HTML)、样式(CSS)、逻辑(JS)通常只加载一次
  • 页面切换本质上是组件的切换或内容的更新
  • URL 的变化不会触发浏览器重新加载页面

MPA(多页面应用)指的是每个页面都是一个独立的 HTML 页面,页面跳转时会重新向服务器请求新的 HTML、CSS、JS 资源。

SPA vs MPA

对比项单页面应用(SPA)多页面应用(MPA)
页面组成一个主页面 + 多个页面片段多个独立主页面
页面刷新局部刷新整页刷新
URL 模式Hash / History普通 URL
页面切换快,体验好慢,需要重新加载
SEO不友好(可用 SSR 改善)天然友好
数据传递状态管理方便依赖 URL / Cookie / Storage
维护成本相对低相对高

SPA 的优缺点

优点:

  • 用户体验好:页面切换快,不需要整页刷新,接近桌面应用的使用体验

  • 前后端分离清晰:前端负责视图和交互,后端负责提供 API 数据

  • 开发和维护效率高:组件化开发,复用性强;状态统一管理,逻辑清晰

缺点:

  • 不利于 SEO:搜索引擎难以抓取 JS 动态渲染内容,通常需要 SSR(服务端渲染)或预渲染解决

  • 首次加载较慢:首屏需要加载较多 JS 资源,需要通过代码分割、懒加载等方式优化

v-show & v-if

两者都能控制元素在页面是否显示,区别如下:

对比项v-showv-if
控制手段通过 CSS display: none 隐藏,DOM 元素始终存在直接添加或删除 DOM 元素
编译过程简单的 CSS 切换局部编译/卸载,切换时销毁和重建事件监听及子组件
初始渲染无论条件真假都会渲染条件为假时不渲染,直到为真才渲染
切换开销低(仅 CSS 变化)高(涉及 DOM 操作和组件生命周期)

使用场景

  • v-show:适用于需要频繁切换显示状态的场景
  • v-if:适用于条件不常变化、或初始条件为假可延迟渲染的场景

v-if & v-for

  • v-if:条件性渲染,表达式为 true 时才渲染
  • v-for:基于数组/对象渲染列表,语法为 item in items

IMPORTANT

使用 v-for 时建议设置唯一的 key 值,便于 diff 算法优化。

优先级问题

Vue 2 中 v-for 优先级高于 v-if,同时使用会导致性能浪费(每次渲染先循环再判断条件)。

解决方案

  1. 条件在循环外部:用 <template> 包裹,在外层进行 v-if 判断
vue
<template v-if="shouldShow">
  <div v-for="item in items" :key="item.id">{{ item.name }}</div>
</template>
  1. 条件在循环内部:使用 computed 提前过滤数据
vue
computed: {
  filteredItems() {
    return this.items.filter(item => item.isActive)
  }
}

Vue 实例创建

js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

new Vue() 时会调用 _init 方法,_init 初始化生命周期、事件、响应式数据(props / data / computed / watch),触发 beforeCreate、created

IMPORTANT

此时 DOM 尚未生成

Vue2 响应式原理

遍历 data 中的属性,用 Object.defineProperty 将其转换为 getter/setter。getter 收集依赖,setter 派发更新。

每个组件实例都对应一个 watcher, 在组件渲染过程中会访问响应式数据,从而将 watcher 收集为依赖。 当数据发生变化时,setter 会通知对应的 watcher 重新计算, 最终触发组件重新渲染。

Vue3 响应式原理

Vue 3 不再用 Object.defineProperty,而是使用 Proxy 代理对象, 通过拦截 get / set 操作来实现依赖收集和触发更新。

Vue 2Vue 3
Object.definePropertyProxy
DepWeakMap / Map / Set
watcherReactiveEffect
getter / settertrack / trigger

Vue 实例挂载

调用 $mount(el) 开始挂载,$mount 内部调用 mountComponent(vm, el) 通过 mountComponent 创建一个渲染 watcher, watcher 首次执行时调用 render 生成虚拟 DOM, 再通过 _update 转换为真实 DOM 并插入页面, 触发 beforeMount / mounted 生命周期

生命周期

在Vue中实例从创建到销毁的过程就是生命周期,即指从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程

Vue2

生命周期描述
beforeCreate执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
created组件初始化完毕,各种数据可以使用,常用于异步数据获取
beforeMount未执行渲染、更新,DOM 未创建
mounted初始化结束,DOM 已创建,可用于获取访问数据和 DOM 元素
beforeUpdate更新前,可用于获取更新前各种状态
updated更新后,所有状态已是最新
beforeDestroy销毁前,可用于一些定时器或订阅的取消
destroyed组件已销毁,作用同上

Vue3

生命周期描述
setup最早执行,等价于 Vue 2 的 beforeCreate + created,用于初始化数据、逻辑处理、请求数据
onBeforeMount组件挂载前,DOM 未创建
onMounted组件挂载完成,DOM 已创建,可操作 DOM、请求依赖 DOM 的数据
onBeforeUpdate组件更新前,可获取更新前状态
onUpdated组件更新完成,状态与 DOM 均为最新
onBeforeUnmount组件卸载前,用于清除定时器、事件监听、订阅等
onUnmounted组件卸载完成,组件已被销毁

Vue2 vs Vue3

阶段Vue 2Vue 3变化点
初始化beforeCreate合并进 setup
初始化created合并进 setup
挂载前beforeMountonBeforeMount名字变
挂载完成mountedonMounted用法变
更新前beforeUpdateonBeforeUpdate名字变
更新完成updatedonUpdated用法变
销毁前beforeDestroyonBeforeUnmount改名
销毁后destroyedonUnmounted改名
keep-aliveactivatedonActivated变成函数
keep-alivedeactivatedonDeactivated变成函数

数据请求在 created 和 mounted 的区别

  • created:DOM 尚未生成,适合发起不依赖 DOM 的请求
  • mounted:DOM 已渲染完成,适合依赖 DOM 或第三方库初始化的请求

TIP

DOM 已渲染但数据异步填充可能导致页面闪动,需注意用户体验。

slot

slot 又名插槽,用于组件内容分发。

默认插槽

  • 匿名插槽,父组件内容填充子组件默认位置
  • 一个组件只能有一个匿名插槽
vue
<!-- 子组件 -->
<template>
  <div>
    <slot></slot>
  </div>
</template>

<!-- 父组件 -->
<Card>
  <p>这是插槽内容</p>
</Card>

<!-- 输出 -->
<div class="card">
  <p>这是插槽内容</p>
</div>

具名插槽

  • 子组件提供多个插槽,父组件用 name 指定
  • 一个组件可以出现多个具名插槽
vue
<!-- 子组件 -->
<template>
  <div>
    <slot name="header"></slot>
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>

<!-- 父组件 -->
<Card>
  <template #header>
    <p>这是插槽内容</p>
  </template>
  <p>这是插槽内容</p>
  <template #footer>
    <p>这是插槽内容</p>
  </template>
</Card>

作用域插槽

  • 父组件可以访问子组件内部的数据,由父组件决定渲染内容
  • 可以是默认插槽或具名插槽
vue
<!-- 子组件 -->
<template>
  <div>
    <slot :user="user"></slot>
  </div>
</template>

<!-- 父组件 -->
<Card>
  <template #default="slotProps">
    <p>{{ slotProps.user.name }}</p>
  </template>
</Card>

实现原理

  1. 编译阶段<slot> 被编译为占位符节点存入虚拟 DOM
  2. 父组件处理:父组件传入的内容被编译为 VNodes
  3. 渲染阶段:渲染时用父组件的 VNodes 替换子组件的 <slot> 占位符
  4. 作用域插槽:子组件通过 props 向父组件暴露数据,父组件决定如何渲染

Vue2 vs Vue3 语法对比

特性Vue 2Vue 3
默认插槽<slot></slot><slot></slot>
具名插槽(子)<slot name="header"><slot name="header">
具名插槽(父)<template slot="header"><template #header>
作用域插槽<template slot-scope="props"><template #default="{ props }">

Vue2 和 Vue3 的区别

1. 响应式系统

Vue 2:Object.defineProperty

  • 原理:递归遍历 data 中的属性,给每个属性添加 getter/setter
  • 缺点
    • 无法监听对象属性的新增和删除
    • 无法直接监听数组索引和长度的变化(这也是为什么需要 Vue.set 的原因)

Vue 3:Proxy

  • 原理:直接代理整个对象,而不是属性
  • 优点
    • 完美支持对象动态属性和数组
    • 性能更好,因为它是惰性递归(只有访问到深层属性时才会进行代理)

2. API 风格

Vue 2:Options API(选项式)

  • 代码按 datamethodscomputed 分块
  • 痛点:逻辑复杂时,同一个功能的代码会散落在不同的选项中

Vue 3:Composition API(组合式)

  • 使用 setup() 函数,配合 refreactive
  • 优点
    • 逻辑关注点更集中
    • 逻辑复用变得极其简单

3. 性能与体积

虚拟 DOM 优化

  • Patch Flag(静态标记):Vue 3 会标记哪些节点是动态的,对比时跳过所有静态节点
  • 静态提升(Hoisting):模板中的静态内容会被提升到 render 函数之外,避免重复创建
  • Tree-shaking:Vue 3 采用模块化设计,如果你没用到 Transitionv-model,最终打包的代码里就不会包含它们,体积更小

4. 其他改进

  • Vue 3 组件不再强制要求只有一个根标签,组件现在支持有多个根节点

为什么data是函数

因为组件是用来复用的。 对象是引用类型,如果 data 是普通对象,那么所有组件实例将共享同一份数据。

每创建一个新的组件实例,Vue 都会调用一次 data() 函数。 函数每次执行都会返回一个全新的对象,从而实现组件状态的隔离。

Vue2如何监听数组

Vue2 的响应式核心是 Object.defineProperty,只能拦截已有属性的 getter/setter 因此无法监听数组的下标赋值和 length 修改

Vue2 重写了 push、pop、shift、unshift、splice、sort、reverse 这 7 个会改变数组内容的方法。 在这些方法执行后调用依赖管理器 dep.notify() 触发视图更新。 每个被观察的数组或对象内部都会有一个隐藏的 __ob__ 属性, 对应一个 Observer 实例,其中保存了 dep 用于管理依赖。