今天我要跟你讨论一下数据传递。
vue有很多数据传递的方式,为了方便讨论,接下来我将花样使用各种方法将一个数据传递到不同的vue文件中,首先是最常用的:
1. props
这应该是vue最为人熟知、也最常用的方式了吧!举个例子:
vue
<script setup>
const one = ref(1)
const two = 2
</script>
<template>
<Child :one :two />
</template>
vue
<script setup lang="ts">
// 子文件
defineProps<{
one: number
two: number
}>()
</script>
<template>
<div>
{{ one }} {{ two }}
</div
</template>
如此,我们就将一个响应式变量和一个普通变量都传递给了子文件。这里自然而然会产生一个问题,在子文件中,明明one和two声明的类型都是number,凭什么在父组件中一个使用了响应式变量,另一个就是普通的number?这里我们将在未来深入讲解,此时如果你还不了解,你只需要知道更新的是父组件的模板中引用的组件就可以了。
由于props实在太常用太普遍,更多内容请见vue官方文档,这里就不赘述了。
2. 顶级作用域
这是一个非常简洁、非常好用、只基于原生js特性,却与vue非常适配的方式。
ts
// test.ts
export const one = ref(1)
vue
<script setup>
import { one } from './test.ts'
one.value = 2
watchEffect(() => {
console.log(one.value)
})
// ...
// 在其他组件里同样用这种方式,可以触发上面的侦测器
</script>
这里只是为了方便演示,实际上你不能创建一个没有template的组件,因为这意味着这是个没有渲染函数的组件,而这是不可能的,详见本文系列下一回
我们都知道ref
函数返回一个叫做RefImpl的复杂类型------这意味着当他被使用时,是以地址的方式被引用的。所有引用他的地方都指向同一片内存,也就是说:
ts
// A.ts
export const one = { value: 1 }
ts
// B.ts
import { one } from './A.ts'
setTimeout(() => one.value = 2, 1000)
ts
// C.ts
import { one } from './A.ts'
console.log(one.value)
setTimeout(() => console.log(one.value), 2000)
上述代码中,one也是一个复杂类型。首先由C.ts打印出1,一秒后one的值被B.ts修改为2, 2秒后C.ts再打印出2。
除此以外,由于ref
作为响应式变量的特性,可以直接被侦测器观察到,这可以做出很多操作。因此,在绝大部分情况下这种方式都可以代替pinia的功能。
ts
// store.ts
export const data = ref()
ts
// 组件A
import { data } from './store.ts'
data.value = 'hello'
ts
// 组件B
import { data } from './store.ts'
watchEffect(() => {
console.log(data.value)
})
- 首先打印出
undefined
- 被组件A修改,触发响应打印出
'hello'
需要注意的是,由于data是一个顶级变量------这意味着他没有生命周期,你应该注意在合适的时候初始化,合适的时候销毁。并且由于他是一个会被直接运行的顶级变量,这意味着你不能在SSR中,比如nuxt中使用这种方案,这会导致非常严重的后果!因为这个顶级变量在服务端被运行,那么这个变量的值会被所有同时使用这段代码的人互相污染!
3. pinia
上面方法是我用的最多的方法,但也说了,他不能在nuxt中使用,这会导致状态污染。但pinia会解决这个问题。
ts
export const useStore = defineStore('store', () => {
const data = ref()
return {
data
}
})
vue
<script setup>
const store = useStore()
watchEffect(() => store.data /* ... */)
</script>
几乎和上面使用起来一样,只是多了点样板代码,由于使用起来相对麻烦一点,基本上pinia只是在nuxt中上述方法的替代品。更多内容请见pinia官方文档,这里就不赘述了。
4. slot
这是一种子传父的方法:
vue
<script setup>
// A.vue
const data = ref()
</script>
<template>
<div>
<slot :data />
</div>
</template>
vue
<template>
<A v-slot="{ data }">
{{ data }}
</A>
</template>
如此,在子中修改data,会在父中直接显示。这是一种简单而作用有限的方法,一般用在简单展示数据的时候。具体内容详见vue官方文档,叫做作用域插槽
,这里就不赘述了。
5. 依赖注入
这也是非常常用的一种方法:
vue
<script setup lang="ts">
// A.vue
const data = ref<number>(1)
provide('hello', data)
</script>
<template>
<B />
</template>
vue
<script setup>
// B.vue
const data = inject('hello')
</script>
看起来只是类似于props的父向子传递,但实际上与最常用的第二种方法类似,他传递了整个ref,而不是像props那样传递了内部值。也就是说,在使用props的子组件中data将会是number
类型,而这里的data将会是RefImpl<number>
类型。
这意味着你在子组件里修改这个值,父组件也会更新。对于同级别而言,只需要在父组件声明这个值,那么对于多个子组件而言就是同级传递。
vue
<script setup>
// 父
const data = ref<number>(1)
provide('hello', data)
</script>
vue
<script setup>
// 子1
const data = inject('hello')
data.value = 2
</script>
vue
<script setup>
// 子2
const data = inject('hello')
watchEffect(() => console.log(data.value))
</script>
依赖注入同样非常非常常用,甚至在我看来比第二种方法更加常用------尽管他繁琐了一点点。第二种方法有一个致命的缺点,那就是他太简单了,简单到需要你手动管理生命周期。想象一下,你使用第二种方法在展示页面传递一个数据,然后用户切换到后台管理页面,而你忘了销毁这个顶级变量,那么即使在不需要它的后台管理页面,这个顶级变量依然占用着内存,甚至可能在用户下一次打开展示页面时,这个顶级变量由于保留了之前的值而出现意料之外的情况。
相比之下,依赖注入由于在组件之内声明,这意味着其上下文与组件绑定,其生命周期与这个组件绑定。当组件被销毁时,自然而然的依赖注入的值就被销毁了,这能省去极多的心智负担,为了方便使用,我写了这个工具函数:
ts
/**
* 依赖注入工厂函数
* @example
* ```js
* export const [useSomethingProvider, useSomething] = createProvider(() => {
* return {
* one: 1
* }
* })
*
* useSomething() // { one: 1 }
* ```
*/
export function createProvider<Fn extends (...args: any[]) => any, R = ReturnType<Fn>>(
onProvider: Fn,
onInject?: (symbol: InjectionKey<R>) => R,
): [(...args: Parameters<Fn>) => R, () => R] {
const injectKey = Symbol('createProvider') as InjectionKey<R>
const useInject = onInject
? () => onInject(injectKey)
: () => inject(injectKey)!
const useProvide = (...args: Parameters<Fn>): R => {
const val = onProvider(...args)
provide(injectKey, val)
return val
}
return [useProvide, useInject]
}
直接复制即可使用
这个函数使用Symbol确保了依赖注入的键绝对独立,并且保留了依赖注入的使用方式,同时还有类型提示。我们可以结合第二种方法使用它:
kotlin
// utils.ts
export const [useNumberProvider, useNumber] = createProvider(() => {
const data = ref(1)
return { data } // 或者直接返回data也可以,并不影响
})
vue
<script setup>
// 在注入的组件中
const { data } = useNumberProvider()
</script>
vue
<script setup>
// 在子组件中
const { data } = useNumber() // 带有类型提示且和provider类型保持一致
</script>
由于 useNumberProvider
和 useNumber
是函数而不是变量,因此他们作为顶级变量是完全可接受的。(相当于一个普通的工具函数)。而如此也省去了依赖注入繁琐的定义和没有类型提示的问题。