Vue的小重点

Vue的小重点

image-20250222000316715

一、生命周期

1、生命周期过程

Vue的生命周期是指Vue实例从创建到销毁的过程。

image-20250220211840274

Vue2生命周期

  • 创建阶段
    • beforeCreate:实例尚未完成初始化,不能访问data、computed、methods等。
    • created:data、computed、methods 等已经初始化,可以访问和修改。DOM还未渲染。
  • 挂载阶段
    • beforeMount:已经编译好模版,但还没有渲染到页面。
    • Mounted:组件已被渲染,可以通过$el获取到真实DOM
  • 更新阶段
    • beforeUpdate:组件的数据变化,DOM还没有更新。
    • Updated:DOM已经重新渲染。
  • 销毁阶段
    • beforeDestroy:组件即将被销毁,仍然可以访问data、methos、computed等
    • destroyed:实例已经销毁。

2、子组件和父组件执行顺序

组件创建时

父组件挂载的时候,要先完成子组件的创建和挂载。

子组件的 mounted 先执行,然后才是父组件的 mounted。

父组件 beforeCreate

父组件 created

父组件 beforeMount

子组件 beforeCreate

子组件 created

子组件 beforeMount

子组件 mounted

父组件 mounted

组件更新时

也是父组件开始和收尾

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

组件销毁时

也是父组件开始和收尾

父组件 beforeDestroy

子组件 beforeDestroy

子组件 destroyed

父组件 destroyed

3、created和mounted的区别

当组件被创建时,created mounted 早执行:

  1. beforeCreate
  2. created ✅ (数据已可用,但 DOM 还未生成)
  3. beforeMount
  4. mounted ✅ (DOM 已生成)

image-20250220232239355

4、一般在哪个生命周期请求异步数据

可以选择created、beforeMount、mounted ,因为在这三个钩子函数中,data 已经创建,可以将服务端返回的数据进行赋值。

首选:created:组件实例已经创建,data、computed、methods可用,适合在页面渲染前提前获取数据,数据和 DOM 交互无关,只需要存入 data

如果需要与DOM交互,选择 mounted

如果使用 Vue 3,推荐在 setup 里请求数据,并使用 onMounted 处理 DOM 相关逻辑。

5、keep-alive 中的生命周期哪些

keep-alive组件用于缓存动态组件,防止它们被销毁后重建,从而提升性能。

image-20250220233328029

1
2
3
<keep-alive>
<component :is="currentView"></component>
</keep-alive>

当从 A 切换到 B

  1. A 触发 deactivated
  2. B 触发 createdmountedactivated

当从 B 切换回 A

  1. B 触发 deactivated
  2. A 触发 activated(不会重新 createdmounted

数据请求:在 activated 请求数据,避免重复请求。

计时器:在 activated 开启,在 deactivated 清除。

滚动恢复:在 deactivated 记录滚动,在 activated 恢复。

二、组件通信

1、props,父传子

1
2
3
4
5
6
7
8
9
<!-- 父组件 -->
<template>
<ChildComponent message="Hello Vue!" />
</template>

<script>
import ChildComponent from './ChildComponent.vue';
export default { components: { ChildComponent } };
</script>
1
2
3
4
5
6
7
8
9
10
<!-- 子组件 -->
<template>
<p>接收到的消息:{{ message }}</p>
</template>

<script>
export default {
props: { message: String }
};
</script>

2、$emit,子传父

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 子组件 -->
<template>
<button @click="sendMessage">点击发送</button>
</template>

<script>
export default {
methods: {
sendMessage() {
this.$emit("send-message", "Hello from Child!");
}
}
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 父组件 -->
<template>
<ChildComponent @send-message="receiveMessage" />
</template>

<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
methods: {
receiveMessage(msg) {
console.log("收到子组件的消息:", msg);
}
}
};
</script>

3、v-model,父子双向

1
2
3
4
5
6
7
8
9
10
<!-- 子组件 -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
props: ["modelValue"]
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 父组件 -->
<template>
<ChildComponent v-model="message" />
</template>

<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
data() {
return { message: "Hello Vue!" };
}
};
</script>

在 Vue 中,v-model 是一个语法糖,它背后做了两件事:

  1. 绑定值:将父组件的数据绑定到子组件的 props
  2. 监听事件:自动监听子组件发出的特定事件(update:modelValue),并更新父组件的数据。

4、provide/inject:祖传后代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 祖先组件 -->
<template>
<ChildComponent />
</template>

<script>
import { provide } from "vue";
import ChildComponent from './ChildComponent.vue';

export default {
components: { ChildComponent },
setup() {
provide("message", "Hello from Parent");
}
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 后代组件 -->
<template>
<p>接收到的消息:{{ message }}</p>
</template>

<script>
import { inject } from "vue";

export default {
setup() {
const message = inject("message");
return { message };
}
};
</script>

5、ref,父操作子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 子组件 -->
<template>
<p>子组件</p>
</template>

<script>
export default {
methods: {
sayHello() {
console.log("子组件方法被调用");
}
}
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 父组件 -->
<template>
<ChildComponent ref="child" />
<button @click="callChildMethod">调用子组件方法</button>
</template>

<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
methods: {
callChildMethod() {
this.$refs.child.sayHello();
}
}
};
</script>

6、Event Bus(Vue2):任意组件通信

1
2
3
// eventBus.js
import Vue from "vue";
export const EventBus = new Vue();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 组件 A(发送消息) -->
<template>
<button @click="sendMessage">发送消息</button>
</template>

<script>
import { EventBus } from "./eventBus";
export default {
methods: {
sendMessage() {
EventBus.$emit("custom-event", "Hello from A");
}
}
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 组件 B(接收消息) -->
<template>
<p>{{ message }}</p>
</template>

<script>
import { EventBus } from "./eventBus";
export default {
data() {
return { message: "" };
},
created() {
EventBus.$on("custom-event", (msg) => {
this.message = msg;
});
}
};
</script>

7、Vuex / Pinia(全局状态管理)

Vue2

1
2
3
4
5
6
7
8
9
10
// store.js
import Vuex from "vuex";
export default new Vuex.Store({
state: { count: 0 },
mutations: {
increment(state) {
state.count++;
}
}
});
1
2
3
4
<!-- 组件 A -->
<template>
<button @click="$store.commit('increment')">增加</button>
</template>
1
2
3
4
<!-- 组件 B -->
<template>
<p>计数: {{ $store.state.count }}</p>
</template>

Pinia(Vue3 推荐)

1
2
3
4
5
6
// store.js
import { defineStore } from "pinia";
export const useStore = defineStore("main", {
state: () => ({ count: 0 }),
actions: { increment() { this.count++; } }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 组件 -->
<template>
<button @click="store.increment()">增加</button>
<p>计数: {{ store.count }}</p>
</template>

<script>
import { useStore } from "./store";
export default {
setup() {
const store = useStore();
return { store };
}
};
</script>

三、Vue Router 路由

1、基本概念

Vue Router 是 Vue.js 官方的 前端路由管理工具,用于构建单页面应用(SPA)。可以在不重新加载页面的情况下,实现不同页面(视图)之间的切换。

为什么要用 Vue Router?

  1. 单页面应用(SPA):让 Vue 应用可以像多页面网站一样切换视图,而无需刷新整个页面。
  2. 管理 URL 显示:基于 URL 变化动态渲染组件,使页面可以直接分享。
  3. 前进/后退:支持浏览器的前进、后退功能。
  4. 嵌套路由:支持多级路由嵌套,适用于复杂的页面结构。
  5. 路由守卫:可以拦截路由导航,实现权限控制或全局前置处理。

怎么使用

1、npm install 安装

2、创建 router/index.js,配置路由

3、在 main.js 里注册 router

4、在组件中使用 Vue Router

  • 路由链接
1
2
3
4
5
6
7
<template>
<nav>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于我们</router-link>
</nav>
<router-view></router-view> // 路由出口,用于渲染匹配到的组件
</template>
  • 动态路由
1
2
3
const routes = [
{ path: '/user/:id', component: User } // `:id` 表示动态参数
];
1
2
3
<template>
<h1>用户 ID: {{ $route.params.id }}</h1>
</template>
  • 路由守卫
1
2
3
4
5
6
7
router.beforeEach((to, from, next) => {
if (to.path === '/admin' && !isLoggedIn()) {
next('/login'); // 未登录时跳转到登录页
} else {
next(); // 继续导航
}
});
  • 编程式导航
1
2
3
this.$router.push('/about'); // 跳转
this.$router.replace('/home'); // 不保留历史记录
this.$router.go(-1); // 返回上一页

2、 Vue-Router 的懒加载如何实现

懒加载,指按需加载,需要的时候才加载路由对应的组件,而不是在应用加载时一次性加载所有组件。这样可以减少首屏加载时间,提高应用性能,特别适用于大型项目。

传统的静态导入:所有组件在页面加载时都会被打包进 bundle.js,即使用户不会访问某些页面,也会加载对应组件。首屏加载慢,影响性能。

1
2
3
4
5
6
7
import Home from '../views/Home.vue';
import About from '../views/About.vue';

const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
];

懒加载(动态导入)

import():组件会在访问对应路由时才加载,而不是在应用启动时全部加载。减小首屏 JS 体积,提升应用性能。

1
2
3
4
const routes = [
{ path: '/', component: () => import('../views/Home.vue') },
{ path: '/about', component: () => import('../views/About.vue') }
];

3、两种路由模式

image-20250221011049252

hash 模式(默认):使用 URL 的 # 符号(锚点)实现路由。

1
2
http://example.com/#/home
http://example.com/#/about

history 模式:使用 HTML5 的 History API 进行路由管理,没有 # 符号,看起来像正常的 URL。

依赖 HTML5 History API (pushStatereplaceState) 进行路由管理。

Vue 监听 popstate 事件进行视图切换,而不是依赖 # 符号。

1
2
http://example.com/home
http://example.com/about

如何切换模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';

Vue.use(VueRouter);

const router = new VueRouter({
mode: 'history', // 或 默认'hash'
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
});

export default router;

4、获取页面hash变化

监听 hashchange 事件,使用window.location.hash或者$route.hash获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
export default {
mounted() {
window.addEventListener('hashchange', this.handleHashChange);
},
methods: {
handleHashChange() {
console.log('当前 hash:', window.location.hash);
}
},
beforeUnmount() {
window.removeEventListener('hashchange', this.handleHashChange);
}
};
</script>

5、route 和router 的区别

$route用于获取当前路由信息

1
2
3
4
5
6
7
8
9
<script>
export default {
mounted() {
console.log('当前路径:', this.$route.path);
console.log('查询参数:', this.$route.query);
console.log('路由参数:', this.$route.params);
}
};
</script>

$router用于跳转页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 跳转到新的路由(会新增一条历史记录)
this.$router.push('/home');

// 替换当前路由(不会新增历史记录)
this.$router.replace('/profile');

// 后退一步
this.$router.back();

// 前进一步
this.$router.forward();

// 前进或后退 n 步
this.$router.go(-1); // 相当于 this.$router.back()
this.$router.go(2); // 前进两步

image-20250221012525120

6、Vue-router 路由守卫(钩子函数)

路由钩子(导航守卫)可以控制组件或者页面在路由变化时的行为。比如跳转前身份验证、拦截、保存数据等。

路由守卫比生命周期更早执行,因为 Vue Router 需要先确定路由是否允许访问,然后才会创建组件。

全局守卫、路由独享守卫、组件内守卫

image-20250221143105753

从A跳转到B的守卫执行顺序:

全局前置守卫 beforeEach
路由独享守卫 beforeEnter
组件前置守卫 beforeRouteEnter
组件生命周期 created → mounted
全局解析守卫 beforeResolve
全局后置守卫 afterEach

7、Vue-router跳转和location.href有什么区别

Vue-router提供了前端路由管理,不会刷新整个页面,而是更新url并且切换组件,实现单页应用的无刷新导航。

router跳转方式

1
2
3
this.$router.push('/about'); // 添加历史记录
this.$router.replace('/about'); // 替换当前历史记录
this.$router.go(-1); // 后退一步

window.location.href 直接跳转

会刷新页面,重新加载所有资源,无法使用router提供的钩子,适用于跳转到其他网站,或强制刷新页面。

1
window.location.href = '/about';

8、params和query的区别

params:动态路由参数,使用动态段(例如/user/:id),其中:id表示一个变量,路由匹配时必须提供这个参数。

对路由匹配有直接影响。如果参数缺失,路由匹配可能失败。

1
2
3
4
5
const routes = [
{ path: '/user/:id',
name: 'user',
component: UserComponent }
];

可以通过对象传递:

1
this.$router.push({ name: 'user', params: { id: 123 } });

也可以直接构造url:

1
2
// 直接跳转到 /user/123
this.$router.push('/user/123');

query:查询参数,无需在路由配置预先定义,查询参数通过?后面的键值对传递。

适用于可选参数,如搜索关键词、排序方式、过滤条件等,这些参数不会影响路由匹配,仅作为额外的信息传递给组件。

可以通过对象传递:

1
2
3
4
this.$router.push(
{ path: '/search',
query: { keyword: 'vue', sort: 'desc' }
});

四、Vuex

1、基本原理

官方状态管理库,核心思想是集中式存储所有组件的共享状态,通过单向数据流来确保状态以可预测的方式发生改变。

state:存储所有共享数据,整个应用只有一个状态树(单一数据源)。支持将 store 分割成多个模块,便于管理大型应用。

mutations:唯一修改state的方式,必须是同步函数,这样所有状态的变更有明确的记录通过commit调用。

**actions:**处理异步操作,比如异步请求数据。不能直接修改state,通过mutations修改。使用dispatch调用。
**getters:**派生状态。类似于computed属性,对state进行加工计算或者过滤后再用。state变化时,依赖state的getters也会自动更新。

状态存储store是一个响应式对象,所有组件都从这个单一的数据源读取数据,保存数据一致性。内部利用vue的响应式系统,当state变化,所有依赖该状态的组件都会自动更新视图。

单向数据流:

读取:组件从store里读取state或者通过getters获取派生状态。

更新:触发actions,actions执行异步操作,再通过commit mutations改变state。

所有状态的改变都可以被 Vue 开发者工具监控。

2、Vuex中actions和mutations的区别

它们都是更改状态相关的。

mutation必须是同步函数,确保状态变化可追踪。

1
2
3
4
5
const mutations = {
increment(state, payload) {
state.count += payload.amount;
}
};

调用mutations使用:this.$store.commit('increment', { amount: 1 })

actions主要处理异步操作。如API调用,然后再通过commit调用mutations修改状态。

接收的参数一般为 state 和传入的 payload(数据)。

1
2
3
4
5
6
7
const actions = {
asyncIncrement({ commit }, payload) {
setTimeout(() => {
commit('increment', payload);
}, 1000);
}
};

调用actions,使用this.$store.dispatch('asyncIncrement', { amount: 1 })

接收一个 context 对象,其中包含 commitstatedispatch 等属性,便于在异步操作中调用其他 action 或 mutation。

3、Vuex 和 localStorage 的区别

vuex:存储在内存,随页面刷新、关闭而丢失。适用管理实时、响应式数据,组件共享的数据。是专为 Vue 应用设计的集中式状态管理库,适用于管理和共享实时的、响应式的数据,但数据不会自动持久化。

localstorage:持久化存储在客户端,刷新页面、关闭浏览器后依然存在。适合用户设置、缓存数据。是浏览器内置的持久化存储解决方案,用于保存跨会话数据,但它不是响应式的,不会自动更新 Vue 组件。

4、Redux 和 Vuex 有什么区别,它们的共同思想

两者都是负责状态管理的。

Redux : JavaScript 状态管理库,最初为 React 生态设计,但也可用于其他框架。

单一数据源:整个应用只有一个全局 store。
状态只读:状态只能通过派发(dispatch)一个 action 来触发状态变化。
纯函数 Reducers:状态的更新由纯函数(reducers)完成,保证同样的输入总会有同样的输出。
中间件支持:借助中间件(如 redux-thunk、redux-saga),可以处理异步操作和副作用。

Vuex:专为 Vue.js 设计

image-20250221175755701

5、Vuex有哪几种属性?

state:整个应用的共享数据,唯一数据源

mutations:唯一修改state的方法,必须是同步函数,每个mutation接受state为第一个参数

actions:用于处理异步操作,不能直接修改state,接收一个context对象(commit,state,dispatch,getters等属性),通过commit调用mutation修改。

getters:类似于计算属性,用来对state进行计算、过滤,加工

modules:可以把store分成多个模块,每个模块有自己的state、getters、mutations、actions。

此外,Vuex 的 store 实例还提供了一些方法,如:

  • commit:用于触发 mutations。
  • dispatch:用于触发 actions。
  • subscribe:订阅每次 mutation 后的状态变更。
  • watch:监听 store 中的 state 变化。

6、Vuex和单纯的全局对象有什么区别?

响应式自动更新:

Vuex:利用vue的响应式系统构建,store里面的数据变化时,所有依赖该状态的组件会自动更新

全局对象:普通的JS对象,不具备内置的响应式机制。

状态管理:

Vuex:集中式状态管理,通过单一状态树管理所有共享状态,保证数据来源唯一。

全局对象:数据分散在各个模块中,缺乏集中管理,容易导致状态难以追踪和维护。

单向数据流:

Vuex:强调单向数据流,状态只能通过提交 mutations 或 dispatch actions 修改。

全局对象:任何地方都可以任意修改全局对象的数据,容易出现数据不一致和难以调试的问题。

7、为什么 Vuex 的 mutation 中不能做异步操作?

调试工具依赖同步的mutation记录每一次的状态变化,如果有异步操作,状态更新的顺序就无法确定。

同步操作保证每次提交 mutation 时,状态都会立即且确定地更新。

8、Vuex的严格模式

strict: true,

严格模式会对 state 进行深度监控。一旦检测到 state 在 mutation 之外发生了变化,就会触发错误或警告。

保证单向数据流

通过严格模式,可以确保所有的状态变更都是通过同步的 mutation 来进行!

1
2
3
4
5
6
7
8
9
10
11
const store = new Vuex.Store({
strict: true, // 开启严格模式
state: {
count: 0
},
mutations: {
increment(state, payload) {
state.count += payload.amount;
}
}
});

9、如何在组件中使用Vuex的getter属性

直接访问

1
this.$store.getters

mapGetters

将 getter 映射为组件的计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<p>计算后的计数值:{{ doubledCount }}</p>
</div>
</template>

<script>
import { mapGetters } from 'vuex';

export default {
computed: {
// 使用展开运算符将 getter 映射到计算属性中
...mapGetters(['doubledCount'])
// 如果需要映射多个 getter:
// ...mapGetters(['doubledCount', 'someOtherGetter'])
// 你也可以使用对象形式重新命名:
// ...mapGetters({ double: 'doubledCount' })
}
}
</script>

10、如何在组件中使用Vuex的mutation

直接使用commit

this.$store.commit('mutationName', payload)

使用mapMutations

1
import { mapMutations } from 'vuex';
1
2
3
4
5
6
methods: {
// 将 store 中的 mutation 映射为组件内的方法
...mapMutations(['increment'])
// 如果需要重命名,也可以用对象形式:
// ...mapMutations({ add: 'increment' })
}

五、Vue3.0

1、Vue3.0有什么更新

基于proxy的响应式系统

vue2依赖Object.defineProperty()实现响应式,但是不能检测对象属性的新增、删除以及数组索引的变化,只能使用Vue.set和Vue.delete动态添加删除。Vue3使用**proxy API **重写响应式系统,可以自动捕获对象属性的新增和删除。

Composition API

Options API (选项式API)是 Vue 2 中的主要编程范式:

基于组件的选项:data、methods、computed、watch等选项。

Composition API 是 Vue 3 引入的一种新的编程范式(保留 Options API ):

基于setup函数,将相关的功能组合在一起。

setup 是 Vue 3 中组件的入口函数:

  • 定义组件的响应式状态(如 refreactive)。
  • 定义计算属性(computed)。
  • 定义方法(methods)。
  • 使用生命周期钩子(如 onMountedonUpdated)。
  • 返回需要在模板中使用的数据、方法或计算属性。

性能优化与体积减小

Vue 3 在运行速度上比 Vue 2 更快,很多内部机制经过优化,渲染速度提升 2~3 倍左右。

核心库体积比 Vue 2 小约 50%,更适合现代前端项目,且更易进行 Tree Shaking。

编译器优化,提前提取静态节点,减少运行时计算,优化了虚拟 DOM 更新策略,使得整体性能更佳。

新特性

允许组件返回多个根节点,不再强制要求组件模板必须只有一个根元素;

允许将组件内容渲染到 DOM 的任意位置,使得组件层级结构与渲染位置解耦。

可以在异步组件加载过程中显示备用内容(如加载动画)

更好的 TypeScript 支持

2、Object.defineProperty和proxy的区别

拦截范围

Object.defineProperty只能对对象中已存在的属性进行拦截,包括读取和写入。

proxy可以拦截整个对象的所有操作,包括读取、写入、新增、删除、枚举、函数调用等。

使用方式

Object.defineProperty为每个属性单独定义getter和setter

proxy只需要一次性包装整个对象,通过handler对象统一处理操作

局限性和优势

Object.defineProperty性能稳定但无法自动响应新属性的添加删除和数组索引变化。

proxy功能更全面,可以捕获所有操作,适用范围更广,但在某些极端场景下可能会有额外开销。

六、虚拟DOM

1、对虚拟DOM的理解

虚拟 DOM 是一种用 JavaScript 对象描述真实 DOM 结构的轻量级表示。当数据变化时,框架会生成一个新的虚拟 DOM 树,并与旧的虚拟 DOM 进行对比(使用 Diff 算法),只将发生变化的部分更新到真实 DOM 上,从而提高页面更新的效率,减少直接操作 DOM 带来的性能消耗。

多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。

2、虚拟DOM的解析过程

模板 → AST 生成 → AST 优化 → 代码生成(渲染函数) → 虚拟 DOM 生成 → Diff 算法 → 最小化 DOM 更新

模板编译:编译器将模板字符串解析成抽象语法树(AST),AST 节点中包含了元素的类型、属性、子节点以及可能的指令(如 v-if、v-for 等)。

AST 优化:在生成 AST 后,Vue 编译器会遍历这棵树,标记出静态节点。这种优化可以在重新渲染时跳过对静态节点的比对,从而提升整体性能。

生成渲染函数:优化后的 AST 会被转换成一个渲染函数(render function)。渲染函数中会使用类似 createElement 或者 h() 的 API 来创建虚拟节点(VNode)。这些虚拟节点是轻量的 JavaScript 对象,描述了最终真实 DOM 的结构。

首次渲染:当组件实例化时,渲染函数会被调用,生成初始的虚拟 DOM 树,随后 Vue 会利用这个虚拟 DOM 来构建实际的 DOM 结构。

响应式更新与 Diff 算法:

当组件中的数据发生变化时:

  • Vue 的响应式系统会触发重新调用渲染函数,生成新的虚拟 DOM 树。
  • Vue 会将新旧虚拟 DOM 树进行对比(diff 算法),找出需要更新的部分。
  • 最后,只将必要的差异更新到真实的 DOM 上,这种“最小更新”策略极大提高了性能。

3、为什么要用虚拟DOM

性能优化

直接操作真实 DOM 通常涉及浏览器的重排(reflow)和重绘(repaint),这些操作开销较大,频繁更新可能导致性能瓶颈。虚拟 DOM 作为内存中的 JavaScript 对象,更新速度非常快,通过 Diff 算法对比新旧虚拟 DOM,可以精确计算出实际需要更新的部分,然后只对这部分进行真实 DOM 操作,从而大大减少不必要的重渲染。

直接修改真实 DOM 时,浏览器需要经过以下几个步骤:
解析 HTML 字符串 → 生成新的 DOM 节点 → 构建 CSSOM → 进行 Layout(重排)和 Paint(重绘)。

虽然构建 vNode 和进行 Diff 的过程会在 JS 层面多消耗一些时间,但相比直接重建整个 DOM 所带来的浏览器重排重绘开销,这部分消耗要便宜很多。

跨平台

Virtual DOM 仅仅是 JavaScript 中的对象描述,它不依赖于具体的浏览器 DOM 实现。它只是一种抽象的、平台无关的描述方式。

  • 服务端渲染(SSR)
    在服务器上,由于没有浏览器的 DOM 环境,直接操作真实 DOM 是不可能的。使用 Virtual DOM,可以先在服务器端生成一个虚拟的 DOM 树,再将其转换为 HTML 字符串发送给客户端。
  • 解耦视图和平台
    虚拟 DOM 使得视图的生成和具体平台渲染机制解耦,开发者只需要关注组件逻辑和数据变化,框架会根据不同平台的特性将虚拟 DOM 转换为对应平台的实际视图。

4、DIFF算法的原理

Diff 算法通过比较新旧虚拟 DOM 树,找出它们之间的差异,只对发生变化的节点进行更新,而不是重建整个 DOM 树。这就避免了频繁的重排(layout)和重绘(paint)。

具体比较流程

  1. 节点级别的比较:

    • 节点类型判断:
      如果新旧 节点的类型不同(例如 <div><span>),则直接替换整个节点。
    • 属性对比:
      对于相同类型的节点,会逐一比较它们的属性(包括 class、style、事件绑定等),只更新发生变化的属性。
  2. 子节点的比较:

    • 递归对比:
      对比相同节点下的子节点,递归地采用相同的比较策略。

    • 数组和 Key 优化:

      当子节点是一个数组(例如列表)时,通常会使用 key 来标识每个子节点。

      • 双端比较:
        通过同时从数组的头尾开始比较,可以快速找出新增、删除或位置变化的节点,从而减少不必要的遍历和重新创建。
  3. 生成差异补丁:

    • 根据对比的结果,Diff 算法会生成一系列的“补丁”(patches),描述每个需要更新的部分。
    • 这些补丁最终会被应用到真实 DOM 上,完成局部更新。

5、key的作用

key 为每个元素提供了一个唯一的标识符,帮助 Vue 跟踪每个节点的身份。当列表中的数据发生变化时(如添加、删除或重新排序),Vue 可以通过 key 快速找到对应的节点,从而进行高效的更新操作。

当列表中的元素顺序发生变化时,没有 key 的情况下,Vue 无法准确判断哪些元素被移动或替换,从而可能导致错误的渲染结果。

  1. 唯一性key 应该是唯一的,否则 Vue 无法准确区分元素。
  2. 稳定性key 应该是稳定的,即在列表中,同一个元素的 key 值不应该改变。
  3. **避免使用索引作为 key**:如果列表中的元素顺序可能会变化,建议不要使用数组索引作为 key,因为索引可能会随着元素的增删而改变,导致 Vue 无法准确跟踪元素。