Appearance
Vue 基础知识
目录
问题 1:MVVM 的理解
MVVM 是一种软件架构模式,MVVM 分为 Model、View、ViewModel:
Model代表数据模型,数据和业务逻辑都在 Model 层中定义;View代表 UI 视图,负责数据的展示;ViewModel负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;
Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。
问题 2:vue 和 react 的区别,有什么相同
不同:
- 模版语法不同,react 采用 JSX 语法,vue 使用基于 HTML 的模版语法
- 数据绑定不同,vue 使用双向数据绑定,react 则需要手动控制组件的状态和属性。
- 状态管理不同,vue 使用 vuex 状态管理,react 使用 redux 状态管理
- 组件通信不同,vue 使用 props 和自定义事件的方式进行父子组件通信,react 则通过 props 和回调函数的方式进行通信。
- 生命周期不同,vue 有 8 个生命周期钩子,react 有 10 个
- 响应式原理不同,vue 使用双向绑定来实现数据更新,react 则通过单向数据流来实现
相同:
- 组件化开发:Vue 和 React 都采用了组件化开发的方式,将用户界面划分为独立、可复用的组件,从而使代码更加模块化、可维护和可扩展。
- 虚拟 DOM:Vue 和 React 都使用虚拟 DOM 技术,通过在 JavaScript 和真实 DOM 之间建立一个轻量级的虚拟 DOM 层,实现高效的 DOM 更新和渲染。
- 响应式更新:Vue 和 React 都支持响应式更新,即当数据发生变化时,会自动更新相关的组件和视图,以保持用户界面的同步性。
问题 3:vue2 和 vue3 的区别
响应式系统:
- Vue 2 通过遍历对象的每个属性,使用
Object.defineProperty来实现其响应式系统。这种方法有一些限制,例如无法检测属性的添加或删除,以及无法直接处理数组索引和长度的变化。 - Vue 3 则采用了基于 ES6 Proxy 的响应式系统,这允许 Vue 拦截对象的任何属性的读取和写入操作,提供更强大和灵活的响应式能力。这也使得 Vue 3 能够检测属性的添加和删除,以及更有效地处理数组更新。
- Vue 2 通过遍历对象的每个属性,使用
组合式 API:
- Vue 2 主要通过选项 API(如
data,methods,computed等)进行组件的逻辑组织。 - Vue 3 引入了组合式 API(如
ref,reactive,computed,watch),这为逻辑复用和代码组织提供了更灵活的方式。
- Vue 2 主要通过选项 API(如
性能相关:
- Vue 3 在性能方面有显著提升。它包括更小的打包大小、更快的虚拟 DOM 重写、更高效的组件初始化等。
- Vue 2 相比之下在性能方面相对较慢,尤其是在处理大型应用和复杂组件时。
TypeScript 支持:
- Vue 3 从一开始就以 TypeScript 编写,提供更好的 TypeScript 支持。
- Vue 2 对 TypeScript 的支持是有限的,通常需要额外的配置和工具来实现更好的集成。
新特性和改进:
- Vue 3 引入了多个新特性,如 Teleport、Fragment、Suspense 等,为开发提供了更多的可能性和便利。
Fragment:
- Vue 3 允许多个根节点(Fragment),这使得组件模板可以有多个并列的根元素。
- Vue 2 要求每个组件必须有一个单独的根节点。
生命周期变化:
- Vue 2 提供了一系列的生命周期钩子,如
created,mounted,updated,destroyed等。 - Vue 3 对这些生命周期钩子进行了重命名和调整,以更好地与 Composition API 配合。例如,
beforeDestroy和destroyed分别更名为beforeUnmount和unmounted。此外,Vue 3 引入了新的生命周期钩子,如onMounted,onUpdated,onUnmounted等,用于组合式 API。
- Vue 2 提供了一系列的生命周期钩子,如
打包体积优化:
- Vue 2 的打包体积相对较大,尤其是在包含了全框架的所有特性时。
- Vue 3 进行了大量的打包体积优化。它采用了更有效的树摇(Tree - shaking)机制,允许去除未使用的代码部分。这意味着如果你只使用 Vue 的一部分功能,最终打包出来的文件会更小。
模版编译优化:
虚拟节点静态标记(Patch Flag):更新类型标记,去标注要变化什么
- Vue 2 在更新组件时,会进行相对全面的虚拟 DOM 比较,这可能会导致性能开销。
- Vue 3 引入了 Patch Flag,这是一种优化技术,它在编译时标记虚拟节点的动态部分。这样在组件更新时,Vue 只需要关注这些被标记的部分,而不是整个组件树,从而显著提升了性能。
对于单个有动态绑定的元素来说,我们可以在编译时推断出大量信息:
html<!-- 仅含 class 绑定 --> <div :class="{ active }"></div> <!-- 仅含 id 和 value 绑定 --> <input :id="id" :value="value"> <!-- 仅含文本子节点 --> <div>{{ dynamic }}</div>在为这些元素生成渲染函数时,Vue 在 vnode 创建调用中直接编码了每个元素所需的更新类型:
jscreateElementVNode("div", { class: _normalizeClass({ active: _ctx.active }) }, null, 2 /* CLASS */)最后这个参数
2就是一个更新类型标记 (patch flag)。一个元素可以有多个更新类型标记,会被合并成一个数字。运行时渲染器也将会使用位运算来检查这些标记,确定相应的更新操作:jsif (vnode.patchFlag & PatchFlags.CLASS /* 2 */) { // 更新节点的 CSS class }内容在官网中有详细介绍:https://cn.vuejs.org/guide/extras/rendering-mechanism.html#patch-flags
缓存静态内容:
html<div> <!-- 需缓存 --> <div>foo</div> <!-- 需缓存 --> <div>bar</div> <div>{{ dynamic }}</div> </div>foo和bar这两个 div 是完全静态的,会完全跳过对它们的差异比对。- 预字符串化:当有足够多连续的静态元素时,它们还会再被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。
会把静态内容提前转换成字符串。
html<div> <div class="foo">foo</div> <div class="foo">foo</div> <div class="foo">foo</div> <div class="foo">foo</div> <div class="foo">foo</div> <div>{{ dynamic }}</div> </div>jsimport { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from 'vue' export function render(_ctx, _cache, $props, $setup, $data, $options) { return ( _openBlock(), _createElementBlock('div', null, [ _cache[0] || (_cache[0] = _createStaticVNode( '<div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div>', 5 )), _createElementVNode( 'div', null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */ ) ]) ) }
问题 4:vue 生命周期
vue2 的生命周期
- 创建阶段(Creation):
beforeCreate:在实例初始化之后,数据观测之前调用。此时实例尚未初始化完成,数据和事件等都未准备好。created:在实例创建完成后被立即调用。此时实例已经完成初始化,但 DOM 元素尚未挂载,适合进行数据初始化和异步操作。
- 挂载阶段(Mounting):
beforeMount:在挂载开始之前被调用,此时虚拟 DOM 已经创建,但尚未渲染到实际 DOM 中。mounted:在实例挂载到 DOM 后被调用。在这一阶段,实例已经成功挂载到 DOM 中,可以执行 DOM 操作和访问 DOM 元素。
- 更新阶段(Updating):
beforeUpdate:在数据更新之前被调用。此时数据变化会触发重新渲染,但尚未更新到 DOM。updated:在数据更新之后被调用。此时数据已经更新到 DOM,适合执行 DOM 依赖的操作。
- 销毁阶段(Destruction):
beforeDestroy:在实例销毁之前被调用。可以用于清理定时器、取消订阅、解绑事件等清理操作。destroyed:在实例销毁后被调用。在这一阶段,实例和所有相关的事件监听器和观察者都已经被销毁。
vue3 的生命周期
- 设置阶段(Setup):
setup:在 Vue 3 中,大部分的配置和逻辑都应该在setup函数中处理。这个函数用于返回组件的状态和行为。在setup函数中,可以设置响应式数据、计算属性、方法,以及处理 props 等。
- 创建阶段(Creation):
beforeCreate:在实例初始化之后,数据观测之前调用。可以用于执行一些初始化操作,但在这个阶段,setup函数中的响应式数据和计算属性尚未准备好。created:在实例创建完成后被立即调用。在这个阶段,setup函数中的响应式数据和计算属性已经准备好。
- 挂载阶段(Mounting):
beforeMount:在挂载开始之前被调用,与 Vue 2 中的beforeMount类似。onBeforeMount:Vue 3 中的新增生命周期钩子,也是在挂载前被调用。mounted:在实例挂载到 DOM 后被调用。与 Vue 2 中的mounted类似。onMounted:Vue 3 中的新增生命周期钩子,也是在挂载后被调用。
- 更新阶段(Updating):
beforeUpdate:在数据更新之前被调用,与 Vue 2 中的beforeUpdate类似。onBeforeUpdate:Vue 3 中的新增生命周期钩子,也是在更新前被调用。updated:在数据更新之后被调用,与 Vue 2 中的updated类似。onUpdated:Vue 3 中的新增生命周期钩子,也是在更新后被调用。
- 卸载阶段(Unmounting):
beforeUnmount:在卸载之前被调用,用于清理资源,与 Vue 2 中的beforeDestroy类似。onBeforeUnmount:Vue 3 中的新增生命周期钩子,也是在卸载前被调用。unmounted:在卸载后被调用,与 Vue 2 中的destroyed类似。onUnmounted:Vue 3 中的新增生命周期钩子,也是在卸载后被调用。
created 和 mounted 这两个生命周期的区别
- created 生命周期钩子:
created生命周期钩子在组件实例被创建之后立即被调用。- 在这个阶段,组件实例已经被创建,但它的模板还没有渲染到 DOM 中。可以在这个阶段执行一些与数据初始化和逻辑处理相关的任务,但无法访问到已经渲染的 DOM 元素。
- 通常用于进行数据的初始化、设置初始状态、进行异步请求(例如获取数据),以及数据准备好后执行逻辑。
- mounted 生命周期钩子:
mounted生命周期钩子在组件的模板已经渲染到 DOM 中后触发。- 在这个阶段,您可以访问和操作已经渲染的 DOM 元素。这通常用于执行需要访问 DOM 的任务,例如操作 DOM 元素、添加事件监听器、或执行与 DOM 相关的操作。
- 通常用于执行需要等待 DOM 渲染完成后才能执行的任务,以确保可以操作已经存在的 DOM 元素。
Vue 的父组件和子组件生命周期钩子函数执行顺序?
- 加载渲染过程:父
beforeCreate-> 父created-> 父beforeMount-> 子beforeCreate-> 子created-> 子beforeMount-> 子mounted-> 父mounted - 子组件更新过程:父
beforeUpdate-> 子beforeUpdate-> 子updated-> 父updated - 父组件更新过程:父
beforeUpdate-> 父updated - 销毁过程:父
beforeDestroy-> 子beforeDestroy-> 子destroyed-> 父destroyed
问题 5:Vue 组件通信方式
- 父传子
- props
- $children
- $refs
- 子传父
- $emit
- $parent
- 兄弟组件
- provied
- inject
- eventBus
- Vuex
问题 6:vue 指令和常见的修饰符有哪些?
指令:
v-if:条件渲染指令,根据表达式的真假来决定是否渲染元素。v-show:条件显示指令,根据表达式的真假来决定元素的显示和隐藏。v-for:列表渲染指令,用于根据数据源循环渲染元素列表。v-bind:属性绑定指令,用于动态绑定元素属性到 Vue 实例的数据。v-on:事件绑定指令,用于监听 DOM 事件,并执行对应的 Vue 方法。v-model:双向数据绑定指令,用于在表单元素和 Vue 实例的数据之间建立双向绑定关系。v-text:文本插值指令,用于将数据插入到元素的文本内容中。v-html:HTML 插值指令,用于将数据作为 HTML 解析并插入到元素中。
事件修饰符:
.stop:阻止冒泡.prevent:阻止默认事件.capture:与事件冒泡的方向相反,事件捕获由外到内;.self:只会触发自己范围内的事件,不包含子元素;.once:只会触发一次。
表单修饰符:
.lazy:在默认情况下,v - model在每次input事件触发后将输入框的值与数据进行同步,可以添加lazy修饰符.number:如果想自动将用户的输入值转为数值类型,可以给v - model添加number修饰符.trim:如果要自动过滤用户输入的首尾空白字符,可以给v - model添加trim修饰符
问题 7:v-html 的原理
会先移除节点下的所有节点,调用 html 方法,通过添加innerHTML属性,归根结底还是设置 innerHTML 为 v-html 的值。
问题 8:v-model 是如何实现的,语法糖实际是什么?
Vue 中数据双向绑定是一个指令 v-model,可以绑定一个响应式数据到视图,同时视图的变化能改变该值。
- 当作用在表单上:通过
v-bind:value绑定数据,v-on:input来监听数据变化并修改value - 当作用在组件上:本质上是一个父子通信语法糖,通过
props和$emit实现。
问题 9:data 为什么是一个函数而不是对象
因为对象是一个引用类型,如果 data 是一个对象的情况下会造成多个组件共用一个 data,data 为一个函数,每个组件都会有自己的私有数据空间,不会干扰其他组件的运行。
问题 10:Vue 中 key 的作用
key 的作用主要是为了高效的更新虚拟 DOM,其原理是 vue 在 patch 过程中通过 key 可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,减少 DOM 操作量,提高性能。
为什么不建议用 index 作为 key?
如果将数组下标作为 key 值,那么当列表发生变化时,可能会导致 key 值发生改变,从而引发不必要的组件重新渲染,甚至会导致性能问题。例如,当删除列表中某个元素时,其后面的所有元素的下标都会发生改变,导致 Vue 重新渲染整个列表。
问题 11:路由的 hash 和 history 模式的区别
hash 模式:开发中默认的模式,地址栏 URL 后携带#,后面为路由。#号后面的内容不会发送到服务器。
原理是通过 onhashchange()事件监听 hash 值变化,在页面 hash 值发生变化后,window 就可以监听到事件改变,并按照规则加载相应的代码。hash 值变化对应的 URL 都会被记录下来,这样就能实现浏览器历史页面前进后退。
history 模式:history 模式中 URL 没有#,这样相对 hash 模式更好看,但是需要后台配置支持。
history 原理是使用 HTML5 history 提供的 pushState、replaceState 两个 API,用于浏览器记录历史浏览栈,并且在修改 URL 时不会触发页面刷新和后台数据请求。
问题 12:watch 和 computed 有什么区别?
computed:
- 计算属性:
computed是用于创建计算属性的方式,它依赖于 Vue 的响应式系统来进行数据追踪。当依赖的数据发生变化时,计算属性会自动重新计算,而且只在必要时才重新计算。 - 缓存:计算属性具有缓存机制,只有在它依赖的数据发生变化时,计算属性才会重新计算。这意味着多次访问同一个计算属性会返回相同的结果,而不会重复计算。
- 无副作用:计算属性应当是无副作用的,它们只是基于数据的计算,并不会修改数据本身。
- 用于模板中:计算属性通常用于模板中,以便在模板中显示派生数据。
- 必须同步:只对同步代码中的依赖响应。
watch:
- 侦听数据:
watch用于监视数据的变化,你可以监视一个或多个数据的变化,以执行自定义的响应操作。 - 副作用操作:
watch中的回调函数可以执行副作用操作,例如发送网络请求、手动操作 DOM,或执行其他需要的逻辑。 - 不缓存:
watch中的回调函数会在依赖数据变化时立即被触发,不会像computed那样具有缓存机制。 - 用于监听数据变化:
watch通常用于监听数据的变化,而不是用于在模板中显示数据。 - 支持异步:在检测数据变化后,可进行同步或异步操作。
问题 13:谈谈 computed 的机制,缓存了什么?
- Vue.js 中的
computed属性确实具有缓存机制,这个缓存机制实际上是指对计算属性的值进行了缓存。当你在模板中多次访问同一个计算属性时,Vue.js 只会计算一次这个属性的值,然后将结果缓存起来,以后再次访问时会直接返回缓存的结果,而不会重新计算。 - 假设你有一个计算属性
fullName,它依赖于firstName和lastName两个响应式数据。当你在模板中使用fullName来显示全名时,Vue.js 会自动建立依赖关系,并在firstName或lastName发生变化时,自动更新fullName的值,然后将新的值渲染到页面上。
问题 14:为什么 computed 不支持异步?
这个是 vue 设计层面决定的,computed 的定义是,“依赖值改变 computed 值就会改变”,所以这里必须是同步的,否则就可能“依赖值改变但 computed 值未改变了”,一旦 computed 支持异步,computed 就违背定义了,会变得不稳定。相反,watch 的定义是,“监控的数据改变后,它做某件事”,那 watch 在监听变化后,做同步异步都可以,并不违背定义。
js
// 有效
computed: {
async value() {
return this.a + this.b; // 有效
}
},
// 无效
computed: {
async value() { // 外部接住promise
const res = await new Promise((resolve) => {
setTimeout(() => {
resolve(this.a + this.b);
});
});
console.log(res); // 输出3
return res;
}
}问题 15:以下两段代码在 vue 中分别渲染多少次? 为什么?
代码 1:
html
<template>
<div>{{rCount}}</div>
</template>
<script setup>
import { ref } from 'vue'
const count = 0
const rCount = ref(count)
for (let i = 0; i < 5; ++i) {
rCount.value = i
}
</script>代码 2:
html
<template>
<div>{{rCount}}</div>
</template>
<script setup>
import { ref } from 'vue'
const count = 0
const rCount = ref(count)
for (let i = 0; i < 5; ++i) {
setTimeout(() => {
rCount.value = i
}, 0)
}
</script>答案:代码 1 1 次,代码 2 5 次。
解释:当数据发生变化,会被 Object.defineProperty(vue2) 或 new Proxy(vue3) 监听到,监听到之后会把调用渲染函数,但渲染函数不是立即执行,而是会放到一个微任务队列中 Promise.reslove().then() (vue3) 或 nextTick (vue2) ,等待当前所有同步代码执行完成后,会调用微任务,一次过更新内容。
问题 16:vue3 中 ref 和 reactive 的区别
ref生成响应式对象,一般用于基础类型reactive代理整个对象,一般用于引用类型
问题 17:vue3 区分 ref 和 reactive 的原因
- 模板解包:基础数据类型(如数字、字符串、布尔值)不是对象,因此无法直接被
Proxy拦截。Proxy可以拦截对象级别的操作,如属性访问、赋值、枚举等。使用ref创建的响应式引用在 Vue 模板中被自动解包。这意味着当你在模板中使用ref创建的变量时,可以直接使用而不需要每次都通过.value访问。如果使用Proxy来处理基础类型,这种自动解包可能就无法实现,从而增加了模板中的代码复杂性。
“模版解包”:在模板里使用 ref 时,会自动进行解包操作,也就是可以直接使用 ref 对象的值,无需额外调用 .value。但是在编译的时候会自动添加上
.value。
reactive 相比 ref 更难实现模板解包的原因:
- ref 是一个包装对象,它只有一个 .value 属性来存储实际的值。这种简单的结构使得在模板解包时,Vue 可以很容易地识别并直接访问 .value 属性,模板中进行解包时不存在歧义。
- reactive 对象,由于其包含多个属性,在模板中使用时可能会产生歧义,Vue 无法确定应该显示哪个属性的值
API 原因:
proxy的参数target可以是任何类型的对象,包括数组、函数,但不能是简单数据类型。为了区分基本类型(如字符串、数字、布尔值)也能享受响应式系统的好处。在 JavaScript 中,基本类型不能直接被代理,因此需要通过一个对象来包裹。通过将值放到.value中进行代理。API 可读性:Vue 3 提供了
ref和reactive两种方式来创建响应式数据,旨在提供一个统一和一致的 API。ref主要用于基础数据类型和单个值,而reactive用于对象和数组。这种区分使得 Vue 3 的响应式系统在概念上更容易理解和使用。
问题 18:vue2 删除数组用 delete 和 Vue.delete 有什么区别?
delete:
delete是 JavaScript 的原生操作符,用于删除对象的属性。当你使用delete删除数组的元素时,元素确实会被删除,但数组的长度不会改变,被删除的元素将变成undefined。delete操作不会触发 Vue 的响应系统,因此不会引起视图的更新。
javascript
const arr = [1, 2, 3]
delete arr[1] // 删除元素2
// 现在 arr 变成 [1, empty, 3]Vue.delete:
Vue.delete是 Vue 2 提供的用于在响应式数组中删除元素的方法。它会将数组的长度缩短,并触发 Vue 的响应系统,确保视图与数据同步。- 使用
Vue.delete来删除数组元素,Vue 会正确追踪更改,并在视图中删除相应的元素。
javascript
const arr = [1, 2, 3]
Vue.delete(arr, 1) // 删除元素2
// 现在 arr 变成 [1, 3]问题 19:vue2、vue3 中 v-if 和 v-for 的优先级
- 在 Vue2 中,
v-for的优先级高于v-if。
这种优先级顺序可能会导致性能问题,因为即使某些元素在 v-if 判断后不会被渲染,v-for 仍会对其进行循环迭代。
- 在 Vue3 中,
v-if的优先级高于v-for。当它们同时出现在同一个元素上时,v-if 会先进行条件判断,若条件不满足,v-for 不会执行。
问题 20:讲讲项目中组件的封装是如何做的?
- 封装的目的:遵循单一职责、可复用性
- 分析组件的功能需求:例如,一个按钮组件,需要实现点击、禁用、加载中、成功、失败等状态。
- 组件的结构和样式
- 实现组件的逻辑和数据处理