1、vue2生命周期
| 阶段名称 | 钩子函数 | 触发时机 | 用途 | 注意 |
|---|---|---|---|---|
| 创建前 | beforeCreate |
组件实例初始化之前 | 插件开发中的初始化任务 | 无法访问 data 和 methods |
| 创建后 | created |
数据观测、计算属性、方法已初始化,但 DOM 未生成 | 异步请求数据(如 API 调用)、初始化非 DOM 操作 | 避免操作 DOM(需等待 mounted) |
| 挂载前 | beforeMount |
模板编译完成,虚拟 DOM 尚未渲染为真实 DOM | 渲染前对状态的最后修改 | 极少使用 |
| 挂载后 | mounted |
实例已挂载到 DOM,可访问 this.$el |
操作 DOM、集成第三方库(如图表初始化) | 使用 this.$nextTick() 确保子组件渲染完成 |
| 更新前 | beforeUpdate |
数据变化后,虚拟 DOM 重新渲染前 | 获取更新前的 DOM 状态(如保存滚动位置) | 避免直接修改数据 |
| 更新后 | updated |
虚拟 DOM 重新渲染并应用更新后 | 执行依赖新 DOM 的操作(如调整布局) | 修改数据可能导致无限循环 |
| 销毁前 | beforeDestroy |
实例销毁前,仍完全可用 | 清理定时器、解绑事件、取消订阅(防止内存泄漏) | 需手动清理非 Vue 管理的资源 |
| 销毁后 | destroyed |
实例销毁后,所有指令和事件监听器已移除 | 执行最终清理操作 | 实例的所有绑定已解除 |
2、Vue3 与 Vue2 生命周期对比详解
-
钩子函数命名规范
- Vue3 :生命周期钩子统一添加
on前缀(如onMounted),需显式引入后使用。 - Vue2 :直接使用选项式 API 中的钩子(如
mounted)。
- Vue3 :生命周期钩子统一添加
-
beforeCreate和created合并- Vue3 :通过
setup()函数替代这两个阶段,初始化逻辑直接写在setup中。 - Vue2 :分别使用
beforeCreate和created钩子。
- Vue3 :通过
-
卸载阶段语义化更名
Vue2 钩子 Vue3 钩子 行为描述 beforeDestroyonBeforeUnmount组件卸载前触发 destroyedonUnmounted组件卸载完成时触发 -
新增调试钩子
onRenderTracked: 追踪响应式依赖的收集过程(开发模式)onRenderTriggered: 追踪数据变更触发的重新渲染(开发模式)
-
API 引入方式
- Vue3 :需从
vue显式引入钩子函数:
javascriptimport { onMounted, onUpdated } from 'vue' - Vue3 :需从
-
完整生命周期对照表
阶段 Vue2 钩子 Vue3 钩子 初始化 beforeCreatesetup() 替代 createdsetup() 替代 挂载 beforeMountonBeforeMount mountedonMounted 更新 beforeUpdateonBeforeUpdate updatedonUpdated 销毁 beforeDestroyonBeforeUnmount destroyedonUnmounted 调试 - onRenderTracked - onRenderTriggered -
代码示例对比
-
Vue2 选项式 API
javascriptexport default { created() { console.log('数据观测/事件初始化完成') }, mounted() { console.log('DOM 渲染完成') }, beforeDestroy() { console.log('实例销毁前清理操作') } } -
Vue3 组合式 API
javascriptimport { onMounted, onBeforeUnmount } from 'vue' export default { setup() { // 替代 created console.log('响应式数据初始化') onMounted(() => { console.log('DOM 挂载完成') }) onBeforeUnmount(() => { console.log('组件卸载前清理') }) } }
-
3、Vue 的父组件和子组件生命周期钩子函数执行顺序?
- 加载渲染过程: 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子mounted -> 父 mounted
- 子组件更新过程:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
- 父组件更新过程:父 beforeUpdate -> 父 updated
- 销毁过程:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
4、OptionsAPI(选项式 API) 与 CompositionAPI(组合式 API)
-
Options API
-
在Vue3之前,我们主要使用的是选项式API(Options API)。这种API的设计方式是基于对象的,我们将一个Vue实例的各个部分拆分成不同的选项,如data、methods、computed、watch等,并在创建Vue实例时将它们作为选项传入。
-
选项式API的优点在于其结构清晰、易于理解和上手。每个选项都有其明确的职责,开发者只需关注自己需要实现的功能,而无需过多关心Vue内部的运行机制。这种开发方式对于小型到中型的应用来说是非常高效的。然而,随着应用规模的扩大和复杂度的增加,选项式API也暴露出了一些问题。当组件的逻辑变得复杂时,代码会变得难以维护和理解。由于数据和逻辑被分散在多个选项中,很难一眼看出它们之间的关系。此外,对于复用逻辑代码也存在一定的困难,因为逻辑代码往往与特定的data和methods紧密耦合。。Options API 的特点包括:
javascript易于上手:Options API 的结构清晰,容易理解和学习,适合初学者入门。 逻辑分离:不同功能的代码被分离到不同的选项中,使得代码更易维护和阅读。 依赖注入:通过 this 上下文可以方便地访问到组件的属性和方法。 -
示例:
javascriptexport default { data() { return { count: 0 }; }, methods: { increment() { this.count++; } }, mounted() { console.log('Mounted'); } }
-
-
CompositionAPI
-
组合式API是 Vue.js 3.x 中引入的新特性,旨在解决选项式API 在复杂组件中难以维护的问题。组合式API允许将组件的逻辑按照功能相关性放在一起,而不是按照选项分散组织。组合式API 的特点包括:
javascript逻辑复用:可以将逻辑抽取为可复用的函数,更方便地在不同组件之间共享逻辑。 代码组织:将相关逻辑放在一起,使得组件更加清晰和易于维护。 更好的类型推断:由于函数可以提供更多信息,TypeScript 在使用 Composition API 时能够提供更好的类型推断。 -
示例:
javascriptimport { ref, onMounted } from 'vue'; export default { setup() { const count = ref(0); const increment = () => count.value++; onMounted(() => console.log('Mounted')); return { count, increment }; } } -
举个栗子:
-
选项式 API 就像你家里整理东西的抽屉:
每个抽屉专门放一类东西(比如一个抽屉放袜子,一个放证件)。
缺点:如果你想找一套衣服(上衣+裤子),得挨个翻不同的抽屉。
代码中:数据在 data 抽屉,方法在 methods 抽屉,生命周期在 mounted 抽屉...同一功能的代码分散在各处。
-
组合式 API 就像你收拾行李:
直接把一套衣服(上衣+裤子+袜子)叠好放一个包里,要用时整个包拿走。
代码中:同一个功能的所有代码(数据、方法、生命周期)都集中写在一起,方便维护和复用。
额外好处:你可以把常用的行李包(比如洗漱包)做好,以后出门直接复用,不会和其他行李搞混。
-
-
对比
Options类型的 API,数据、方法、计算属性等,集中在:data、methods、computed中的,若想改动一个需求,就需要分别修改:data、methods、computed,不便于维护和复用。
Composition 可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。
-
5、vue3 setup
- 在 Vue3 中,setup 函数是一个新引入的概念,它代替了之前版本中的 data、computed、methods 等选项。setup 是 Vue 3 组合式 API 的"大本营",用来集中写组件的核心逻辑(数据、方法、生命周期等)。至于为什么用它,是因为它告别选项式 API 的代码分散问题,让同一功能的代码"扎堆"写在一起,方便维护和复用!。在setup中不用写
this.所有数据通过变量名直接访问。
以下是setup的特点:- 更灵活的组织逻辑:setup 函数可以将相关逻辑按照功能进行组织,使得组件更加清晰和易于维护。不再受到 Options API 中选项的限制,可以更自由地组织代码。
- 逻辑复用:可以将逻辑抽取为可复用的函数,并在 setup 函数中进行调用,实现逻辑的复用,避免了在 Options API 中通过 mixins 或混入对象实现逻辑复用时可能出现的问题。
- 更好的类型推断:由于 setup 函数本身是一个普通的 JavaScript 函数,可以更好地与 TypeScript 配合,提供更好的类型推断和代码提示。
- 更好的响应式处理:setup 函数中可以使用 ref、reactive 等函数创建响应式数据,可以更方便地处理组件的状态,实现数据的动态更新。
- 更细粒度的生命周期钩子:setup 函数中可以使用 onMounted、onUpdated、onUnmounted 等函数注册组件的生命周期钩子,可以更细粒度地控制组件的生命周期行为。
- 更好的代码组织:setup 函数将组件的逻辑集中在一个地方,使得代码更易读、易维护,并且可以更清晰地看到组件的整体逻辑。
- 两种写法
-
选项式写法(传统)
javascript<script> import { ref } from 'vue'; export default { setup() { const count = ref(0); const increment = () => count.value++; return { count, increment }; } } </script> -
<script setup>语法糖写法javascript<script setup> import { ref } from 'vue'; const count = ref(0); const increment = () => count.value++; </script>
-
6、vue3 setup语法糖
-
直接在script标签中添加setup属性就可以直接使用setup语法糖了。
使用setup语法糖后,不用写setup函数,组件只需要引入不需要注册,属性和方法也不需要再返回,所有在
<script setup>顶层声明的变量函数自动暴露给模板。-
示例 :
html<template> <my-component @click="func" :numb="numb"></my-component> </template> <script lang="ts" setup> import {ref} from 'vue'; import myComponent from '@/component/myComponent.vue'; //此时注册的变量或方法可以直接在template中使用而不需要导出 const numb = ref(0); let func = ()=>{ numb.value++; } </script> -
setup语法糖中新增的api
- defineProps:子组件接收父组件中传来的props
- defineEmits:子组件调用父组件中的方法
- defineExpose:子组件暴露属性,可以在父组件中拿到
defineProps
-
父组件代码
html<template> <my-component @click="func" :numb="numb"></my-component> </template> <script lang="ts" setup> import {ref} from 'vue'; import myComponent from '@/components/myComponent.vue'; const numb = ref(0); let func = ()=>{ numb.value++; } </script> -
子组件代码
html<template> <div>{{numb}}</div> </template> <script lang="ts" setup> import {defineProps} from 'vue'; defineProps({ numb:{ type:Number, default:NaN } }) </script>
defineEmits
-
子组件代码
html<template> <div>{{numb}}</div> <button @click="onClickButton">数值加1</button> </template> <script lang="ts" setup> import {defineProps,defineEmits} from 'vue'; defineProps({ numb:{ type:Number, default:NaN } }) const emit = defineEmits(['addNumb']); const onClickButton = ()=>{ //emit(父组件中的自定义方法,参数一,参数二,...) emit("addNumb"); } </script> -
父组件代码
html<template> <my-component @addNumb="func" :numb="numb"></my-component> </template> <script lang="ts" setup> import {ref} from 'vue'; import myComponent from '@/components/myComponent.vue'; const numb = ref(0); let func = ()=>{ numb.value++; } </script>
defineExpose
-
子组件代码
html<template> <div>子组件中的值{{numb}}</div> <button @click="onClickButton">数值加1</button> </template> <script lang="ts" setup> import {ref,defineExpose} from 'vue'; let numb = ref(0); function onClickButton(){ numb.value++; } //暴露出子组件中的属性 defineExpose({ numb }) </script> -
父组件代码
html<template> <my-comp ref="myComponent"></my-comp> <button @click="onClickButton">获取子组件中暴露的值</button> </template> <script lang="ts" setup> import {ref} from 'vue'; import myComp from '@/components/myComponent.vue'; //注册ref,获取组件 const myComponent = ref(); function onClickButton(){ //在组件的value属性中获取暴露的值 console.log(myComponent.value.numb) //0 } //注意:在生命周期中使用或事件中使用都可以获取到值, //但在setup中立即使用为undefined console.log(myComponent.value.numb) //undefined const init = ()=>{ console.log(myComponent.value.numb) //undefined } init() onMounted(()=>{ console.log(myComponent.value.numb) //0 }) </script>
-
7、在 Vue3 中引入组件主要有 全局注册 和 局部注册 两种方式,以下是具体实现和对比:
-
手动引入组件
-
全局注册(Global Registration)
在 main.ts 中一次性注册全局组件,适用于高频使用的公共组件(如按钮、弹窗)。
typescript// main.ts import { createApp } from 'vue' import App from './App.vue' // 导入组件 import MyButton from '@/components/MyButton.vue' import MyModal from '@/components/MyModal.vue' const app = createApp(App) // 全局注册组件 app.component('MyButton', MyButton) app.component('MyModal', MyModal) app.mount('#app')- 特点 :
全局可用,任何模板中直接使用 标签
适合基础组件,但可能导致打包体积冗余
- 特点 :
-
局部注册(Local Registration)
在单个 .vue 文件中按需引入,适用于低频或专用组件。
1.使用 Options API(传统写法)
html<!-- ParentComponent.vue --> <template> <ChildComponent /> </template> <script> import ChildComponent from './ChildComponent.vue' export default { components: { ChildComponent } // 局部注册 } </script>2.使用
<script setup>语法糖(推荐)html<!-- ParentComponent.vue --> <template> <ChildComponent /> </template> <script setup> // 直接导入即可使用,无需显式注册 import ChildComponent from './ChildComponent.vue' </script>- 特点 :
组件仅在当前文件中可用
避免全局污染,更利于 Tree-shaking 优化
- 特点 :
-
-
自动注册组件(Auto Registration)
-
使用 Vite 的 Glob 导入(推荐)
动态扫描 components 目录下的所有 .vue 文件,批量全局注册。
typescript// src/components/auto-register.ts import { App } from 'vue' export default { install(app: App) { // 匹配 components 目录下所有 .vue 文件 const modules = import.meta.glob('@/components/**/*.vue', { eager: true }) Object.entries(modules).forEach(([path, module]) => { // 从文件路径提取组件名(如 MyButton.vue -> MyButton) const name = path.split('/').pop()?.replace('.vue', '') || '' app.component(name, (module as any).default) }) } } // main.ts 中调用 import autoRegister from '@/components/auto-register' app.use(autoRegister)命名规则:
MyButton.vue→<my-button>(推荐小写短横线命名)强制命名规范:可在注册逻辑中添加 PascalCase 转换
-
使用 unplugin-vue-components 插件(按需自动注册)
通过插件自动识别模板中的组件并动态导入(类似 Uniapp 的 Easycom)。
typescript// vite.config.ts import Components from 'unplugin-vue-components/vite' export default defineConfig({ plugins: [ Components({ // 指定扫描目录(默认 src/components) dirs: ['src/components'], // 生成类型声明文件(支持TS) dts: 'src/components.d.ts' }) ] })
-
特点 :
无需手动导入,直接在模板中使用
<MyComponent>自动生成类型声明,完美支持 TypeScript
-
最佳实践选择
场景 推荐方案 高频基础组件(如按钮、输入框) 全局手动注册 或 unplugin 插件 低频专用组件 局部注册 + <script setup>UI 库组件(如 Element Plus) unplugin 插件 + 按需导入 旧项目迁移 Vite Glob 自动注册
-
8、Vue2 和 Vue3 的区别
- 响应式原理:Vue2 使用 Object.defineProperty,Vue3 改用 Proxy(支持数组和深层对象监听)。
- API 设计:Vue3 引入 Composition API(逻辑复用更灵活),Vue2 使用 Options API。
- 性能优化:Vue3 的虚拟 DOM 更高效,支持 Tree-shaking(减少打包体积)。
- 生命周期:部分钩子重命名(如 beforeDestroy → beforeUnmount)。
- 新特性:Fragment(多根节点)、Teleport(传送组件)、Suspense(异步组件加载)。
- 全局 API:Vue3 通过 createApp 创建实例,避免全局污染。
9、Vue2/Vue3 全家桶
-
Vue2
javascript核心库:Vue.js 路由:Vue Router 状态管理:Vuex 构建工具:Vue CLI -
Vue3
javascript核心库:Vue.js 路由:Vue Router 状态管理:Pinia(官方推荐,替代 Vuex) 构建工具:Vite 或 Vue CLI。
10、 Vue2 不能监听数组下标原因
-
Vue 2 用的是 Object.defineProperty 劫持数据实现数据视图双向绑定。
-
Object.defineProperty 是可以劫持数组的
javascriptconst arr = [1, 2, 3, 4]; Object.keys(arr).forEach(function(key) { Object.defineProperty(arr, key, { get: function() { console.log('key:' + key) }, set: function(value) { console.log('value:' + value) } }); }); arr[1]; arr[2] = 4; -
真实情况:是 Object.defineProperty 可以劫持数组而 vue2 没有用来劫持数组。
-
原因:Object.defineProperty 是属性级别的劫持,如果按上面代码的方式去劫持数组,随着数组长度增加,会有很大的性能损耗,导致框架的性能不稳定,因此vue2 放弃一定的用户便捷性,提供了 $set 方法去操作数组,以最大程度保证框架的性能稳定。
11、vue 的通讯方式
通讯用于组件间数据传递与共享,vue 提供了多种方式解决该问题。
-
vue中8种常规的通信方案:
javascript通过 props 传递 通过 $emit 触发自定义事件 使用 ref EventBus $parent 或$root attrs 与 listeners Provide 与 Inject Vuex -
组件间通信的分类可以分成以下:
javascript父子关系的组件数据传递选择 props 与 $emit进行传递,也可选择ref 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递 祖先与后代组件数据传递可选择attrs与listeners或者 Provide与 Inject 复杂关系的组件数据传递可以通过vuex存放共享的变量
11、vue3 主流的通讯方式
- defineProps、defineEmits、defineExpose、Pinia
12、为什么 vue 中的 data 是一个 function 而不是普通 object?
- 因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
13、watch 和 computed 有什么区别?
-
computed :
计算属性: computed是用于创建计算属性的方式,它依赖于Vue的响应式系统来进行数据追踪。当依赖的数据发生变化时,计算属性会自动重新计算,而且只在必要时才重新计算。
缓存: 计算属性具有缓存机制,只有在它依赖的数据发生变化时,计算属性才会重新计算。这意味着多次访问同一个计算属性会返回相同的结果,而不会重复计算。
无副作用: 计算属性应当是无副作用的,它们只是基于数据的计算,并不会修改数据本身。
用于模板中: 计算属性通常用于模板中,以便在模板中显示派生数据。
必须同步 :只对同步代码中的依赖响应。
-
watch :
监听数据: watch用于监视数据的变化,你可以监视一个或多个数据的变化,以执行自定义的响应操作。
副作用操作: watch中的回调函数可以执行副作用操作,例如发送网络请求、手动操作DOM,或执行其他需要的逻辑。
不缓存: watch中的回调函数会在依赖数据变化时立即被触发,不会像computed那样具有缓存机制。
用于监听数据变化: watch通常用于监听数据的变化,而不是用于在模板中显示数据。
支持异步:在检测数据变化后,可进行同步或异步操作。
我自己的理解watch 当数据变化后需要触发外部动作(如接口请求、DOM 操作)或处理异步任务时,而computed 当需要实时同步计算且结果需直接显示时(如购物车总价、表单验证)
14、谈谈 computed 的机制,缓存了什么?
-
Vue.js 中的
computed属性确实具有缓存机制,这个缓存机制实际上是指对计算属性的值进行了缓存。当你在模板中多次访问同一个计算属性时,Vue.js只会计算一次这个属性的值,然后将结果缓存起来,以后再次访问时会直接返回缓存的结果,而不会重新计算。假设你有一个计算属性
fullName,它依赖于firstName和lastName两个响应式数据。当你在模板中使用{``{ fullName }}来显示全名时,Vue.js会自动建立依赖关系,并在firstName或lastName发生变化时,自动更新fullName的值,然后将新的值渲染到页面上。 -
我的理解如果 computed 所依赖的响应式数据(如 data 中的属性或其他 computed 属性)没有发生变化,则无论多少次访问该 computed 属性,直接返回上一次的缓存值,不会重新计算,如果依赖发生变化了那么就重新计算。
15、为什么 computed 不支持异步
- 这个是 vue 设计层面决定的,computed 的定义是,"依赖值改变computed值就会改变",所以这里必须是同步的,否则就可能 "依赖值改变但computed值未改变了",一旦computed 支持异步,computed 就违背定义了,会变得不稳定。相反,watch 的定义是,"监控的数据改变后,它做某件事",那 watch 在监听变化后,做同步异步都可以,并不违背定义。
16、vue3 中 ref 和 reactive 的区别
-
处理的数据类型不同
-
ref:
-
适合处理基本类型(数字、字符串、布尔值)。
-
也能处理对象或数组。
javascriptconst num = ref(0); // 数字 ✅ (但要用 num .value) const obj = ref({ a: 1 }); // 对象 ✅(但要用 obj.value.a)
-
-
reactive:
-
只能处理对象或数组(不能直接处理基本类型)。
javascriptconst state = reactive({ count: 0 }); // 对象 ✅ const list = reactive([1, 2, 3]); // 数组 ✅ const num = reactive(0); // ❌ 错误!
-
-
-
使用方式不同
-
ref:
-
在 JS 中必须用 .value 访问或修改值。
-
在模板中自动解包,不用写 .value。
javascript// JS 中 const count = ref(0); count.value = 1; // ✅ 修改值html<!-- 模板中 --> <div>{{ count }}</div> <!-- 直接写 count,不用 .value -->
-
-
reactive:
-
直接访问属性,不用 .value。
javascriptconst state = reactive({ count: 0 }); state.count = 1; // ✅ 直接修改属性
-
-
-
如何保持响应性?
-
ref:
-
解构时会丢失响应性(比如 const { value } = count)。
-
但可以传递整个 ref 给其他函数或组件,保持响应性。
javascript// 父组件 import { ref } from 'vue'; // 定义一个 ref const count = ref(0); // 定义一个函数,接收整个 ref function increment(counter) { counter.value++; // 直接修改 ref 的 value } // 调用函数,传递整个 ref increment(count); console.log(count.value); // 1 ✅(值被修改,且保持响应性)
-
-
reactive:
-
解构也会丢失响应性!
javascriptconst state = reactive({ count: 0 }); const { count } = state; // ❌ count 不再响应式! -
解决办法:用 toRefs 转换
javascriptconst { count } = toRefs(state); // ✅ count 是 ref,保持响应式
-
-
-
替换对象时的区别
-
ref:可以直接替换整个对象。
javascriptconst obj = ref({ a: 1 }); obj.value = { a: 2 }; // ✅ 替换整个对象 -
reactive:不能直接替换整个对象!
javascriptconst state = reactive({ a: 1 }); state = { a: 2 }; // ❌ 错误!会破坏响应性
-
-
一句话总结
- 用 ref:处理基本类型,或想统一写法时(不怕写 .value)。
- 用 reactive:处理对象/数组,且想直接操作属性(不想写 .value)。
-
举个栗子 🌰
-
计数器(基本类型)→ 用 ref
javascriptconst count = ref(0); const add = () => count.value++; -
表单对象(多个属性)→ 用 reactive
javascriptconst form = reactive({ name: '小明', age: 18, submit() { /* 提交逻辑 */ } });
-
17、vue3 区分 ref 和 reactive 的原因
-
因为「基本类型」和「对象」的响应式实现方式不同
-
基本类型(数字、字符串等)本身是"不可变的",Vue3 想要监听它的变化,必须把它包成一个对象(比如 { value: 0 }),这就是 ref 的由来。
-
对象本身是"可变的",Vue3 可以直接用 Proxy 代理它的属性变化,所以直接用 reactive 处理更简单。
-
简单说:
- ref 是给「单个值」穿个马甲(包成对象),强行让它能变。
- reactive 是直接给「对象」装个监听器(Proxy),监听属性变化。
-
-
为了开发体验更灵活
-
ref 的 .value 虽然麻烦,但统一了写法(不管数据是简单值还是对象,都用 .value 操作),适合简单场景。
-
reactive 不用写 .value,直接操作属性,适合复杂对象(比如表单、配置项)。
-
举个栗子:
- 如果只有 reactive,处理一个数字也得写成 reactive({ value: 0 }),反而更啰嗦。
- 如果只有 ref,操作对象属性时一直要写 .value.xxx,代码会很难看。
-
-
避免开发者踩坑
-
基本类型用 reactive 会报错:比如 reactive(0) 直接无效,强制你用 ref,防止错误使用。
-
对象用 ref 需要写 .value:提醒你这是个响应式对象,避免和普通对象混淆。
-
类比:
- 就像药盒上贴标签,告诉你"这是外用药"还是"内服药",防止用错。
-
-
一句话总结
- Vue3 区分 ref 和 reactive,是因为基本类型和对象的响应式实现原理不同,同时让开发者能根据场景选择更顺手的写法,少写 bug,多摸鱼 🐟。
18、vue3 为什么要用 proxy 替换 Object.defineproperty
- Vue 3 在设计上选择使用
Proxy替代Object.defineProperty主要是为了提供更好的响应性和性能。
Object.defineProperty是在 ES5 中引入的属性定义方法,用于对对象的属性进行劫持和拦截。Vue 2.x 使用Object.defineProperty来实现对数据的劫持,从而实现响应式数据的更新和依赖追踪。Object.defineProperty只能对已经存在的属性进行劫持,无法拦截新增的属性和删除的属性。这就意味着在 Vue 2.x 中,当你添加或删除属性时,需要使用特定的方法(Vue.set 和 Vue.delete)来通知 Vue 响应式系统进行更新。这种限制增加了开发的复杂性。Object.defineProperty的劫持是基于属性级别的,也就是说每个属性都需要被劫持。这对于大规模的对象或数组来说,会导致性能下降。因为每个属性都需要添加劫持逻辑,这会增加内存消耗和初始化时间。- 相比之下,Proxy 是 ES6 中引入的元编程特性,可以对整个对象进行拦截和代理。Proxy 提供了更强大和灵活的拦截能力,可以拦截对象的读取、赋值、删除等操作。Vue 3.x 利用 Proxy 的特性,可以更方便地实现响应式系统。
- 使用 Proxy 可以解决 Object.defineProperty 的限制问题。它可以直接拦截对象的读取和赋值操作,无需在每个属性上进行劫持。这样就消除了属性级别的劫持开销,提高了初始化性能。另外,Proxy 还可以拦截新增属性和删除属性的操作,使得响应式系统更加完备和自动化。
19、Vue 与 React 的区别
- 设计理念 :
- Vue:渐进式框架,内置路由/状态管理。
- React:库性质,依赖社区生态(如 React Router/Redux)。
- 语法:Vue 用模板,React 用 JSX。
- 响应式:Vue 自动追踪依赖,React 是状态驱动需手动 setState 或使用 Hooks。
- 打包体积:Vue3 更小(Tree-shaking),React + React DOM 约 40KB+(gzip)。
20、Vue Router 3.x Hash vs History 模式
- Hash 模式 :
- URL 带 #,通过 hashchange 监听路由变化。
- 无需后端支持,兼容性好。
- History 模式 :
- 基于 history.pushState,URL 更简洁。
- 需服务器配置(如 Nginx 的 try_files uri uri/ /index.html)。
21、Vue2 的 $nextTick
-
作用:在下次 DOM 更新循环后执行回调,用于获取更新后的 DOM。
-
原理:基于微任务(如 Promise.then)或宏任务(如 setTimeout)实现异步队列。
javascriptthis.$nextTick(() => { // DOM 已更新,可以安全操作 const element = document.getElementById('my-element'); console.log(element.offsetHeight); });
22、Vue2 数组变更刷新
- 限制 :
- 直接通过索引修改(如 arr = 1)或修改长度(arr.length = 0)不会触发视图更新。
- 解决方案 :
- 使用变异方法:push、pop、splice 等。
- Vue.set(arr, index, newValue) 或 this.set(arr,index,newValue)。或者使用this.set(arr, index, newValue)。或者使用this.set(arr,index,newValue)。或者使用this.forceUpdate强制刷新
23、watch 怎么深度监听对象变化?为什么要深度监听默认监听不行吗?
-
设置deep: true来启用深度监听
javascriptwatch: { myObject: { handler(newVal, oldVal) { console.log('对象发生变化'); }, deep: true, // 设置 deep 为 true 表示深度监听 } } -
问题所在:默认监听的局限性
- 对象深层属性变化无法被检测
javascriptexport default { data() { return { user: { name: '张三', address: { city: '北京', street: '朝阳路' } } } }, watch: { user: { handler(newVal) { console.log('user 变化了'); } // 默认情况下,只有 user 被整体替换时才会触发 } }, methods: { updateUserCity() { // ❌ 这里不会触发 watch! this.user.address.city = '上海'; // ✅ 只有这种方式会触发 // this.user = { ...this.user, address: { ...this.user.address, city: '上海' } }; } } } ``` 2. 数组元素变化无法被检测 ```javascript export default { data() { return { list: ['a', 'b', 'c'] } }, watch: { list: { handler(newVal) { console.log('list 变化了'); } // 默认情况下,数组元素变化不会触发 } }, methods: { updateArray() { // ❌ 这些都不会触发 watch! this.list[0] = 'x'; // 修改元素 this.list.length = 0; // 修改长度 // ✅ 只有这些会触发 // this.list.push('d'); // Vue 重写的数组方法 // this.list = ['x', 'y']; // 整体替换 } } } -
解决方案:深度监听
-
开启深度监听
javascriptexport default { data() { return { user: { name: '张三', profile: { age: 25, hobbies: ['篮球', '音乐'] } } } }, watch: { user: { handler(newVal, oldVal) { console.log('user 或其嵌套属性变化了'); // 现在无论修改 user.name 还是 user.profile.age 都会触发 }, deep: true, // ⭐ 关键:开启深度监听 immediate: true // 可选:立即执行一次 } }, methods: { testDeepWatch() { // ✅ 所有这些现在都会触发 watch: this.user.name = '李四'; this.user.profile.age = 30; this.user.profile.hobbies.push('阅读'); } } } -
深度监听的工作原理
javascript// 简化版原理:Vue 会递归遍历对象的所有属性 function enableDeepWatch(obj, watcher) { // 遍历对象的所有属性 for (let key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key]; // 如果是对象或数组,递归设置响应式 if (typeof value === 'object' && value !== null) { // 为嵌套属性也创建依赖收集 defineReactive(value); enableDeepWatch(value, watcher); // 递归 } } } }
-
-
实际应用场景
场景1:表单的复杂嵌套数据
javascriptexport default { data() { return { formData: { basic: { name: '', age: '' }, contact: { phone: '', address: { province: '', city: '' } } } } }, watch: { formData: { handler(newVal) { // 监听整个表单的任何变化,用于自动保存等 this.autoSave(); }, deep: true, immediate: true } } }场景2:监控复杂配置对象
javascriptexport default { data() { return { chartConfig: { title: { text: '图表', show: true }, xAxis: { data: [] }, yAxis: { type: 'value' }, series: [{ data: [], type: 'line' }] } } }, watch: { chartConfig: { handler() { // 配置的任何变化都重新渲染图表 this.renderChart(); }, deep: true } } } -
性能考虑和替代方案
问题:深度监听可能性能开销大
javascriptexport default { watch: { hugeObject: { handler() { // 如果 hugeObject 很大,深度监听会递归所有属性 }, deep: true // ⚠️ 对大对象可能有性能问题 } } }解决方案:精确监听
javascriptexport default { watch: { // 方案1:只监听特定嵌套属性 'user.profile.age'(newVal) { console.log('年龄变化:', newVal); }, // 方案2:监听多个关键路径 'user.name': function(newVal) { console.log('姓名变化:', newVal); }, // 方案3:使用计算属性作为中介 userBasicInfo() { // 在 computed 中返回需要监听的部分 console.log('用户基本信息变化'); } }, computed: { userBasicInfo() { return { name: this.user.name, age: this.user.profile.age }; } } } -
总结
为什么 watch 需要深度监听?-
引用类型特性:对象和数组是引用类型,直接修改嵌套属性不会触发 setter
-
响应式系统限制:Vue 2 默认只监听属性值的直接变化
-
实际业务需求:复杂数据结构的任何变化都需要被感知
-
开发体验:避免手动处理深层数据变化的繁琐工作
使用建议:
-
小到中等规模对象:直接使用 deep: true
-
大型对象或性能敏感场景:使用精确监听路径
-
数组变化:考虑使用 Vue 重写的数组方法或 deep: true
-
24、vue2 删除数组用 delete 和 Vue.delete 有什么区别?
- delete
-
delete是JavaScript的原生操作符,用于删除对象的属性。当你使用delete删除数组的元素时,元素确实会被删除,但数组的长度不会改变,被删除的元素将变成undefined。 -
delete操作不会触发Vue的响应系统,因此不会引起视图的更新。
javascriptconst arr = [1, 2, 3]; delete arr[1]; // 删除元素2 // 现在 arr 变成 [1, empty, 3]
-
- Vue.delete
-
Vue.delete是Vue 2提供的用于在响应式数组中删除元素的方法。它会将数组的长度缩短,并触发Vue的响应系统,确保视图与数据同步。 -
使用
Vue.delete来删除数组元素,Vue会正确追踪更改,并在视图中删除相应的元素。
javascriptconst arr = [1, 2, 3]; Vue.delete(arr, 1); // 删除元素2 // 现在 arr 变成 [1, 3]
-
25、Vue3.0 编译做了哪些优化?
- 静态树提升(Static Tree Hoisting): Vue 3.0 引入了静态树提升优化,它通过分析模板并检测其中的静态部分,将静态节点提升为常量,减小渲染时的开销。可显著降低渲染函数的复杂性,减少不必要的运行时开销。
- 源码优化: Vue 3.0 在编译器的源码生成方面进行了优化,生成的代码更加精简和高效。这有助于减小构建后的包的体积,提高运行时性能。
- Patch Flag: Vue 3.0 引入了 Patch Flag,它允许 Vue 在渲染时跳过不需要更新的节点,从而进一步提高性能。Patch Flag 为 Vue 提供了一种方法来跟踪哪些节点需要重新渲染,以及哪些节点可以被跳过。
- Diff 算法优化: Vue 3.0 使用了更高效的Virtual DOM diff算法,与Vue 2相比,减少了不必要的虚拟节点创建和比对,提高了渲染性能。
- 模板嵌套内联: Vue 3.0 允许在模板中内联子组件的模板,从而避免了运行时编译。这有助于减小构建后的包的大小,提高初始化性能。
- 模板块提取: Vue 3.0 允许在编译时将模板块提取到独立的模块中,这有助于代码分割和按需加载,从而减小初始化时需要加载的代码量。
- 更好的类型支持: Vue 3.0 支持更好的类型推断,借助TypeScript等类型检查工具,可以提供更好的开发体验和更强的类型安全性。
26、问题:Vue3.0 新特性 ------ Composition API 与 React.js 中 Hooks 的异同点
-
相似点
-
核心目标一致:解决「逻辑复用」难题
- Vue Composition API:通过 useXXX() 函数(比如 useCounter())把相关逻辑打包成块,哪里需要就"搬"到哪里。
- React Hooks:通过自定义 Hook(比如 useCounter())复用逻辑,直接插到组件里就能用。
说白了:两者都能像拼乐高一样,把代码块随意组合复用,告别"复制粘贴"大法。
-
告别「类组件」,拥抱函数式
- Vue:用 setup() 函数替代 data、methods 等选项,所有逻辑写成函数。
- React:函数组件 + Hooks 取代类组件,不用再写 this 和生命周期方法。
说白了:以前用类写的复杂组件,现在都能用函数搞定,代码更简洁,脑子更清醒。
-
状态管理:让数据跟着逻辑走
- Vue:用 ref、reactive 定义响应式数据,数据一变,视图自动更新。
- React:用 useState、useReducer 管理状态,状态变化触发组件重新渲染。
说白了:两者都让"数据驱动视图",把数据和操作数据的逻辑放在一起,不再东一块西一块。
-
副作用处理:集中管理「搞事情」的代码
- Vue:用 watch、watchEffect 监听数据变化,处理副作用(比如调接口)。
- React:用 useEffect 统一处理副作用(比如订阅、调接口)。
说白了:以前散落在生命周期里的"搞事情"代码(如 componentDidMount),现在都能集中管理,一目了然。
-
代码组织:把「相关的东西」放一起
- Vue:在 setup() 里,可以把"用户登录"、"表单验证"等逻辑各自打包成块。
- React:在函数组件里,用多个 Hooks 把"计数"、"动画"等逻辑拆分成独立单元。
说白了:以前按选项(data、methods)分类,现在按功能分类,改代码不用上下乱跳。
一句话总结 :
Composition API 和 React Hooks 就像麦当劳和肯德基,虽然做法不同(响应式 vs 状态驱动),但核心目标都是让开发者吃上更香(复用逻辑)、更爽(代码清晰)的汉堡(写代码)!
-
-
不同点
1. 响应式原理不同
-
Vue Composition API:
基于"响应式系统",数据变化自动触发更新。
(比如用 ref、reactive 定义数据,修改时视图自动跟着变,不用手动触发。)
-
React Hooks:
基于"状态 + 副作用",数据变化需要手动触发重新渲染。
(比如用 useState 定义数据,改完数据后,React 会自动重新执行组件函数来更新视图,但依赖闭包,容易遇到"过期值"问题。)
-
代码组织逻辑不同
-
Vue Composition API:
在 setup 函数里,可以把相关逻辑的变量、方法、计算属性等写在一起,像一个乐高积木块。
(比如把用户登录的逻辑集中在一个 useAuth 函数里,清晰隔离。)
-
React Hooks:
逻辑分散在多个 useState、useEffect 中,需要靠开发者自己拆分组合。
(比如一个功能可能要用到多个 useEffect,代码容易分散在不同位置。)
-
-
对生命周期的依赖不同
-
Vue Composition API:
生命周期钩子(如 onMounted)可以直接写在 setup 里,但更多时候不需要关心生命周期,因为响应式系统自动跟踪依赖。
(比如一个数据变了,用到它的视图会自动更新,不用手动监听。)
-
React Hooks:
重度依赖 useEffect 来模拟生命周期(如组件挂载、更新、卸载),需要手动管理依赖数组。
(比如忘记写依赖项,可能导致闭包问题,拿到旧值。)
-
-
条件限制不同
-
Vue Composition API:
没有条件限制,可以在任何地方写逻辑。
(比如在 if 语句里定义 ref,完全没问题。)
-
React Hooks:
必须遵守"不能在条件、循环、嵌套函数中调用 Hooks"的规则。
(比如在 if 里写 useState,React 会直接报错。)
-
-
复用逻辑的方式不同
-
Vue Composition API:
通过组合函数(如 useXXX())返回响应式数据和方法,直接使用即可。
(复用逻辑像拼积木,拿来就能用。)
-
React Hooks:
通过自定义 Hook(如 useXXX())返回状态和方法,但每次调用 Hook 会创建独立的状态。
(复用逻辑时,每个组件实例的状态是隔离的。)
-
一句话总结
Vue Composition API 像自动挡汽车,响应式系统帮你处理依赖和更新,代码可以自由组织;
React Hooks 像手动挡汽车,灵活但需要自己管理状态更新和副作用,还要遵守严格的规则。
-
选哪个?看你是喜欢省心(Vue)还是追求极致控制(React)!
26、Vue-Router 3.x hash模式 与 history模式 的区别
- ** Hash 模式(默认)**:利用 #号使用 hash 来模拟一个完整的 URL,如:http://xxx.com/#/path/to/route。
- History 模式 :利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法来完成 URL 跳转而无须重新加载页面。服务端增加一个覆盖所有情况的候选页面,如果 URL 匹配不到任何资源,则返回这个页面。
27、什么是虚拟dom
-
在Vue的响应式系统中,数据变化会触发组件级别的重新渲染。如果一个组件包含大量DOM节点,比如1000个dom,当我只修改了期中一个dom的数据时,也会导致整个组件重新生成,响应式更新只能更改到组件层级无法精准找到是那个dom发生变化。所以就有了虚拟DOM,虚拟dom就是在初始渲染时生成了一个用JS 对象 创建虚拟 DOM,当有数据改变的时候又会生成新的虚拟 DOM 树,新旧dom通过Diff 算法进行对比,找出差异(可以通过设置key来提高对比速度减少无意义对比),对比完成后将新的内容一次提交更新真实dom避免频繁操作dom造成回流和重绘,浪费性能,还有就是如果dom树对比发现新旧节点的标签类型或组件类型不同时就会变成直接销毁旧子树(解除旧节点的 事件监听 和 数据绑定、递归移除旧子树对应的 真实 DOM 节点、触发组件 生命周期钩子),新树替换旧树根据新虚拟 DOM 节点递归生成 真实 DOM 节点设置新节点的 属性(如 class、style)和 事件监听(如 addEventListener),由于出现销毁和重新创建所以会造成高额的开销,也会触发回流和重绘。通过这些操作就实现了从'组件级别'的更新粒度,细化到了'具体DOM节点'级别的更新粒度,大大提升了性能。还有就是通过虚拟"
-
Vue3 的虚拟 DOM 通过 算法优化(双端 Diff、Patch Flag)、静态提升、事件侦听器缓存 等策略,显著降低了渲染开销和内存占用,同时结合 Fragments 支持 和 Tree Shaking 优化了开发体验。这些改进使得 Vue3 在复杂应用场景下(如动态列表、高频交互)的渲染性能远超 Vue2
28、算法优化、静态提升、事件侦听器缓存、Fragments、Tree Shaking
-
一、算法优化
-
1.双端 Diff 算法:
双端指针策略:对比新旧虚拟 DOM 时,同时从首尾向中间遍历,减少非必要节点比较次数,提升动态列表渲染效率。
最长递增子序列算法:针对动态子节点顺序调整场景,通过数学方法计算最小移动次数,避免全量重建。
-
2.Patch Flag(静态标记)
在编译阶段标记动态属性(如文本、class、style),Diff 时仅对比带标记的节点,跳过全量遍历。
例如:动态文本节点标记为 1/* TEXT */,仅需检查文本内容变化。
-
-
二、静态提升(HoistStatic)
-
原理:将模板中无动态绑定的静态节点(如固定文本、无响应式数据的元素)提取为常量,避免每次渲染重复创建。
效果:
-
内存占用降低:静态节点仅初始化一次,后续复用;
-
减少计算开销:跳过 Diff 流程中的静态节点对比。
-
-
三、事件侦听器缓存(CacheHandlers)
-
机制:对动态绑定的事件处理函数(如 @click)进行缓存,避免每次渲染生成新函数对象。
-
优势:减少内存消耗和垃圾回收(GC)压力;避免因函数引用变化触发不必要的子组件更新。
-
-
四、Fragments 支持
-
功能:允许组件模板包含多个根节点(如
<div></div><span></span>),无需外层包裹冗余元素。 -
意义:简化布局结构,提升代码可读性和灵活性。
-
-
五、Tree Shaking 优化
-
实现:虚拟 DOM 相关代码模块化,构建时通过静态分析剔除未使用的功能(如未启用的过渡动画)。
-
效果:减少最终打包体积,提升应用加载速度。
-
29、vue3中如何引入react18封装的组件呢
-
方法:将 React 组件封装为 Web Components
步骤 1:创建 React Web Component 封装器
使用 @lit/react 或自定义封装方法将 React 组件转换为 Web Component。
javascript# 创建 React 项目(如果尚未创建) npx create-react-app my-react-component --template typescript cd my-react-component npm install @webcomponents/webcomponentsjs @lit/reactjavascript// src/ReactCounter.tsx import React, { useState } from 'react' import { createComponent } from '@lit/react' import { html, css, LitElement } from 'lit' // 1. 创建 Lit 元素封装 React 组件 class ReactCounterWrapper extends LitElement { static styles = css` div { border: 1px solid blue; padding: 10px; } ` @property({ type: Number }) count = 0 @eventOptions({}) private _onIncrement!: () => void render() { return html` <div> <button @click=${() => this.dispatchEvent(new CustomEvent('increment'))}> React Count: ${this.count} </button> </div> ` } } // 2. 将 React 组件与 Lit 元素绑定 const ReactCounter = createComponent({ react: React, elementClass: ReactCounterWrapper, tagName: 'react-counter', events: { onIncrement: 'increment' } }) export default ReactCounter -
步骤 2:构建为独立 JS 文件
配置构建工具(如 Vite)生成浏览器兼容的包:
javascript// vite.config.js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], build: { lib: { entry: 'src/ReactCounter.tsx', formats: ['es'], fileName: 'react-counter' } } })构建命令:
vite build将生成的 dist/react-counter.js 复制到 Vue 项目的 public 目录。
-
步骤 3:在 Vue 中引入 Web Component
javascript<!-- VueComponent.vue --> <template> <div> <react-counter ref="reactCounterRef" :count="count" @increment="handleIncrement" /> <p>Vue 中的计数:{{ count }}</p> </div> </template> <script setup> import { ref, onMounted } from 'vue' const count = ref(0) const reactCounterRef = ref(null) // 确保 Web Component 加载完成 onMounted(() => { if (reactCounterRef.value) { // 动态更新属性 reactCounterRef.value.count = count.value } }) // 监听事件 const handleIncrement = () => { count.value += 1 } </script> <!-- 在入口文件引入 JS --> <script> // main.js import './public/react-counter.js' </script>
30、pinia和vuex有什么区别?
- Pinia 和 Vuex 均为 Vue 的状态管理工具,核心区别在于:Pinia 专为 Vue 3 设计,采用模块化架构,允许直接修改状态且无需 mutations,原生支持 TypeScript 并简化了异步操作(仅用 actions 统一处理),体积更小(约 1KB);而 Vuex 基于全局单例模式,严格区分同步(mutations)和异步(actions),对 TypeScript 支持较弱,体积较大(约 10KB),更适合 Vue 2 或需要严格数据流控制的大型项目。
31、vue3为什么使用pinia?
- Pinia 作为 Vue 官方新一代状态管理工具,专为 Vue 3 设计,彻底简化了状态管理流程------通过移除 Vuex 中繁琐的 mutations、统一用 actions 处理同步/异步操作,原生深度集成 TypeScript 实现开箱即用的类型推断,同时依托 Composition API 实现更直观的响应式状态管理。其模块化架构天然规避命名空间冲突,体积仅 1KB(远小于 Vuex 的 10KB),完美适配 Vue 3 的轻量化与高效渲染特性,成为现代 Vue 应用开发的首选方案。
32、proxy能监听基础类型吗?
- 不能。Proxy 无法直接监听基本类型(如数字、字符串、布尔值),这是由 JavaScript 语言本身的特性决定的。
33、vue3 组合式api的响应原理
-
组合式 API 的原理 = 函数化逻辑组织方式 + 响应式系统依赖收集机制(Proxy + effect)
-
函数化逻辑组织方式:
-
组合式API的函数化逻辑组织方式的核心原理是:
-
函数作为逻辑封装单元:将完整业务逻辑封装在setup()函数中,方便复用
javascript// 传统选项式API vs 组合式API // 选项式:逻辑分散在不同选项中 export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } }, mounted() { console.log('mounted') } } // 组合式:逻辑聚合在一个函数中 function useCounter() { const count = ref(0) const increment = () => count.value++ onMounted(() => console.log('counter mounted')) return { count, increment } } -
闭包作为状态保持机制:通过闭包保持响应式数据的引用和私有状态
javascript// 组合函数利用闭包保持私有状态 function usePrivateState() { // 这个变量通过闭包保持,外部无法直接访问 let internalCount = 0 const publicCount = ref(0) const increment = () => { internalCount++ // 闭包保持 publicCount.value++ } return { publicCount, increment } } -
响应式系统深度集成:利用Vue3响应式系统自动追踪函数内的依赖
javascriptimport { effect, reactive } from 'vue' function useUser() { const user = reactive({ name: '张三', age: 25 }) // 自动追踪依赖 const isAdult = computed(() => user.age >= 18) // effect自动追踪响应式依赖 effect(() => { console.log(`${user.name} 的年龄是 ${user.age}`) }) return { user, isAdult } } -
声明式副作用管理:副作用与相关逻辑声明在同一作用域,形成逻辑闭环
javascriptfunction useMouse() { const x = ref(0) const y = ref(0) // 副作用与相关逻辑声明在同一作用域 const update = (e) => { x.value = e.pageX y.value = e.pageY } onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) return { x, y } } -
编译时优化支持:
<script setup>语法糖提供编译时优化和类型推导javascript<!-- <script setup> 语法糖 --> <script setup> import { ref } from 'vue' // 编译时: // 1. 自动暴露顶层变量到模板 // 2. 更好的类型推导 // 3. 更少的运行时开销 const count = ref(0) </script> <template> <button @click="count++">{{ count }}</button> </template>
6.逻辑关注点分离,而非选项类型分离
-
组合式API的核心优势在于按逻辑功能组织代码,而不是按技术类型(data、methods、computed等):
javascript// 传统选项式:同一功能分散在不同选项中 export default { data() { return { user: null, // 用户数据 posts: [] // 文章数据 } }, computed: { userName() { return this.user?.name }, // 用户相关 postCount() { return this.posts.length } // 文章相关 }, methods: { fetchUser() {}, // 用户相关 fetchPosts() {} // 文章相关 }, mounted() { this.fetchUser() // 用户相关 this.fetchPosts() // 文章相关 } } // 组合式:按功能组织 export default { setup() { // 用户相关逻辑集中在一起 const { user, userName, fetchUser } = useUser() // 文章相关逻辑集中在一起 const { posts, postCount, fetchPosts } = usePosts() onMounted(() => { fetchUser() fetchPosts() }) return { user, userName, posts, postCount } } }
-
这种设计使得Vue3组件逻辑具备了更好的可读性、可维护性、可复用性和类型安全性,同时保持了响应式系统的所有能力。
-
-
响应式系统依赖收集机制(Proxy + effect + track + trigger)
javascriptVue 3 响应式系统全链路图 ┌──────────────────────────┐ │ 应用启动 │ └───────────────┬──────────┘ │ ▼ ┌──────────────────────────┐ │ createApp() / mount() │ │ 创建组件、执行 setup() │ └───────────────┬──────────┘ │ ▼ ┌──────────────────────────┐ │ setup() 内创建响应式数据 │ │ ref() / reactive() │ └───────────────┬──────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ reactive() 创建 Proxy(不递归子属性) │ │ → 采用「惰性代理」Lazy Proxy │ └───────────────┬────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 首次渲染:创建渲染 effect(副作用函数) │ │ 组件渲染函数会自动包裹成一个 effect │ └───────────────┬────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ 渲染过程中访问响应式数据 → 触发 get │ │ → 调用 track(target, key) │ │ 收集依赖:activeEffect 记录到依赖表中 │ └───────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ 用户交互/定时器/事件 → 修改响应式数据 │ │ → 触发 Proxy 的 set handler │ │ → 调用 trigger(target, key) │ └───────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ trigger 查找依赖 key 的所有 effect │ │ → 将需要执行的 effect 放入 scheduler │ └───────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Vue 的 Scheduler(异步任务队列) │ │ - 把多次更新合并(批处理) │ │ - 下一次微任务 flush │ │ → 实现"异步更新 DOM"优化 │ └───────────────┬─────────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ effect 再次执行 → 组件重新渲染 │ │ DOM patch 更新视图 │ └──────────────────────────────────────────┘ -
<script setup>在组件初始化时立即执行这个时候给没有用ref或reactive 定义的变量count 通过proxy包装。 -
随后在模板
<div>{``{ count }}</div>中渲染时, 以及在watchEffect、computed、watch等函数执行时, 如果访问到count,就会为当前正在执行的回调创建一个effect并让count订阅这个effect。watchEffect、computed、watch的调用会生成不同的effect,但模板本身只有一个渲染effect因为版里面所有count都是在render里 所以effect订阅render就行,当count改变时把render里用到的地方修改了就行。 -
访问到数据时会触发该数据的 proxy.get,在 get 中调用 track,把当前正在执行的 effect 记录为订阅者。
javascripteffect 执行 ↓ 访问 count ↓ 触发 proxy.get ↓ get 调用 track ↓ track 建立订阅 模版形式 1. 创建响应式数据 const state = reactive({ count: 0 }) 2. 模板 <div>{{ count }}</div> 3. 会编译成 function render() { console.log("模板里用到了 count:", count.value) } 4. Vue 自动做的事情是: effect(render) watchEffect形式 1. 创建响应式数据 const state = reactive({ count: 0 }) 2. watchEffect(() => { console.log("watchEffect 用到了 count", count.value) }) 3. 拆开写 function watchEffect(fn) { // 内部实际上是创建一个 effect,并且立即执行 fn effect(() => { console.log("watchEffect 用到了 count", count.value) }) } -
当你修改响应式数据时(例如 state.count++):
javascript1. 会触发 Proxy 的 set 拦截器 2. set 内部会调用 trigger() 3. trigger 会从当前数据对应的依赖列表中取出所有订阅它的 effect 4. 然后依次执行这些 effect,从而更新页面或重新计算值 function trigger(target, key) { // 1. 先根据 target 取出它的依赖表 const depsMap = targetMap.get(target) if (!depsMap) return // 2. 直接从依赖表中"取出"这个 key 对应的所有订阅它的 effect const effects = depsMap.get(key) if (!effects) return // 3. 依次执行这些 effect(即订阅者) effects.forEach(effect => effect()) }
概念 你的比喻 技术实现 effect 订阅者 副作用函数 get + track 登记订阅 依赖收集 set + trigger 通知更新 触发更新 Proxy 报纸/电台 数据代理 javascriptProxy:给"原始数据对象"装上一层拦截器。 effect:依赖响应式数据、需要在数据变化时"重新执行"的函数。 track:数据被读取时,记下"谁用了我"。 trigger:数据被修改时,就通知"所有用了我的人"更新。 数据访问 → 触发 get(Proxy)→ 调用 track → 建立effect记录依赖。 数据修改 → 触发 set (Proxy)→ 调用 trigger → 通知所有建立依赖的 effect 更新 ┌─────────┐ 读取数据 ┌─────────┐ 调用 track ┌─────────┐ │ effect │ -----------> │ Proxy │ -----------> │ track │ │ (副作用)│ │ (代理层) │ │(依赖收集)│ └─────────┘ └─────────┘ └─────────┘ ^ │ │ │ │ 修改数据 │ 记录依赖关系 │ ↓ ↓ │ ┌─────────┐ 调用 trigger ┌─────────┐ │ │ Proxy │ -----------> │ trigger │ └────────────────────│ (代理层) │ │(更新触发)│ effect重新执行 └─────────┘ └─────────┘ -
34、track/trigger 是如何实现的
-
track(追踪、收集依赖)
白话: "你用了我的数据,我得拿小本本记下来,以后我好找你。"
什么时候发生? 当你读取(使用)一个响应式数据(比如 console.log(state.count) 或在模板里用了 {{ state.count }})的时候。
发生了什么? Vue 会立刻在背后偷偷执行 track 操作。它有一个"全局小本本",会记录下:
哪个数据被读取了(比如 state.count)
是谁读取了它(比如当前正在运行的组件渲染函数、或者一个 watchEffect 函数)
记下来干嘛? 这样 Vue 就知道这个数据(state.count)和这个函数(比如渲染函数)之间存在一种 "依赖" 关系。
-
trigger(触发、通知更新)
白话: "我变了!小本本上所有用过我的人,你们赶紧去更新!"
什么时候发生? 当你修改一个响应式数据(比如 state.count = 5)的时候。
发生了什么? Vue 会立刻在背后执行 trigger 操作。它马上翻出之前的"全局小本本",查找:
所有依赖了这个数据的地方(比如之前记录的那个渲染函数)
找到后干嘛? Vue 会自动执行所有找到的函数。这意味着组件会重新渲染、watchEffect 会重新运行,视图也就自动更新了。
-
它们是怎么实现的?(超级简化版)
用 Proxy 挖陷阱:当你用 reactive() 包裹一个对象时,Vue 会用 Proxy 把它包起来。这个 Proxy 设置了"陷阱"(拦截器),专门抓你的"获取"(get)和"修改"(set)操作。
get 陷阱里调用 track:只要你一读取某个属性(obj.count),get 陷阱就触发,立马执行 track(target, 'count'),把当前正在运行的函数(依赖)记到 count 名下。
set 陷阱里调用 trigger:只要你一修改某个属性(obj.count = 5),set 陷阱就触发,立马执行 trigger(target, 'count'),去小本本里找到所有记在 count 名下的函数,挨个执行一遍。
-
总结:
track 是在 get(读)的时候,记下谁依赖了我。
trigger 是在 set(写)的时候,通知所有依赖我的人更新。
这就是 Vue 响应式魔法背后的核心机制,一切都是自动的!
35、为什么不能只用 Proxy 的 get/set,还需要 track/trigger?
-
"因为仅仅通过 key 无法建立精确的 effect-属性 依赖关系:
-
缺少执行上下文:在 Proxy 的 get 中,你不知道当前是哪个 effect 在读取属性
-
需要动态依赖收集:effects 可能有条件地依赖不同的属性
-
处理嵌套 effects:需要维护 effect 执行栈来处理嵌套场景
-
-
track 和 trigger 通过维护 activeEffect 和依赖映射表,解决了这些问题:
-
track:在属性读取时,记录"当前活跃的 effect 依赖这个属性"
-
trigger:在属性修改时,查找"所有依赖这个属性的 effects"并执行
这种设计让 Vue 3 能够智能地管理复杂的依赖关系,实现精确的响应式更新。"
这样的解释说明了为什么响应式系统需要比简单的 Proxy get/set 更复杂的机制!
-
track
track 函数的核心作用是在读取响应式数据时,建立数据与副作用函数(effect)之间的依赖关系。
javascript// 全局的依赖存储仓库,用于存储所有响应式对象的依赖关系 const targetMap = new WeakMap() export function track(target: object, key: unknown) { // 如果当前没有活跃的 effect 或者不允许追踪,直接返回 if (!activeEffect || !shouldTrack) return // 1. 获取 target 对应的 depsMap let depsMap = targetMap.get(target) if (!depsMap) { // 如果 target 还没有对应的 depsMap,就创建一个并存入 targetMap targetMap.set(target, (depsMap = new Map())) } // 2. 获取 key 对应的 dep 集合 let dep = depsMap.get(key) if (!dep) { // 如果 key 还没有对应的 dep 集合,就创建一个 Set 并存入 depsMap depsMap.set(key, (dep = new Set())) } // 3. 将当前活跃的 effect 添加到 dep 中 if (!dep.has(activeEffect)) { dep.add(activeEffect) // 同时,将 dep 添加到 effect 的依赖列表中,用于后续清理 activeEffect.deps.push(dep) } }关键点解析:
targetMap 结构:WeakMap<Target, Map<Key, Set>>,这是一个三层嵌套结构
activeEffect:全局变量,指向当前正在执行的副作用函数
依赖关系:当 effect 执行时,内部访问的响应式数据都会通过 track 与这个 effect 建立联系
-
trigger
trigger 函数的核心作用是在响应式数据变化时,找出所有依赖这个数据的 effect,并重新执行它们。
javascriptexport function trigger( target: object, key: unknown, type: TriggerOpTypes = TriggerOpTypes.SET ) { // 1. 获取 target 对应的 depsMap const depsMap = targetMap.get(target) if (!depsMap) { // 如果 target 没有被追踪过,直接返回 return } // 2. 收集需要触发的 effects const effects: ReactiveEffect[] = [] // 添加直接关联的依赖 const dep = depsMap.get(key) if (dep) { effects.push(...dep) } // 处理数组的特殊情况 if (type === TriggerOpTypes.ADD && isArray(target)) { // 数组添加元素时,需要触发 length 属性的依赖 const lengthDep = depsMap.get('length') if (lengthDep) { effects.push(...lengthDep) } } // 3. 执行所有收集到的 effects for (const effect of effects) { if (effect !== activeEffect) { // 避免循环触发 if (effect.scheduler) { // 如果有调度器,通过调度器执行 effect.scheduler() } else { // 否则直接执行 effect.run() } } } }关键点解析:
性能优化:通过 effect !== activeEffect 避免在 effect 中修改自身依赖的数据导致的无限循环
调度器:effect.scheduler 允许自定义 effect 的执行时机,这是实现批量更新的基础
-
-
完整的工作流程示例
为了帮你更好地理解,这里有一个简化的示例展示它们如何配合工作:
javascript// 1. 创建响应式对象 const state = reactive({ count: 0 }) // 2. 创建 effect effect(() => { console.log('Count:', state.count) // 读取 state.count,触发 track }) // 3. 修改数据 state.count++ // 触发 trigger执行流程:
-
effect 执行,activeEffect 指向这个函数
-
执行 console.log(state.count),触发 Proxy 的 get 陷阱
-
get 陷阱调用 track(target, 'count'),建立依赖关系
-
state.count++ 触发 Proxy 的 set 陷阱
-
set 陷阱调用 trigger(target, 'count')
-
trigger 找到所有依赖 count 的 effects 并执行
设计思想总结
理解 track 和 trigger 的关键在于明白 Vue 3 响应式系统的核心思想:
-
精确依赖追踪:不像 Vue 2 那样递归追踪整个对象,Vue 3 只追踪实际被访问的属性
-
懒收集依赖:只有真正在 effect 中被访问的属性才会被追踪
-
高效更新:数据变化时,只重新执行真正依赖这个数据的 effect
这就是为什么 Vue 3 的响应式系统在性能和内存占用上都有显著提升。track 和 trigger 虽然代码简单,但它们背后的设计思想确实很精妙!
-
36、Vue2响应式原理:Object.defineProperty
- 首先gtter这个环节是如何产生的
- 我在data中定义一个 a:'',项目启动的时候会循环data里的变量 通过 Object.defineProperty 把它变成响应式数据,并为它创建一个 Dep(依赖管理器),当有地方使用这个数据a了就会产生一个watcher观察这个数据,因为使用了a那么就是触发触发Object.defineProperty里的getter,在getter里会把该数据的观察者watcher添加到dep中。
- dep是依赖收集器,如果a是报纸的话,那么dap就是订阅名单,可以在里面找到每个订阅的人
- watcher是订阅人,只不过一份报纸给多个人看,当我报纸更新的时候,通过dep去找到每个订阅的人告诉他们要更新报纸了
-
启动项目初始化流程
应用启动 ↓ new Vue() 实例化 ↓ 初始化 data ↓ 遍历所有属性 → Object.defineProperty ← 只在这里执行! ↓ 建立 getter/setter ↓ 响应式系统就绪 ✅ ↓ ┌─────────────────────────────────────────┐ │ 运行时数据更新 │ │ │ │ vm.name = 'new value' → 触发 setter │ │ console.log(vm.name) → 触发 getter │ │ this.count++ → 触发 getter + setter │ └─────────────────────────────────────────┘ -
数据初始化
javascriptdata() { return { a: '' // ← 只是一个普通数据 } }Vue 通过 Object.defineProperty 把它变成响应式数据,并为它创建一个 Dep(依赖管理器),但此时还没有 Watcher!
-
创建 Watcher 的时机
Watcher 是在使用数据的地方创建的:
情况1:在模板中使用
javascript<template> <div>{{ a }}</div> <!-- 这里使用了 a --> </template>Vue 会为这个组件创建一个渲染 Watcher
情况2:在计算属性中使用
javascriptcomputed: { computedA() { return this.a + '!'; // 这里使用了 a } }Vue 会为这个计算属性创建一个 计算属性 Watcher
-
完整的正确流程
javascript// 步骤1:初始化数据 data: { a: '' } ↓ // Vue 为 a 创建响应式getter/setter + Dep Object.defineProperty(data, 'a', { get() { if (Dep.target) { // 如果有Watcher正在运行 dep.depend(); // 把这个Watcher加入到a的Dep中 } return value; }, set(newVal) { value = newVal; dep.notify(); // 通知所有Watcher更新 } }) // 步骤2:组件渲染,创建渲染Watcher new Watcher(component, updateComponent); ↓ // Watcher执行渲染函数 updateComponent() { // 这里读取了 this.a } ↓ // 触发 a 的 getter ↓ // 此时 Dep.target = 渲染Watcher ↓ // a 的 getter 把【渲染Watcher】加入到【a的Dep】中更准确的比喻
-
数据 (a):像是一个出版社
-
Dep:出版社的订阅者名单
-
Watcher:像是读者
-
使用数据的地方:像是读者下单订阅的动作
总结
-
✅ 数据有自己的 Dep
-
✅ 使用数据的地方创建 Watcher
-
✅ Watcher 执行时触发数据的 getter
-
✅ getter 把当前 Watcher 加入到数据的 Dep 中
-
-
怎么修改了set试图就跟着刷新了
我的理解是 ,数据变化触发object.defineproperty的set,if (newVal === value) return;判断是否是该set是就触发dep.notify(),在notify里循环调用每个Watcher 的.update(),执行在Watcher 里调用run执行内部get方法执行 updateComponent()生成新虚拟DOM → 对比差异 → 更新真实DOM
-
你修改数据
javascriptthis.a = 'new value'; -
触发 Object.defineProperty 的 setter
javascriptset(newVal) { if (newVal === value) return; value = newVal; dep.notify(); // 【关键】通知所有依赖这个数据的 Watcher } -
Dep 通知所有 Watcher
javascriptclass Dep { notify() { for (let i = 0; i < this.subs.length; i++) { this.subs[i].update(); // 调用每个 Watcher 的 update 方法 } } } -
Watcher 执行更新
javascriptclass Watcher { update() { // 对于渲染 Watcher,这会触发重新渲染 this.run(); } run() { const value = this.get(); // 重新执行 getter 函数 // 对于渲染 Watcher,getter 就是 updateComponent } get() { Dep.target = this; const value = this.getter.call(this.vm); // 【关键】执行渲染函数! Dep.target = null; return value; } } -
重新渲染组件
javascript// 渲染 Watcher 的 getter 就是 updateComponent function updateComponent() { // 1. 生成新的虚拟DOM const vnode = vm._render(); // 2. 对比新旧虚拟DOM,更新真实DOM vm._update(vnode); }
完整的链条
javascript你修改 this.a = 'new value' ↓ 触发 a 的 setter ↓ dep.notify() ↓ 遍历所有订阅了 a 的 Watcher,调用 watcher.update() ↓ Watcher 执行 this.get() ↓ 执行 updateComponent() // 重新渲染! ↓ 生成新虚拟DOM → 对比差异 → 更新真实DOM ↓ 视图更新完成!关键点说明
-
为什么修改 set 视图就刷新了?
-
因为 setter 中调用了 dep.notify(),这个方法:
-
找到所有依赖这个数据的 Watcher(之前通过 getter 收集的)
-
通知它们执行更新
-
对于渲染 Watcher,更新就是重新执行渲染函数
-
重新渲染 = 生成新虚拟DOM + 更新真实DOM
-
举个例子
javascript// 初始状态 data: { message: 'Hello' } template: `<div>{{ message }}</div>` // 流程: 1. 渲染时,渲染Watcher读取 this.message,被收集到 message 的 Dep 中 2. 你执行:this.message = 'World' 3. message 的 setter 被触发 → dep.notify() → 找到渲染Watcher → 调用 watcher.update() → 执行 updateComponent() → 重新读取 this.message (现在是 'World') → 生成新的虚拟DOM → 更新真实DOM显示 'World'所以本质上:修改 set → 触发通知 → 找到依赖的 Watcher → 重新执行渲染函数 → 视图更新!
-
37、Vite vs Webpack 核心区别

38、Vite的底层组件
-
Vite底层主要依赖以下组件:
-
ESBuild - 用于依赖预构建,由Go编写,编译速度极快
-
Rollup - 用于生产环境打包,提供优秀的Tree-shaking
-
Koa - 开发服务器基于Koa框架
-
原生ES模块(ESM) - 利用浏览器原生支持实现按需加载
-
40、inheritAttrs
inheritAttrs 是 Vue 组件的一个选项,控制是否将父组件传递的非 prop 属性自动绑定到组件的根元素上。
不加 inheritAttrs: false(默认)
html
<!-- 爸爸给 -->
<Child class="红色" @click="处理" data-test="1" />
<!-- 儿子接收 -->
<template>
<!-- 所有东西自动塞到第一个div -->
<div class="红色" @click="处理" data-test="1">
内容
</div>
</template>
加了 inheritAttrs: false(手动模式)
html
<!-- 爸爸给 -->
<Child class="红色" @click="处理" data-test="1" />
<!-- 儿子接收 -->
<script setup>
defineOptions({ inheritAttrs: false }) // 先说:别自动给我
const attrs = useAttrs() // 再拿到所有东西:{class: "红色", onClick: 处理函数, "data-test": "1"}
</script>
<template>
<!-- 自己决定怎么分 -->
<div :class="attrs.class"> <!-- 只拿class -->
内容
</div>
<button @click="attrs.onClick">按钮</button> <!-- 只拿点击事件 -->
<span :data-test="attrs['data-test']">测试</span> <!-- 只拿data -->
</template>
也就是说如果不加inheritAttrs: false父元素引入子元素,然后再子元素上增加的内容,子元素会默认增加到第一个div上,如果有了inheritAttrs: false我可以通过useAttrs()拿到所有父元素给子元素设置项自己手动分配
实际项目中的选择
-
情况1:Vue 2 老项目
javascript// 保持原样 export default { inheritAttrs: false, customOptions: {} } -
情况2:Vue 3 新项目
javascript<script setup> defineOptions({ inheritAttrs: false }) </script> -
情况3:底层使用
javascript<script setup> getCurrentInstance().type.inheritAttrs = false </script>
41、vite里esbuild和rollup是什么?
- esbuild:影响开发环境
- rollup:影响生产环境
42、声明式和命令式的区别
- 声明式:我们写html结构的代码,编译器会把html转换成js然后渲染界面
- 命令式:我们自己写js代码然后自己把创建的节点挂在到界面中实现渲染
43、vue模版的本质
-
vue2和vue3的模版本质就是一个语法糖它表达的是一个渲染过程,最终得到的是一个界面的结构。
这是一个模版
javascript<template> <div id="app"> <h1 v-if="showTitle">{{ title }}</h1> <button @click="toggleTitle">Toggle</button> </div> </template>经过编译后,可能会生成类似这样的渲染函数(简化理解,实际更复杂):
javascriptfunction render() { return h('div', { id: 'app' }, [ this.showTitle ? h('h1', this.title) : null, h('button', { onClick: this.toggleTitle }, 'Toggle') ]); }在真正运行的时候是没有模板的,真正运行的是js代码,vue会通过render函数把模版传化成js,在vue2和vue3中由于写法不同选项式和组合式api所以render的实现也不一样。
-
vue2
javascriptexport default { data() { return { title: 'Hello Vue 2!' } }, render(h) { return h('h1', this.title) // 通过this访问数据 } } -
vue3
javascriptimport { h } from 'vue' // 需要显式导入h函数 export default { setup(props, context) { const title = 'Hello Vue 3!' // 返回一个渲染函数 return () => h('h1', title) } }
看上面的代码可以发现Vue 也是声明式的语法,这种写法的好处就是更方便开发因为是就是html我们上手会很快,如果让我们直接写js创建dom会很麻烦,声明式能更高效地描述 UI。
我们写的 .vue 文件中的 标签里的内容,并不是最终的 HTML。它在构建阶段(例如使用 Vue CLI 或 Vite)会被 Vue 的编译器 处理。
编译过程大致如下:
模板 (Template) -> 编译 (Compile) -> 渲染函数 (Render Function) -> 虚拟 DOM (Virtual DOM) -> 挂载/打补丁 (Mount/Patch) -> 真实 DOM (Real DOM)
-
编译:Vue 的编译器会解析你的模板,分析其中的指令(如 v-if, v-for)、插值({{ }})、事件绑定(@click)等。
-
生成渲染函数:编译器将分析结果转换成一个或多个 JavaScript 函数,这些函数就是渲染函数。渲染函数的返回值是 虚拟 DOM 节点。
举个例子,这样一个模板:
javascript<template> <div id="app"> <h1 v-if="showTitle">{{ title }}</h1> <button @click="toggleTitle">Toggle</button> </div> </template>经过编译后,可能会生成类似这样的渲染函数(简化理解,实际更复杂):
javascriptfunction render() { return h('div', { id: 'app' }, [ this.showTitle ? h('h1', this.title) : null, h('button', { onClick: this.toggleTitle }, 'Toggle') ]); }这里的 h 函数(是 createElement 的通用简写)就是用于创建虚拟节点的。
所以,Vue 模板最终干活的是 JavaScript 的渲染函数。
如何生成的虚拟dom呢
执行过程:
-
调用 render() 函数
-
执行 h() 函数(创建虚拟节点的函数)
-
返回虚拟DOM对象(类似下面的结构):
javascript// 这就是虚拟DOM - 一个普通的JS对象 { tag: 'div', props: { id: 'app' }, children: [ { tag: 'h1', props: {}, children: ['Hello World'] // 假设 this.title 的值 }, { tag: 'button', props: { onClick: this.toggleTitle }, children: ['Toggle'] } ] }
如果我们是首次创建节点也是就失业初始化的时候
javascriptconst container = document.getElementById('app'); const realDOM = createElement(vnode); container.appendChild(realDOM);会把render生成到的JS对象appendChild页面
如果是更新某个dom那么会进行一下操作
javascript// 旧虚拟DOM(更新前) const oldVNode = { tag: 'ul', props: { class: 'todo-list' }, children: [ { tag: 'li', key: 'item1', props: { class: 'todo-item' }, children: ['Buy milk'] }, { tag: 'li', key: 'item2', props: { class: 'todo-item' }, children: ['Walk dog'] }, { tag: 'li', key: 'item3', props: { class: 'todo-item' }, children: ['Read book'] } ] }; // 新虚拟DOM(更新后) const newVNode = { tag: 'ul', props: { class: 'todo-list updated' }, // 类名更新了 children: [ { tag: 'li', key: 'item1', props: { class: 'todo-item completed' }, // 类名更新 children: ['Buy milk and eggs'] // 文本更新 }, // item2 被删除了 { tag: 'li', key: 'item3', props: { class: 'todo-item' }, children: ['Read book'] } ] };经过Diff比较后,会生成这样的差异报告:
javascriptconst patches = { // 根节点的属性更新 props: { class: { oldValue: 'todo-list', newValue: 'todo-list updated' } }, // 子节点的变化 children: [ { type: 'UPDATE', index: 0, // 第一个li patches: { props: { class: { oldValue: 'todo-item', newValue: 'todo-item completed' } }, children: [ { type: 'UPDATE_TEXT', value: 'Buy milk and eggs' } ] } }, { type: 'REMOVE', index: 1 // 第二个li被删除 }, { type: 'MOVE', from: 2, // 原来的第三个li to: 1 // 移动到第二个位置 } ] };Patch过程:将差异应用到真实DOM
有了差异报告后,开始执行具体的DOM操作:
javascriptfunction applyPatch(realDOM, patches) { const ulElement = realDOM; // 真实的ul元素 // 1. 更新ul的属性 if (patches.props) { ulElement.className = 'todo-list updated'; } // 2. 处理子节点的变化 patches.children.forEach(change => { switch (change.type) { case 'UPDATE': // 更新第一个li const firstLi = ulElement.children[0]; firstLi.className = 'todo-item completed'; firstLi.textContent = 'Buy milk and eggs'; break; case 'REMOVE': // 删除第二个li const secondLi = ulElement.children[1]; ulElement.removeChild(secondLi); break; case 'MOVE': // 移动第三个li到第二个位置 const thirdLi = ulElement.children[1]; // 注意:删除第二个后,第三个变成了第二个 // 实际上这里可能不需要移动,因为删除后自然就到了正确位置 break; } }); }好了回归正题继续说模版
我们知道vue是通过编译器把html转换成js的那么是在什么时候转换的呢
编译时机
-
开发环境 (Development)
时机:在构建时编译
javascript# 当你运行开发服务器时 npm run dev # 或 vue-cli-service serve # 或 vite过程:
-
你保存.vue文件
-
构建工具(Webpack/Vite)检测到文件变化
-
vue-loader/@vitejs/plugin-vue 立即编译模板为渲染函数
-
热重载更新浏览器
-
-
生产环境 (Production)
时机:在构建时预编译
javascript# 当你构建生产版本时 npm run build # 或 vue-cli-service build过程:
-
所有.vue文件被一次性编译为渲染函数
-
编译后的JavaScript代码被打包到最终文件
-
浏览器只收到纯JS,无需编译模板
-
使用的编译器
-
主要编译器
-
@vue/compiler-dom (Vue 3)
-
Vue 3的官方模板编译器
-
将模板编译为渲染函数
-
-
vue-template-compiler (Vue 2)
- Vue 2的官方模板编译器
构建工具集成
javascript// webpack.config.js (Vue 2) module.exports = { module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', // 使用vue-loader处理.vue文件 options: { compiler: require('vue-template-compiler') // Vue 2编译器 } } ] } } // vite.config.js (Vue 3) import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // 使用Vite的Vue插件 export default defineConfig({ plugins: [vue()] // 内部使用@vue/compiler-dom })具体编译过程示例
-
开发阶段实时编译
javascript<!-- 你写的Single File Component --> <template> <div class="hello"> <h1>{{ message }}</h1> <button @click="count++">Click {{ count }}</button> </div> </template> <script> export default { data() { return { message: 'Hello Vue!', count: 0 } } } </script> -
被编译为:
javascript// vue-loader / @vitejs/plugin-vue 编译后的结果 import { openBlock, createElementBlock, createElementVNode, toDisplayString } from 'vue' export default { data() { return { message: 'Hello Vue!', count: 0 } }, render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", { class: "hello" }, [ _createElementVNode("h1", null, _toDisplayString(_ctx.message), 1 /* TEXT */), _createElementVNode("button", { onClick: _cache[0] || (_cache[0] = ($event) => (_ctx.count++)) }, _toDisplayString(_ctx.count), 1 /* TEXT */) ])) } }
不同使用场景的编译方式
-
场景1: Single File Components (.vue文件)
javascript<template> <div>Hello {{ name }}</div> </template>编译方式: vue-loader 或 @vitejs/plugin-vue 在构建时编译
-
场景2: 内联模板字符串
javascript// 不推荐 - 需要在运行时编译 const app = Vue.createApp({ template: `<div>Hello {{ name }}</div>`, data() { return { name: 'Vue' } } })编译方式: 浏览器中运行时编译(需要完整版Vue)
-
场景3: 直接使用渲染函数
javascript// 推荐 - 无需编译,最高性能 const app = Vue.createApp({ render() { return Vue.h('div', `Hello ${this.name}`) }, data() { return { name: 'Vue' } } })编译方式: 无需编译
版本差异
-
Vue 2
javascript// 需要区分运行时版和完整版 // 完整版:包含编译器,可以在运行时编译模板 // 运行时版:只包含运行时,需要预编译 -
Vue 3
javascript// Vue 3 更模块化,但同样推荐预编译 // 生产环境默认使用不包含编译器的版本
-
44、vue2用this$ref获取组件信息,vue3在组件上定义ref就行,那么在for循环里怎么获取组件?
Vue 2 中获取循环组件引用
在 Vue 2 中,主要通过 this.$refs 来访问。
-
同名 Ref 自动转为数组:在 v-for 循环中,如果多个元素或组件设置了相同的 ref 名称,Vue 2 会将这些引用自动收集为一个数组。
html<template> <div> <child-component v-for="item in list" :key="item.id" ref="childComponents" /> </div> </template> <script> export default { mounted() { // 通过 this.$refs.childComponents 访问所有子组件实例数组 console.log(this.$refs.childComponents); // 这是一个数组 } }; </script> -
使用动态 Ref 名称:你也可以通过绑定动态的 ref 名称来为每个循环项创建单独的引用,这些引用会以你定义的名字存储在 $refs 对象中。
html<template> <div> <div v-for="(item, index) in list" :key="index"> <child-component :ref="'child' + index" /> </div> </div> </template> <script> export default { mounted() { // 通过动态生成的键名访问 console.log(this.$refs.child0); // 第一个子组件 } }; </script> -
使用 Ref 回调函数(函数式 Ref):这是一种更灵活的方式,允许你自定义引用收集的逻辑。
html<template> <div> <div v-for="(item, index) in list" :key="index"> <child-component :ref="el => setItemRef(el, index)" /> </div> </div> </template> <script> export default { data() { return { itemRefs: [] // 用于存储组件引用 }; }, methods: { setItemRef(el, index) { if (el) { this.itemRefs[index] = el; // 将引用存储到指定位置 } } }, mounted() { console.log(this.itemRefs); // 访问存储的引用数组 } }; </script>
⚡ Vue 3 中获取循环组件引用
Vue 3 提供了 Composition API 和 Options API 两种方式。
-
mposition API 中使用 ref 回调函数:这是 Vue 3 Composition API 中推荐使用的方法。
html<template> <div> <child-component v-for="(item, index) in list" :key="item.id" :ref="(el) => setItemRef(el, index)" /> </div> </template> <script> import { ref, onMounted } from 'vue'; export default { setup() { const list = ref([...]); // 你的列表数据 const itemRefs = ref([]); // 创建一个响应式数组来存储引用 const setItemRef = (el, index) => { if (el) { itemRefs.value[index] = el; // 将引用存储到数组的指定位置 } }; onMounted(() => { console.log(itemRefs.value); // 在挂载后访问引用数组 }); return { list, setItemRef, itemRefs }; } }; </script>注意:当组件卸载时,el 参数为 null,你可能需要清理对应的引用。
-
ue 3.5+ 使用 useTemplateRef:Vue 3.5 引入了 useTemplateRef 函数,可以更简洁地处理循环 ref,其值是一个数组,在元素被挂载后包含整个列表的所有元素。
html<script setup> import { useTemplateRef, onMounted } from 'vue' const list = ref([...]) const itemRefs = useTemplateRef('items') // 使用 useTemplateRef onMounted(() => { console.log(itemRefs.value) // 访问所有循环项的引用数组 }) </script> <template> <ul> <li v-for="item in list" :key="item.id" ref="items"> {{ item }} </li> </ul> </template>在 3.5 之前的版本,你需要声明一个与模板引用 attribute 同名的 ref,其值也需要是一个数组。
-
vue 3 的 Options API:如果你在 Vue 3 中仍然使用 Options API,其用法与 Vue 2 类似,主要通过 this.$refs 访问。在 v-for 中同名 ref 的行为也类似,会是一个数组。
html<template> <div> <child-component v-for="item in list" :key="item.id" ref="childComponents" /> </div> </template> <script> export default { mounted() { console.log(this.$refs.childComponents); // 组件实例数组 } }; </script>
💡 重要注意事项
-
确保访问时机:无论 Vue 2 还是 Vue 3,ref 都需要在 DOM 渲染完成后才会填充。因此,务必在 mounted (Vue 2 / Options API) 或 onMounted (Composition API) 生命周期钩子中,或者使用 this.$nextTick (Vue 2 / Options API) / nextTick (Composition API) 确保 DOM 已更新后再访问 ref。
-
处理可能的 null 值:在使用 ref 回调函数时,当元素或组件被卸载时,回调函数会以 null 作为参数被调用。你需要处理好引用失效的情况,避免内存泄漏或访问错误。
-
组件使用
html
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from 'vue'
const privateData = ref('私有数据')
const publicData = ref('公共数据')
defineExpose({
publicData
})
</script>
45、vue2中使用冻结数据可以避免响应式从而提升性能,那在vue3中用该怎么做
-
在Vue 2中,使用 Object.freeze() 可以防止数据被响应式处理,从而提升性能,特别是对于大型静态数据列表。
javascript<template> <div> <h2>用户列表</h2> <div v-for="user in users" :key="user.id"> {{ user.name }} - {{ user.email }} </div> </div> </template> <script> export default { data() { return { // 使用 Object.freeze 冻结静态数据,避免响应式开销 users: Object.freeze([ { id: 1, name: '张三', email: 'zhangsan@email.com' }, { id: 2, name: '李四', email: 'lisi@email.com' }, { id: 3, name: '王五', email: 'wangwu@email.com' } ]) } } } </script> -
在vue3中定义普通数据就行不使用ref和reactive,vue3也不推荐使用object.freeze
javascriptimport { markRaw, reactive } from 'vue' // 创建一个永远不会是响应式的原始对象 const staticData = markRaw({ veryLargeNestedObject: { ... }, someThirdPartyInstance: new ThirdPartyLibrary() }) // 即使在响应式对象中使用,它也不会变成响应式 const state = reactive({ reactiveProperty: '我是响应式的', staticData: staticData // 这个不会变成响应式 })
46、vue 的生命周期和浏览器的事件循环
-
Vue 的异步更新队列确实是响应式系统的一部分,而不是生命周期与事件循环关系的核心。对于文档来说,这部分确实不需要详细展开。
让我们重新梳理一下生命周期和事件循环更直接的关系:
-
正确的核心联系:生命周期的执行时机受事件循环驱动
-
生命周期钩子本身的执行是同步的
- 当组件创建、挂载、更新、销毁时,Vue 会在对应时间点同步调用相应的生命周期钩子函数
-
但生命周期中经常包含异步操作
javascriptmounted() { console.log('mounted 同步执行'); // 同步 // 异步操作 - 受事件循环控制 setTimeout(() => { console.log('setTimeout 回调'); // 宏任务 }, 0); this.$nextTick(() => { console.log('nextTick 回调'); // 微任务 }); }-
关键理解:$nextTick 是桥梁
-
$nextTick 利用事件循环的微任务机制
-
在生命周期中,如果你想在 DOM 更新后执行操作,必须用 $nextTick
-
这体现了生命周期与事件循环的紧密配合
-
-
47、vue的mixins是什么
-
mixins就是我们可以定义一个公共的js里面存有data、methon和vue的生命周期,在需要使用这个功能js的组件中使用
mixins: []引入,我们就可以在模版中直接调用js里的方法或变量。
javascript// toggleMixin.js export const toggleMixin = { data() { return { isShowing: false }; }, methods: { toggleShow() { this.isShowing = !this.isShowing; } }, mounted() { console.log('来自混入的 mounted 钩子!'); } };javascript<template> <div> <button @click="toggleShow">切换显示</button> <p v-if="isShowing">你好,我是混入的例子!</p> </div> </template> <script> import { toggleMixin } from './toggleMixin.js'; // 引入混入 export default { mixins: [toggleMixin], // 注册混入 mounted() { console.log('来自组件自身的 mounted 钩子!'); } // 组件自身的其他选项... }; </script>重要注意事项
选项合并规则:
-
数据对象:同名字段合并时,组件数据优先。
-
生命周期钩子:同名的钩子函数会被合并成一个数组,混入对象的钩子先执行。
-
方法、组件、指令:同名的键值冲突时,组件优先。
全局混入:由于会影响所有 Vue 实例,可能导致难以追踪的 bug,因此不推荐大规模使用。通常仅用于编写插件或处理自定义选项等特定场景。
-
48、vue的Provider 模式 和 react的Context API
- Vue 中的 AppProvider 组件内部使用的是 provide/inject API,实现了与 React Context 相同的跨组件数据传递功能。
49、vue-router路由守卫
-
全局守卫
- 作用于所有路由的守卫。
-
beforeEach、beforeResolve、afterEach
javascript// 全局前置守卫 router.beforeEach((to, from, next) => { // 在路由跳转前执行 console.log('从', from.path, '跳转到', to.path) // 检查是否需要登录 if (to.meta.requiresAuth && !isLoggedIn()) { next('/login') // 重定向到登录页 } else { next() // 继续导航 } }) // 全局解析守卫 router.beforeResolve((to, from, next) => { // 在导航被确认前,组件内守卫和异步路由组件被解析后调用 next() }) // 全局后置钩子 router.afterEach((to, from) => { // 路由跳转完成后执行,没有 next 参数 // 适合用于页面统计、修改页面标题等 document.title = to.meta.title || '默认标题' })
-
- 作用于所有路由的守卫。
-
路由独享守卫
- 只作用于特定路由的守卫。
-
beforeEnter
javascriptconst router = new VueRouter({ routes: [ { path: '/admin', component: Admin, beforeEnter: (to, from, next) => { // 仅对该路由生效 if (!isAdmin()) { next('/unauthorized') } else { next() } } } ] })
-
- 只作用于特定路由的守卫。
-
组件内守卫
- 在组件内部定义的守卫。
-
beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
javascriptexport default { name: 'UserProfile', // 在渲染该组件的对应路由被验证前调用 beforeRouteEnter(to, from, next) { // 不能访问组件实例 `this` // 因为当守卫执行时,组件实例还没被创建 next(vm => { // 通过 `vm` 访问组件实例 vm.fetchUserData(to.params.id) }) }, // 在当前路由改变,但是该组件被复用时调用 beforeRouteUpdate(to, from, next) { // 可以访问组件实例 `this` this.userId = to.params.id this.fetchUserData() next() }, // 在导航离开该组件的对应路由时调用 beforeRouteLeave(to, from, next) { // 可以访问组件实例 `this` if (this.hasUnsavedChanges) { const answer = window.confirm('您有未保存的更改,确定要离开吗?') if (answer) { next() } else { next(false) // 取消导航 } } else { next() } } }
-
- 在组件内部定义的守卫。
50、vue3路由守卫和vue2区别
Vue 3 的路由守卫与 Vue 2 在核心概念上基本一致,但有一些重要区别:
主要区别
-
创建方式不同
Vue 2:
javascriptconst router = new VueRouter({ ... })Vue 3:
javascriptimport { createRouter } from 'vue-router' const router = createRouter({ ... }) -
组件内守卫的 Composition API 用法
-
Vue 2 (Options API):
javascriptexport default { beforeRouteEnter(to, from, next) { // 不能访问 this next(vm => { console.log(vm) // 通过回调访问实例 }) }, beforeRouteUpdate(to, from, next) { // 可以访问 this this.getData() next() } }Vue 3 (Composition API):
javascriptimport { onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router' export default { setup() { onBeforeRouteUpdate((to, from, next) => { // 在 setup 中处理路由更新 next() }) onBeforeRouteLeave((to, from, next) => { // 离开守卫 next() }) } }
-
beforeRouteEnter 的特殊处理
Vue 3 中 beforeRouteEnter 仍需要在 Options API 中使用,Composition API 没有直接对应的函数。
-
TypeScript 支持
Vue 3 路由守卫有更好的 TypeScript 类型推断:
typescriptimport { NavigationGuard } from 'vue-router' const authGuard: NavigationGuard = (to, from, next) => { // 完整的类型提示 if (!isAuthenticated) next('/login') else next() }
守卫类型(两者都有)
-
全局前置守卫 router.beforeEach
-
全局解析守卫 router.beforeResolve
-
全局后置钩子 router.afterEach
-
路由独享守卫 beforeEnter
-
组件内守卫 beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
总结
Vue 3 路由守卫主要变化在于:
创建路由实例的方式不同
支持 Composition API 形式的组件内守卫
更好的 TypeScript 支持
核心功能和行为基本保持一致
实际使用中,如果你熟悉 Vue 2 的路由守卫,迁移到 Vue 3 会非常顺畅。
51、vu3如何异步更新数据不会主塞渲染层
-
Vue 3 中实现异步更新数据不阻塞渲染主要有以下几种方式:
-
使用 nextTick
javascriptimport { ref, nextTick } from 'vue' const data = ref([]) async function fetchData() { const response = await fetch('/api/data') const result = await response.json() // 使用 nextTick 确保在 DOM 更新后执行 data.value = result await nextTick() // 此时 DOM 已更新,可以执行相关操作 } -
使用 Suspense(实验性)
javascript<template> <Suspense> <template #default> <AsyncComponent /> </template> <template #fallback> <div>Loading...</div> </template> </Suspense> </template> <script setup> const AsyncComponent = defineAsyncComponent(() => import('./components/AsyncComponent.vue') ) </script> -
使用响应式更新 + 虚拟列表
javascriptimport { ref, onMounted } from 'vue' const largeData = ref([]) const visibleData = ref([]) // 分批加载数据,避免一次性阻塞 async function loadDataInBatches() { const allData = await fetchLargeData() // 分批更新,给浏览器渲染机会 for (let i = 0; i < allData.length; i += 100) { const batch = allData.slice(i, i + 100) largeData.value.push(...batch) // 使用 setTimeout 让出主线程 await new Promise(resolve => setTimeout(resolve, 0)) } } -
使用 Web Workers
javascript// main.js const worker = new Worker('./data-worker.js') worker.onmessage = (e) => { data.value = e.data } // data-worker.js self.onmessage = async function(e) { const response = await fetch('/api/large-data') const data = await response.json() self.postMessage(data) } -
使用 requestIdleCallback
javascriptimport { ref } from 'vue' const data = ref([]) function loadDataIdle() { requestIdleCallback(async () => { const result = await fetchData() data.value = result }) } -
优化大量数据渲染
javascript<template> <div> <!-- 使用虚拟滚动或分页 --> <div v-for="item in visibleItems" :key="item.id" > {{ item.name }} </div> </div> </template> <script setup> import { ref, computed, onMounted } from 'vue' const allData = ref([]) const currentPage = ref(1) const pageSize = 100 const visibleItems = computed(() => { const start = (currentPage.value - 1) * pageSize return allData.value.slice(start, start + pageSize) }) async function loadData() { allData.value = await fetchLargeData() } </script> -
使用防抖和节流
javascriptimport { ref, watch, debounce } from 'vue' const searchQuery = ref('') const searchResults = ref([]) // 防抖搜索,避免频繁请求阻塞 const debouncedSearch = debounce(async (query) => { if (query) { searchResults.value = await searchAPI(query) } }, 300) watch(searchQuery, debouncedSearch)
-
-
总结
-
Vue 3 中避免异步更新阻塞渲染的关键策略:
-
使用 nextTick 管理更新时机
-
分批处理大数据,避免一次性更新
-
利用 Web Workers 处理复杂计算
-
使用虚拟列表/分页 减少 DOM 操作
-
合理使用浏览器空闲时间(requestIdleCallback)
-
防抖节流控制更新频率
这些方法可以确保 Vue 应用的流畅性和响应性。
-
52、Vuex 中的 get 和 set(其实没有get和set而是Getters和Mutations)
- Vuex 中的 "Get" - Getters
-
作用:用于从 state 中派生计算状态,相当于计算属性的 getter
-
特点:
-
基于 state 进行计算,具有缓存机制
-
响应式:依赖的 state 变化时自动更新
-
只读,不能直接修改
-
示例:
javascript
getters: {
doneTodos: state => state.todos.filter(todo => todo.done)
}
- Vuex 中的 "Set" - Mutations(重点!)
-
Vuex 没有直接的 setter,修改状态必须通过 mutation
-
Mutation 的作用:是修改 state 的唯一途径,相当于受控的 setter
-
设计原理:
-
保证状态变化的可追踪性
-
所有修改都同步进行,便于调试
-
通过 commit 提交,不能直接调用
-
- 完整的数据流理解
-
读取:通过 getters 或直接访问 state → 相当于 get
-
修改:通过 commit mutation → 相当于受控的 set
-
响应式机制:基于 Vue 的响应式系统(Object.defineProperty/Proxy)
53、vux如何改动内容其他地方同步更新
-
异步或复杂业务逻辑:组件通过 dispatch 触发一个 action,action 中执行异步操作或复杂逻辑,然后通过 commit 提交一个 mutation,mutation 最终同步修改状态。
-
同步简单修改:组件直接通过 commit 提交一个 mutation,mutation 同步修改状态。
54、请详细说明 Vuex 中 this.store.dispatch()和this.store.dispatch() 和 this.store.dispatch()和this.store.commit() 方法的区别,包括它们各自的用途、执行流程和适用场景。
- this.$store.commit()
-
对应操作:提交 mutation
-
主要用途:同步、直接地修改 Vuex 状态
-
执行流程:直接调用对应的 mutation 函数修改 state
-
特点:
-
必须是同步操作
-
直接修改状态
-
在 DevTools 中可追踪每一个状态变化
-
适用场景:简单的状态变更,如计数器增减、开关切换等
-
javascript
// 定义
mutations: {
setUser(state, user) {
state.user = user;
}
}
// 使用
this.$store.commit('setUser', userData);
- this.$store.dispatch()
-
对应操作:触发 action
-
主要用途:处理业务逻辑,特别是异步操作
-
执行流程:执行 action 中的业务逻辑,最终通过 commit 提交 mutation 来修改状态
-
特点:
-
可以包含异步操作
-
不能直接修改状态,必须通过 commit
-
适合处理复杂业务逻辑
-
适用场景:API 调用、多个状态变更的组合、条件判断等复杂逻辑
-
javascript
// 定义
actions: {
async login({ commit }, credentials) {
const user = await api.login(credentials);
commit('setUser', user);
commit('setLoggedIn', true);
}
}
// 使用
this.$store.dispatch('login', { username, password });
34、vue的订阅发布有哪些
-
响应式系统:reactive/ref 内部基于 track/trigger 实现依赖收集与更新。
-
组件通信:props/emit、provide/inject。
-
全局通信:事件总线(EventBus/mitt)、状态管理(Vuex/Pinia)。
-
工具函数:watch/watchEffect 用于订阅数据变化。