我要成为vue高手02:数据传递

今天我要跟你讨论一下数据传递。

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>

由于 useNumberProvideruseNumber 是函数而不是变量,因此他们作为顶级变量是完全可接受的。(相当于一个普通的工具函数)。而如此也省去了依赖注入繁琐的定义和没有类型提示的问题。

相关推荐
老华带你飞22 分钟前
校园交友|基于SprinBoot+vue的校园交友网站(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·校园交友网站
roamingcode41 分钟前
Claude Code NPM 包发布命令
前端·npm·node.js·claude·自定义指令·claude code
码哥DFS42 分钟前
NPM模块化总结
前端·javascript
灵感__idea1 小时前
JavaScript高级程序设计(第5版):代码整洁之道
前端·javascript·程序员
唐璜Taro1 小时前
electron进程间通信-IPC通信注册机制
前端·javascript·electron
陪我一起学编程3 小时前
创建Vue项目的不同方式及项目规范化配置
前端·javascript·vue.js·git·elementui·axios·企业规范
LinXunFeng3 小时前
Flutter - 详情页初始锚点与优化
前端·flutter·开源
GISer_Jing3 小时前
Vue Teleport 原理解析与React Portal、 Fragment 组件
前端·vue.js·react.js
Summer不秃4 小时前
uniapp 手写签名组件开发全攻略
前端·javascript·vue.js·微信小程序·小程序·html
coderklaus4 小时前
Base64编码详解
前端·javascript