描述 Vu3 生命周期
Options API 的生命周期:
beforeCreate
:在实例初始化之后、数据观测(initState)和 event/watcher 事件配置之前被调用。此阶段一般会注册组件使用到的 store 或者 service 等单例的全局组件。相比Vue2没有变化。created
:一个新的 Vue 实例被创建后(包括组件实例),立即调用此函数。在这里做初始的数据处理、异步请求等操作,当组件完成创建时就能展示这些数据。相比Vue2没有变化。beforeMount
:在挂载之前调用,相关的 render 函数首次被调用,在这里可以访问根节点,在执行 mounted 钩子前,DOM 渲染成功,相对 Vue2 改动不明显。mounted
:在挂载后调用,也就是所有相关的 DOM 都已入图,有了相关的 DOM 环境,可以在这里执行节点的 DOM 操作。在这之前执行 beforeUpdate。beforeUpdate
:在数据更新时同时在虚拟 DOM 重新渲染和打补丁之前调用。我们可以在这里访问先前的状态和 DOM,如果想要在更新之前保存状态的快照,这个钩子非常有用。相比 Vue2 改动不明显。updated
:在数据更新完毕后,虚拟 DOM 重新渲染和打补丁也完成了,DOM 已经更新完毕。这个钩子函数调用时,组件 DOM 已经被更新,可以执行操作,触发组件动画等操作。beforeUnmount
:在卸载组件之前调用。在这里执行清除操作,如清除定时器、解绑全局事件等。unmounted
:在卸载组件之后调用,调用时组件的 DOM 结构已经被拆卸,可以释放组件用过的资源等操作。activated
:被 keep-alive 缓存的组件激活时调用。deactivated
:被 keep-alive 缓存的组件停用时调用。errorCaptured
:当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数,错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。
Composition API 的生命周期:
除了beforeCreate
和created
(它们被setup
方法本身所取代),可以在setup
方法中访问的其他9个生命钩子选项:onMounted
、onUpdated
、onUnmounted
、onBeforeMount
、onBeforeUpdate
、onBeforeUnmount
、onErrorCaptured
、onActivated
和onDeactivated
。
生命周期钩子:
setup()
替代了 beforeCreate 和 created,是 Composition API 的入口。- onBeforeMount 替代了 beforeMount,用于组件挂载前的操作。
- onMounted 替代了 mounted,用于组件挂载后的操作。
- onBeforeUpdate 替代了 beforeUpdate,用于数据更新前的操作。
- onUpdated 替代了 updated,用于数据更新后的操作。
- onBeforeUnmount 替代了 beforeDestroy,用于组件卸载前的操作。
- onUnmounted 替代了 destroyed,用于组件卸载后的操作。
- 新增了 onActivated 和 onDeactivated 用于
keep-alive
缓存组件的激活和停用。 - onErrorCaptured 用于捕获子孙组件的错误。
Composition API 中的钩子函数,通过在生命周期钩子前面加上 on 来访问组件的生命周期钩子。Vue3 的生命周期钩子需要在setup()
函数中同步注册,不能异步注册,因为它们依赖于内部的全局状态来定位当前组件实例,这与 Vue2 的生命周期钩子使用方式有所不同。Vue3 的 onUnmounted 和 onBeforeUnmount 钩子合并了 Vue2 中的 beforeDestroy 和 destroyed,简化了组件的销毁过程。
Composition API 和 Options API 有什么区别? //TODO
Composition API 和 Options API 是 Vue.js 中的两种组件编写方式。
Options API 是 Vue.js 早期版本中使用的编写方式,通过定义一个 options 对象进行组件的配置,包括 props、data、methods、computed、watch 等选项。这种方式的优点在于结构清晰、易于理解,在小型项目中比较实用。但当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解。
Composition API 是 Vue.js 3.x 版本中新引入的一种组件编写方式,它以函数的形式组织代码,允许将相关部分组合起来,提高了代码的可维护性和重用性。Composition API 还提供了模块化、类型推断等功能,可以更好地实现面向对象编程的思想。
两种API各有优缺点,使用哪种API取决于具体的项目需求。对于小型项目,Options API更为简单方便;对于大型项目,Composition API可以更好地组织代码。
Composition API 相对 Options API 有什么优点?
- 逻辑组织:
- Options API 在处理一个大型的组件时,内部的逻辑点容易碎片化,可能同时存在于 method,computed,watch 等 API 中,我们必须不断地跳转相关代码的选项块,这种碎片化使得理解和维护复杂组件变得困难。
- Composition API 将某个逻辑关注点相关的代码全都放在一个函数里,这样,当需要修改一个功能时,就不再需要在文件中跳来跳去。
- 逻辑复用:
- 在 Vue2 中,当混入多个 mixin 会存在两个非常明显的问题:命名冲突、数据来源不清晰。
- 而 Composition API 可以通过编写多个 hooks 函数就很好的解决了。
总结:
- 在逻辑组织和逻辑复用方面,Composition API 优于 Options API。
- Composition API 几乎是函数,会有更好的类型推断。
- Composition API 对 tree-shaking 友好,代码也更容易压缩。
- Composition API 中见不到 this 的使用,减少了 this 指向不明的情况。
- 如果是小型组件,可以继续使用 Options API,也是十分友好的。
Vue3 组合式 API 的缺点
- 代码组织难度提升(对于复杂场景):在大型项目或复杂组件中,如果代码组织不合理,使用组合式 API 可能会导致代码混乱。因为没有了像选项式 API 中明确的 data、methods、computed 等划分,开发者需要自己规划如何将功能相关的代码组合在一起。例如,多个 watchEffect 函数如果没有很好的规划,可能会使得数据变化的跟踪逻辑变得复杂和难以维护。
- 缺少 this 上下文:组合式 API 中没有 this 指针,这在一些场景下可能会不适应。例如,在 Vue2 选项式 API 中,this 可以方便地访问组件的属性和方法,但在组合式 API 中,需要通过其他方式(如将变量和函数从 setup 函数中返回)来让模板访问这些内容。
- 类型推断可能更复杂:当使用 TypeScript 时,组合式 API 的类型推断相对选项式 API 可能会更复杂。因为要考虑 ref、reactive 等函数返回的数据类型,以及如何正确地为它们添加类型注释。例如,ref 返回的是一个带有 value 属性的对象,在为其添加类型注释时需要准确地指定 value 的类型。
Vue3 比 Vue2 有什么优势?(Vue3 有什么更新?)
-
性能优化
- Vue3 使用了
Proxy
替代Object.defineProperty
实现响应式 - 重写了虚拟DOM,优化了 Diff 算法,增加静态标志
- 使用了静态提升技术来提高渲染性能,新增了编译时优化,在编译时进行模板静态分析,并生成更高效的渲染函数。
- 更加高效的组件初始化
- Vue3 使用了
-
Composition API
- Composition API 是一个全新的组件逻辑复用方式,可以更好地组合和复用组件的逻辑
- 比 mixin 更强大。它可以把各个功能模块独立开来,提高代码逻辑的可复用性,同时代码压缩性更强
- 在 Vue3 中,定义 methods、watch、computed、data 数据等都放在了
setup()
函数中 setup()
函数会在created()
生命周期之前执行。执行顺序为:beforeCreate > setup > created
-
TypeScript 支持增强
- Vue3 完全支持 TypeScript,在编写 Vue 应用程序时可以更方便地利用 TS 的类型检查和自动补全功能
- Vue2 选用 Flow 做类型检查,来避免一些因类型问题导致的错误,但是 Flow 对于一些复杂场景类型的检查,支持得并不好;Vue3 抛弃了 Flow ,使用 TypeScript 重构了整个项目
-
新的自定义渲染器:Vue3 的自定义渲染器允许开发者在细粒度上控制组件渲染行为,包括自定义渲染器、组件事件和生命周期等。
-
新增组件
- Fragment 不再限制 template 只有一个根节点
- Teleport 传送门,允许我们将控制的内容传送到任意的 DOM 中
- Suspense 等待异步组件时渲染一些额外的内容,让应用有更好的用户体验
-
Tree-shaking:支持摇树优化,摇树优化后会将不需要的模块修剪掉,真正需要的模块打到包内。优化后的项目体积只有原来的一半,加载速度更快
-
改进的 Vue CLI:Vue3 使用了改进的 Vue CLI,可以更加灵活地配置项目,同时支持 Vue2 项目升级到 Vue3
- 对 Vue3 的原生支持:
- Vue CLI 4.5 及以上版本支持直接创建 Vue3 项目
- 提供了专门的
vue create
命令来生成 Vue3 项目模板 - 默认生成的项目结构和配置已经针对 Vue3 进行了优化
- TypeScript 支持增强:
- Vue CLI 默认支持 TypeScript,并提供了更好的类型检查和智能提示
- 生成的项目模板中包含了 TypeScript 相关的配置和示例代码
- Webpack 5 支持:
- Vue CLI 4.5 及以上版本支持 Webpack 5,提高了构建性能和优化效果
- 插件系统优化:
- Vue CLI 插件系统得到了增强,开发者可以更方便地创建和共享自定义插件
- 内置的一些插件,如 Vuex、Vue Router 等也已经更新到支持 Vue3
- 浏览器兼容性自动配置:
- Vue CLI 会根据目标浏览器自动配置 Babel 和 PostCSS 等工具,以确保应用程序的正确运行
- 更好的 HMR(热模块替换)支持:
- Vue CLI 的 HMR 功能已经得到改进,可以更快速地反映代码变更
- 更友好的命令行界面:
- Vue CLI 的命令行界面进行了优化,在创建、管理和构建项目时提供了更好的用户体验
- 对 Vue3 的原生支持:
-
移除一些 API:Vue3 移除了一些不常用的 API,如过渡相关 API,部分修饰符(
.sync
)等- 过滤器(Filters):Vue2 中的过滤器功能已经被移除,开发者需要使用计算属性或方法来实现类似的功能
$on
、$off
和$once
方法:这些事件 API 已经从 Vue 实例中移除,开发者需要使用原生的 addEventListener 和 removeEventListener 方法来处理事件$children
和$parent
属性:这些直接访问组件实例的属性已经被移除,开发者需要使用 provide/inject 或 emits/emit 等机制来进行组件间通信$attr
和$listeners
属性:这些属性已经被移除,开发者需要使用v-bind="$attrs"
和v-on="$listeners"
来实现类似的功能- Inline 模板:Vue2 中支持在
<component>
标签上使用inline-template
属性,这个功能已经被移除 - keyCode 修饰符:在 Vue3 中,开发者应该使用 key 修饰符来代替 keyCode 修饰符
为什么 Vue3 对 TypeScript 的支持比 Vue2 强?
- 组合式 API(Composition API)与类型推断
- Vue3 引入了组合式 API,使得代码的组织更加灵活和可维护。同时,组合式 API 与 TypeScript 的结合更加紧密,可以更好地进行类型推断。在组合式 API 中,可以使用函数来封装和复用逻辑,并且可以通过参数和返回值的类型声明来明确函数的输入和输出类型。在下面这个例子中,使用ref函数创建了一个响应式的变量 count,并指定了它的类型为 number。这样,在使用这个函数的地方,可以通过类型推断得到 count 的类型,从而提高了代码的可读性和可维护性。
import { ref } from 'vue'; function useCounter() { const count = ref<number>(0); const increment = () => { count.value++; }; return { count, increment }; }
- Vue2 的选项式 API 在类型推断方面相对较弱。虽然可以使用
Vue.extend
和Vue.component
来定义组件,但是在处理复杂的组件逻辑时,类型推断可能不够准确。
- Vue3 引入了组合式 API,使得代码的组织更加灵活和可维护。同时,组合式 API 与 TypeScript 的结合更加紧密,可以更好地进行类型推断。在组合式 API 中,可以使用函数来封装和复用逻辑,并且可以通过参数和返回值的类型声明来明确函数的输入和输出类型。在下面这个例子中,使用ref函数创建了一个响应式的变量 count,并指定了它的类型为 number。这样,在使用这个函数的地方,可以通过类型推断得到 count 的类型,从而提高了代码的可读性和可维护性。
- 更好的类型定义和泛型支持
- Vue3 对其核心库和生态系统进行了更好的类型定义,使得在使用 TypeScript 开发 Vue 应用时,可以获得更准确的类型提示和错误检查。例如,在 Vue3 的模板中,可以使用类型推断来确定组件的 props 和 emits 的类型,从而避免了类型错误。在下面这个例子中,定义了一个组件的 props 类型为 Props,其中包含一个名为 message 的字符串属性。这样,在使用这个组件时,可以通过类型推断得到 message 的类型,从而提高了代码的可读性和可维护性。
import { defineComponent } from 'vue'; interface Props { message: string; } export default defineComponent({ props: { message: { type: String, required: true, } }, template: `<div>{{ message }}</div>` });
- Vue3 还提供了更好的泛型支持,可以在定义组件和函数时使用泛型来提高代码的灵活性和可复用性。在下面这个例子中,定义了一个泛型函数 useList,它可以接受一个类型为 ListItem 的数组,并返回一个包含选中项和选择项方法的对象。这样,在使用这个函数时,可以传入不同类型的列表项,从而提高了代码的灵活性和可复用性。
import { defineComponent } from 'vue'; interface ListItem { id: number; name: string; } function useList<T extends ListItem>(items: T[]) { const selectedItem = ref<T | null>(null); const selectItem = (item: T) => { selectedItem.value = item; }; return { selectedItem, selectItem }; } export default defineComponent({ setup() { const { selectedItem, selectItem } = useList([ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ]); return { selectedItem, selectItem }; }, template: `<div> <ul> <li v-for="item in items" @click="selectItem(item)">{{ item.name }}</li> </ul> <div v-if="selectedItem">Selected: {{ selectedItem.name }}</div> </div>` });
- Vue3 对其核心库和生态系统进行了更好的类型定义,使得在使用 TypeScript 开发 Vue 应用时,可以获得更准确的类型提示和错误检查。例如,在 Vue3 的模板中,可以使用类型推断来确定组件的 props 和 emits 的类型,从而避免了类型错误。在下面这个例子中,定义了一个组件的 props 类型为 Props,其中包含一个名为 message 的字符串属性。这样,在使用这个组件时,可以通过类型推断得到 message 的类型,从而提高了代码的可读性和可维护性。
- 更好的工具支持和生态系统
- Vue3 与 TypeScript 的结合得到了更好的工具支持,例如在 VS Code 等编辑器中,可以获得更准确的类型提示、自动补全和错误检查。编辑器可以识别 Vue3 的组件和组合式 API,并提供相应的类型信息,从而提高了开发效率和代码质量。
- Vue3 的生态系统也更加注重对 TypeScript 的支持,许多第三方库和工具都提供了良好的 TypeScript 类型定义,使得在使用这些库时可以获得更好的开发体验。
Vue2 对 Typescript 支持不好的用例
- 选项式 API 与类型推断
- 在 Vue2 的选项式 API 中,类型推断不够强大。例如,对于组件的 data 选项返回的对象,很难明确其确切的类型。在这个例子中,很难确定 message 的类型是字符串,count 的类型是数字,除非手动添加类型注释,但这样会比较繁琐。
export default { data() { return { message: '', count: 0, }; } };
- 对于组件的方法,也难以进行准确的类型推断。例如:
export default { methods: { increment() { // 这里无法明确知道 this 指向的类型,以及 this 上的属性类型 this.count++; } } };
- 在 Vue2 的选项式 API 中,类型推断不够强大。例如,对于组件的 data 选项返回的对象,很难明确其确切的类型。在这个例子中,很难确定 message 的类型是字符串,count 的类型是数字,除非手动添加类型注释,但这样会比较繁琐。
- Prop 类型定义的局限性
- 在 Vue2 中,定义 prop 的类型相对比较简单,并且在处理复杂类型时可能会出现问题。在下面这个例子中,只能指定 items 是一个数组,但无法明确数组中元素的具体类型。如果想要指定更复杂的类型,比如数组中是包含特定属性的对象,就比较困难。
export default { props: { items: { type: Array, default: () => [] } } };
- 在 Vue2 中,定义 prop 的类型相对比较简单,并且在处理复杂类型时可能会出现问题。在下面这个例子中,只能指定 items 是一个数组,但无法明确数组中元素的具体类型。如果想要指定更复杂的类型,比如数组中是包含特定属性的对象,就比较困难。
- 缺乏对泛型的良好支持
- Vue2 在处理泛型时比较困难。例如,在自定义指令中,如果想要使用泛型来处理不同类型的数据,会比较麻烦。示例:
Vue.directive('myDirective', { bind(el, binding, vnode) { // 这里很难使用泛型来处理不同类型的 binding.value console.log(binding.value); } });
- Vue2 在处理泛型时比较困难。例如,在自定义指令中,如果想要使用泛型来处理不同类型的数据,会比较麻烦。示例:
- 混入(Mixins)与类型冲突
- 在 Vue2 中,使用混入可能会导致类型冲突。当多个混入合并到一个组件时,很难确保属性和方法的类型一致性。在下面这个例子中,如果没有仔细管理类型,可能会出现属性类型不明确或者冲突的情况。
const mixin1 = { data() { return { property1: '' }; } }; const mixin2 = { data() { return { property2: 0 }; } }; export default { mixins: [mixin1, mixin2] };
- 在 Vue2 中,使用混入可能会导致类型冲突。当多个混入合并到一个组件时,很难确保属性和方法的类型一致性。在下面这个例子中,如果没有仔细管理类型,可能会出现属性类型不明确或者冲突的情况。
Vue2 为什么不能很好地支持 Tree-shaking?
- 运行时与编译时的架构限制 Vue2 采用了运行时 + 编译时的架构。在这种架构下,很多功能是在运行时动态添加到 Vue 实例上的。这使得在静态分析时,很难确定哪些代码是真正被使用的,哪些是可以被安全地移除的。例如,Vue2 的响应式系统是通过在运行时对对象进行遍历和劫持来实现的,这种方式使得在编译阶段难以确定哪些响应式属性是实际被用到的,从而难以进行有效的 Tree-shaking。
- 模块设计和依赖关系 Vue2 的模块设计和依赖关系也对 Tree-shaking 造成了一定的阻碍。一些模块的导出方式可能不够清晰或者存在不必要的依赖关系,导致打包工具难以准确地识别和剔除未使用的代码。例如,如果一个模块导出了多个函数或对象,但在实际使用中只用到了其中的一部分,而打包工具无法准确判断哪些部分是被使用的,就无法进行有效的 Tree-shaking。
- 缺乏对现代打包工具的优化支持 Vue2 在设计时可能没有充分考虑到现代打包工具的 Tree-shaking 特性。随着时间的推移,打包工具如 Webpack 和 Rollup 不断发展和优化,对 Tree-shaking 的支持也越来越好。然而,Vue2 可能没有及时跟进这些变化,没有提供足够的优化措施来配合打包工具进行 Tree-shaking。例如,Vue2 的代码结构可能没有遵循一些有利于 Tree-shaking 的最佳实践,如使用 ES6 模块的静态导入和导出、避免副作用等。
Vue3 的自定义渲染器
自定义渲染器的工作原理如下:
- 开发者创建一个自定义渲染器,实现 Vue 所需的一系列渲染器 API,如 createApp、mount、patch 等
- 在 Vue 应用程序中使用这个自定义渲染器,而不是默认的 DOM 渲染器
- 通过自定义渲染器,Vue 应用程序可以在目标平台上渲染和运行
实现步骤包括以下几个主要部分:
- 定义渲染器 API: 实现 Vue 所需的一系列渲染器 API,如 createApp、mount、patch、unmount 等 这些 API 需要与 Vue 的内部机制兼容,用于管理组件的生命周期、响应式更新等
- 创建应用实例: 实现 createApp API,返回一个应用实例,用于管理整个应用程序
- 挂载应用: 实现 mount API,将应用程序挂载到目标容器上 处理组件的初始化、渲染等操作
- 更新应用: 实现 patch API,用于更新组件的状态和 DOM 结构 处理组件的更新、添加、删除等操作
- 卸载应用: 实现 unmount API,用于卸载应用程序 清理应用程序占用的资源
- 事件处理: 实现事件处理机制,将 Vue 组件的事件与目标平台的事件系统对接
- 状态管理: 实现响应式系统,保持组件状态与目标平台状态的同步
- 生命周期钩子: 实现组件生命周期钩子,如 created、mounted、updated 等,与目标平台集成
- 其他辅助功能: 根据需要实现其他辅助功能,如错误处理、调试工具等
自定义渲染器的主要优势:
- 跨平台支持:开发者可以创建针对特定平台(如 Web、移动端、桌面应用等)的渲染器,从而构建跨平台的应用程序
- 性能优化:自定义渲染器可以针对特定平台进行优化,提高应用程序的性能
- UI 库集成:开发者可以将 Vue 应用程序与各种 UI 库(如 Three.js、D3.js 等)集成,创建更丰富的用户界面
- 特殊用例支持:自定义渲染器可以支持一些特殊的用例,如服务端渲染(SSR)、静态网站生成(SSG)等
Teleport 组件 //TODO
Vue3 新增了 teleport(瞬移)组件,可以将组件的 DOM 插到指定的组件层,而不是默认的父组件层,可以用于在应用中创建模态框、悬浮提示框、通知框等组件。
Teleport 组件可以传递两个属性:
- to (必填):指定组件需要挂载到的 DOM 节点的 ID,如果使用插槽的方式定义了目标容器也可以传入一个选择器字符串。
- disabled (可选):一个标志位指示此节点是否应该被瞬移到目标中,一般情况下,这个 props 建议设为一个响应式变量来控制 caption 是否展示。
需要注意的是,虽然 DOM 插头被传送到另一个地方,但它的父组件仍然是当前组件,这一点必须牢记,否则会导致样式、交互等问题:
- 事件处理:传送的内容中触发的事件仍然会在组件的上下文中进行处理,就好像内容没有被传送一样。
- 数据绑定:与组件数据的绑定也会正常工作,因为 Vue 会确保数据的一致性和响应性。
原理:
- 组件标记与识别
当使用<template> <Teleport to="body"> <div>Teleported content</div> </Teleport> </template>
<Teleport>
标签时,Vue 编译器会识别这个特殊的标签。如上例,Vue 在编译过程中会标记出包含<Teleport>
的组件,并记录其相关信息,如目标位置body
和要传送的内容<div>Teleported content</div>
。 - 创建独立的 DOM 节点
在组件挂载阶段,Vue 会根据
<Teleport>
的配置,在指定的目标位置创建一个新的 DOM 节点。这个节点将作为传送内容的容器。例如,当目标位置是body时,Vue 会在文档的<body>
元素内部创建一个新的 DOM 元素,用来容纳传送过来的内容。 - 内容渲染
- Vue 会将
<Teleport>
包裹的内容渲染到新创建的 DOM 节点中,而不是在组件自身的默认位置渲染。 - 这意味着,即使组件在组件树中的位置很深,但传送的内容会直接出现在指定的目标位置。
- Vue 会将
- 内容更新:
- 当组件的状态发生变化导致需要重新渲染时,Vue 会检测到这种变化,并将更新后的内容再次渲染到
<Teleport>
指定的 DOM 节点中。这样可以确保传送的内容始终与组件的状态保持同步。
- 当组件的状态发生变化导致需要重新渲染时,Vue 会检测到这种变化,并将更新后的内容再次渲染到
Vue3 性能提升主要是通过哪几方面体现的?
-
编译阶段优化
- diff 算法优化 Vue3 在 diff 算法中相比 Vue2 增加了静态标记,其作用是为了会发生变化的地方添加一个 flag 标记,下次发生变化的时候直接找该地方进行比较。
- 静态提升
Vue3 中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用。免去了重复的创建操作,优化内存。没做静态提升之前,未参与更新的元素也在 render 函数内部,会重复创建阶段。做了静态提升后,未参与更新的元素,被放置在 render 函数外,每次渲染的时候只要取出即可。同时该元素会被打上静态标记值为
-1
,特殊标志是负整数表示永远不会用于 diff。 - 事件监听缓存 默认情况下绑定事件行为会被视为动态绑定(没开启事件监听器缓存),所以每次都会去追踪它的变化。开启事件侦听器缓存后,没有了静态标记。也就是说下次 diff 算法的时候直接使用。
- SSR优化
当静态内容大到一定量级时候,会用
createStaticVNode
方法在客户端去生成一个静态节点,这些静态节点,会被直接 innerHtml,就不需要创建对象,然后根据对象渲染。
-
源码体积 相比 Vue2,Vue3 整体体积变小了,除了移出一些不常用的 API,最重要的是 Tree-shaking。任何一个函数,如ref、reactive、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小。
-
响应式 Vue2 中采用
defineProperty
来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加 getter 和 setter,实现响应式。Vue3 采用 proxy 重写了响应式系统,因为 proxy 可以对整个对象进行监听,所以不需要深度遍历:- 可以监听动态属性的添加
- 可以监听到数组的索引和数组length属性
- 可以监听删除属性
说说 Vue3 中 Tree-shaking 特性?
Tree-shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination。简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码。
在 Vue2 中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是 Vue 实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到。而 Vue3 源码引入 Tree-shaking 特性,将全局 API 进行分块。如果不使用其某些功能,它们将不会包含在基础包中。
原理: Tree-shaking 是基于 ES6 模板语法(import 与 exports),主要是借助 ES6 模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。总结为两步:
- 编译阶段利用ES6 Module判断哪些模块已经加载
- 判断那些模块和变量未被使用或者引用,进而删除对应代码
作用:
- 减少程序体积(更小)
- 减少程序执行时间(更快)
- 便于将来对程序架构进行优化(更友好)
Proxy
和Object.defineProperty
的区别? //TODO
- 实现方式:
Proxy
是 ES6 新增的一种特性,使用了一种代理机制来实现响应式。而Object.defineProperty
是在 ES5 中引入的,使用了 getter 和 setter 方法来实现。 - 作用对象:
Proxy
可以代理整个对象,包括对象的所有属性、数组的所有元素以及类似数组对象的所有元素。而Object.defineProperty
只能代理对象上定义的属性。 - 监听属性:
Proxy
可以监听到新增属性和删除属性的操作,而Object.defineProperty
只能监听到已经定义的属性的变化。 - 性能:由于
Proxy
是 ES6 新增特性,其内部实现采用了更加高效的算法,相对于Object.defineProperty
来说在性能方面有一定的优势。
Vue3 里为什么要用 Proxy API 替代 defineProperty API?
Vue2 中采用defineProperty
来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加 getter 和 setter,实现响应式。但是存在以下问题:
- 检测不到对象属性的添加和删除
- 数组 API 方法无法监听到
- 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题
Vue3 的 proxy 监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作。
总结:
Object.defineProperty
只能遍历对象属性进行劫持。- Proxy 直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的。
- Proxy 可以直接监听数组的变化(push、shift、splice)。
- Proxy 有多达13种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等,这是
Object.defineProperty
不具备的。
Vue3 的响应式原理 //TODO
-
参考答案 1
使用 Proxy 和 Reflect API 实现 Vue3 响应式。
Vue3 会将响应式对象转换为一个 Proxy 对象,并利用 Proxy 对象的 get 和 set 拦截器来实现对属性的监听和更新。当访问响应式对象的属性时,get 拦截器会被触发,此时会收集当前的依赖关系,并返回属性的值;当修改响应式对象的属性时,set 拦截器会被触发,此时会触发更新操作,并通知相关的依赖进行更新。
Reflect API 则可以更加方便地实现对对象的监听和更新,可以用来访问、检查和修改对象的属性和方法,比如
Reflect.get
、Reflect.set
、Reflect.has
等。优点:可监听属性的变化、新增与删除,监听数组的变化。
-
参考答案 2
Vue3 响应式是使用 ES6 的 Proxy 和 Reflect 相互配合实现数据响应式,解决了 Vue2 中视图不能自动更新的问题。Proxy 是深度监听,所以可以监听对象和数组内的任意元素,从而可以实现视图实时更新。
响应式大致分为三个阶段:
- 初始化阶段:初始化阶段通过组件初始化方法形成对应的 Proxy 对象,然后形成一个负责渲染的 Effect。
- get 依赖收集阶段:通过解析 template,替换真实 data 属性,来触发 get,然后通过 stack 方法,通过 Proxy 对象和 key 形成对应的 Deps,将负责渲染的 Effect 存入 Deps。(这个过程还有其他的 Effect,比如 watchEffect 存入 Deps 中 )。
- set 派发更新阶段:当我们
this[key] = value
改变属性的时候,首先通过 trigger 方法,通过 Proxy 对象和 key 找到对应的 Deps,然后给 Deps分类分成 computedRunners 和Effect,然后依次执行,如果需要调度的,直接放入调度。
Proxy 只会代理对象的第⼀层,那么 Vue3 ⼜是怎样处理这个问题的呢?
判断当前Reflect.get
的返回值是否为 Object,如果是则再通过 reactive ⽅法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次 get/set,那么如何防⽌触发多次呢?
可以判断 key 是否为当前被代理对象 target ⾃身属性,也可以判断旧值与新值是否相等,只有满⾜以上两个条件之⼀时,才有可能执⾏ trigger。
Vue3 的 diff 算法
核心原理:
- 深度优先,同层比较,时间复杂度只有 O(n);
- 双端对比算法:先看左侧,看完左侧看右侧,然后锁定中间乱序的部分
- 最长递增子序列:针对中间乱序部分,采用最长递增子序列的算法,计算出乱序部分可以复用的最长连续节点
diff 算法流程:
- diff 算法是发生在更新的过程,而更新的情况有以下几种情况:
- 旧的是 Text,新的是 Text,直接更新
- 旧的是 Array,新的是 Text,把旧的全部删掉,更新为 Text
- 旧的是 Text,新的是 Array,删掉旧的文本,更新为新的 Array 元素
- 旧的是 Array,新的是 Array,调用 updateChildren 函数比较子节点,这是 diff 的核心
- 旧的是 Array,新的是 Array 的情况下,调用 updateChildren 函数,diff 核心流程。新旧数组之间的对比,无非是通过更新、删除、添加和移除节点来完成的,diff 算法的核心以较低的成本完成子节点的更新为目的,求解生成新子节点 DOM 的系列操作。过程:
- 同步头节点
- 同步尾节点
- 添加新节点 新子节点有剩余
- 删除多余节点 旧节点有剩余
- 处理未知子序列
- 处理未知子序列
有时会碰到比较复杂的未知子序列:对于移动、删除、添加、更新这些操作,其中最复杂的就是移动操作,Vue 针对未知子序列的核心是通过最长递增子序列查找到需要移动的最小值。在查找过程中需要对比新旧子序列,那么我们就要遍历某个序列,如果在遍历旧子序列的过程中需要判断某个节点是否在新子序列中存在,这就需要双重循环,而双重循环的复杂度是
O(n2)
,为了优化这个复杂度,可以用一种空间换时间的思路,建立索引图,把时间复杂度降低到O(n)
。建立索引图:- 根据for循环中的key建立新子序列中的索引图
- 然后再创建一个新旧子序列索引的映射关系,用于确定最长递增子序列、
- 然后正序遍历旧子序列,看看是否在新子序列的索引图中,如果不再就删除,如果在根据索引去判断这个节点是否在最长递增子序列中,如果在就不需要进行移动,如果不再就要进行移动操作
- 然后在遍历的过程中对新节点打上标记,对于没有被查找的标识为0,需要进行添加操作
Vue2 和 Vue3 核心 diff 算法区别? //TODO
Vue2 使用的是双向指针遍历的算法,也就是通过逐层比对新旧虚拟DOM树节点的方式来计算出更新需要做的最小操作集合。但这种算法的缺点是,由于遍历是从左到右、从上到下进行的,当发生节点删除或移动时,会导致其它节点位置的计算出现错误,因此会造成大量无效的重新渲染。
Vue3 使用了经过优化的单向遍历算法,也就是只扫描新虚拟DOM树上的节点,判断是否需要更新,跳过不需要更新的节点,进一步减少了不必要的操作。此外,在虚拟DOM创建后,Vue3 会缓存虚拟DOM节点的描述信息,以便于复用,这也会带来性能上的优势。同时,Vue3还引入了静态提升技术,在编译时将一些静态的节点及其子节点预先处理成HTML字符串,大大提升了渲染性能。
因此,总体来说,Vue3 相对于 Vue2 拥有更高效、更智能的 diff 算法,能够更好地避免不必要的操作,并提高了渲染性能。
Vue3 为什么比 Vue2 快? //TODO
- 响应式系统优化:Vue3 引入了新的响应式系统,这个系统的设计让 Vue3 的渲染函数可以在编译时生成更少的代码,这也就意味着在运行时需要更少的代码来处理虚拟DOM。这个新系统的一个重要改进就是提供了一种基于 Proxy 实现的响应式机制,这种机制为开发人员提供更加高效的API,也减少了一些运行时代码。
- 编译优化:Vue3 的编译器对代码进行了优化,包括减少了部分注释、空白符和其他非必要字符的编译,同时也对编译后的代码进行了懒加载优化。
- 更快的虚拟DOM:Vue3 对虚拟DOM进行了优化,使用了跟 React 类似的 Fiber 算法,这样可以更加高效地更新DOM节点,提高性能。
- Composition API:Vue3 引入了 Composition API,这种API通过提供逻辑组合和重用的方法来提升代码的可读性和重用性。这种API不仅可以让Vue3应用更好地组织和维护业务逻辑,还可以让开发人员更加轻松地实现优化。
Vue3 编译做了哪一些优化? //TODO
- 静态树提升:Vue3 通过重写编译器,实现对静态节点(即不改变的节点)进行编译优化,使用 HoistStatic 功能将静态节点移动到 render 函数外部进行缓存,从而服务端渲染和提高前端渲染的性能。
- Patch Flag:在 Vue3 中,编译生成的虚拟节点会根据节点 patch 的标记,只对需要重新渲染的数据进行响应式更新,不需要更新的数据不会重新渲染,从而大大提高了渲染性能。
- 静态属性提升:Vue3 中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用。免去了重复的创建操作,优化内存。没做静态提升之前,未参与更新的元素也在 render 函数内部,会重复创建阶段。做了静态提升后,未参与更新的元素,被放置在 render 函数外,每次渲染的时候只要取出即可。同时该元素会被打上静态标记值为
-1
,特殊标志是负整数表示永远不会用于 diff。 - 事件监听缓存:默认情况下绑定事件行为会被视为动态绑定(没开启事件监听器缓存),所以每次都会去追踪它的变化。开启事件侦听器缓存后,没有了静态标记。也就是说下次 diff 算法的时候直接使用。
- 优化 render 函数:包含 render 函数的换行和缩进、render 函数的条件折叠、render 函数的常量折叠等等。
$nextTick
原理 //TODO
源码:
// Vue3这里不再进行向下兼容,直接使用了Promise来操作异步
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null
function nextTick( this: T,fn?: (this: T) => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
nextTick 直接相关的源码很简单,就是给传入 nextTick 中的回调函数包了一层,通过Promise.resolve
,将回调函数 fn 放在了微任务队列,实现了 fn 的延迟执行,从而能够顺利的拿到 DOM 更新后的数据。
与 Vue2 的$nextTick
有什么区别? //TODO
Vue2 中的 nextTick 是基于 JavaScript 的事件循环和不同的浏览器 API(如 Promise、MutationObserver 和 setTimeout)来实现的,它会根据当前环境选择最合适的异步方法。Vue3 的 nextTick 同样使用 Promise,但进行了简化,直接使用 Promise.resolve().then(fn)
来实现。
watch 和 watchEffect 的区别? //TODO
watch 和 watchEffect 都是监听器,watchEffect 是一个副作用函数。它们之间的区别有:
- watch 既要指明监视的数据源,也要指明监视的回调;watchEffect 可以自动监听数据源作为依赖,不用指明监视哪个数据,监视的回调中用到哪个数据,那就监视哪个数据。
- watch 可以访问改变之前和之后的值,watchEffect 只能获取改变后的值。
- watch 运行的时候不会立即执行,值改变后才会执行;watchEffect 运行后可立即执行,这一点可以通过 watch 的配置项 immediate 改变。
- watchEffect 有点像 computed,但 computed 注重的计算出来的值(回调函数的返回值), 所以必须要写返回值,而 watchEffect 注重的是过程(回调函数的函数体),所以不用写返回值。
watch 与 Vue2 中 watch 配置功能一致,但有两个小坑:
- 监视 reactive 定义的响应式数据时,oldValue 无法正确获取,强制开启了深度监视(deep 配置失效)。
- 监视 reactive 定义的响应式数据中某个属性时,deep 配置有效。
ref 与 reactive 的区别?
- 数据类型
- ref:用于创建单一的响应式值。支持基础数据类型(如字符串、数字、布尔值等)。
- reactive:用于创建响应式对象。适合用于复杂数据结构,如对象和数组。
- 数据访问
- ref:访问时需要使用
.value
,例如myRef.value
。在模板中可以直接使用,无需.value
。 - reactive:直接访问属性,无需额外的解包。例如:
state.count
。
- ref:访问时需要使用
- 响应式深度
- ref:默认是浅响应式,只有 ref 本身是响应式的,内部对象不会自动变成响应式。
- reactive:创建深层响应式对象,所有嵌套的属性也会是响应式的。
- 适用场景
- ref:更适合用于简单的、单一值的场景,如计数器、状态标记等。
- reactive:更适合用于管理复杂的状态,如表单数据、配置对象等。
toRef 和 toRefs 的区别?
- 功能
- toRef:将一个响应式对象的单一属性转换为一个响应式引用。返回的是一个 ref 类型的对象,允许你单独访问和修改该属性。
- toRefs:将一个响应式对象的所有属性转换为响应式引用的对象。返回的是一个包含多个 ref 的对象,适用于将整个响应式对象展开。
- 用法
- toRef:用于获取特定属性的 ref。语法:
toRef(object, key)
。import { reactive, toRef } from 'vue'; const state = reactive({ count: 0 }); const countRef = toRef(state, 'count'); // 获取 count 的 ref
- toRefs:用于将一个响应式对象的所有属性转换为 ref。语法:
toRefs(object)
。import { reactive, toRefs } from 'vue'; const state = reactive({ count: 0, name: 'Vue' }); const { count, name } = toRefs(state); // 获取所有属性的 ref
- toRef:用于获取特定属性的 ref。语法:
- 返回结果
- toRef:返回单一的 ref 对象。
- toRefs:返回一个包含多个 ref 的对象,属性名与原对象相同。
- 适用场景
- toRef:当只需要访问或修改响应式对象的某个特定属性时,使用 toRef 可以直接获取该属性的 ref,方便进行响应式更新。
import { reactive, toRef } from 'vue'; const state = reactive({ count: 0 }); const countRef = toRef(state, 'count'); // 获取 count 的 ref // 直接使用 countRef 进行修改 countRef.value += 1;
- toRefs:当需要解包一个响应式对象的所有属性,以便在模板或函数中单独使用时,使用 toRefs 很方便。它将对象的所有属性变为 ref,保持响应式。
import { reactive, toRefs } from 'vue'; const state = reactive({ count: 0, name: 'Vue' }); const { count, name } = toRefs(state); // 解包所有属性为 refs // 在模板或逻辑中使用 count.value += 1; console.log(name.value); // 访问 name
- toRef:当只需要访问或修改响应式对象的某个特定属性时,使用 toRef 可以直接获取该属性的 ref,方便进行响应式更新。
Vue2 / Vue3 组件通信方式?
Vue2 组件通信
父子组件通信:
props
父传子;$emit
/v-on
子通过派发事件的方式传给父$attrs
包含父作用域里除 class 和 style 除外的非 props 属性集合,$listeners
包含父作用域里.native
除外的监听事件集合ref
获取子组件的属性或者调用子组件的方法.sync
/v-model
实现父传子数据的双向绑定;通过$emit
子改父$children
获取到一个包含所有子组件的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法;$parent
获取到父节点的 VueComponent 对象,包含父节点中所有数据和方法provide
/inject
依赖注入
兄弟组件通信:
- EventBus
定义:
使用:// 方法一:抽离成一个单独的 js 文件 Bus.js ,然后在需要的地方引入 // Bus.js import Vue from "vue" export default new Vue() // 方法二:直接挂载到全局 // main.js import Vue from "vue" Vue.prototype.$bus = new Vue() // 方法三:注入到 Vue 根对象上 // main.js import Vue from "vue" new Vue({ el: "#app", data: { Bus: new Vue() } })
// 在需要向外部发送自定义事件的组件内 <template> <button @click="handlerClick">按钮</button> </template> import Bus from "./Bus.js" export default { methods: { handlerClick() { // 自定义事件名 sendMsg Bus.$emit("sendMsg", "向外部发送的数据") } } } // 在需要接收外部事件的组件内 import Bus from "./Bus.js" export default { mounted() { // 监听事件的触发 Bus.$on("sendMsg", data => { console.log("接收到的数据", data) }) }, beforeDestroy() { // 取消监听 Bus.$off("sendMsg") } }
- Vuex
$parent
跨层级组件通信:
provide
/inject
- EventBus
- Vuex
$attrs
/$listeners
Vue3 组件通信
props
父传子;$emit
/v-on
子通过派发事件的方式传给父ref
获取子组件的属性或者调用子组件方法$attrs
包含父作用域里除 class 和 style 除外的非 props 属性集合- v-model 数据双向绑定
provide
/inject
依赖注入- Pinia
- 事件总线 mitt
谈谈 Pinia? //TODO
Pinia 是 Vue 官方团队成员专门开发的一个全新状态管理库。
优点:
- 更加轻量级,压缩后提交只有 1.6kb。
- 完整的 TS 的支持,Pinia 源码完全由 TS 编码完成。
- 移除 mutations,只剩下 state 、 actions 、 getters 。
- 没有了像 Vuex 那样的模块镶嵌结构,它只有 store 概念,并支持多个 store,且都是互相独立隔离的。当然,你也可以手动从一个模块中导入另一个模块,来实现模块的镶嵌结构。
- 无需手动添加每个 store,它的模块默认情况下创建就自动注册。
- 支持服务端渲染(SSR)。
- 支持 Vue DevTools。
- 更友好的代码分割机制:
- Vuex 的代码分割:打包时,Vuex 会把3个 store 合并打包,当首页用到 Vuex 时,这个包会引入到首页一起打包,最后输出1个 js chunk。这样的问题是,其实首页只需要其中1个 store,但其他2个无关的 store 也被打包进来,造成资源浪费。
- Pinia 的代码分割:打包时,Pinia 会检查引用依赖,当首页用到某个 store,打包只会把用到的 store 和页面合并输出1个 js chunk,其他2个 store 不耦合在其中。Pinia 能做到这点,是因为它的设计就是 store 分离的,解决了项目的耦合问题。
EventBus 与 mitt 区别? //TODO
Vue2 中我们使用 EventBus 来实现跨组件之间的一些通信,它依赖于 Vue 自带的$on
/$emit
/$off
等方法,这种方式使用非常简单方便,但如果使用不当也会带来难以维护的毁灭灾难。而 Vue3 中移除了这些相关方法,这意味着 EventBus 这种方式我们使用不了,Vue3 推荐尽可能使用props/emits
、provide/inject
、Vuex 等其他方式来替代。
如果 Vue3 内部的方式无法满足,官方建议使用一些外部的辅助库,例如:mitt。优点:
- 非常小,压缩后仅有 200 bytes。
- 完整 TS 支持,源码由 TS 编码。
- 跨框架,它并不是只能用在 Vue 中,React、JQ 等框架中也可以使用。
- 使用简单,仅有 on、emit、off 等少量实用API。
script setup
是什么? //TODO
script setup
是 Vue3 的语法糖,简化了组合式 API 的写法,并且运行性能更好。使用 script setup
语法糖的特点:
- 属性和方法无需返回,可以直接使用。
- 引入组件的时候,会自动注册,无需通过 components 手动注册。
- 使用 defineProps 接收父组件传递的值。
- useAttrs 获取属性,useSlots 获取插槽,defineEmits 获取自定义事件。
- 默认不会对外暴露任何属性,如果有需要可使用 defineExpose 。
setup 中如何获得组件实例? //TODO
在 setup 函数中,你可以使用 getCurrentInstance()
方法来获取组件实例。getCurrentInstance()
方法返回一个对象,该对象包含了组件实例以及其他相关信息。
import { getCurrentInstance } from 'vue'
export default {
setup() {
const instance = getCurrentInstance()
// ...
return { instance }
}
}
在上面的示例中,我们使用 getCurrentInstance()
方法获取当前组件实例。然后,我们可以将该实例存储在一个常量中,并在 setup 函数的返回值中返回。
注意:getCurrentInstance()
方法只能在 setup 函数中使用,而不能在组件的生命周期方法(如 created、mounted 等方法)中使用。另外,如果在 setup 函数返回之前访问了 instance 对象,那么它可能是 undefined ,因此我们需要对其进行处理。
v-if
和v-for
的优先级哪个高?
- 在 Vue2 中
v-for
的优先级更高,但是在 Vue3 中优先级改变了,v-if
的优先级更高。 - Vue2 中,可以一起使用,但是非常不推荐,
v-if
也能获取到v-for
中的 item - Vue3 中,不能一起使用,因为
v-if
不能使用v-for
中的 item(会直接报错,不能从 undefined 中读取 xxx 属性)
Vue3 有哪些 hook?
- 生命周期相关 Hook
- onBeforeMount
- 原理:在组件挂载之前被调用,此时组件的 DOM 尚未创建,但可以访问组件的实例数据和方法等。它类似于 Vue2 中的 beforeMount 生命周期钩子。
- 示例:假设我们有一个组件用于展示用户信息,在获取用户数据后,在 DOM 挂载前可以进行数据格式的初步处理。
- onMounted
- 原理:在组件挂载完成后被调用,此时组件的 DOM 已经创建,可以访问和操作 DOM 元素。这与 Vue2 中的 mounted 生命周期钩子功能相似。
- 示例:在一个地图组件中,当组件挂载后,可以获取地图容器的 DOM 元素,并初始化地图实例。
- onBeforeUpdate
- 原理:在组件数据更新导致 DOM 重新渲染之前被调用。它可以用于在更新之前记录数据状态或者执行一些特定的逻辑,如暂停某些动画效果等。
- 示例:在一个数据可视化组件中,在数据更新前暂停图表的动画效果,更新完成后再恢复。
- onUpdated
- 原理:在组件数据更新导致 DOM 重新渲染完成后被调用。可以用于在更新后执行一些操作,如检查 DOM 元素的最终状态等。
- 示例:在一个表单组件中,当表单数据更新后,检查表单元素是否符合某些样式规则。
- onBeforeUnmount
- 原理:在组件卸载之前被调用。可以用于清理一些定时器、取消事件订阅或者释放资源等操作。
- 示例:在一个实时数据更新的组件中,当组件要卸载时,清除定时器以避免内存泄漏。
- onUnmounted
- 原理:在组件卸载完成后被调用。主要用于执行一些最后的清理工作或者记录组件卸载的相关信息。
- 示例:在一个组件中,当卸载完成后记录日志,表示该组件已经从页面中移除。
- onBeforeMount
- 响应式数据相关 Hook
- ref
- 原理:用于创建一个响应式的数据引用。ref 返回一个对象,其 value 属性保存着实际的数据。当 value 的值发生变化时,与之绑定的模板或者计算属性等会自动更新。
- 示例:在一个计数器组件中,使用ref来创建一个计数器变量。
- reactive
- 原理:用于创建一个响应式的对象。reactive 会将对象的所有属性转换为响应式数据,当对象中的任何属性发生变化时,相关的模板和计算属性等都会更新。
- 示例:在一个用户信息管理组件中,使用 reactive 来创建一个用户对象。
- toRefs
- 原理:将一个响应式对象转换为普通对象,其中每个属性都是一个 ref 引用。这样做的好处是在解构响应式对象时,不会丢失响应式连接。
- 示例:假设我们有一个响应式对象 user,想要在 setup 函数中解构它并保持响应式。
- computed
- 原理:用于创建计算属性。计算属性是基于其他响应式数据的值进行计算得到的属性,它会自动缓存计算结果,只有在其依赖的响应式数据发生变化时才会重新计算。
- 示例:在一个购物车组件中,计算购物车中商品的总价。
- ref
- 其他常用 Hook
- watch
- 原理:用于监视一个或多个响应式数据的变化。它可以接收一个数据源(可以是 ref、reactive 对象的属性等)和一个回调函数。当数据源发生变化时,回调函数会被执行,并且可以获取到变化前后的值。
- 示例:在一个表单组件中,监视表单输入框的数据变化,当数据变化后发送数据到服务器进行验证。
- watchEffect
- 原理:立即执行一个函数,并自动追踪函数内部使用的响应式数据。当这些响应式数据发生变化时,函数会重新执行。它不需要像 watch 那样明确指定要监视的数据源,而是自动检测函数内部的响应式依赖。
- 示例:在一个组件中,根据用户的权限设置来动态加载不同的模块。
- watch
对比一下 Vue 和 React
- 框架设计
- Vue:采用渐进式 JavaScript 框架的设计理念,意味着它可以根据项目的需求逐步引入功能。Vue.js 核心库只关注视图层,对于大型应用,可以搭配 Vue Router 进行路由管理、Vuex 进行状态管理等周边库来构建复杂的单页应用。它在模板语法上更接近 HTML,对初学者比较友好,数据绑定使用
v-model
等指令,开发人员可以直观地理解数据和视图之间的交互。 - React:基于组件化和函数式编程的理念。一切都是组件,组件通过接收 props(属性)来传递数据,并且可以返回一个描述 UI 的React Element(可以是原生 DOM 元素或者其他组件)。React 使用 JSX(JavaScript XML)语法,它允许在 JavaScript 代码中嵌入类似 HTML 的语法来描述 UI,这使得代码在逻辑和视图的结合上更加紧密。通过
ReactDOM.render
或者在函数组件中使用 useEffect 等钩子函数来将组件渲染到 DOM 中。
- Vue:采用渐进式 JavaScript 框架的设计理念,意味着它可以根据项目的需求逐步引入功能。Vue.js 核心库只关注视图层,对于大型应用,可以搭配 Vue Router 进行路由管理、Vuex 进行状态管理等周边库来构建复杂的单页应用。它在模板语法上更接近 HTML,对初学者比较友好,数据绑定使用
- 数据绑定与更新机制
- Vue:使用双向数据绑定
v-model
和响应式数据系统。当数据发生变化时,Vue.js 会自动更新与之绑定的 DOM 元素。它通过Object.defineProperty
(在 Vue2 中)或者 Proxy(在 Vue3 中)来进行数据劫持,追踪数据的变化。例如,在组件的 data 函数返回的对象中的属性是响应式的,当这些属性的值改变时,Vue.js 能够感知并更新视图。 - React:是单向数据流,数据从父组件通过 props 传递给子组件,子组件不能直接修改父组件传递过来的 props。当组件的 state或者 props 发生变化时,React 会重新渲染组件。通过 setState(在类组件中)或者 useState(在函数组件中)来更新状态,并且 React 会根据虚拟 DOM 的差异来高效地更新真实 DOM。
- Vue:使用双向数据绑定
- 组件化的差异
- Vue:组件可以是单文件组件(
.vue
文件),一个文件中包含了模板(template)、脚本(script)和样式(style)三个部分,这种方式使得组件的组织和维护更加方便。在组件内部,通过 props 接收父组件传递的数据,通过$emit
事件来向父组件发送消息。组件可以通过 mixins 来共享代码逻辑,在 Vue3 中还可以使用组合式 API(setup 函数)来提取和复用逻辑。 - React:组件分为类组件和函数组件。类组件通过继承
React.Component
来定义,有自己的生命周期方法,如 componentDidMount、componentWillUnmount 等。函数组件在 React 16.8 引入钩子函数(Hooks)后变得更加强大,可以通过 useEffect 来模拟生命周期方法,通过 useContext 来共享上下文数据,通过 useReducer 来处理复杂的状态逻辑等。组件之间通过 props 和 children 来传递数据和嵌套组件。
- Vue:组件可以是单文件组件(