flingyp

Vue3 最佳实践探索

Vue3 从 2020-09-18 正式发布到今天为止已经快三年的时间了。经过这么长时间的沉淀,很多新项目也开始逐渐向向Vue3转移了。

那么如何更优雅的写好Vue相关代码,那么应该就需要探讨一下Vue3的最佳实践了(本篇文章的观点完全是个人想法)。

Composition API 🆚 Options API

Vue3 中提供了两种组件的写法,分别是 Composition APIOptions API。那么这两种写法有什么区别呢?

Options API

Options API 是 Vue2 中的写法,也是大家最熟悉的写法。

Options API 的写法是通过 datamethodscomputedwatch 等属性来组织代码的。那么一般使用 Options API 的时候,我们会把相关的代码放在对应的属性中,这样就会导致代码的结构比较混乱。

缺点:项目代码过于复杂时,代码量增多,会导致代码可读性较差,不易于拆分和维护

Composition API

Composition API 是 Vue3 中新增的写法,它是通过 setup 函数来组织代码的。

虽然这套 API 的风格是基于函数的组合,但组合式 API 并不是函数式编程。组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的,而函数式编程通常强调数据不可变。(官方文章内容)

优点:解决 Options API 的缺点,可以更好的组织代码,提高代码的可读性,便于拆分和维护。

缺点:由于 Composition API 是基于函数的组合,所以在使用的时候,需要注意函数的调用顺序,否则会导致代码执行顺序的混乱。

两者对比

Composition API Structure

Ref、Reactive 和 Computed

Ref 🆚 Reactive

Vue3 的数据响应式系统是基于 Proxy 实现的。而 Proxy 只针对引用数据类型可用,所以需要考虑基础数据类型也实现相同的功能。

Vue 官方源码如下:

// ref.ts
export function ref<T extends Ref>(value: T): T
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
// 如果 value 是个引用数据类型,会执行 reactive(value)
// 如果是基础数据类型,那么就直接赋值给 _value
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
// reactive.ts
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW]
&& !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
proxyMap.set(target, proxy)
return proxy
}

总结

Ref:

  1. Ref 在创建的时候,会对传入的值进行判断,如果是引用数据类型,会执行 reactive(value),如果是基础数据类型,那么就直接赋值给 _value
  2. 当访问 Ref 的 value 属性时,会执行 trackRefValue(this),这个方法会对 Ref 进行依赖收集。
  3. 当访问 Ref 的 value 属性时,会执行 triggerRefValue(this, newVal),这个方法会对 Ref 进行派发更新。

Reactive:

  1. Reactive 在创建的时候,会对传入的值进行判断,如果是引用数据类型,会执行 createReactiveObject,如果是基础数据类型,那么就直接返回。
  2. 针对引用数据类型,Reactive 会使用 ES6 的 Proxy 来代理对象, Proxy 的 getset 方法中,会执行 tracktrigger 方法,从而实现依赖收集和派发更新。

Computed

Computed 是一个惰性的计算属性,它的值是由其他响应式数据计算而来,只有当它的依赖发生改变时,才会重新计算。

通俗易懂的来说 Computed 可以依靠其他响应式数据来计算出一个新的响应式数据,并且只有当数据发生变化时,才会重新计算。

export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _dirty = true
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean,
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self)
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions
): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false,
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack
cRef.effect.onTrigger = debugOptions.onTrigger
}
return cRef as any
}

总结

Computed:

  1. 计算属性的目的是根据状态衍生属性,我们希望这个属性有缓存功能,如果依赖的数据不变就不会重新计算
  2. 默认不执行,当我们取值时才会调用(具有缓存功能当取值多次只执行一次,只有当依赖的值发生变化才会重新执行)
  3. 计算属性内部有个变量来控制是否重新执行 dirty(默认是 true,此时用户会执行此方法,拿到返回结果返回并且缓存起来,将 dirty 变为 false)。再次取值则 dirtyfalse 就去拿缓存的结果
  4. 如果依赖项发生变化,会再次更新 dirty 变为 true,再取值的时候就会执行拿到新值

可组合式函数 Composable

可组合式函数:利用 Vue 的响应式系统 + Composition API 来组合代码。从而实现项目中代码的抽离和复用。

以下是一个简单的例子,去理解它的概念。

<!-- Counter.vue -->
<script setup lang="ts">
import { useCounter } from './composable'
const { count, setCounter } = useCounter(10)
const onClick = () => {
// Custom logic content
setCounter(count.value + 1)
}
</script>
<template>
<button @click="onClick">
{{ count }}
</button>
</template>
// useCounter.ts
export const useCounter = (initValue: number) => {
const count = ref(initValue)
const setCounter = (value: number) => {
count.value = value
}
return {
/**
* 如果不需要通过外界来修改 count 的值可以使用 readonly 包裹
* 或使用 computed 来衍生一个响应式数据
*/
count: readonly(count),
setCounter,
}
}

这样就可以把 Counter.vue 中的逻辑代码抽离出来,放到 useCounter.ts 中。

从而实现代码的复用,而组件本身只关注数据视图的展示,组件或业务相关的逻辑代码统统都可以抽离放在一个或多个Composable中。

🚀 Vue3 最佳实践(个人看法)

通过上面内容的学习,我们对 Vue3 的响应式原理有了一个大致的了解,那么我们该如何在实际项目中使用 Vue3 呢?

巧用 Vue 的响应式系统 + Composition API 来组合代码,实现可组合式函数 Composable。

  1. 可以统一使用 Ref 来包裹响应式数据,因为 Ref 兼容 Reactive,并且通过 xx.value 的形式更容易看出数据是一个响应式数据。
  2. 多用 Computed 来衍生响应式数据,因为 Computed 有缓存功能,可以减少不必要的计算。
  3. 尽量避免外部组件直接修改响应式数据,可以通过 Computed 或者 Ref 的 readonly 来包裹响应式数据,并且通过 Composable 来封装响应式数据的修改方法。