1. v-html 会导致什么问题
v-html
的内容会作为普通的 HTML 插入。但在网站上动态渲染任意 HTML 是不安全的,容易导致 XSS 攻击,所以只能在可信内容上使用该指令。
2. 响应式 API:ref 和 reactive
reactive
方法仅对对象类型(对象、数组、Map、Set等)有效,而对基本类型无效。这是因为 reactive 内部的 Proxy 不适用基本类型。而ref
可以对任何类型值做响应式处理(当值为对象类型时,会用reactive自动转换它的value属性)。- 数据访问方式:访问 ref 数据时需要以
.value
属性的形式(模板中会解包),reactive 数据直接访问即可。 - 响应性丢失:当响应式对象的属性赋值给其他变量或解构时,ref 可以不让它失去响应性;而 reactive 则会丢失响应性。
let state = reactive({ count: 0 });
let n = state.count; // 赋值,此时 n 不具有响应性
let { count } = state; // 解构,此时 count 不具有响应性
const obj = { // 此时 ref 用于普通对象
foo: ref(1),
bar: ref(2)
}
const { foo, bar } = obj;
日常开发时全用 ref!!ref 的 value 属性是响应式的,reactive 返回的 proxy 对象是响应式的。
3. shallowReactive、shallowRef、toRefs 函数
Vue 中的状态都是默认深层响应式的。如果想要对象仅在顶层具有响应性,则:
shallowReactive
不会对对象进行深层响应式转换,只有根级别的属性才是响应式的。shallowRef
不会对内部值进行深层响应式转换,只有对.value
的访问是响应式的。shallowReadonly
的对象中只有根级别的属性变成了只读的。
toRefs():toRefs
会将一个响应式对象转换为一个普通对象,并且这个普通对象的每个属性都是源对象中属性的 ref,每个 ref 都是使用toRef
创建的。
应用场景:toRefs
解决了响应式对象在解构/展开时的响应性丢失问题。
4. readonly 和 const 的区别
readonly
返回一个对象的只读 Proxy,且内部的属性也都是只读的。
- const 可以用于基本类型和引用类型。当作用于引用类型时,属性可以改变,对象引用不变即可。
- readonly 用于对象属性,不能给属性重新赋值。
5. watch 和 watchEffect 的区别
监听数据并在数据变化时执行回调函数,通常是一些副作用,如更改 DOM、发送网络请求等。
watch
默认是懒监听的,只在数据变化时才执行回调;而watchEffect
初始化时就会执行一次(watch 传参 immediate 也可实现)。watch
可以添加需要监听的数据,因此能更加明确是由哪个状态触发的回调;而watchEffect
是在执行回调时自动分析出的监听数据源。watch
可以访问所监听数据的旧值和新值,而watchEffect
访问不到旧值。
- watch 的参数:
- 第一个参数是监听的数据源;
- 第二个参数是回调函数,接收新值、旧值和清理副作用的回调;
- 第三个参数支持 immediate:true、deep:true 等选项。
- watchEffect 的参数:
- 第一个参数是回调函数;
- 第二个参数可以改变回调的触发时机等。
通常无脑 watch。
6. watch 和 computed 的区别
watch
是侦听属性,每当监听的数据变化时都会执行回调。
computed
是计算属性,它接收一个 getter 函数然后默认返回一个只读ref
。另外,计算属性会自动追踪其响应式依赖(遇到非响应式的依赖不会重新计算)。
- 缓存性:
computed
会返回一个只读的ref,只有当它的响应式依赖变化时才会重新计算;watch
没有缓存性(或者说没有这种说法)。 - 设计思想/应用场景:
computed
适合“一个数据依赖于其他数据、将多个state合成一个新的state”的情况,比如计算逻辑;watch
适合“当数据变化时需要做一些事情”,比如数据请求、更新DOM等业务逻辑。(管道思想和观察者思路) - 支持异步:
computed
不支持异步;watch
支持。
7. 响应式原理
(1)Vue2 的响应式原理:
Vue2 通过Object.defineProperty
来劫持监听对象的所有属性,当读取属性时会触发getter
进行依赖收集,修改属性时会触发setter
通知相关依赖进行更新操作。
(2)Vue3 的响应式原理:
Vue3 使用Proxy
代理目标对象。具体来说,通过reactive()
来创建响应式数据,reactive 的返回值是一个 Proxy 对象。当访问 Proxy 对象时会触发get
方法调用track()
来收集依赖;当修改 Proxy 对象时会触发set
方法调用trigger()
来执行相应的依赖。
为什么需要 Reflect?
- Reflect 和 Proxy 的方法是一一对应的。这可以让
Proxy
对象搭配Reflect
负责完成对目标对象的原生行为。 - 修改
Proxy/Object
对象某些方法的返回值,以满足要求。
(3)Proxy 和 Object.defineProperty 对比
- Proxy 是代理整个对象,而 O.d 只能代理某个属性;
- O.d 监听不到对象上的新增属性、删除属性行为;
- O.d 监听不到数组的变化,只能通过劫持重写几个数组方法(性能代价,length 属性不能被删除、修改);
- O.d 不支持 Map、Set 这些新增的数据结构;
- Proxy 有13种拦截方法,如 has、ownKeys等;
- Proxy 兼容性较差,IE 不支持。
8. v-model 双向绑定原理
双向绑定:响应式数据到视图,同时视图的变化能改变该数据。
v-model 可以在组件上使用以实现双向绑定,本质是一个语法糖。比如:
- 将内部原生
<input>
元素的value
属性绑定到modelValue
属性上。 - 当监听到原生的
input
事件触发时,会去触发自定义事件update:modelValue
。
<CustomInput
:modelValue="searchText"
@update:modelValue="newVal => searchText = newVal"
/>
9. v-if 和 v-show 的区别
v-if
会真实的销毁、重建 dom,而v-show
只是改变display
属性值(visibility 和 none)。- 初始渲染条件为 false 时,v-show 会将元素先渲染出来,v-if 不会。
- 应用场景:由于 v-if 有更高的切换开销,v-show 有更高的初始渲染开销。所以当判断条件会频繁改变(tab页签)时推荐使用 v-show;条件很少改变(根据用户权限显示不同内容)推荐使用 v-if。
- v-show 不支持在
<template>
元素上使用,也不能和v-else
搭配使用。
10. v-for 列表渲染
(1) v-for 为什么要设置 key?
Vue 中当列表项元素的顺序改变时,DOM 元素并不会跟着移动,而是默认就地更新
每个元素,让它们在原来的索引上渲染。但不能很好的复用 dom、不利于性能优化,因此需要设置 key 来唯一标识元素以最小化更新 dom、高效 diff。
(2)key 值设置有什么考量?index 可以吗?
key 值的设置需要确保 “如果数组中该元素没有发生变化,那么它的 key 值也应该不变” 。期望绑定key 值唯一且是基础类型,一般设置item.id
。
不能使用 index,当插入或删除某元素时会导致后面元素的 key 值发生变化,进而导致重新渲染。
(3)v-for 是按什么顺序遍历对象的?如何保证顺序?
- 如果有
iterator
接口就执行next
方法;对象的话就调用Object.keys()
。 - 不同浏览器不能保证遍历顺序一致,想保证顺序可以将对象放在数组中。
就地更新?:不会移动DOM元素的顺序,而是就地更新内容。比如在列表中新增一个元素,按照就地更新策略会在列表最后创建一个DOM元素,然后新增元素和它后面的元素的值都会更新。
11. v-if 和 v-for
不推荐两者同时使用,v-if
优先级高于v-for
、会先被执行(vue2中相反),所以 v-if 无法访问到 v-for 作用域内定义的变量。
如果需要同时出现,那可以在 v-if 的元素外层加一个 v-for 指令的 template 标签
。
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
12. vue3 的全局API:nextTick()
响应式状态改变时,DOM 更新并不会同步生效,而是缓存到队列中一起执行。nextTick()
是将回调函数推迟到下次 dom 更新结束
后执行。
(1)原理
vue3 的 nextTick 利用了事件循环机制,它将回调函数放到 promise的then中来当作一个微任务执行,可以拿到最新的 DOM。
补充:微任务 => DOM 渲染 => 宏任务。虽然看起来与 nextTick 矛盾,但实际上 nextTick 确实可以拿到最新的 dom,这是因为此时浏览器已经计算好了 dom,只是还没更新渲染。
(2)应用场景
-
在
created
中的 dom 操作一定要放在nextTick
回调里,因为此时 dom 还未更新。而在mounted
中操作 dom 就没有问题。 -
如果在数据变化后要执行某个操作,但该操作又依赖于更新后的 dom 元素,那这个操作应该放在
nextTick
的回调函数中。
13. 内置组件:keep-alive
当在多个组件之间动态切换时,<KeepAlive>
负责缓存被移除的组件实例、保留它们被切换时的状态,避免重新渲染导致性能降低。
-
提供 include 和 exclude 属性:两者都接受字符串或正则做参数。include 表示只有名称匹配的组件才会缓存,exclude 表示任何名称匹配的组件都不会被缓存,后者优先级高。
-
keep-alive 的组件会多出两个生命周期
onActivated
和onDeactivated
:当一个组件实例从DOM上移除(不会销毁)但被keep-alive
缓存时会触发 onDeactivated;当被缓存的组件重新插入DOM中时会触发 onActivated。这两个钩子适用于缓存的组件以及它们的后代组件。 -
可以通过 max 属性指定可缓存的最大组件实例数,之后更新时采用LRU(最近最少使用)算法,淘汰最近最久未使用的组件。
<KeepAlive>
<component :is="activeComponent"> // 动态组件由<component>元素和 is 属性实现
</KeepAlive>
14. vue3 中常见的生命周期
生命周期 | 描述 |
---|---|
onBeforeMount | 组件挂载之前执行,Vue开始解析模板,调用 render() 生成了 vDOM |
onMounted | 组件挂载完成后执行,通常用于执行一些副作用,如发送请求、获取数据、和DOM相关的其他副作用等。 |
onBeforeUpdate | 组件更新DOM之前调用。适合在更新之前访问现有的 DOM |
onUpdated | 组件更新DOM后调用。父组件的更新钩子会在子组件的更新钩子之后调用 |
onBeforeUnmount | 组件卸载之前调用 |
onUnmounted | 组件卸载之后调用,可以手动清理一些副作用,如计时器、DOM事件监听器 |
onActivated | keep-alive 缓存的组件激活时调用 |
onDeactivated | keep-alive 缓存的组件停用时调用 |
onErrorCaptured | 在捕获到后代组件传递的错误时执行,可以阻止错误继续向上传递 |
beforeCreate
中的 data 和 methods 还未初始化;created
中的 data 和 methods 已经完成初始化,但是模板还没有编译,也就是说还不能获取 dom。- 生命周期的实现是利用发布订阅模式。
(1)常用哪些生命周期做哪些事?
onMounted\~onBeforeUnmounted
:都可以获取 dom 元素。created
也可以执行一些发送请求等副作用,但不如mounted
好。因为 created 中 dom 还没渲染,此时发送 Ajax 请求可能会导致 JS 和 GUI 线程矛盾进行出现渲染中断的问题。- 但在SSR中时应该在
created
中发送请求,因为 SSR 不支持 beforeMount、mounted。
(2)组件的生命周期调用顺序?
- 加载渲染过程:
父onBeforeMount->子onBeforeMount->子onMounted->父onMounted
。 - 子组件更新过程:
- 影响父组件:
父onBeforeUpdate->子onBeforeUpdate->子onUpdated->父onUpdated
; - 不影响父组件(通常):
子onBeforeUpdate->子onUpdated
。
- 影响父组件:
- 父组件更新过程:
- 影响子组件(通常):
父onBeforeUpdate->子onBeforeUpdate->子onUpdated->父onUpdated
; - 不影响子组件:
父onBeforeUpdate->父onUpdated
。
- 影响子组件(通常):
- 销毁过程:
父onBeforeUnmount->子onBeforeUnmount->子onUnmounted->父onUnmounted
。
15. setup 函数
setup 是组件中使用组合式 API 的入口。
setup 中没有选项式 API 的 beforeCreate、created
,这是因为 setup 就是组件实例创建的过程,在作用上取代了这两个钩子,所以这两个钩子中的代码都可以直接写在 setup 函数中。
16. 模板引用:ref 属性
ref 可以获取对 DOM 元素或子组件实例的引用。只可以在组件挂载后才能访问引用信息。
- ref 属性可以直接访问底层 DOM 元素。示例
- ref 也可以用于子组件上,这时引用获得的值是子组件实例。引用信息将会被注册在父组件的
$refs
对象上,此时可以用于父子组件通信。
17. Vue 组件间通信方式
(1)父子组件通信:props、emit/on、ref
-
props/emit:父组件通过
props
向子组件传递数据,子组件通过$emit
触发事件向父组件传递数据(父组件v-on
监听该事件)。 -
ref 属性:当 ref 用于子组件时,引用会指向子组件实例,因此可以用于父组件向子组件传值。
<template>
<child ref="msg"></child>
</template>
<script>
this.$refs.msg.getMessage('我是子组件');
</script>
子组件可以修改父组件的 props 吗?
组件化开发遵循单向数据流原则,不建议修改父组件的 props,会导致数据流向难以理解、不容易维护等问题。
(2)兄弟组件通信:vuex、eventBus
- 状态管理工具 Vuex/Pinia,支持任何通信方式。
- 事件总线:mitt,支持任何通信方式。
- 初始化一个 Vue 实例作为事件总线
Vue.prototype.$bus = new Vue
。 - vue3 中移除了事件总线,但可以借助第三方库
mitt
来完成,适用于全局通信。 - 大多数情况下不推荐使用全局事件总线的方式来实现组件通信,后期维护会比较麻烦。
- 初始化一个 Vue 实例作为事件总线
(3)跨层级组件通信:vuex、eventBus、provide/inject
provide/inject
依赖注入:祖先组件通过provide
向后代组件注入值,后代组件通过inject
接收祖先组件提供的值。
18. 透传 Attribute
传递给一个组件,但没有被组件声明为props
或emits
的 attribute 或事件监听函数。常见的class、style、id
。
- 当组件中只有一个根元素时,透传的 attribute 会自动添加到该根元素上;
- 当组件中有多个根元素时,则不会自动透传 attribute,需要显式绑定
$attrs
的节点; - 如果子组件的根元素已经有了
class
或style
,那么它会和透传的 attribute 进行合并。
19. 插槽
插槽允许让父组件向子组件传递一些模板片段,让子组件进行渲染。
插槽的作用域:插槽内容可以访问到父组件的数据,但无法访问子组件数据。
- 默认插槽:当父组件没有提供内容时,我们可以为子组件的插槽指定默认内容。
- 具名插槽:当子组件需要多个slot时,可以通过
name
属性来区分插槽,其中没有指定name
的插槽为default
,并且父组件中需要使用<template>
元素为具名插槽传入内容。 - 作用域插槽:子组件可以在
<slot>
中绑定 props 来将数据传给父组件,让父组件中的插槽内容能够访问到子组件的数据。- 默认作用域插槽
- 具名作用域插槽
20. 组合式 API 和 React Hooks
两者都是为了更好的逻辑组合与复用!
- 生命周期:hooks弱化了生命周期的概念,推荐使用函数组件;composition api 继续沿用生命周期;
- 数据的可变性:hooks 中数据是
immutable
的;VCA 的数据则是mutable
的,因为 vue 的响应式是基于对象属性的; - 调用顺序:hooks 必须保证每次调用的顺序一致,否则会出现数据错乱;VCA 不存在这些限制。
- (不用每次渲染时重复调用):组件每次渲染时 Hooks 都会重新执行一遍,而 VCA 只调用 setup 的代码一次,之后只更新变化的部分。
21. 虚拟 DOM 和 key 属性
- 什么是虚拟dom?
- 虚拟dom的优缺点?
- 怎么实现虚拟dom?
- key 的作用?
- 虚拟dom映射到真实dom:经历虚拟dom的create、diff、patch等阶段。
22. Vue3 的 快速 diff 算法
- 预处理:先把可以直接排除的数据去掉,降低 diff 的操作量。
- 对齐头部和尾部的节点:分别从头部向后、从尾部向前遍历新旧节点,直到节点的 key 值不同时跳出循环;
- 针对仅有节点新增或被删除的情况:如果旧列表已经全部 patch,新列表还没有 patch 完,则会创建新节点,反之会卸载旧节点。
- 在预处理后我们通常会得到两个没有 patch 完的序列:
- 建立新节点 key 和下标的映射:遍历没有 patch 过的新列表,通过 map 保存 key 值和在新节点列表中的下标
{key: index}
; - 处理删除的操作:遍历旧列表,通过之前的 map 判断旧节点是否还存在于新节点中,不存在则要移除;
- 处理移动/新增的操作:通过
getSequence
函数基于贪心和二分查找算法得到最长递增子序列,然后根据最长递增子序列、对剩下的元素(子序列之外的元素)进行移动操作。最长递增子序列对应的是最少的移动次数,有利于提高性能。
23. Vue3 与 Vue2 的区别
(1)组合式 API
vue2 使用的是选项式 API,vue3 使用组合式 API 的写法。
- 组合式 API 直接将数据定义在函数内,能够更好的组织和复用逻辑,之后重构时也较容易。
- 选项式 API 的代码位置比较明确,数据放在 data 里,方法放在 methods 里,易于学习和使用。
(2)生命周期
- vue3 的
setup()
代替了 vue2 的created、beforeCreate
。 - 写法上,大部分生命周期的名称 +“on”,例如 mounted 和 onMounted。
- beforeDestory 和 destoryed 改为 onBeforeUnmount、onUnmounted。
(3)多根节点/支持 Fragments
vue2 在模板中只能有一个根节点;但 vue3 支持片段(Fragments)
,即组件可以拥有多个根节点。
(4)响应式原理
(5)diff 算法优化
1. Vue2 的双端 diff 算法
-
首先对比新旧节点判断
tag
。如果不为相同节点,则删除该节点重新创建节点进行替换; -
如果相同节点,则处理它们的子节点(递归比较):
- 旧节点有子节点&新节点没有子节点,直接删除旧节点的子节点;
- 旧节点没有子节点&新节点有子节点,将新节点的子节点添加到旧节点上;
- 新旧节点都没有子节点,判断是否有文本节点,有则直接进行对比;
- 新旧节点都有子节点,进行
双端 diff
算法。
双端 diff 算法就是从新旧子节点列表的头尾节点开始对比,借助 key 值对比两个列表的头头、头尾、尾头和尾尾四种情况来找出可复用节点,然后不断向中间靠拢直到遍历完成。
2. Vue3 快速 diff 的优势
- vue2 是全量 diff;vue3 是静态标记 + 非全量 diff。
- vue3 使用了最长递增子序列减少了 DOM 移动次数,优化了 diff 过程。
(6)v-for 和 v-if 的优先级
vue2 中 v-for 优先级高,vue3 中 v-if 优先级高。
24. Vue 模板编译原理
模板编译目的是将模板转换成 render 函数。 之后 render 函数能生成虚拟 DOM,最后进行渲染。
- 解析模板:将模板转换成 AST 语法树,用 JS 对象的形式描述整个模板;
- 优化 AST 树:编译器会做一些优化工作,例如静态节点提取(对那些非响应式的节点进行标记,之后就可以跳过对它们的 diff),以提高渲染性能;
- 生成render:将优化后的 AST 树转换为可执行的代码,即渲染函数(渲染函数会返回一个虚拟 DOM 节点)。
AST 语法树是源代码语法结构的一种抽象表示。
25. 渲染过程:模板渲染为真实 DOM 节点
-
编译:Vue 模板被编译为 render 函数。24. Vue模板编译原理
-
挂载:render 函数生成后,vue 会调用它来生成虚拟 DOM,并基于虚拟 dom 创建真实 dom。
-
更新:当数据发生变化后,会创建一个更新后的虚拟 DOM 树。然后对比新旧虚拟 DOM 树计算出需要更新的部分,并渲染到真实 DOM 上。
26. template 预编译
Vue 组件只会在实例化的时候模板编译一次,生成 render 函数后就不会再编译了。因此,编译对组件的运行是一种性能消耗。
预编译指的是在项目构建的过程中完成模板编译,这样能让组件在实际运行时跳过模板编译,进而提升性能。
27. Vue 事件绑定原理
- 原生事件绑定是通过
addEventListener
绑定到真实 DOM 元素的; - 组件事件绑定是通过
v-on
指令实现的,通过emit
触发事件。
28. 服务端渲染 SSR
CSR
指的是页面上的内容是我们加载的 js 文件渲染出来的,服务端只返回一个 html 模板。
SSR
指的是页面上的内容是由服务端渲染生成的,服务端直接返回拼接好的 html,然后浏览器直接显示服务端返回的 html 就可以了。
- SSR 优点
- 更快的首屏加载速度:服务端渲染的 HTML 无需等待所有的 js 文件下载完成,可以直接显示,因此首屏渲染速度更快、用户体验更好。
- 更好的 SEO:SPA 页面的内容是通过 Ajax 获取,但爬虫抓取不到通过 Ajax 获取到的内容。
- SSR 缺点
- Vue SSR 只支持
beforeCreate
和created
两个钩子,因此对一些外部扩展库需要进行处理; - 更多的服务端负载:在 Node 中渲染一个完整的应用会占用更多的 CPU 资源。
- Vue SSR 只支持
怎么使用的? 同构就是一套 React/Vue 代码在服务器上运行一遍,到达浏览器后又运行一遍。SSR 负责完成页面结构,CSR 负责绑定事件、实现交互。
29. SSR 和 SSG(预渲染)的区别
SSG 又叫静态站点生成、预渲染,同样也有着更快的首屏加载、更好的 SEO。
- 输出资源:
- SSG 输出的是静态HTML,被服务器托管;
- SSR 输出的是动态HTML,需要随着请求去渲染资源。
- 使用场景:
- SSG 适用于数据是静态的页面,并且在多次部署期间不会改变。例如去优化一些 about/contact 等页面的 SEO、文档站点、博客等;
- SSR 适用于页面数据处理较多、含有动态数据的网站,比如电商网站。
静态资源:编译好的html/css/js文件,图片视频、文件等。
30. Vue 中手动的性能优化
- 编码阶段
- 采用 keep-alive 缓存组件
- 路由懒加载
- 图片懒加载
- 防抖、节流
- v-for 必须保证 key 唯一
- v-if 和 v-show 区分使用场景
- 按需导入第三方模块(babel-plugin-component)
- 长列表时滚动到可视区域再动态加载
- SEO 优化
- 预渲染
- 服务端渲染
- 基础的 Web 技术优化
- 浏览器缓存
- 使用 CDN
- 服务端开启 gzip 压缩
31. Vue 的理解
- 响应式系统:组件状态都是响应式的 JS 对象,当状态改变时视图会自动更新。(Vue 的响应式是基于追踪对象属性的读写的)
- 虚拟dom、diff 后更新:当更改响应式状态后,DOM 会自动更新。但更新并不是同步的,将缓冲到更新周期的“下个时机”。
- 指令系统:v-if、v-for、v-model 等。
参考