Vue复习
基本特性
基本使用
模板(插值、指令)
- 插值、表达式
- 指令、动态属性
v-html
:会有XSS风险、会覆盖子组件
options API写法
js
<template>
<div>
<p>文本插值 {{message}}</p>
<p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>
<p :id="dynamicId">动态属性 id: {{ dynamicId }}</p>
<hr/>
<p v-html="rawHtml">
</p>
</div>
</template>
<script lang="ts">
export default {
data() {
return {
message: 'hello vue',
flag: true,
rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜体</i>',
dynamicId: `id-${Date.now()}`
}
}
}
</script>
composition API写法
js
<template>
<div>
<p>文本插值 {{message}}</p>
<p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>
<p :id="dynamicId">动态属性 id: {{ dynamicId }}</p>
<hr/>
<p v-html="rawHtml">
</p>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const message = ref('hello vue')
const flag = ref(true)
const rawHtml = ref('指令 - 原始 html <b>加粗</b> <i>斜体</i>')
const dynamicId = ref(`id-${Date.now()}`)
</script>
v-html
会覆盖子组件
computed
computed
有缓存data
不变不会重新计算
options API写法
js
<template>
<div>
<p>num {{num}}</p>
<p>double1 {{double1}}</p>
<input v-model="double2"/>
</div>
</template>
<script>
export default {
data() {
return {
num: 20
}
},
computed: {
double1() {
return this.num * 2
},
double2: {
get() {
return this.num * 2
},
set(val) {
this.num = val/2
}
}
}
}
</script>
composition API写法
js
<template>
<div>
<p>num {{num}}</p>
<p>double1 {{double1}}</p>
<input v-model="double2"/>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
const num = ref(20)
const double1 = computed(() => num.value * 2)
const double2 = computed<number>({
get: () => num.value * 2,
set: (value) => {
num.value = value / 2
}
})
</script>
watch
watch
如何深度监听?watch
监听引用类型,拿不到oldValue
options API写法
js
<template>
<div>
<input v-model="name"/>
<input v-model="info.city"/>
</div>
</template>
<script lang="ts">
export default {
data() {
return {
name: 'ljx',
info: {
city: '上海'
}
}
},
watch: {
name(oldVal, val) {
// 值类型,可正常拿到 oldVal 和 val
console.log('watch name', oldVal, val)
},
info: {
handler(oldVal, val) {
// 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
console.log('watch info', oldVal, val)
},
deep: true // 深度监听
}
}
}
</script>
composition API写法
js
<template>
<div>
<input v-model="name"/>
<input v-model="info.city"/>
</div>
</template>
<script lang="ts" setup>
import { watch, ref } from 'vue'
const name = ref('ljx')
const info = ref({
city: '上海'
})
watch(name, (oldVal, val) => {
// 值类型,可正常拿到 oldVal 和 val
console.log('watch name', oldVal, val)
})
watch(info, (oldVal, val) => {
// 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
console.log('watch info', oldVal, val)
}, {
// deep: true
})
</script>
如图:引用类型,获取不到oldVal
如果使用shallowRef代替ref深度监听有用吗?
不能
js
<script lang="ts" setup>
import { watch, shallowRef } from 'vue'
const name = shallowRef('ljx')
const info = shallowRef({
city: '上海'
})
watch(name, (oldVal, val) => {
// 值类型,可正常拿到 oldVal 和 val
console.log('watch name', oldVal, val)
})
watch(info, (oldVal, val) => {
// 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
console.log('watch info', oldVal, val)
}, {
deep: true
})
</script>
class和style
- 使用动态属性
- 使用驼峰式写法
scoped
scoped
是 Vue 中的一个样式作用域修饰符,用于将样式限制在当前组件的作用域内。使用 scoped
可以确保组件中的样式不会影响到其他组件或全局样式,提供了更好的样式隔离和组件复用性。
- 样式隔离:当在组件的
<style>
标签上使用scoped
修饰符时,该组件的样式只会应用于当前组件的 DOM 元素,而不会影响其他组件中的相同类名样式。这样,即使组件名称相同,样式也能保持彼此独立,不会发生冲突。 - 组件复用:通过将样式限制在组件的作用域内,
scoped
可以增加组件的可复用性。当组件被用于不同的场景时,其样式不会被外部样式所污染,可以在不同的上下文中保持一致的样式表现。 - 组件样式优先级:使用
scoped
后,组件内部的样式会优先级比全局样式高,这意味着在组件中可以方便地覆盖和修改全局样式。这对于需要进行样式调整或自定义的组件尤为有用。
如何击穿修改样式?v-deep()
optionsAPI写法
js
<template>
<div :class="myClass" :style="myStyle">
<p>Example Component</p>
<button @click="toggleStyle">toggle</button>
</div>
</template>
<script lang="ts">
export default {
data() {
return {
myClass: 'my-component',
myStyle: {
color: 'red',
fontSize: '20px'
}
};
},
methods: {
toggleStyle() {
// 切换样式
this.myClass = this.myClass === 'my-component' ? 'my-component-alt' : 'my-component';
this.myStyle.color = this.myStyle.color === 'red' ? 'blue' : 'red';
}
}
};
</script>
<style scoped>
.my-component {
background-color: #ccc;
padding: 10px;
}
.my-component-alt {
background-color: #eee;
padding: 20px;
}
</style>
compositionAPI写法
js
<template>
<div :class="myClass" :style="myStyle">
<p>Example Component</p>
<button @click="toggleStyle">toggle</button>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const myClass = ref('my-component');
const myStyle = ref({
color: 'red',
fontSize: '20px'
});
function toggleStyle() {
myClass.value = myClass.value === 'my-component' ? 'my-component-alt' : 'my-component';
myStyle.value.color = myStyle.value.color === 'red' ? 'blue' : 'red';
}
</script>
<style scoped>
.my-component {
background-color: #ccc;
padding: 10px;
}
.my-component-alt {
background-color: #eee;
padding: 20px;
}
</style>
条件渲染
-
v-if
、v-else
可使用变量,也可以使用===
表达式 -
v-if
和v-show
的区别-
初始化渲染:
v-if
在初始渲染时会根据条件决定是否渲染元素,如果条件为假,则元素不会被渲染到DOM中;而v-show
会在初始渲染时始终将元素渲染到DOM中,但是通过CSS的display
属性来控制元素的显示与隐藏。 -
条件变化时的渲染:当条件变化时,
v-if
会根据新的条件来重新渲染或销毁元素,从而改变DOM结构,因此,当条件频繁变化时,v-if
的性能相对较低;而v-show
只是通过修改CSS的display
属性来控制元素的显示与隐藏,不会引起DOM的重新渲染和销毁,因此,当条件频繁变化时,v-show
的性能相对较高。 -
切换开销:由于
v-if
在条件为假时会销毁元素,再次变为条件为真时需要重新创建元素,因此在切换时可能存在较大的开销;而v-show
只是通过修改CSS的display
属性来控制元素的显示与隐藏,不会销毁和重新创建元素,因此在切换时开销较小。 -
编译条件:
v-if
的条件表达式在编译时会被完全忽略,只有在条件为真时才进行编译;而v-show
的条件表达式在编译时会被解析和编译,不管条件为真还是为假,都会进行编译。
-
-
v-if
和v-show
的使用场景有哪些?v-if
适用于在渲染时对条件进行判断并且可能较少更改的场景,而v-show
适用于在渲染时频繁切换条件的场景。在使用时,可以根据具体需求和性能考量选择适合的指令来控制组件或元素的显示与隐藏。
js
<template>
<div>
<p v-if="type === 'a'">A</p>
<p v-else-if="type === 'b'">B</p>
<p v-else>other</p>
<p v-show="type === 'a'">A by v-show</p>
<p v-show="type === 'b'">B by v-show</p>
</div>
</template>
<script lang="ts">
export default {
data() {
return {
type: 'a'
}
}
}
</script>
循环渲染
v-for
既可以遍历数组,也可以遍历对象v-for
和v-if
不能一起使用key
的重要性(diff算法)
js
<template>
<div>
<p>遍历数组</p>
<ul>
<li v-for="(item, index) in listArr" :key="item.id">
{{index}} - {{item.id}} - {{item.title}}
</li>
</ul>
<p>遍历对象</p>
<ul >
<li v-for="(val, key, index) in listObj" :key="key">
{{index}} - {{key}} - {{val.title}}
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const listArr = ref([
{ id: 'a', title: '标题1' }, // 数据结构中,最好有 id ,方便使用 key
{ id: 'b', title: '标题2' },
{ id: 'c', title: '标题3' }
])
const listObj = ref({
a: { title: '标题1' },
b: { title: '标题2' },
c: { title: '标题3' },
})
</script>
事件
- event参数
- 自定义参数
- 事件被绑定到哪里?
vue
的事件是原生的,绑定到当前元素上的 - 事件修饰符有哪些?
js
<!-- 单击事件将停止冒泡 -->
<a @click.stop="doThis"></a>
<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>
<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>
<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>
optionsAPI写法
js
<template>
<div>
<p>{{num}}</p>
<button @click="increment1">+1</button>
<button @click="increment2(2, $event)">+2</button>
</div>
</template>
<script lang="ts">
export default {
data() {
return {
num: 0
}
},
methods: {
increment1(event: MouseEvent) {
// @ts-ignore
console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
console.log(event.target === event.currentTarget)
console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
this.num++
// 1. event 是原生的
// 2. 事件被挂载到当前元素
// 和 DOM 事件一样
},
increment2(val: number, event: MouseEvent) {
console.log(event.target)
this.num = this.num + val
},
loadHandler() {
// do some thing
console.log('load')
}
},
mounted() {
window.addEventListener('load', this.loadHandler)
},
beforeDestroy() {
//【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
// 自己绑定的事件,需要自己销毁!!!
window.removeEventListener('load', this.loadHandler)
}
}
</script>
compositionAPI写法
js
<template>
<div>
<p>{{num}}</p>
<button @click="increment1">+1</button>
<button @click="increment2(2, $event)">+2</button>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue'
const num = ref(0)
const increment1 = (event: MouseEvent) => {
// @ts-ignore
console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
console.log(event.target === event.currentTarget)
console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
num.value++
// 1. event 是原生的
// 2. 事件被挂载到当前元素
// 和 DOM 事件一样
}
const increment2 = (val: number, event: MouseEvent) => {
console.log(event.target)
num.value = num.value + val
}
const loadHandler = () => {
// do some thing
console.log('load')
}
onMounted(() => {
window.addEventListener('load', loadHandler)
})
onUnmounted(() => {
//【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
// 自己绑定的事件,需要自己销毁!!!
window.removeEventListener('load', loadHandler)
})
</script>
表单
v-model
- 常见表单项
textarea
、checkbox
、radio
、select
- 修饰符
.lazy
、.number
、.trim
组件
props和emit
optionsAPI写法
js
emits: ['inFocus', 'submit'],
// props: ['list']
props: {
// prop 类型和默认值
list: {
type: Array,
default() {
return []
}
}
}
optionsApi写法可通过this.$emit()
触发父组件传递的自定义事件
compositionAPI写法
js
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>
注意:如果解构了 props
对象,解构出的变量将会丢失响应性。因此需要通过 props.xxx
的形式来使用其中的 props
。
如果确实需要解构 props
对象,或者需要将某个 prop
传到一个外部函数中并保持响应性,应该使用 toRefs() 和 toRef() 这两个工具函数:
js
import { toRefs, toRef } from 'vue'
// 将 `props` 转为一个其中全是 ref 的对象,然后解构
const { title } = toRefs(props)
// `title` 是一个追踪着 `props.title` 的 ref
console.log(title.value)
// 或者,将 `props` 的单个属性转为一个 ref
const title = toRef(props, 'title')
事件总线
Vue2
可以通过new Vue实现事件总线
js
import Vue from 'vue'
export default new Vue()
vue3
请使用mitt
组件生命周期
-
创建阶段(Creation)
beforeCreate
:在实例初始化之后,数据观测和事件配置之前被调用。created
:在实例创建完成后被立即调用。此阶段完成了数据观测和事件配置,并且还没有开始编译模板。
-
挂载阶段(Mounting)
beforeMount
:在挂载开始之前被调用,相关的 render 函数首次被调用。mounted
:el 被新创建的 vm.$el 替换,并且挂载到实例上后调用该钩子。
-
更新阶段(Updating)
beforeUpdate
:在数据更新之前调用,发生在虚拟 DOM
重新渲染和打补丁之前。updated
:在由于数据更改导致的虚拟 DOM
重新渲染和打补丁之后调用。
-
销毁阶段(Destruction)
beforeDestroy
:在实例销毁之前调用。在这一步,实例仍然完全可用。destroyed
:在实例销毁之后调用。此时,所有的事件监听器都已被移除,子实例也被销毁。
此外,还有一个错误捕获阶段 (Error Capturing):
errorCaptured
:当捕获一个来自子孙组件的错误时被调用。此钩子可以返回false
以阻止该错误继续向上传播。
这些生命周期钩子允许你在组件的不同生命周期阶段执行代码,从而实现想要的功能。注意,不建议在 beforeCreate
和 created
阶段依赖于响应式的数据或计算属性,因为这些在此阶段还未初始化。
vue3的生命周期有哪些变化?
setup()
: 在组件实例化之前被调用,是一个新的组件组合 API 的入口点,代替beforeCreate
和created
。beforeUnmount
: 在卸载开始之前被调用,相关的卸载逻辑被触发。替代了Vue 2中的beforeDestroy
。unmounted
: 在卸载之后调用。替代了Vue 2中的destroyed
。Vue 3
不再提供beforeUpdate
和updated
, 而是通过watch
和watchEffect
等响应式API来实现类似的功能。
高级特性
自定义v-model
nextTick
为什么需要nextTick
?
-
vue
是异步渲染的,data
改变之后,DOM
不会立刻渲染,因此紧接着执行DOM
操作可能会产生问题 -
nextTick
会在DOM
完成渲染之后触发,以获取最新DOM
节点
个人应用点:
- 自定义一个组件渲染
echarts
图表,并管理其生命周期,通过props
接收配置项 onMounted
调用echarts.init
+watch
监听props
配置变化及时setOption
- 如果在
onMounted
之前props
发生了很多次改变就会多次触发setOption
,报错 - 因此需要确保每一次
setOption
都发生在对应的DOM
已经挂载了的情况下,所以使用nextTick
,确保在DOM
发生渲染之后再setOption
slot
- 具名插槽
- 动态插槽名
- 作用域插槽
动态插槽名:
js
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
作用域插槽:
js
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
个人实际应用场景:
- 基于
element-plus
二次封装表格组件 - 每次都把后端数据传入该组件,使表格渲染该数据
- 原始数据里,有的是状态码,ui需要展示对应的中文tag
- 遍历生成每个
column
时,绑定动态插槽名与column
对应字段一致,通过作用域插槽,将原始数据吐到父组件中 - 父组件可通过插槽名,获取数据,并可以自定义该
column
的单元格使用何种组件
动态组件
<component :is="component-name">
用法- 需要根据数据,动态渲染的场景。即组件类型不确定
应用场景:vue-router
、keep-alive
根据路由动态加载组件
异步组件
import()
- 按需加载,异步加载大组件
注意: 对比同步方式,异步组件的
import
位置明显不同
一个同时结合了动态组件和异步组件的例子
js
<template>
<div>
<button @click="handleToggle">toggle</button>
<Suspense>
<component :is="AsyncComponent"></component>
<!-- 加载中状态 -->
<template #fallback>
正在加载...
</template>
</Suspense>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref, computed } from 'vue'
const state = ref< 'A' | 'B'>('A')
const AsyncComponent = computed(() => {
if(state.value === 'B') return defineAsyncComponent({
loader: () => import('./watchO.vue')
})
return defineAsyncComponent({
loader: () => import('./classAndStyleO.vue')
})
})
const handleToggle = () => {
if (state.value === 'A') {
state.value = 'B'
} else if (state.value === 'B') {
state.value = 'A'
}
}
</script>
应用场景:可以根据权限,动态加载页面对应的模块,让不同角色看到的同一模块是不同的组件
keep-alive
- 缓存组件
- 频繁切换,不需要重复渲染
- vue常见性能优化
mixin
- 多个组件有相同的逻辑,抽离出来
mixin
并不是完美的解决方案,会有一些问题,如:变量来源不明确,多mixin
造成命名冲突
组件之间有哪些通信方式?
-
父子组件通信:父组件可以通过
props
向子组件传递数据,而子组件可以通过事件(emit
)向父组件发送消息。这是最常见和简单的组件通信方式。 -
子组件访问父组件的方法:子组件可以通过
this.$parent
访问父组件的属性和方法。这种方式适用于简单的层次结构。 -
父组件可以通过
this.$refs
访问子组件的属性和方法,需注意Vue2
和Vue3
的区别,再compositionAPI
中,子组件可以通过defineExport
主动暴露供父组件访问的属性和方法。 -
兄弟组件通信:对于没有直接父子关系的组件,可以通过一个共同的父组件作为中间人来进行通信。兄弟组件可以通过在父组件中定义的事件来进行通信。
-
事件总线:可以使用一个空的
Vue
实例作为事件总线,在其中定义事件和监听器。组件可以通过事件总线来进行通信,利用emit
发送消息和on
监听消息。vue3
使用mitt
-
Vuex/Pinia
状态管理:Vuex/Pinia
是Vue
的官方状态管理库,用于处理应用程序中的共享状态。组件可以通过Vuex/Pinia
存储数据,而其他组件可以从Vuex/Pinia
中获取数据。这种方式适用于较大的应用程序,有复杂的数据共享和状态管理需求。 -
使用
provide/inject
实现祖先与后代之间的直接通信等 -
使用插槽(
slot
)进行内容分发
周边
vuex
state
getters
action
异步操作、整合多个mutation
mutation
原子操作,必须是同步的vue
组件调用时dispatch
commit
mapState
mapGetters
mapActions
mapMutations
有了vuex为什么还要发展出来pinia
-
API 设计:
Pinia
是在Vue 3
基础上重新设计的状态管理库,它的设计更加符合Vue 3
的响应性原理和 CompositionAPI。Pinia
的 API 更加简洁和直观,使用类似于组件的方式来定义和使用状态。相比之下,Vuex
是为Vue 2
设计的,其 API 更加基于对象和选项,需要通过定义 mutations、actions 和 getters 等来管理状态。 -
TypeScript
支持:Pinia
是完全使用TypeScript
编写的,并且提供了完整的类型推导和类型安全支持。Pinia
可以更好地与TypeScript
集成,提供更好的开发体验和更可靠的类型检查。虽然Vuex
也支持TypeScript
,但它的类型定义相对较简单并且基于鸭子类型。 -
性能优化:
Pinia
和Vuex
都采用了类似的响应式原理,但Pinia
在内部进行了一些性能优化,例如对状态的惰性获取、局部更新等,以提供更快的状态访问和更新速度。Pinia
还支持批量读取和写入状态,以减少响应式更新的次数。
pinia
-
Pinia
没有mutation
,只有state
、getter
、action
(异步、同步) -
Pinia
没有Module
配置,但是我们可以通过defineStore
创建Store
甚至隐式嵌套Store
实现不同Store
间的state
、getter
、action
共享,也就是灵活性更强
原理
Vue2响应式原理
js
function defineReactive(target, key, value) {
//深度监听
observer(value);
Object.defineProperty(target, key, {
get() {
return value;
},
set(newValue) {
//深度监听
observer(value);
if (newValue !== value) {
value = newValue;
updateView();
}
}
});
}
function observer(target) {
if (typeof target !== "object" || target === null) {
return target;
}
if (Array.isArray(target)) {
target.__proto__ = arrProto;
}
for (let key in target) {
defineReactive(target, key, target[key]);
}
}
// 重新定义数组原型
const oldAddrayProperty = Array.prototype;
const arrProto = Object.create(oldAddrayProperty);
["push", "pop", "shift", "unshift", "spluce"].forEach(
methodName =>
(arrProto[methodName] = function() {
updateView();
oldAddrayProperty[methodName].call(this, ...arguments);
})
);
// 视图更新
function updateView() {
console.log("视图更新");
}
// 声明要响应式的对象
const data = {
name: "zhangsan",
age: 20,
info: {
address: "北京" // 需要深度监听
},
nums: [10, 20, 30]
};
// 执行响应式
observer(data);
Vue2
如何深度监听data变化?
当初始化,以及set
时,都可以进行深度监听
Vue2
如何监听数组变化
通过重新定义数组原型的方式监听数组变化
Vue3为何使用proxy
替换了Object.defineProperty
?
Vue2
相比Vue3
有哪些不足?
- 对象新增属性时,
Object.defineProperty
监听不到,无法触发视图更新,所以有Vue.set
- 对象删除属性时,
Object.defineProperty
也监听不到,无法触发视图更新,所以有Vue.delete
- 深度监听时,
Object.defineProperty
需要一次性递归到底,无法分段处理,一次性计算量很大 Mixin
容易出现命名冲突,它的本质是代码片段,容易被不停覆盖,无法解决逻辑复用- 对于
ts
支持不足 - 过度依赖
this
,导致无法进行treeshaking
Vue3响应式原理
可以先看我的这篇源码调试文章,再看下面,会感觉更清晰 我的ref调试之旅
reactive的响应式是如何实现的?
js
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 执行一些特定的操作,如依赖追踪等
track(target, key);
const value = Reflect.get(target, key, receiver); // 使用 Reflect.get() 获取原对象的属性值
// 如果属性的值仍然是对象,则递归地将该对象转换为响应式对象
if (typeof value === 'object' && value !== null) {
return reactive(value);
}
return value;
},
set(target, key, value, receiver) {
const oldValue = target[key];
// 执行一些特定的操作,如派发更新等
trigger(target, key, value, oldValue);
const result = Reflect.set(target, key, value, receiver); // 使用 Reflect.set() 设置原对象的属性值
return result;
}
});
}
const data = reactive({
name: 'Alice',
age: 25
});
// 进行数据读取和设置的操作
console.log(data.name); // 控制台输出:读取数据 name
data.age = 30; // 控制台输出:设置数据 age 30
- 我们通过
reactive
接收到一个原始对象,并且创建一个proxy
对象,对其进行代理 - 我们可以通过
proxy
的get
、set
拦截到对原始对象的赋值操作以及获取操作。比如:set
时通知视图渲染 - 如果属性的值仍然是对象,递归将其也转为响应式数据,
shallowRef
则会跳过这一步 proxy
还可以正确处理Array
和Map
等数据结构
ref的响应式是如何实现的?
js
function ref(value) {
const r = {
value: value
};
return new Proxy(r, {
get(target, key, receiver) {
// 执行一些特定的操作,如依赖追踪等
track(target, 'value');
const result = Reflect.get(target, key, receiver); // 使用 Reflect.get() 获取原对象的属性值
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
// 执行一些特定的操作,如派发更新等
trigger(target, 'value', value, oldValue);
const result = Reflect.set(target, key, value, receiver); // 使用 Reflect.set() 设置原对象的属性值
return result;
}
});
}
const count = ref(0);
// 进行数据读取和设置的操作
console.log(count.value); // 控制台输出:读取数据 0
count.value = 1; // 控制台输出:设置数据 1
ref和reactive有什么区别呢?
从上可以看出,其实二者差距很小
ref
包容性更强,可以将普通值转为响应式对象reactive
则只针对引用类型
Virtual DOM 和 diff
- DOM操作非常耗费性能,js操作快得多,性能好得多,因此有了
VDOM
VDOM
是实现vue
和react
的重要基石,vdom
即用JS
模拟DOM
结构- diff算法是
VDOM
中最核心,最关键的部分,Vue
和React
通过diff算法比较新老VDOM
树,找出最小变更,精确操作DOM
JS模拟DOM结构(用VNode模拟HTML片段):
通过snabbdom学习vdom
vue3重写了VDOM
的代码,优化了性能,react的VDOM
和Vue也不同,但VDOM
的基本理念不变
js
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
// 通过传入模块初始化 patch 函数
classModule, // 开启 classes 功能
propsModule, // 支持传入 props
styleModule, // 支持内联样式同时支持动画
eventListenersModule, // 添加事件监听
]);
const container = document.getElementById("container");
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// 传入一个空的元素节点 - 将产生副作用(修改该节点)
patch(container, vnode);
const newVnode = h(
"div#container.two.classes",
{ on: { click: anotherEventHandler } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
// 再次调用 `patch`
patch(vnode, newVnode); // 将旧节点更新为新节点
diff算法
diff
算法是VDOM
中最核心,最关键的部分diff
算法能在日常中使用vue
,react
时体现出来(如:key)diff
算法即对比,是一个广泛的概念,如linux diff
命令,git diff
等
树的普通diff
算法:
- 遍历tree1
- 遍历tree2
- 更新,排序
1000个节点计算了1亿次,时间复杂度On3,算法不可用
优化时间复杂度到On:
-
只比较同一层级,不跨级比较
-
tag
不相同,则直接删除重建,不再深度比较 -
tag
和key
,两者都相同,则认为是相同节点,不再深度比较
h函数:生成vnode
我们通过h
函数传入tag
,data
,children
/text
等数据,内部进行一系列处理后,构造出vnode
进行返回,因此我们也可以说通过h
函数构造了vnode
patch函数:将vnode渲染为真实DOM
ts
function patch(
oldVnode: VNode | Element | DocumentFragment,
vnode: VNode
): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果传入的不是Vnode而是真实DOM
if (isElement(api, oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
} else if (isDocumentFragment(api, oldVnode)) {
oldVnode = emptyDocumentFragmentAt(oldVnode);
}
// 相同的Vnode key和sel(tag)都相等
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
// 不同的vnode,直接删掉重建
} else {
elm = oldVnode.elm!;
parent = api.parentNode(elm) as Node;
// 重建vnode
createElm(vnode, insertedVnodeQueue);
// 删除老的vnode
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
}
patchVnode 对比并且更新vnode的细节部分
ts
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
// 执行 prepatch hook
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
// 设置vnode的element,复用老的真实dom
const elm = (vnode.elm = oldVnode.elm)!;
if (oldVnode === vnode) return;
// 执行 update hook
if (
vnode.data !== undefined ||
(isDef(vnode.text) && vnode.text !== oldVnode.text)
) {
vnode.data ??= {};
oldVnode.data ??= {};
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
vnode.data?.hook?.update?.(oldVnode, vnode);
}
// 老的children
const oldCh = oldVnode.children as VNode[];
// 新的children
const ch = vnode.children as VNode[];
// vnode.test === undefined,意味着有children
if (isUndef(vnode.text)) {
// 如果vnode与oldVnode都有children
if (isDef(oldCh) && isDef(ch)) {
// 如果vnode.children不等于oldVnode.children,更新children
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
// 如果vnode有children而oldVnode没有children
} else if (isDef(ch)) {
// oldVnode没有chilrden,清空text
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
// 新建children的vnode
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
// oldVnode有children而vnode没有children,干掉所有children
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
// vnode与oldVnode都有text,更新text
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, "");
}
// 新的vnode有text且与oldVnode.text不相同
} else if (oldVnode.text !== vnode.text) {
// oldVnode有children,直接干掉所有children
if (isDef(oldCh)) {
// 移除children的vnode
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 设置真实DOM的text
api.setTextContent(elm, vnode.text!);
}
// 执行 postpatch hook
hook?.postpatch?.(oldVnode, vnode);
}
updateChildren(双端算法位置)
ts
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
// 定义4指针,分别指向oldChildren开始与结束,newChildren开始与结束
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
// 当新老children的指针都相碰了,终止循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
// oldChildren开始节点 与 newChildren开始节点 相同
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
// 向中间移动指针
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
// oldChildren结束节点 与 newChildren结束节点 相同
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
// 向中间移动指针
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
// oldChilren开始节点 与 newChildren结束节点 相同
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
// 向中间移动指针
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
// oldChildren结束节点 与 newChildren开始节点 相同
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
// 向中间移动指针
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
// 以上4个都未命中
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// vnode的key是否能够在oldChildren下的vnode中找到
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 如果找不到
if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
// 如果找的到key相同的oldVnode,说明是vnode内容发生了更新
} else {
elmToMove = oldCh[idxInOld];
// tag不同,直接删除重新创建
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
// tag相同,其他不同,更新vnode
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
// 移动指针
newStartVnode = newCh[++newStartIdx];
}
}
if (newStartIdx <= newEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
}
if (oldStartIdx <= oldEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
- 如果找得到完全相同的
vnode
,直接移动vnode
,并且移动指针即可 - 找不到完全相同的
vnode
,开始找key
是否能对应上,看是不是oldVnode
内容发生了改变- 如果找不到对应的
key
,直接创建 - 如果找到了对应的
key
tag
不同,直接删除,重新创建tag
相同,其他不同,更新vnode
- 如果找不到对应的
模板编译
- 模板是
vue
开发中最常用的部分,即与使用相关联的原理 - 它不是html,有指令、插值、js表达式,到底是什么?
- 面试官不会直接问,但会通过"组件渲染和更新过程"考察
- vue template compiler 将模板编译为
render
函数 - 执行
render
函数生成vnode
js的with语法
- 改变代码快内自由变量的查找规则,当作接受参数的属性来查找
- 如果找不到匹配的参数属性,就会直接报错
with
打破了作用域规则,易读性变差,别瞎鸡脖用
with(obj)
能将代码快内的自由变量当作obj的属性来查找
vue模板被编译成什么
- 模板不是
html
,有指令、插值、js表达式
,能实现判断、循环 html
是标签语言,只有js
才能实现判断、循环(图灵完备)- 图灵完备的语言:能顺序执行,判断,循环的语言
- 因此,模板一定是转换为了某种JS代码(
render
函数),才能执行v-if
,v-for
等对应的判断,循环操作 render
函数本身是with
代码块,执行render
就是执行这个with
,返回vnode
- 基于
vnode
再执行patch
和diff
- 使用
webpack
的vue-loader
会在开发环境下编译模板
vue组件中可以使用render代替template
- 因为不容易理解,不易读,所以都是在用
template
- 有些复杂情况,不能用
template
,可以考虑用render
react
一直用render
,没有模板
小结
- 一个组件渲染到页面,修改
data
触发更新(数据驱动视图) - 响应式:监听
data
的getter
,setter
,数组的监听 - 模板编译:模板 =>
render
=>vnode
- vdom:
patch(elem, vnode)
和patch(oldVnode,vnode)
vue组件时如何渲染和更新的
初次渲染 => 更新过程 => 异步渲染
初次渲染过程:
- 解析模板为
render
函数(或在开发环境已完成,vue-loader
) - 触发响应式,监听
data
属性getter
、setter
- 执行
render
函数,生成vnode
,patch(elem, vnode)
,我们在执行render
的时候就已经触发了data
的getter
更新过程:
- 修改
data
,触发setter
(此前在getter
中已被监听) - 重新执行
render
函数,生成vnode
patch(oldVnode, vnode)
常问题
为什么vue组件要做异步渲染?
- 汇总
data
的修改,一次性更新视图 - 减少
DOM
操作次数,提高性能
Vue的每个生命周期都做了什么?
beforeCreate
- 创建一个空白的
Vue
实例 data
、method
尚未被初始化,不可使用
created
Vue
实例初始化完成,完成响应式绑定data
、method
初始化完成,可调用- 尚未开始渲染模板
beforeMount
- 编译模板,调用
render
生成VDom
- 还没有开始渲染
DOM
mounted
- 完成
DOM
渲染 - 组件创建完成
- 开始由 创建阶段 进度 运行阶段
beforeUpdate
data
发生变化之后- 准备更新
DOM
updated
data
发生变化,且DOM
更新完成- 不要再
updated
中修改data
,可能会导致死循环
beforeUnmount
- 组件进如销毁阶段(尚未销毁,可正常使用)
- 可移除、解绑一些全局事件、自定义事件
unMounted
- 组件被销毁了
- 所有子组件也都被销毁了
onActivated
- 缓存组件被激活
onDeactivated
- 缓存组件被隐藏
Vue什么时候操作DOM比较合适?
Mounted
和updated
都不能保证子组件全部挂在完成- 使用
nextTick
渲染DOM
Ajax应该放在哪个生命周期?
created
或者Mounted
- 推荐
Mounted
CompositionAPI生命周期由什么区别?
- 使用
setup
代替了beforeCreate
和created
- 使用
Hooks
函数的形式
React、Vue2、Vue3的diff算法的区别是什么?
React:
总结: 如果是左移需要操作2个节点,右移仅需要操作1个节点,更清晰,更简便
Vue2 双端相互比较:
总结:更好的应对中间插入节点
Vue3 最长递增子序列:
Vue React为什么必须使用key?
diff算法
根据key
判断元素是否需要删除?- 匹配了
key
则只移动元素 - 未匹配
key
则删除重建
你在实际工作中,做过哪些Vue的优化?
v-if
和v-show
- 使用
computed
缓存 keep-alive
缓存组件- 异步组件
- 路由懒加载
- SSR
你在使用Vue过程中遇到过哪些坑?
- 内存泄漏:全局变量,全局事件,全局定时器,自定义事件
- Vue2响应式的缺陷:无法直接修改数组数据
arr[index]=value
、Vue.set
、Vue.delete
- 路由切换时scroll到了顶部,在列表页缓存数据和
scrollTop
,再次返回该页面时,手动设置到之前的滚动位置
如何监听Vue组件报错
window.onerror
监听所有JS错误errorCaptured
捕捉下级组件错误errorHandler
监听Vue全局报错,配置在main.ts
中,无法监听异步错误,需要靠window.onerror
window.onunhandledrejection
监听promise
未catch
到的错误
watch和watchEffect有什么区别?
watch
需要明确监听哪个属性?watchEffect
根据其中的属性,自动监听其变化,初始化时一定会执行一次,收集要监听的数据
setup中如何获取组件实例?
需要借助getCurrentInstance
这个API