Vue的书写风格
选项式 API
用包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。
组合式 API
使用导入的 API 函数来描述组件逻辑,组合式 API 通常会与 <script setup>
搭配使用,在导入的和顶层变量/函数都能够在模板中直接使用。
选项式 API && 组合式 API的特点
选项式 API 是在组合式 API 的基础上实现的。
选项式 API 是在对应的属性中编写对应的功能模块,易于学习和使用,写代码的位置已经约定好。但功能对应的代码逻辑会被拆分到各个属性中,当组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散,不便复用,也不便阅读,处理问题,需要不断跳到对应的代码中。
组合式 API 会把各个功能逻辑的代码组织在一起,便于阅读和维护。但需要有良好的代码组织能力和拆分逻辑能力;
模板语法
在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。
文本插值
最基本的数据绑定形式是文本插值,它使用的是"Mustache"语法 (即双大括号)。
js
<span>Message: {{ msg }}</span>
动态绑定多个值
若有像这样的一个包含多个 attribute 的 JavaScript 对象:
js
const objectOfAttrs = { id: 'container', class: 'wrapper' };
<div v-bind="objectOfAttrs"></div>
受限的全局访问
模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表
。该列表中会暴露常用的内置全局对象,比如 Math
和 Date
。也可以自行在app.config.globalProperties
上显式地添加它们,供所有的 Vue 表达式使用。
动态参数
同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:
js
<a :[attributeName]="url"> ... </a>
<a @[eventName]="doSomething"> ... </a>
动态参数中表达式的值应当是一个字符串,或者是 null
。特殊值 null
意为显式移除该绑定。其他非字符串的值会触发警告。
响应式基础
在组合式 API 中,推荐使用ref()
函数来声明响应式状态,ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回。
js
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
在模板中使用 ref 时,我们不需要附加 .value
。当在模板中使用时,ref 会自动解包。
ref
当一个组件首次渲染时,Vue 会追踪 在渲染过程中通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作,该 .value
属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。
js
// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
对于浅层 ref,只有 .value
的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。
DOM 更新时机
当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在"next tick"更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
reactive
与将内部值包装在特殊对象中的 ref 不同,reactive()
将使对象本身具有响应性。
响应式对象是JavaScript Proxy
,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。
reactive()
将深层地转换对象:当访问嵌套对象时,它们也会被 reactive()
包装。当 ref 的值是一个对象时,ref()
也会在内部调用它(reactive)。
reactive()
的局限性
有限的值类型 :它只能用于对象类型 (对象、数组和如 Map
、Set
这样的集合类型)。它不能持有如 string
、number
或 boolean
这样的原始类型。
不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地"替换"响应式对象,因为这样的话与第一个引用的响应性连接将丢失。
对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接。
计算属性
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存 。一个计算属性仅会在其响应式依赖更新时才重新计算。方法调用总是会在重渲染发生时再次执行函数。
可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到"可写"的属性,你可以通过同时提供 getter 和 setter 来创建。
js
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>
Class 与 Style 绑定
js
// Class绑定一
<div :class="{ active: isActive }"></div>
// Class绑定二
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
// Class绑定三
const classObject = reactive({
active: true,
'text-danger': false
});
<div :class="classObject"></div>
// Class绑定四
const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': error.value && error.value.type === 'fatal'
}));
<div :class="classObject"></div>
// Class绑定五
const activeClass = ref('active')
const errorClass = ref('text-danger')
<div :class="[activeClass, errorClass]"></div>
// Class绑定六
<div :class="[isActive ? activeClass : '', errorClass]"></div>
// Style绑定一
const activeColor = ref('red')
const fontSize = ref(30)
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
// Style绑定二
<div :style="{ 'font-size': fontSize + 'px' }"></div>
// Style绑定三
const styleObject = reactive({
color: 'red',
fontSize: '13px'
})
<div :style="styleObject"></div>
// Style绑定四
<div :style="[baseStyles, overridingStyles]"></div>
条件渲染
v-if
v-if
严格意义来说就是条件判断,符合就加载DOM(对象模型)元素,不符合就不显示。
v-show
v-show
严格意义来说其实是条件隐藏,直接在页面初始化的时候将DOM(对象模型)元素也初始化,因为它就是将它所在的元素添加一个display属性为none,如果条件符合就显示。
v-if & v-show
v-if每切换一次就要重新走一次生命周期,比如说重新构建内部事件和函数,而v-show则就是页面初始时走一遍生命周期,将其加载完毕,其他时候则都不会走相关的周期了。
-
v-if有更高的切换性能,比如说需要判断多个条件时,就使用if。
-
如果需要频繁的切换,选择v-show,因为show是动态的改变样式,不需要增删DOM(对象模型)元素,大项目推荐使用show,能极大减少浏览器后期的操作性能。
-
show不支持
<template>
语法。
侦听器
在组合式 API 中,我们可以使用watch
函数在每次响应式状态发生变化时触发回调函数。
watch
的第一个参数可以是不同形式的"数据源":它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组。
js
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
watchEffect()
允许我们自动跟踪回调的响应式依赖。只有一个依赖项的例子来说,watchEffect()
的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect()
可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect()
可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
watch && watchEffect
watch
和 watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。
js
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
组件
Vue 组件在使用前需要先被"注册",这样 Vue 才能在渲染模板时找到其对应的实现。
动态组件
当使用 <component :is="...">
来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过<KeepAlive>
组件强制被切换掉的组件仍然保持"存活"的状态。
单向数据流
所有的组件传递的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。
若是想父组件的状态,子组件应该抛出一个事件来通知父组件做出改变。
组件 v-model
v-model
可以在组件上使用以实现双向绑定。
js
<input v-model="searchText" />
// 等价于
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
// 在组件上
<CustomInput
:model-value="searchText"
@update:model-value="newValue => searchText = newValue"
/>
无渲染组件
无渲染组件是指只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件。
插槽 Slots
组件能够接收任意类型的 JavaScript 值作为 props,若想要为子组件传递一些模板内容片段呢 ?
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
通过使用插槽,组件更加灵活和具有可复用性。现在组件可以用在不同的地方渲染各异的内容,但同时还保证都具有相同的样式。
作用域插槽
插槽内容无法访问子组件的数据,只能访问其定义时所处的作用域。但我们想要同时使用父组件域内和子组件域内的数据,可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
js
// <MyComponent> 的模板
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
依赖注入 provide
&& inject
父组件向子组件传递数据时,会使用props
,当多层级嵌套的组件传递数据时,使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦。
provide
和 inject
可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者 。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
js
// 在供给方组件内
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
// 在注入方组件
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
逻辑复用
组合式函数
"组合式函数"(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
在组合式函数中使用 ref()
而不是 reactive()
,我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性。
组合式函数只能在 <script setup>
或 setup()
钩子中被调用。
组合式函数和 Mixin 的对比
- 不清晰的数据来源
- 命名空间冲突
- 隐式的跨 mixin 交流
组合式函数和无渲染组件的对比
- 组合式函数不会产生额外的组件实例开销
- 无渲染组件产生的额外组件实例会带来无法忽视的性能开销
推荐在纯逻辑复用时使用组合式函数 ,在需要同时复用逻辑和视图布局时使用无渲染组件。
自定义指令
自定义指令是由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑。自定义指令则主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
js
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
插件
插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。
插件发挥作用的常见场景:
- 通过
app.component()
和app.directive()
注册一到多个全局组件或自定义指令。 - 通过
app.provide()
使一个资源可被注入进整个应用。 - 向
app.config.globalProperties
中添加一些全局实例属性或方法
状态管理
每一个 Vue 组件实例都已经在"管理"它自己的响应式状态,当多个组件共享一个共同的状态时,数据间的交互就比较麻烦了。
性能优化
web 应用性能的两个主要方面:
- 页面加载性能 :首次访问时,应用展示出内容与达到可交互状态的速度。最大内容绘制 和首次输入延迟。
- 更新性能:应用响应用户输入更新的速度。比如当用户在搜索框中输入时结果列表的更新速度,或者用户在一个单页面应用 (SPA) 中点击链接跳转页面时的切换速度。
web页面加载优化
1、核心网页指标 构成核心网页指标目前侧重于用户体验的三个方面(加载性能、对用户输入的响应速度和布局稳定性),并包含以下指标(及其各自的阈值):
- 衡量加载性能:2.5s <= 阈值 < 4.0s
- 衡量互动: 100ms <= 阈值 < 300ms
- 衡量视觉稳定性: 0.1s <= 阈值 < 0.25s
2、使用CDN 服务器资源缓存,负载均衡、图像优化、视频流式传输、边缘计算和安全产品。
3、划分资源优先级,通过多种方式使用 <link rel="preload">
、<link rel="preconnect">
和 <link rel="prefetch">
、<script async>
;
4、预加载关键资源,以提高加载速度
5、使用延迟加载来提高加载速度
6、优化图片:使用 loading
属性延迟加载图片;延迟加载屏幕外图片;
7、优化css:延迟加载非关键css;压缩css;提取关键css;
8、优化 JavaScript:优化耗时较长的任务;通过代码拆分减少 JavaScript 载荷;压缩和移除未使用代码
在使用vue的优化
- 采用构建步骤
- 引入新的依赖项时,使用对现代浏览器ES 模块格式更友好的
- 在渐进式增强的场景下使用 Vue,使用 petite-vue (只有 6kb) 来代替
- 使用 petite-vue (只有 6kb) 来代替
- 将构建后的 JavaScript 包拆分为多个较小的,可以按需或并行加载的文件,从而提高性能。
- 懒加载路由,懒加载组件
v-once
是一个内置的指令,可以用来渲染依赖运行时数据但无需再更新的内容。v-memo
是一个内置指令,可以用来有条件地跳过某些大型子树或者v-for
列表的更新。- 实现列表虚拟化
- 减少大型不可变数据的响应性开销:Vue 的响应性系统默认是深度的,在数据量巨大时,深度响应性也会导致不小的性能负担,因为每个属性访问都将触发代理的依赖追踪。
- 避免不必要的组件抽象,在大型列表中去掉不必要的组件抽象,可能会减少数百个组件实例的无谓性能消耗。
组合式 API
组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。
- 组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了mixins 的所有缺陷。
- 更灵活的代码组织
- 更好的类型推导
- 更小的生产包体积
和 React Hooks 的对比
React Hooks 在组件每次更新时都会重新调用。
- Hooks 有严格的调用顺序,并不可以写在条件分支中。
- React 组件中定义的变量会被一个钩子函数闭包捕获,需要传入正确的依赖数组
- 昂贵的计算需要使用
useMemo
,这也需要传入正确的依赖数组 - 传递给子组件的事件处理函数会导致子组件进行不必要的更新,需要显式的调用
useCallback
作优化,这也需要传入正确的依赖数组
相比起来,Vue 的组合式 API:
- 仅调用
setup()
或<script setup>
的代码一次。这使得代码更符合日常 JavaScript 的直觉,不需要担心闭包变量的问题。组合式 API 也并不限制调用顺序,还可以有条件地进行调用。 - Vue 的响应性系统运行时会自动收集计算属性和侦听器的依赖,因此无需手动声明依赖。
- 无需手动缓存回调函数来避免不必要的组件更新。Vue 细粒度的响应性系统能够确保在绝大部分情况下组件仅执行必要的更新。对 Vue 开发者来说几乎不怎么需要对子组件更新进行手动优化。
响应式系统
组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新。
Vue 中的响应性是如何工作的
在 JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。
js
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
响应性调试
组件调试钩子
组件渲染时使用 onRenderTracked
生命周期钩子来调试查看哪些依赖正在被使用, 使用 onRenderTriggered
来确定哪个依赖正在触发更新。
js
<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue'
onRenderTracked((event) => {
debugger
})
onRenderTriggered((event) => {
debugger
})
</script>
计算属性调试
我们可以向 computed()
传入第二个参数,是一个包含了 onTrack
和 onTrigger
两个回调函数的对象:
onTrack
将在响应属性或引用作为依赖项被跟踪时被调用。onTrigger
将在侦听器回调被依赖项的变更触发时被调用。
js
const plusOne = computed(() => count.value + 1, {
onTrack(e) {
// 当 count.value 被追踪为依赖时触发
debugger
},
onTrigger(e) {
// 当 count.value 被更改时触发
debugger
}
})
// 访问 plusOne,会触发 onTrack
console.log(plusOne.value)
// 更改 count.value,应该会触发 onTrigger
count.value++
侦听器调试
和 computed()
类似,侦听器也支持 onTrack
和 onTrigger
选项:
js
watch(source, callback, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
watchEffect(callback, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
渲染机制
浏览器渲染引擎工作流程都差不多,大致分为 5 步,创建 DOM 树------创建 StyleRules------创建 Render 树------布局Layout(重排)------绘制 Painting(重绘)。
- 分析HTML元素,并构建DOM树。
- 分析CSS样式,并生成样式表。
- 将DOM树和样式表关联起来,遍历每一个
可见
节点,构建一棵Render树。 - 基于Render树布局,也就是根据树节点的描述,确定每一个节点应该在屏幕上出现的具体位置。
- 调用每个节点的paint方法,绘制它们自身。
虚拟 DOM
虚拟 DOM
是为了解决浏览器性能问题而被设计出来。虚拟 DOM
不会立即操作 DOM
,而是将新的 diff
内容保存到本地一个 JS
对象中,最终将这个 JS
对象一次性 attch
到 DOM
树上,再进行后续操作,避免大量无谓的计算量。所以,用 JS
对象模拟 DOM
节点的好处是,页面的更新可以先全部反映在 JS
对象(虚拟 DOM
)上,操作内存中的 JS
对象的速度显然要更快,等更新完成后,再将最终的 JS
对象映射成真实的 DOM
,交由浏览器去绘制。
Vue 编译器性能的主要优化:
静态提升
将静态的节点或属性(静态节点,即不带任何动态绑定的元素节点)提升到render函数外,避免重新渲染时的再次创建和执行。
当有足够多连续的静态元素时,它们还会再被压缩为一个"静态 vnode",这些静态节点会直接通过 innerHTML 来挂载,生成代码的体积减少;减少创建 VNode 的开销;减少内存占用。
- 20个及以上的连续静态元素
- 5个及以上的连续仅具有静态绑定属性的静态元素
更新类型标记
在为这些元素生成渲染函数时,Vue 在 vnode 创建调用中直接编码了每个元素所需的更新类型。通过这样的更新类型标记,Vue 能够在更新带有动态绑定的元素时做最少的操作。
树结构打平
当这个组件需要重渲染时,只需要遍历这个打平的树而非整棵树,仅包含所有动态的后代节点。这也就是我们所说的树结构打平,这大大减少了我们在虚拟 DOM 协调时需要遍历的节点数量。模板中任何的静态部分都会被高效地略过。
缓存事件处理函数
vue在处理事件时,会对事件处理函数进行缓存,以便下次触发相同事件,无需重新创建和绑定。
事件缓存机制主要通过两个方面来实现:事件监听器的缓存 和事件处理函数的缓存。