MVVM原理

MVVM(Model-View-ViewModel)是一种软件架构设计模式,特别适合用于现代前端框架中,帮助实现数据驱动视图的开发方式。Vue.js 就是基于 MVVM 模式的框架之一。理解 MVVM 原理有助于理解 Vue 的工作机制,以及它是如何实现响应式数据绑定的。以下是 MVVM 模式的各个部分及其工作原理的详细描述:

MVVM 模式中的三个核心部分

Model(模型):

Model 是应用程序的核心数据层,代表应用程序的数据和业务逻辑。它负责直接与后台 API 进行通信,获取和处理数据。在 Vue 中,Model 通常由 Vue 的响应式对象(例如使用 refreactive 创建的对象)表示。

View(视图):

View 是用户界面部分,负责显示数据给用户。它代表了用户直接与之交互的部分,比如 HTML 模板。在 Vue 中,View 就是模板文件(例如 .vue 文件中的模板部分),它会在浏览器中渲染成 DOM 元素。

ViewModel(视图模型)

ViewModel 是连接 ModelView 的桥梁。它包含所有的展示逻辑,负责将 Model 中的数据绑定到 View 中,以及将用户的操作传递回 Model 中。在 Vue 中,ViewModel 由 Vue 实例负责,这个实例具有数据观察、双向数据绑定等功能。

MVVM 原理

MVVM 的核心在于通过 ViewModel 实现双向数据绑定,使得 ModelView 之间保持同步,从而实现数据驱动的界面更新。这种绑定主要依赖于 Vue 的响应式系统,具体来说,它利用了以下技术:

数据劫持与响应式

Vue 使用 数据劫持 的方式来实现 Model 的响应式更新。它通过 Object.defineProperty()(在 Vue 3 中则使用 Proxy)对对象的每一个属性进行拦截,以实现以下目标:

  • Getter:在读取数据时收集依赖,记录哪些视图(模板)依赖于该数据。
  • Setter:在数据发生改变时通知视图更新,以使得用户看到最新的变化。

例如,当你通过 data 选项定义了一个响应式数据对象,Vue 会使用数据劫持的技术来监控所有对数据的读取和写入操作。这样,当你修改数据时,Vue 就会知道需要更新相关的视图。

依赖收集与观察者模式

在 Vue 中,响应式系统采用了观察者模式。当视图模板中的某个数据被渲染时,Vue 会把这个视图组件注册为该数据的依赖。当 Model 中的数据变化时,Vue 会通知这些依赖,自动重新渲染。

Vue 使用一个 Watcher(观察者) 来管理对每个依赖的数据的追踪。Watcher 可以理解为负责“观察”某个 Model 数据的变化并自动执行回调,更新相应的 View

双向数据绑定

在 Vue 中,双向数据绑定是通过 v-model 指令来实现的。v-model 绑定的是 ModelView 之间的双向通信机制,它会在 View 中的输入发生变化时更新 Model,同时在 Model 更新时重新渲染 View

这种双向数据绑定让开发者只需要关注数据本身的变化,Vue 会负责同步到视图上,不需要手动地操作 DOM。

MVVM 工作流程

  1. 数据初始化:Vue 实例初始化时,会通过 reactiverefModel 变为响应式数据,这时 Model 就可以被观察(订阅)了。

  2. 依赖收集:当 View 中使用 Model 数据进行渲染时(例如在模板中绑定 {{ message }}),Vue 会通过 getter 收集依赖,将 ViewModel 关联起来。

  3. 数据更新:当用户在界面上操作(例如输入文本框)修改数据时,这会通过双向绑定机制更新 Model 中的数据。

  4. 视图更新:当 Model 数据变化后,Vue 的响应式系统会通过 setter 监听到数据的变化,触发更新通知,使 View 中的相关部分自动更新。

MVVM 的优势

  • 低耦合性ViewModel 是分离的。View 不需要关心数据从何而来,Model 也不关心数据如何呈现。
  • 自动化更新:由于 Vue 的响应式机制,View 的更新是自动完成的,无需手动操作 DOM,这减少了开发复杂性。
  • 便于测试:由于 ModelView 是分离的,我们可以独立测试业务逻辑(Model),而不用担心 UI 相关的内容。

总结

  • Model:应用的数据和业务逻辑部分。
  • View:用户界面的展现层。
  • ViewModel:连接 ModelView 的桥梁,负责双向数据绑定和响应式管理。

Vue 中通过数据劫持、依赖收集、双向绑定等技术实现了 ModelView 的自动同步,使得开发者可以专注于业务逻辑,而不用担心繁琐的 UI 操作,这就是 MVVM 模式的核心优势。

  • MVVM是 Model-View-ViewModel 缩写,也就是把 MVC 中的 Controller 演变成 ViewModel
  • Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并⾃动将数据渲染到⻚⾯中,视图变化的时候会通知viewModel层更新数据

vue 中组件 data 为什么是 return ⼀个对象的函数,⽽不是直接是个对象?

在 Vue 中,组件的 data 选项必须是一个返回对象的函数,而不是直接是一个对象。这是为了确保每个组件实例都有自己独立的 data 状态,从而避免多个组件实例之间共享相同的 data 对象。

原因:避免组件实例间共享同一个 data 对象

如果组件的 data 是一个直接的对象(而非返回一个对象的函数),那么所有该组件的实例都会共享这一个对象。当其中一个实例修改 data 中的属性时,其他所有实例也会受到影响,这将导致意想不到的行为和错误。

举个例子

如果你使用了一个对象作为组件的 data,代码可能像这样:

1
2
3
4
5
6
// 错误示例:直接使用对象作为 data
Vue.component('my-component', {
data: {
count: 0
}
});

假设你在页面上创建了两个 my-component 实例:

1
2
<my-component></my-component>
<my-component></my-component>

这两个组件实例将共享同一个 data 对象。如果你修改其中一个组件实例的 count,另一个组件实例的 count 也会随之改变,因为它们都指向了同一个对象。这显然不是我们想要的结果。

解决方案:使用函数返回对象

为了解决这个问题,Vue 规定在定义组件时,data 选项必须是一个函数,且该函数返回一个新的对象。这样每次创建组件实例时,都会调用 data 函数,生成一个新的对象,使每个组件实例都有自己独立的 data 状态。

正确示例

1
2
3
4
5
6
7
Vue.component('my-component', {
data() {
return {
count: 0
};
}
});

每次创建一个新的 my-component 实例时,data 函数都会返回一个新的对象,确保每个实例都有自己独立的 count 属性。这意味着在一个组件实例中修改 count,不会影响到其他实例。

为什么 Vue 实例本身可以直接使用对象?

在 Vue 的根实例中(即使用 new Vue() 创建的实例),data 可以直接是一个对象,而不需要是返回对象的函数:

1
2
3
4
5
6
new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});

这是因为 Vue 根实例只会被创建一次,不会存在多个实例共享同一个 data 对象的问题。而组件则不同,通常会在页面上被创建多次,所以必须使用函数来确保每个组件实例有独立的状态。

总结

  • 组件 data 是函数:是为了确保每个组件实例都有自己独立的 data 对象,防止实例之间相互干扰。
  • 根实例 data 是对象:根实例通常只会创建一次,不存在多个实例间共享数据的问题,所以可以直接是一个对象。

这种设计方式保证了组件的封装性和数据的独立性,使得 Vue 组件在不同实例间不会互相干扰。

v-model是如何实现双向绑定的?

  • vue 2.0 v-model 是⽤来在表单控件或者组件上创建双向绑定的,他的本质是 v-bindv-on 的语法糖,在 ⼀个组件上使⽤ v-model ,默认会为组件绑定名为 valueprop 和名为 input 的事件。

  • Vue3.0 在 3.x 中,⾃定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件

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

Vuex和全局对象主要有两⼤区别:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发⽣变 化,那么相应的组件也会相应地得到⾼效更新。
  2. 不能直接改变 store 中的状态。改变 store 中的状态的唯⼀途径就是显式地提交 (commit)mutation。这样使得我们可以⽅便地跟踪每⼀个状态的变化,从⽽让我们能够实现⼀些⼯具帮助我们更好地了解我们的应⽤。

Vue 的父子组件生命周期钩子执行顺序

渲染过程: ⽗组件挂载完成⼀定是等⼦组件都挂载完成后,才算是⽗组件挂载完,所以⽗组件的mounted在⼦组件mouted之后

⽗beforeCreate -> ⽗created -> ⽗beforeMount -> ⼦beforeCreate -> ⼦created -> ⼦beforeMount -> ⼦mounted -> ⽗mounted

⼦组件更新过程:

  1. 影响到⽗组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
  2. 不影响⽗组件: ⼦beforeUpdate -> ⼦updated

⽗组件更新过程:

  1. 影响到⼦组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
  2. 不影响⼦组件: ⽗beforeUpdate -> ⽗updated

销毁过程: ⽗beforeDestroy -> ⼦beforeDestroy -> ⼦destroyed -> ⽗destroyed

组件通信方式

vue2.0组件通信方式?

  • ⽗⼦组件通信: propseventv-model.syncref$parent$children
  • 非⽗⼦组件通信: $attr$listenersprovideinjecteventbus、通过根实例$root访问、 vuexdispatchbrodcast

Vue3组件通信

  • props$emit
  • provideinject

v-show 和 v-if 有哪些区别?

  • v-if 会在切换过程中对条件块的事件监听器和⼦组件进⾏销毁和重建,如果初始条件是false,则什么都不做,直到条件第⼀次为true时才开始渲染模块。

  • v-show 只是基于css进⾏切换,不管初始条件是什么,都会渲染。

所以, v-if 切换的开销更⼤,⽽ v-show 初始化渲染开销更⼤,在需要频繁切换,或者切换的部分dom很复杂时,使⽤ v-show 更合适。渲染后很少切换的则使⽤ v-if 更合适。

Vue 中 v-html 会导致什么问题

在⽹站上动态渲染任意 HTML,很容易导致 XSS 攻击。所以只能在可信内容上使⽤ v-html,且永远不能⽤于⽤户提交的内容上。

v-for 中 key 的作⽤是什么?

key 是给每个 vnode 指定的唯⼀ id ,在同级的 vnode diff 过程中,可以根据 key 快速的对⽐,来判断是否为相同节点,并且利⽤ key 的唯⼀性可以⽣成 map 来更快的获取相应的节点。

另外指定 key 后,就不再采⽤“就地复⽤”策略了,可以保证渲染的准确性。

为什么 v-for 和 v-if 不建议⽤在⼀起

  • v-forv-if 处于同⼀个节点时, v-for 的优先级⽐ v-if 更⾼,这意味着 v-if 将分别重复运⾏于每个 v-for 循环中。如果要遍历的数组很⼤,⽽真正要展示的数据很少时,这将造成很⼤的性能浪费。
  • 这种场景建议使⽤ computed ,先对数据进⾏过滤。

Vue-Router

路由懒加载

在 Vue.js 项目中,使用 Vue Router 实现懒加载(lazy loading)是一种优化应用性能的常用方法。通过懒加载,路由对应的组件只有在用户访问该路由时才会被加载,而不是在应用初始化时一次性加载所有的组件资源。这对大型应用尤为重要,可以显著减少初次加载的体积,提升首屏加载速度。

在 Vue Router 中,懒加载通常结合 动态导入(Dynamic Import) 语法 import() 使用。以下是如何在 Vue Router 中实现懒加载的详细方法。

基本的 Vue Router 懒加载实现

Vue Router 通过将组件的 import 语句改为动态导入的形式,来实现路由组件的懒加载。动态导入返回一个 Promise,只有在路由被访问时才会加载该组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createRouter, createWebHistory } from 'vue-router';

// 定义路由规则,使用动态导入来懒加载组件
const routes = [
{
path: '/home',
name: 'Home',
component: () => import('@/components/Home.vue'), // 懒加载 Home 组件
},
{
path: '/about',
name: 'About',
component: () => import('@/components/About.vue'), // 懒加载 About 组件
},
];

// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes,
});

export default router;

结合 Vue 3 的 defineAsyncComponent 实现懒加载

Vue 3 提供了一个新的 API **defineAsyncComponent**,用于定义异步加载的组件。它提供了更多的配置项,比如加载状态、错误处理等,适合在需要自定义加载行为时使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { defineAsyncComponent } from 'vue';

// 定义异步组件
const AsyncHome = defineAsyncComponent(() =>
import('@/components/Home.vue')
);

const routes = [
{
path: '/home',
name: 'Home',
component: AsyncHome, // 使用异步组件
},
];

export default createRouter({
history: createWebHistory(),
routes,
});

处理加载状态和错误

使用 defineAsyncComponent 可以为懒加载组件配置加载中的状态组件、超时处理以及错误组件。当加载组件的时间过长时,Vue 可以显示一个备用的加载状态组件;当加载失败时,可以显示错误组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineAsyncComponent } from 'vue';

// 定义异步组件,设置加载状态和错误处理
const AsyncComponent = defineAsyncComponent({
loader: () => import('@/components/MyComponent.vue'), // 异步加载的组件
loadingComponent: LoadingComponent, // 加载中显示的组件
errorComponent: ErrorComponent, // 加载失败时显示的组件
delay: 200, // 延迟显示 loadingComponent,单位是毫秒
timeout: 3000, // 超时错误处理,单位是毫秒
});

const routes = [
{
path: '/my-component',
name: 'MyComponent',
component: AsyncComponent,
},
];
  • loadingComponent:当异步组件正在加载时显示的占位组件。
  • errorComponent:当组件加载失败时显示的组件。
  • delay:设置延迟显示加载状态组件的时间。如果加载时间小于 200ms,加载组件不会被展示。
  • timeout:加载超时的时间,超过这个时间后会显示错误组件。

路由级懒加载 vs 组件级懒加载

  • 路由级懒加载:是最常见的懒加载方式。它将大型应用的不同页面对应的组件按需加载,减少初次加载的体积。
  • 组件级懒加载:在一个页面内,你可能也希望按需加载某些子组件。这时可以使用 defineAsyncComponent 直接在模板中进行懒加载子组件,而不是通过路由。

总结

  • Vue Router 懒加载 是通过 动态导入(import() 实现的,能够显著减少应用的初始加载时间,特别适用于大型应用或复杂的前端项目。
  • webpack 的代码分割(Code Splitting)会将懒加载的组件打包到独立的 chunk 文件中,只有当用户访问相关路由时才会加载这些文件。
  • 可以使用 defineAsyncComponent 来实现更灵活的懒加载,支持加载状态、错误处理等高级功能。
  • 懒加载既可以用于 路由组件 也可以用于 子组件,根据具体需求选择合适的懒加载方式。

路由模式的区别

vue一共有三种路由:hash、history、abstract

区别:

  1. url 展示上,hash模式有 “#”,history 模式没有
  2. 刷新⻚⾯时,hash 模式可以正常加载到 hash 值对应的⻚⾯,⽽ history 没有处理的话,会返回404,⼀般需要后端将所有⻚⾯都配置重定向到⾸⻚路由。
  3. 兼容性。hash 可以⽀持低版本浏览器和 IE

Vue Router 提供了几种路由模式,用于定义如何在浏览器中同步 URL 与你的 Vue 应用。这些路由模式主要包括:

hash

# 后⾯ hash 值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新⻚⾯。同时通过监听 hashchange 事件可以知道 hash 发⽣了哪些变化,然后根据 hash 变化来实现更新⻚⾯部分内容的操作。

  • 描述: hash 模式是 Vue Router 的默认模式。它使用 URL 的哈希(即 URL 中 # 符号后面的部分)来模拟一个完整的 URL,从而实现不重新加载页面的情况下进行页面跳转。
  • 优点: 不需要服务器特别配置,可以在所有支持前端 JavaScript 的浏览器上运行。
  • 缺点: 哈希变化不会被包括在 HTTP 请求中,因此,哈希模式下的 URL 不会被搜索引擎索引。

history

history 模式的实现,主要是 HTML5 标准发布的两个 API, pushStatereplaceState ,这两个 API 可以在改变 url,但是不会发送请求。这样就可以监听 url 变化来实现更新⻚⾯部分内容的操作。

  • 描述: history 模式利用了 HTML5 History API 来实现 URL 的导航而无需重新加载页面。
  • 优点: URL 看起来更美观,像正常的 URL 那样(没有 #)。这种类型的 URL 有利于 SEO。
  • 缺点: 服务器必须配置好对所有路由都返回同一个 index.html 页面,否则在用户直接访问非首页 URL 或刷新页面时会收到 404 错误。

abstract

  • 描述: abstract 模式并不依赖于浏览器的 History API 或哈希变化。它是用在任何 JavaScript 环境中,如 Node.js 或者 Electron。
  • 优点: 在非浏览器环境中使用 Vue,或者在测试过程中不想涉及浏览器特性时,这种模式非常有用。
  • 缺点: 在常规的浏览器应用中很少使用。

在创建 Vue Router 实例时,可以通过 mode 选项来设置路由模式。例如,要设置为 history 模式,可以这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

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

export default router;

vue3的setup

在 Vue 3 中,setup 函数和 <script setup> 是两种用于编写组合式 API(Composition API)的方式。虽然它们在功能上有相似之处,但在使用方式和语法上有一些重要的区别。以下是对它们的详细比较和说明。

setup 函数

setup 函数是 Vue 3 组合式 API 的核心部分,它在组件实例创建之前执行,并且是定义响应式状态、计算属性和方法的地方。

特点

  1. 函数形式

    • setup 函数是组件选项对象中的一个方法,类似于 datamethods 等选项。
  2. 接收参数

    • setup 函数接收两个参数:propscontextprops 是传递给组件的属性,context 包含 attrsslotsemit
  3. 返回值

    • setup 函数返回一个对象,包含需要在模板中使用的响应式状态和方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>{{ count }}</div>
<button @click="increment">Increment</button>
</template>

<script>
import { ref } from 'vue';

export default {
setup(props, context) {
const count = ref(0);

const increment = () => {
count.value++;
};

return {
count,
increment
};
}
};
</script>

<script setup>

<script setup> 是 Vue 3.2 引入的一种语法糖,简化了使用组合式 API 的方式。它将 setup 函数的逻辑直接放在 <script setup> 标签中,无需显式定义和返回。

特点

  1. 语法糖

    • <script setup> 是一种更简洁的语法糖,自动将脚本中的顶层变量和函数暴露给模板。
  2. 自动导入

    • 不需要显式返回对象,顶层的响应式变量和方法会自动暴露给模板。
  3. 更少的样板代码

    • 由于不需要显式定义 setup 函数和返回对象,代码更简洁、更易读。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>{{ count }}</div>
<button @click="increment">Increment</button>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(0);

const increment = () => {
count.value++;
};
</script>

区别总结

语法

  • setup 函数:需要显式定义 setup 函数,并返回一个对象。
  • <script setup>:直接在 <script setup> 标签中编写逻辑,顶层变量和函数自动暴露给模板。

简洁性

  • setup 函数:需要更多的样板代码,包括定义和返回对象。
  • <script setup>:更少的样板代码,更简洁和易读。

自动导入

  • setup 函数:需要显式返回需要在模板中使用的变量和方法。
  • <script setup>:顶层变量和函数自动暴露给模板,无需显式返回。

类型支持

  • <script setup> 支持 TypeScript 类型声明,能够更好地与 TypeScript 集成。

选择使用哪一个

  • 简洁和易读:如果你喜欢更简洁的代码和更少的样板代码,<script setup> 是一个更好的选择。
  • 传统方式:如果你更习惯于 Vue 2 的选项式 API,或者需要在组件定义中使用其他选项(如 datamethods 等),setup 函数可能更适合你。

总结

  • setup 函数:传统的组合式 API 方式,需要显式定义和返回对象,适用于需要更多控制和与其他选项式 API 结合使用的场景。
  • **<script setup>**:Vue 3.2 引入的语法糖,更简洁,自动暴露顶层变量和函数,适用于喜欢简洁代码和更少样板代码的开发者。

通过理解这两种方式的区别和特点,可以根据项目需求和个人偏好选择合适的方式来编写 Vue 3 组件。

Vue 中的 computed 是如何实现的

流程总结如下:只有当计算属性的依赖发生变化时,计算属性的值才会重新计算。如果依赖没有变化,Vue 会返回计算属性上一次计算的结果,从而避免不必要的计算开销。

  1. 当组件初始化的时候, computeddata 会分别建⽴各⾃的响应系统, Observer 遍历 data中每个属性设置 get/set 数据拦截。

  2. 初始化 computed 会调⽤ initComputed 函数

  • 注册⼀个 watcher 实例,并在内实例化⼀个 Dep 消息订阅器⽤作后续收集依赖(⽐如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher

  • 调⽤计算属性时会触发其 Object.definePropertyget 访问器函数

  • 调⽤ watcher.depend() ⽅法向⾃身的消息订阅器 depsubs 中添加其他属性的 watcher

  • 调⽤ watcherevaluate ⽅法(进⽽调⽤ watcherget ⽅法)让⾃身成为其他 watcher 的消息订阅器的订阅者,⾸先将 watcher 赋给 Dep.target ,然后执⾏ getter 求值函数,当访问求值函数⾥⾯的属性(⽐如来⾃ dataprops 或其他 computed )时, 会同样触发它们的 get 访问器函数从⽽将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并 返回求值函数结果。

  1. 当某个属性发⽣变化,触发 set 拦截函数,然后调⽤⾃身消息订阅器 depnotify ⽅法,遍 历当前 dep 中保存着所有订阅者 wathcersubs 数组,并逐个调⽤ watcherupdate ⽅ 法,完成响应更新。

Vue的响应式原理

Vue 的响应式系统是其核心功能之一,它负责在数据变化时自动更新视图。Vue 2 和 Vue 3 的响应式原理有所不同,Vue 2 主要使用了 Object.defineProperty,而 Vue 3 则引入了 Proxy 来实现更灵活和高效的响应式系统。接下来,我们分别详细讲解 Vue 2 和 Vue 3 的响应式原理。

Vue 2 的响应式原理

核心机制:Object.defineProperty

Vue 2 使用 Object.defineProperty 来拦截对对象属性的访问和修改。通过这个方法,Vue 2 可以在对象的属性被读取或写入时,触发相关的依赖收集或更新操作。

数据劫持

Vue 2 在组件实例化时,会递归地遍历 data 对象中的每个属性,并使用 Object.defineProperty 将它们转换为 gettersetter。每次访问这些属性时,都会通过 getter 进行依赖收集(即记录哪些地方使用了这个数据),而在修改属性值时,通过 setter 触发依赖更新(即通知相关的组件重新渲染)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let data = { message: 'Hello Vue' };

Object.defineProperty(data, 'message', {
get() {
// 访问时触发
console.log('Getting message:', data.message);
return data.message;
},
set(newValue) {
// 修改时触发
console.log('Setting message:', newValue);
data.message = newValue;
// 通知视图更新
}
});

data.message; // 触发 getter
data.message = 'Hi Vue'; // 触发 setter

依赖收集与通知

  • 依赖收集:在每次访问数据(即执行 getter)时,Vue 会记录哪些地方(组件、计算属性等)依赖了这个数据。这些地方被称为“依赖”。
  • 依赖通知:当数据变化时(即执行 setter),Vue 会通知所有依赖于这个数据的地方进行更新。

响应式的局限性

Vue 2 使用 Object.defineProperty 存在一些局限性:

  • 数组和对象的新增/删除属性问题:由于 Object.defineProperty 只能监听已经存在的属性,无法监听新增或删除属性。例如,直接给一个响应式对象添加新属性,或直接修改数组的长度,Vue 无法自动追踪这些变化。
  • 无法监听对象嵌套属性的变化:Vue 2 需要对每一个属性都进行递归处理,对于深层嵌套的对象会造成性能问题。

解决方案:

  • 使用 Vue.setVue.delete 来确保新添加或删除的属性能被响应式系统追踪。
  • 对于数组使用 Vue 的特殊方法来确保操作是响应式的,例如 pushpopsplice 等。
1
2
3
4
5
// Vue.set 解决对象新增属性不响应的问题
Vue.set(obj, 'newProperty', 'value');

// Vue.delete 解决删除属性的问题
Vue.delete(obj, 'propertyToDelete');

Vue 3 的响应式原理

核心机制:Proxy

Vue 3 的响应式系统全面使用了 ES6 的 Proxy 对象。Proxy 能够拦截对象的所有操作(包括属性的读写、属性的新增删除等),从而克服了 Vue 2 中 Object.defineProperty 的局限性。

Proxy 实现的响应式

Vue 3 使用 Proxy 来创建响应式对象,通过代理对象拦截对目标对象的访问。相比于 Object.definePropertyProxy 提供了更多拦截操作的能力,例如:

  • 拦截属性读取(get
  • 拦截属性设置(set
  • 拦截属性删除(deleteProperty
  • 拦截 has 操作符(例如 key in obj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const handler = {
get(target, prop) {
console.log(`Getting ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
// 通知视图更新
return true;
}
};

const data = { message: 'Hello Vue 3' };
const proxyData = new Proxy(data, handler);

proxyData.message; // 触发 get 拦截器
proxyData.message = 'Hi Vue 3'; // 触发 set 拦截器

响应式的优势

  • 无需递归Proxy 可以直接拦截对象本身,所有对对象的操作都可以被监听,无需像 Vue 2 那样递归遍历对象的每个属性。
  • 支持对象的新增/删除属性Proxy 可以拦截 setdeleteProperty 操作,这样 Vue 3 可以轻松监听对象的属性添加和删除操作,而不需要像 Vue 2 那样使用 Vue.setVue.delete
  • 更好的性能:Vue 3 中对响应式系统进行了优化,在大型数据集或复杂操作下,性能有了明显的提升。

依赖追踪与触发机制

Vue 3 使用了 effect 函数来管理副作用(即视图更新、计算属性等)。每当你读取一个响应式数据时,Vue 3 会通过 track 进行依赖收集,而每当你修改数据时,Vue 3 会通过 trigger 通知依赖更新。

  • track:在数据被读取时,追踪哪些地方依赖了这个数据(即副作用)。
  • trigger:在数据被修改时,触发相应的副作用更新。
1
2
3
4
5
6
7
8
9
import { reactive, effect } from 'vue';

const state = reactive({ count: 0 });

effect(() => {
console.log(`count is: ${state.count}`);
});

state.count++; // 触发 effect,打印新值

在这个例子中,effect 会在 state.count 变化时自动重新执行,类似于 Vue 2 中的依赖收集和依赖通知。

响应式 API

Vue 3 提供了更灵活的响应式 API,例如 refreactivetoRefscomputed 等,使得开发者能够更好地控制响应式数据的创建和管理。

  • **reactive**:将普通对象转换为响应式对象。
  • **ref**:用于创建一个单一的响应式值(原始类型也可以响应)。
  • **toRefs**:将 reactive 对象的每个属性转换为 ref,这样可以解构对象的同时保持响应性。
  • **computed**:用于声明一个基于其他响应式数据的派生值,它会自动追踪依赖并在依赖改变时更新。

总结

  • Vue 2 的响应式系统 使用 Object.defineProperty,通过 getter 和 setter 进行依赖收集和通知,存在不能监听对象新增/删除属性、深层嵌套对象性能差等局限性。
  • Vue 3 的响应式系统 使用 Proxy,可以监听所有操作,包括属性读取、写入、删除等。它克服了 Vue 2 的局限性,性能更优,并且提供了更灵活的响应式 API 供开发者使用。

Vue 3 的响应式系统更现代、更高效,也更符合开发者对响应式编程的需求。

  • Vue 的响应式是通过 Object.defineProperty 对数据进⾏劫持,并结合观察者模式实现。
  • Vue 利⽤Object.defineProperty 创建⼀个 observe 来劫持监听所有的属性,把这些属性全部转为 gettersetter
  • Vue 中每个组件实例都会对应⼀个 watcher 实例,它会在组件渲染的过程中把使⽤过的 数据属性通过 getter 收集为依赖。之后当依赖项的 setter 触发时,会通知 watcher ,从⽽使它关联的组件重新渲染。

vue3的reactive定义的变量解构的时候会失去响应式

在 Vue 3 中,当你使用 reactive 创建一个响应式对象并对其进行解构时,解构出的变量本身不会保持响应性。也就是说,如果你解构 reactive 对象中的属性,然后修改这些属性的值,Vue 的响应式系统无法检测到这些变化。因此,这些解构后的属性不会被监听

原因

reactive 返回的对象是深层的响应式代理对象,当你解构这个对象时,解构操作仅仅将对象属性的值拷贝出来,失去了响应式系统的代理,因此无法继续追踪这些值的变化。

下面是一个解构 reactive 对象属性的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { reactive, watch } from 'vue';

export default {
setup() {
const state = reactive({
count: 0,
name: 'John'
});

// 解构 reactive 对象的属性
const { count, name } = state;

// 监听 state 中的 count 变化
watch(() => state.count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} -> ${newVal}`);
});

function increment() {
// 这将不会触发 watch,因为 count 已经不再是响应式的
count++;
}

return {
count,
name,
increment
};
}
};

在上面的示例中,countname 被解构出来后,它们不再是响应式的。如果你调用 increment 函数并尝试修改 count 的值,Vue 的响应式系统不会检测到这些变化。

要解决这个问题,避免直接解构 reactive 对象的属性,可以通过以下几种方式保持属性的响应性:

方案 1:继续使用 reactive 对象

最简单的解决方案是直接使用 reactive 对象而不对其进行解构。在模板中直接引用 state.countstate.name,这样它们将保持响应性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { reactive, watch } from 'vue';

export default {
setup() {
const state = reactive({
count: 0,
name: 'John'
});

watch(() => state.count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} -> ${newVal}`);
});

function increment() {
state.count++; // 响应式更新
}

return {
state,
increment
};
}
};

方案 2:使用 toRefs 保持响应性

如果你确实需要解构 reactive 对象,可以使用 Vue 提供的 toRefs 函数,它可以将 reactive 对象中的属性转化为单独的 ref,从而保持响应性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { reactive, toRefs, watch } from 'vue';

export default {
setup() {
const state = reactive({
count: 0,
name: 'John'
});

const { count, name } = toRefs(state); // 使用 toRefs 保持响应性

watch(count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} -> ${newVal}`);
});

function increment() {
count.value++; // count 是一个 ref,需要通过 .value 修改
}

return {
count,
name,
increment
};
}
};

在这个例子中,toRefsstate.countstate.name 转化为 ref 对象,解构后的 countname 仍然保持响应性,因此修改 count.value 时会触发响应式更新。

方案 3:使用 toRef 只解构单个属性

如果你只需要解构 reactive 对象中的某个属性,可以使用 toRef 函数来单独解构这个属性并保持响应性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { reactive, toRef, watch } from 'vue';

export default {
setup() {
const state = reactive({
count: 0,
name: 'John'
});

const count = toRef(state, 'count'); // 只将 count 转换为 ref

watch(count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} -> ${newVal}`);
});

function increment() {
count.value++; // count 仍然是响应式的 ref
}

return {
count,
increment
};
}
};

toRef 只解构并保持了 state.count 的响应性,而不影响其他属性。这适用于只需要单个或少量属性解构的场景。

总结

  • 直接解构 reactive 对象的属性 会导致这些属性失去响应性。
  • 解决方案
    • 保持使用整个 reactive 对象,不进行解构。
    • 使用 toRefs 来解构多个属性,同时保持它们的响应性。
    • 使用 toRef 来解构单个属性,并保持响应性。

Vue2.0中如何检测数组变化?

Vue 的 Observer 对数组做了单独的处理,对数组的⽅法进⾏编译,并赋值给数组属性的 __proto__属性上,因为原型链的机制,找到对应的⽅法就不会继续往上找了。编译⽅法中会对⼀些会增加索引的⽅法( pushunshiftsplice )进⾏⼿动 observe。

在 Vue 2 中,数组的响应式机制通过重写数组的变更方法来实现,Vue 2 对这些方法进行了“拦截”,以便当数组发生变化时,能够正确地触发视图更新。然而,Vue 2 使用的 Object.defineProperty 方法无法直接检测到数组的索引变化和长度变化,因此对于数组的某些操作有特定的解决方案。

Vue 2 中的响应式数组

Vue 2 对数组的一些原生方法进行了改写,以便可以检测到数组的变化。以下是 Vue 2 中的响应式数组操作方法,它们可以触发视图更新:

  • **push()**:向数组末尾添加元素。
  • **pop()**:移除数组末尾的元素。
  • **shift()**:移除数组开头的元素。
  • **unshift()**:向数组开头添加元素。
  • **splice()**:通过移除、添加或替换数组中的元素来更改数组的内容。
  • **sort()**:对数组进行排序。
  • **reverse()**:反转数组的元素顺序。

示例:

1
2
3
4
5
6
const vm = new Vue({
el: '#app',
data: {
items: [1, 2, 3]
}
});

你可以使用这些变更方法修改数组并触发视图更新:

1
2
vm.items.push(4); // 视图会更新,items 变为 [1, 2, 3, 4]
vm.items.splice(1, 1); // 视图会更新,items 变为 [1, 3, 4]

无法直接检测的情况

由于 Vue 2 的响应式系统使用 Object.defineProperty,它存在以下无法自动追踪的情况:

  • 通过索引直接设置数组项:Vue 2 无法直接检测到这种操作。
    1
    vm.items[1] = 10; // 无法检测到该变化,视图不会更新
  • 直接修改数组的长度:Vue 2 无法自动监听数组长度的变化。
    1
    vm.items.length = 1; // 无法检测到该变化,视图不会更新

解决方案

使用 Vue.set() 方法

为了解决直接通过索引设置数组项的问题,Vue 提供了 Vue.set() 方法,它可以确保你通过指定索引向数组中添加新项时,触发响应式更新。

1
Vue.set(vm.items, 1, 10); // 视图会更新,items 变为 [1, 10, 4]

使用 splice() 方法

对于数组项的添加或删除,你也可以使用 splice() 方法,它不仅可以添加或删除数组元素,而且能确保响应式更新。

1
vm.items.splice(1, 1, 10); // 视图会更新,items 变为 [1, 10, 4]

使用 Vue.set() 修改数组长度

虽然你无法直接修改数组的长度来触发响应式更新,但你可以通过 splice()Vue.set() 来达到修改数组长度并响应的效果。

1
vm.items.splice(2); // 视图会更新,items 变为 [1, 10]

总结

在 Vue 2 中,数组的响应式操作主要通过重写数组的变更方法实现(如 pushsplice 等)。然而,对于通过索引直接修改数组项或直接修改数组长度的操作,Vue 2 无法自动检测到变化。为了解决这个问题,可以使用 Vue.set() 方法或者 splice() 方法进行数组的变更操作,确保这些变更能够被 Vue 的响应式系统捕捉到并更新视图。

关键点总结:

  • 响应式的数组操作push()pop()shift()unshift()splice()sort()reverse()
  • 通过索引直接修改数组项:使用 Vue.set()
  • 直接修改数组长度:使用 splice() 方法。

通过这些方式,你可以在 Vue 2 中确保数组的变化能够正确触发响应式更新。

nextTick是做什么⽤的,其原理是什么?

nextTick 是 Vue 提供的一个全局方法,它用于延迟回调的执行直到下一次 DOM 更新循环之后。在 Vue 的数据绑定机制中,当响应式数据发生变化后,视图不会立即更新,而是异步更新。这意味着 Vue 会将所有数据变更放入一个队列中,并在一个事件循环中批量处理这些变更,以优化性能和避免不必要的 DOM 操作。nextTick 允许你在 DOM 更新完成后,立即执行某些操作,这对于需要在视图完全渲染后执行的 DOM 依赖操作非常有用。

使用场景

  • 验证 DOM 更新的结果,如测试或检查元素位置。
  • 在数据变化后,执行依赖于新 DOM 结构的操作。

原理

Vue 的异步更新队列意味着你在同一个事件循环中对响应式数据进行的多次修改会被合并,从而减少实际的渲染次数。nextTick 则提供了一种方式,确保在所有数据变更导致的视图更新之后再运行指定的回调函数。

Vue 内部会维护一个待处理的回调函数队列。当你调用 nextTick 时,Vue 会将你的回调函数添加到这个队列中。一旦当前事件循环中的所有数据变更都处理完毕,Vue 会在下一个事件循环“tick”开始之前,处理这个队列中的所有回调函数。

实现细节

Vue 2.x 和 Vue 3.x 在实现 nextTick 方面略有不同,但基本原理相似,都是利用了 JavaScript 事件循环和宏任务(MacroTask)或微任务(MicroTask)的概念。

  • 微任务优先: Vue 会首先尝试使用如 Promise.thenMutationObserverObject.observe(已废弃)这样的微任务API,因为微任务会在当前事件循环的末尾执行,保证了执行时机的及时性。
  • 宏任务备选: 如果环境不支持上述微任务API,Vue 会回退到宏任务API,如 setTimeout,尽管它的执行时机比微任务稍晚。

通过这种机制,nextTick 能够确保回调函数在 DOM 更新完成后尽快执行,同时不会阻塞浏览器的 UI 渲染。这对于高性能的响应式数据绑定和用户界面更新至关重要。

  • 在下次 DOM 更新循环结束后执⾏延迟回调,在修改数据之后⽴即使⽤ nextTick 来获取更新后的DOM。
  • nextTick 对于 micro task 的实现,会先检测是否⽀持 Promise ,不⽀持的话,直接指向 macrotask,⽽ macro task 的实现,优先检测是否⽀持 setImmediate (⾼版本IE和Etage⽀持),不⽀持的再去检测是否⽀持 MessageChannel,如果仍不⽀持,最终降级为 setTimeout 0;
  • 默认的情况,会先以 micro task ⽅式执⾏,因为 micro task 可以在⼀次 tick 中全部执⾏完毕,在⼀些有重绘和动画的场景有更好的性能。
  • 但是由于 micro task 优先级较⾼,在某些情况下,可能会在事件冒泡过程中触发,导致⼀些问题,所以有些地⽅会强制使⽤ macro task (如 v-on )。

注意:之所以将 nextTick 的回调函数放⼊到数组中⼀次性执⾏,⽽不是直接在 nextTick 中执⾏回调函数,是为了保证在同⼀个tick内多次执⾏了 nextTcik ,不会开启多个异步任务,⽽是把这些异步任务都压成⼀个同步任务,在下⼀个tick内执⾏完毕。

Vue 的template模板编译原理

Vue 的模板编译原理可以分为三个主要步骤:解析优化代码生成。在 Vue 中,模板编译的作用是将模板字符串转换为可以渲染的 JavaScript 渲染函数,这个过程让 Vue 的模板变得高效且可执行。

解析(Parse)

Vue 首先将模板字符串解析为抽象语法树(AST, Abstract Syntax Tree)。AST 是一个用 JavaScript 对象表示的结构化树,描述了模板中的各个元素和它们之间的关系。

解析阶段大致包含以下几部分:

  • 词法分析:将模板字符串分割成独立的标记(token),如标签、属性、文本等。
  • 语法分析:将这些标记转换为抽象语法树(AST)。

每个节点在 AST 中都有具体的描述,比如标签类型、属性、指令和事件处理程序等。通过这个 AST,Vue 可以对模板结构有完整的了解。

优化(Optimize)

在生成 AST 后,Vue 会对它进行优化。这一步的目的是标记出模板中的静态内容,静态内容在渲染过程中不会发生变化,因此可以跳过它们的重新渲染。

  • 静态节点标记:标记出模板中的静态节点,使这些节点不需要在每次渲染时都重新生成 DOM。
  • 静态根节点标记:标记静态根节点,用于进一步优化更新时的性能。

这种优化机制帮助 Vue 减少 DOM 更新的次数,提升渲染性能。

代码生成(Codegen)

在完成优化之后,Vue 会将 AST 转换为 JavaScript 渲染函数。渲染函数是 Vue 实际用来生成虚拟 DOM(vDOM)的函数,类似于 React 中的 JSX 编译结果。

生成的渲染函数大致如下:

1
2
3
4
5
6
7
function render() {
return createElement('div', {
attrs: { id: 'app' }
}, [
createElement('p', null, 'Hello, Vue!')
]);
}

虚拟DOM渲染

最终的渲染函数返回虚拟 DOM 树(vDOM),vDOM 是 Vue 用来描述视图结构的轻量级 JavaScript 对象。Vue 使用虚拟 DOM 来跟踪组件状态的变化,并通过高效的 DOM diff 算法在最小化真实 DOM 操作的情况下更新界面。

总结

Vue 的模板编译过程主要包括:

  1. 解析:将模板转换为抽象语法树(AST)。
  2. 优化:标记静态节点以提高渲染性能。
  3. 生成代码:将 AST 转换为渲染函数。
  4. 虚拟 DOM 渲染:渲染函数返回虚拟 DOM,通过 Vue 的 diff 算法更新真实 DOM。

Vue的性能优化有哪些

Vue 的性能优化可以从多个角度进行,包括数据绑定、组件结构、渲染和加载优化等。

使用计算属性而不是方法或表达式

计算属性是基于依赖缓存的,只有当依赖变化时才会重新计算。相比在模板中直接使用方法或复杂表达式,计算属性更高效,尤其在需要多次访问相同数据时,可以避免不必要的重新计算。

避免频繁的 DOM 操作

尽量减少在方法或生命周期钩子中直接操作 DOM,Vue 的虚拟 DOM 已经能很好地优化渲染过程。如果必须访问 DOM,可以通过 $refs 获取节点并缓存结果,以减少 DOM 操作的次数。

使用 v-if 和 v-show

如果需要在多个组件之间切换,v-if 在组件不需要时不会渲染节点,适合需要频繁销毁和创建的情况。v-show 则通过控制 CSS 显示与否,只在切换时影响渲染,适合频繁切换但不销毁的情况。

按需引入和按需加载

按需引入 Vue 的功能或插件,并通过 Vue 的异步组件或懒加载组件,减少初始加载时的体积。使用 import() 语法可以实现路由级别的代码分割,只有在路由被访问时才加载相应的组件。

合理使用 key

在 v-for 列表渲染时,提供唯一的 key 可以帮助 Vue 更准确、高效地追踪节点的变化,避免不必要的 DOM 重排或销毁。确保 key 唯一且稳定,这样 Vue 可以正确复用已有节点。

使用 v-once 指令

对于那些一旦渲染后就不会改变的静态内容,可以使用 v-once 指令,告知 Vue 只渲染一次,之后不会再更新。这种方式可以减少不必要的虚拟 DOM 比较和更新。

控制组件的重渲染

避免在不必要的情况下重新渲染组件。使用 shouldComponentUpdate 或通过 $watch 监听特定数据变化,尽量减少不必要的数据绑定或减少响应式数据的深层次监听。

分离大型组件

对于特别复杂的组件,可以将其分成多个小组件,提高每个组件的可维护性和复用性,也可以使得虚拟 DOM 的计算量减少。避免过于庞大的单一组件,提高组件的渲染和更新效率。

事件节流和防抖

在处理高频率事件(如滚动、输入)时,使用防抖或节流来减少事件触发频率。可以使用第三方库(如 lodash)或通过自定义函数实现,避免过于频繁地触发更新。

使用服务端渲染(SSR)或静态站点生成(SSG)

对于首屏渲染要求高的场景,服务端渲染(SSR)和静态站点生成(SSG)可以极大提升加载速度。使用 Vue 的 Nuxt.js 框架可以很方便地实现 SSR 或 SSG,减轻客户端渲染的负担,提升 SEO 和用户体验。

懒加载图片和其他资源

通过懒加载的方式,仅在资源进入视口时加载它们。对于图片,可以使用 IntersectionObserver 或 Vue 中的第三方懒加载插件来实现。这样可以减少初始加载时间和带宽消耗,提高页面的响应速度。

使用 Vuex、Pinia 模块化和动态注册模块

当应用的状态管理较复杂时,可以利用 Vuex 的模块化管理,提高代码可维护性。对于只在特定场景下使用的 Vuex 模块,可以在需要时动态注册,避免全局状态的臃肿和性能下降。

监控和分析性能

借助 Chrome DevToolsVue DevTools等工具来检测组件渲染性能,找出性能瓶颈。也可以使用 Vue.config.performance = true 开启 Vue 的性能追踪,监控并分析应用的渲染和更新速度,从而进行针对性的优化。

这些方法和技巧可以有效地提升 Vue 应用的性能,但优化的重点取决于具体场景。根据应用需求和用户体验,结合这些技术,可以最大化地提升 Vue 应用的性能。

优化第三方库的使用

对于大型第三方库(如 lodash、element-ui 等),尽量按需引入所需的功能模块,避免引入整个库。

ref和reactive的区别

在 Vue 3 Composition API 中,refreactive 都是用来创建响应式数据的函数,但它们在使用方式和适用场景上有一些区别:

定义

  • ref

    • 用于包装一个基本类型值(如 stringnumberboolean)或对象,使其成为响应式的。
    • ref 返回的是一个包含 value 属性的对象。无论何时访问或修改该值,都需要通过 .value 属性。
    • 对于基本类型数据,ref 是必须的,因为基本类型不能被直接转换为响应式数据。
  • **reactive**:

    • 用于创建一个响应式的对象或数组。
    • 直接返回原始对象的响应式代理,不需要通过 .value 访问内部值。
    • 不能用于基本数据类型。

使用场景

  • ref 适用于:

    • 需要响应式的基本数据类型。
    • 需要一个响应式对象,但希望在模板中直接使用而不是作为对象属性访问时。
  • reactive 适用于:

    • 对象或数组,特别是当你需要深层响应式支持时。
    • 复杂的数据结构,如嵌套对象。

深层响应式

  • reactive 提供深层响应式。这意味着无论对象结构有多复杂,其内部所有层级的属性都是响应式的。
  • ref 对于对象,也能提供深层响应式,但当直接修改对象内部的属性时(不是 ref 对象本身的 .value),需要使用 reactive 来确保深层属性的响应性。

模板引用

  • 在模板中使用时,ref 创建的响应式数据在引用时不需要 .value,Vue 会自动解包。
  • reactive 对象直接作为响应式数据使用,不需要任何额外的解包。

TypeScript 支持

  • ref 在 TypeScript 中提供了更好的类型推断。因为你总是通过 .value 访问值,所以 TypeScript 可以更准确地知道值的类型。
  • reactive 对象的类型推断也很好,但在某些复杂的情况下可能需要显式类型声明。

响应式转换注意事项

  • 使用 reactive 对一个对象进行响应式转换时,如果该对象被重新赋值为另一个对象,原对象会失去响应式能力。而 ref 则可以通过 .value 属性来更新其内部值,保持响应式。
  • ref 可以被用来跟踪基本类型值的变化,这是 reactive 无法做到的。

总的来说,refreactive 各有适用场景,在 Vue 3 Composition API 中灵活使用这两者可以更好地管理和组织响应式状态。

vue2和vue3 核心 diff 算法核心区别?

Vue 2 中的 diff 流程

Vue 2 中的 diff 算法用于比较虚拟 DOM 树的新旧节点,找到变化并高效地更新真实的 DOM。Vue 2 的 diff 算法是基于 Snabbdom 的,采用的是双端比较的策略,即从新旧两端同时进行比较,通过递归的方式对比每个子节点,最终对差异的地方进行最小化的 DOM 更新。

Vue 2 的 diff 算法的基本流程:

  1. 同层比较:Vue 2 的 diff 只会比较同一层次的节点,不会跨层次比较。
  2. 递归对比子节点:当 diff 找到节点不同时,会通过递归的方式比较子节点。
  3. 双端比较:Vue 2 会从新旧节点的两端开始同时进行比较,即头部和尾部节点进行比较,四种可能的组合是:
    • 旧前 vs 新前
    • 旧后 vs 新后
    • 旧前 vs 新后
    • 旧后 vs 新前
  4. 相同节点复用:通过 key 来判断两个节点是否相同(sameVnode 函数),如果 key 相同,Vue 2 会复用旧节点,并进行进一步的递归比较。
  5. 处理剩余节点:当头尾比较结束后,如果仍有剩余节点,可能会将新的节点插入或移除旧的节点。

Vue 2 diff 流程示意:

1
2
3
4
5
6
7
旧前 ->           <- 旧后
新前 -> <- 新后

1. 旧前 vs 新前
2. 旧后 vs 新后
3. 旧前 vs 新后
4. 旧后 vs 新前

优化点:

  • 双端比较:同时从头部和尾部开始比较,提升了对一些场景(如节点位置调换)的性能。
  • 静态节点优化:Vue 2 对模板中的静态节点进行了标记,避免对它们的重复对比。

Vue 3 中的 diff 流程

Vue 3 进行了大量的架构改进和性能优化,其中一个重要的部分是对 diff 算法的优化。Vue 3 使用了更轻量和高效的块级静态标记(block tree with static nodes optimization),并在虚拟 DOM 的 diff 中进行了进一步优化。

Vue 3 的 diff 算法的核心优化点:

  1. 静态提升(Static Hoisting)

    • Vue 3 在编译阶段就会将模板中的静态节点进行提升,生成的渲染函数中,不再重复处理这些静态内容,减少了运行时的开销。
    • diff 过程中,Vue 3 不再关心静态节点的变化,因为它们永远不会改变,这样 diff 只需要关注动态内容的更新。
  2. 基于块的动态节点更新

    • Vue 3 的 diff 算法引入了“块”(block)概念,每个块内的节点都有动态标记,diff 过程中只会检查动态节点,而忽略静态节点。
    • 这种块级更新机制,减少了 Vue 3 在 diff 过程中的计算量。
  3. 事件绑定的优化

    • Vue 3 对事件绑定的处理进行了优化,在 diff 过程中,事件的比较更高效,避免不必要的事件解绑和重新绑定。
  4. 双端比较(与 Vue 2 类似):

    • Vue 3 中,diff 仍然采用了双端比较的策略:即从新旧节点的两端同时进行比较,通过头尾、尾头的顺序来加速对比。
    • 也包括新旧节点位置交换的处理优化。
  5. 最长递增子序列优化(LIS, Longest Increasing Subsequence)

    • 当新旧子节点序列存在较多相同的顺序时,Vue 3 会计算出最长递增子序列,以尽量减少对 DOM 的操作。
    • 通过找到最小的需要移动的节点位置序列,减少 DOM 节点的移动。

Vue 3 diff 流程的关键步骤:

  1. 同层对比:与 Vue 2 一样,Vue 3 只进行同层次的 diff 比较。
  2. 块级优化:Vue 3 生成的虚拟 DOM 是基于 block 的,不会对静态内容进行 diff,只对动态内容进行处理。
  3. 双端比较:与 Vue 2 类似,采用四种组合的双端对比。
  4. 最长递增子序列(LIS):通过找到最长递增子序列,减少 DOM 节点移动的次数。
  5. 处理剩余节点:如果头尾比较结束后仍有剩余节点,进行插入或删除操作。

Vue 3 diff 流程示意:

1
2
3
4
1. 静态节点被标记并提升,跳过不必要的 `diff`。
2. 仅对动态内容进行 `diff`。
3. 从前后两端同时进行双端比较,四种组合情况。
4. 通过 LIS 算法优化节点的移动操作。

Vue 3 与 Vue 2 diff 流程的对比

特性 Vue 2 Vue 3
静态节点处理 对静态节点会进行 diff 静态提升,编译时标记,不参与 diff
动态节点更新 无块级优化,所有子节点都会检查 引入块(block)优化,静态节点不再参与 diff
双端比较 双端比较 双端比较
最长递增子序列(LIS)优化 无 LIS 优化 引入 LIS 优化,减少 DOM 操作
性能表现 对于静态内容和复杂节点操作较低效 更高效,特别是对于大量静态节点的页面
事件处理优化 较为基础,需手动优化 事件处理更加高效,自动优化
内存管理和响应式性能 基于 Object.defineProperty 实现 基于 Proxy 实现,性能和灵活性更好

总结

  • Vue 2diff 算法是基于 Snabbdom 实现的,使用双端比较,依赖 key 值来高效更新,但在处理静态节点和复杂的 DOM 结构时性能并不理想。
  • Vue 3 通过静态提升、块优化、最长递增子序列(LIS)等新特性,在 diff 算法上做了大量改进,提升了性能,特别是在复杂页面和大量静态节点的场景下效果更好。

Vue 3 的这些改进显著提高了性能,特别是在大型项目和复杂应用中,diff 算法的优化能带来更好的用户体验。

Vue3相对于Vue2进行了哪些优化?

Vue 3 相比 Vue 2 进行了大量优化和改进,包括性能、API 设计、代码结构、开发体验等多个方面的提升。以下是 Vue 3 相对于 Vue 2 的主要优化和改进:

性能优化

Vue 3 在核心架构上进行了许多性能优化,使得其性能比 Vue 2 更加优秀,特别是在大型应用中。

更快的虚拟 DOM

  • 优化的虚拟 DOM diff 算法:Vue 3 对虚拟 DOM 的 diff 过程进行了优化,减少了不必要的 DOM 操作。比如在 Vue 3 中的静态内容会被标记为不需要重新渲染的部分,这减少了每次更新时对不变节点的计算。
  • 编译时优化:Vue 3 可以在编译阶段静态分析模板中哪些部分是动态的,哪些是静态的,从而减少运行时需要比较和更新的内容。

更小的包体积

Vue 3 通过 Tree-shaking(摇树优化)来减少打包体积,只有你使用的功能才会被打包进最终的项目。Vue 3 的包体积比 Vue 2 更小,尤其是在移除了对不必要功能的依赖时。

roxy 替代 Object.defineProperty

Vue 3 使用了现代浏览器支持的 Proxy 对象替代 Vue 2 中的 Object.defineProperty,实现了更高效和灵活的响应式系统。

  • Proxy 的优势:能够监听数组、对象的新增或删除属性等操作,这是 Vue 2 的 defineProperty 无法做到的,因此 Proxy 提供了更高的响应性和灵活性。

Composition API(组合式 API)

Vue 3 引入了全新的 Composition API,它作为对 Vue 2 中 Options API 的补充,提供了一种更灵活、可重用的代码组织方式,特别适合处理复杂的逻辑。

更好的逻辑复用

  • 在 Vue 2 中,逻辑复用主要依靠 mixins,但 mixins 存在命名冲突和复用不够明确的问题。
  • Vue 3 的 Composition API 通过 setup 函数和组合函数 (composables) 来分离关注点,使得逻辑复用更加明确、灵活,解决了 mixins 带来的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ref } from 'vue';

export default {
setup() {
const count = ref(0);

const increment = () => {
count.value++;
};

return {
count,
increment
};
}
};

更灵活的代码组织

  • Composition API 允许开发者根据功能而不是组件拆分代码,代码组织更加灵活,特别适合大型项目。
  • 对于 TypeScript 支持更友好,代码更容易进行类型推断。

全新的响应式系统

Vue 3 的响应式系统基于 Proxy 实现,较 Vue 2 中基于 Object.defineProperty 的实现更加高效和强大。

完全支持动态属性

  • 在 Vue 2 中,动态添加新属性不会触发响应式更新,除非使用 Vue.set 手动添加。而在 Vue 3 中,Proxy 可以直接追踪对象和数组的动态属性,无需额外处理。

更加直观的 API

  • Vue 3 提供了 refreactive 两种方式来处理响应式数据:
    • ref:用于创建对原始值(primitive types)的响应式引用。
    • reactive:用于创建对象或数组的响应式数据。

支持 Fragments(多根节点)

Vue 2 要求组件模板必须有一个根元素,而在 Vue 3 中,组件可以返回多个根元素,这个特性被称为 Fragments

1
2
3
4
<template>
<h1>Title</h1>
<p>This is a paragraph</p>
</template>
  • Vue 3 允许模板返回多个根节点,减少了在模板中使用不必要的容器元素(如 div)的问题,使模板更加简洁。

Teleport(传送)

Vue 3 引入了 Teleport 组件,允许开发者将组件的 DOM 结构渲染到指定的 DOM 节点之外。这个特性对于处理模态框、弹出层等场景非常实用。

1
2
3
4
5
<template>
<teleport to="body">
<div class="modal">This is a modal</div>
</teleport>
</template>
  • 这样 modal 的内容会被直接渲染到 body 中,而不是当前组件的 DOM 结构内。

新生命周期钩子

Vue 3 对生命周期钩子做了调整,并引入了一些新的名称:

钩子名称变化

  • beforeDestroy 改为 beforeUnmount
  • destroyed 改为 unmounted

onMounted 等 Composition API 生命周期钩子

Vue 3 在 Composition API 中提供了组合函数风格的生命周期钩子,如 onMountedonUnmounted,让逻辑代码与生命周期钩子结合更加灵活。

改进的 TypeScript 支持

Vue 3 从设计上对 TypeScript 提供了更好的支持,TypeScript 在 Vue 3 中的使用更加流畅,开发体验更好:

  • 更好的类型推断:Vue 3 提供了对 propsemit 和组件内部状态的更强的类型推断。
  • Composition API 对 TypeScript 的天然支持:使用 Composition API 编写的代码,更易于使用 TypeScript 进行类型检查和推断。

Tree-shaking 支持

Vue 3 是完全支持 Tree-shaking 的,也就是说,未使用的代码不会被打包到最终的应用中。这一优化在构建大型应用时可以显著减少打包体积,提升性能。

  • Vue 3 的模块设计是按需导入的,只有使用的功能才会被打包进来。相比 Vue 2,Vue 3 在减少包体积上有了显著改进。

自定义渲染 API

Vue 3 提供了全新的 Custom Renderer API,允许开发者创建自定义渲染器,将 Vue 组件渲染到非 DOM 环境中。例如,可以使用 Vue 3 创建游戏引擎的渲染器,将 Vue 组件渲染到 WebGL、Canvas 或其他平台上。

更好的内存管理与资源回收

Vue 3 在内存管理上做了改进,特别是对组件和响应式系统中的资源回收进行了优化,减少了不必要的内存占用。

  • 使用 Proxy 实现的响应式系统可以更高效地跟踪和清理无效的依赖,避免 Vue 2 中某些情况下可能产生的内存泄漏。

全新的编译器架构

Vue 3 的编译器进行了重构,采用了更为模块化和灵活的设计:

  • 模板编译器模块化:Vue 3 的模板编译器现在更加灵活,支持更多的自定义优化和插件。
  • 运行时特性优化:Vue 3 的运行时代码与编译时更加紧密结合,通过编译时生成优化的渲染函数,进一步提升了性能。

总结

Vue 3 相比 Vue 2 在性能、开发体验、API 灵活性等方面做了全面的优化和改进,主要包括:

  • 更快的响应式系统(基于 Proxy 实现)。
  • Composition API 提供了更灵活的代码组织方式。
  • 支持 TypeScript 更加流畅。
  • 提供更小的打包体积(通过 Tree-shaking)。
  • 新增了 Fragments、Teleport 等特性,提升了开发灵活性。
  • 生命周期钩子与逻辑代码结合更加灵活,API 使用更加简单直观。

这些改进使得 Vue 3 更加适合大型项目的开发,同时也保持了良好的向后兼容性,让 Vue 社区用户可以逐步升级到 Vue 3。

为什么vue的组件加了scoped就可以限制样式只在这个组件?

Vue 的组件中使用 scoped 关键字,能够将样式局限于特定组件内,这一原理主要依赖于样式的作用域隔离,具体过程如下:

  1. 动态生成的唯一标识符
    当你在 Vue 组件中添加 scoped 属性时,Vue 会在编译阶段为这个组件生成一个独特的 data-attribute(例如 data-v-xxxxxx)。这个属性会自动附加到该组件的根元素以及其所有子元素上,使这些元素具有唯一的标识符。

  2. CSS 选择器变异
    在有 scoped 属性的情况下,编译器会自动对样式中的选择器进行变异处理。它会为每一个选择器后添加一个对应的 data-attribute 选择器,例如:

    1
    2
    3
    .my-class {
    color: red;
    }

    编译后会变成:

    1
    2
    3
    .my-class[data-v-xxxxxx] {
    color: red;
    }
  3. 样式局部化
    由于这些样式选择器包含了该组件独特的 data-attribute,只有在该组件模板内的元素才会匹配这些样式。即使其他组件中有相同的类名 my-class,由于没有相同的 data-v-xxxxxx 属性,它们不会受到这个样式的影响。这就实现了样式的局部化

  4. 浏览器渲染机制
    当组件渲染时,浏览器会根据样式选择器和元素属性进行匹配。由于 data-v-xxxxxx 只存在于特定组件的 DOM 元素上,只有这些元素会应用对应的样式,从而确保样式不会“泄漏”到其他组件中。

总的来说,scoped 的工作原理是通过自动为样式选择器添加唯一的标识符,结合组件元素的特殊属性匹配机制,使得样式仅作用于当前组件的 DOM 树,避免了全局样式污染。

Vuex和Pinia的理解

Vuex 和 Pinia 的理解

Vuex 和 Pinia 都是 Vue.js 的状态管理工具,用来在 Vue 组件间共享和管理状态。在大型的 Vue 应用中,随着组件的复杂性和组件间状态的共享需求增加,状态管理工具变得尤为重要。

以下是对 VuexPinia 的理解及对比。


Vuex 的理解

1. 什么是 Vuex?

Vuex 是 Vue.js 的官方状态管理库,它基于 Flux 架构,专为处理 Vue 组件间共享状态的管理而设计。Vuex 提供了一种集中式的状态存储机制,可以让我们在整个应用中共享状态,同时确保状态变更可以被追踪和调试。

2. Vuex 的核心概念

  • State(状态): Vuex 的核心是全局的状态对象,所有组件都可以访问这个状态。
  • Getters(派生状态): 类似于 Vue 的计算属性,getters 用于从状态中派生出新的数据。
  • Mutations(突变): mutations 是同步地修改状态的唯一方式,必须显式地提交(commit)才能更改状态。它使状态变更过程更加可追踪和调试。
  • Actions(动作): actions 用于处理异步操作,类似于 mutations,但不同的是它们是异步的,通常在 actions 中提交 mutations
  • Modules(模块化): Vuex 支持将状态分割成模块,每个模块拥有自己的 statemutationsactionsgetters,从而更好地组织大型应用。

3. Vuex 的优缺点

优点:

  • 集中式管理: 所有的状态都集中在一个地方,方便调试和追踪。
  • 严格的规范: 使用 mutations 提交同步修改,状态变化过程透明可控,尤其在大型应用中有助于调试。
  • Vue DevTools 支持: 集成了 Vue DevTools,支持时间旅行调试和状态快照。

缺点:

  • 样板代码多: 每次修改状态必须通过 mutations,需要写较多的样板代码,特别是在状态变动频繁的情况下,显得比较繁琐。
  • 学习曲线: 对于初学者来说,理解 mutationsactions 和模块化管理可能比较复杂。
  • 冗余代码: 在处理简单应用时,由于严格的规范化,常常需要写额外的 mutationsactions,即便是小型状态变更。

Pinia 的理解

1. 什么是 Pinia?

Pinia 是 Vue 3 的新一代状态管理工具,被视为 Vuex 的替代品或未来版本。它灵感来源于 Vuex,但更轻量化、简洁,并且直接与 Vue 3 的 组合式 API 配合使用。Pinia 提供了更现代化的 API 和更简单的代码结构。

2. Pinia 的核心概念

  • Store: Pinia 中的每个 Store 都是独立的模块,可以自由定义和使用,类似 Vuex 的模块化,但更轻量。
  • State: 定义 Store 的状态,组件可以直接访问。
  • Getters: 类似于计算属性,getters 用于计算和返回从状态中派生出的值。
  • Actions: 用于修改状态,既可以是同步操作也可以是异步操作。Pinia 允许直接在 actions 中修改状态,而不需要像 Vuex 那样通过 mutations

3. Pinia 的优缺点

优点:

  • 轻量且现代化: Pinia 的 API 更加简洁,没有像 Vuex 那样的 mutations 概念,状态可以直接在 actions 中修改,减少了样板代码。
  • TypeScript 支持优秀: Pinia 原生支持 TypeScript,开发时可以自动获得类型推断,开发体验更加流畅。
  • 组合式 API: Pinia 与 Vue 3 的组合式 API 配合得非常好,支持使用 setup 函数定义状态管理。
  • 模块化简单: 每个 Store 都是独立的,没有 Vuex 那样复杂的模块系统,开发时更直观。

缺点:

  • 社区生态尚在成长: Vuex 因为存在时间长,生态系统更成熟。Pinia 虽然在 Vue 3 中表现良好,但由于是新工具,生态系统还在逐步成长。
  • 大型项目的设计模式: 对于非常复杂的大型项目,Pinia 的模块设计虽然更简洁,但缺乏 Vuex 那样的复杂场景管理和严格的状态变更控制。

Vuex 和 Pinia 的对比

特性 Vuex Pinia
发布版本 Vue 2 和 Vue 3 Vue 3 及以后的默认状态管理
API 复杂性 较为复杂,包含 stategettersmutationsactions 更简洁,没有 mutations,直接修改状态
状态修改方式 必须通过 mutations 提交状态变更 可以直接在 actions 中修改状态
模块化支持 支持模块化,适合大型项目 每个 Store 都是模块化,简单易用
TypeScript 支持 支持,但需要更多手动类型定义 原生支持 TypeScript,自动推断类型
组合式 API 支持 兼容,但代码较繁琐 与 Vue 3 的组合式 API 配合非常自然
生态系统 生态系统成熟,第三方插件丰富 正在发展,虽然简洁但生态系统尚不如 Vuex 完善

总结

  • Vuex:更适合于大型、复杂的 Vue 应用,因为它的严格状态管理流程让应用的状态更可预测、更易调试。对于已经使用 Vue 2 或正在维护老项目的开发者,Vuex 是一个可靠的选择。

  • Pinia:更适合 Vue 3 项目,尤其是偏向使用组合式 API 的项目。Pinia 更加现代化、简洁,并且减少了样板代码和状态管理的复杂性。如果你正在开发新的 Vue 3 项目,Pinia 是一个非常推荐的选择。

Pinia 是 Vue 未来的发展方向,特别适合追求现代开发方式的团队。Vuex 虽然历史悠久,功能全面,但面对 Vue 3 的进步,Pinia 的简洁和灵活性使其成为了一个极具吸引力的状态管理工具。