Vue2.x中常见的几种值传递方式

您好,我是沧沧凉凉,是一名前端开发者,目前在掘金知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。


在面试中,Vue值传递问题一直是各大面试官最喜欢问到的问题之一,了解了各种值传递后,遇到一些比较复杂的界面,需要大量运用值传递时,你就可以从众多的值传递方式中找到一种最能简化代码逻辑的传值方式。

1. Prop/$emit

1.1 Prop

在Vue中最为常用的传值方式和变更值的方式,也是最重要的值传递方式,在Vue中起码有百分之80的值都是通过Prop进行传递。

对于Prop,官方文档的介绍也是相当的详细,所以这里就不过多去介绍细节,对Prop不了的朋友可以直接参考官方的Prop文档

Prop的使用方式就是对子组件使用v-bind绑定一个属性,例如:

<template>
  <div>
    <!-- 给子组件绑定一个message -->
    <Son :message="message" />
  </div>
</template>

<script>
import Son from "@/views/Son";

export default {
  name: "Parents",
  components: { Son },
  data() {
    return {
      message: "Hello World!",
    };
  },
};
</script>

<style scoped></style>

在子组件中就这样进行调用:

<template>
  <div>
    <label>
      {{ message }}
    </label>
  </div>
</template>

<script>
export default {
  name: "Son",
  props: {
    // 我通常习惯这样写,因为可以对Prop类型进行验证
    // 甚至可以让父组件必传该值
    message: {
      type: String,
    },
  },
};
</script>

<style scoped></style>

注意:HTML中的attribute名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用DOM中的模板时,camelCase (驼峰命名法) 的Prop名需要使用其等价的kebab-case(短横线分隔命名) 命名。

但是!如果你使用字符串模板,那么这个限制就不存在了。现在Vue应用的HTML代码都使用了字符串模板。

1.2 $emit

通过v-on的方式向子组件中传递事件,然后通过this.$emit("事件名")的方式进行调用,其主要目的是修改Prop的值,因为Vue中推崇单向数据流,所谓的单向数据流就是通过Prop向子组件中传递的值,只有传递该值的组件才能对其修改,如果你对Prop的值进行修改,那么Vue会给你一个错误警告。

// 父组件
<Son :message="message" @set-message="setMessage" />

// 子组件
this.$emit("set-message", "改变");

注意:不同于组件和Prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。

并且v-on事件监听器在DOM模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent导致 myEvent 不可能被监听到。

所以推荐始终使用 kebab-case 的事件名

1.3 额外讲解

当然,Vue中的单向数据流也并不是绝对的,如果你向子组件中传递的是一个对象,那么你可以在子组件中修改对象的属性,这一点在平时做项目的时候会大量应用,虽然知道这种行为是不对的,但是这样做会大大简化开发时的代码量,对于需要快速上线的项目来说也不见得不是一个好方法。

例如:

<template>
  <div>
    <label>
      <!-- 改变Prop值中的属性,不会报错 -->
      <input v-model="message.content" />
      {{ message.content }}
    </label>
  </div>
</template>

<script>
export default {
  name: "Son",
  props: ["message"],
};
</script>

<style scoped></style>

可以看到上面虽然可以更改message的值,但是并不会报错。

2. $refs/$parent/$children/$root

我个人是非常不推荐使用这几种方式进行值传递,可能你自己用起来很爽,但是代码维护火葬场,尤其是对于一些不爱写注释的人来说,你在某个方法中对$refs赋值后,然后在其它组件进行调用,最后查找代码的时候很难发现该值究竟是从哪儿传递下来的!

而且使用这种方式IDE无法追踪到该值:

image-20210321194855629

2.1 $refs

给子组件绑定ref,然后可以直接通过this.$refs.xxx.xxx的方式来访问子组件的属性。

<template>
  <div>
    <!-- 给子组件绑定ref -->
    <Son ref="son" />
  </div>
</template>

<script>
import Son from "@/views/Son";

export default {
  name: "Parents",
  components: { Son },
  mounted() {
    // 访问子组件
    console.log(this.$refs.son);
  },
};
</script>

<style scoped></style>

2.2 $parent/$children

$parent

父实例,如果有父实例的话。返回一个VueComponent,可以直接通过this.$parent.xxx的形式访问到父组件中的数据。

$children

调用后会返回一个数组,数组中包含所有的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合v-for来生成子组件,并且使用 Array 作为真正的来源。

和**$parent**相同,可以通过this.$children[x].xxx的形式访问到子组件中的数据。

2.3 $root

当前组件树的根Vue实例。如果当前实例没有父实例,此实例将会是其自己,访问数据的方式和上面一样。

3. $attrs/$listener

制作高阶组件中非常实用的值传递方式。

3.1 $attrs

包含了父作用域中不作为Prop被识别 (且获取) 的attribute绑定 (classstyle 除外)。当一个组件没有声明任何Prop时,这里会包含所有父作用域的绑定 (classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件。

意思就是子组件上面通过v-bind绑定的属性,如果你没有在子组件Prop中声明,那么可以通过this.$attrs获取到这些值。

<template>
  <div>
    <label>
      <!-- 调用父组件传递的message属性 -->
      {{ $attrs.message }}
      <Grandson v-bind="$attrs" />
    </label>
  </div>
</template>

<script>
import Grandson from "@/views/Grandson";
export default {
  name: "Son",
  components: { Grandson },
};
</script>

<style scoped></style>

一旦声明了Prop,那么$attrs中就不再能获取到该属性:

<template>
  <div>
    <label>
      <!-- 因为使用了prop声明,所以无法通过$attrs再访问到message属性为undefined -->
      {{ $attrs.message }}
      <!-- 将该组件上通过v-bind绑定的没有在props中声明的属性,全部传递给子组件 -->
      <Grandson v-bind="$attrs" />
    </label>
  </div>
</template>

<script>
import Grandson from "@/views/Grandson";
export default {
  name: "Son",
  components: { Grandson },
  props: {
    // 使用props进行了声明
    message: {
      type: String,
    },
  },
};
</script>

<style scoped></style>

3.2 $listener

包含了父作用域中的 (不含.native修饰器的) v-on事件监听器。它可以通过v-on="$listeners"传入内部组件。

跟上面的$attrs差不多,可以通过v-on="$listener"将事件全部传递给子组件。

4. provide/inject

封装高阶组件的神器,只要在父组件中通过provide声明一次,在所有子组件中都可以通过inject访问到声明的值,无论它们之间的层级有多深,解决了组件层级比较深时使用Prop需要一层一层的传递值的问题。

使用起来也很简单,下面是爷组件中的代码,通过provide传递属性:

<template>
  <div>
    <Son />
  </div>
</template>

<script>
import Son from "@/views/Son";
export default {
  name: "Parents",
  components: { Son },
  data() {
    return {
      message: "Hello World!"
    };
  },
  // 通过provide传递的属性在任何子组件中都能访问,无论层级有多少
  provide() {
    return {
      message: this.message
    };
  }
};
</script>

下面是孙组件中的代码:

<template>
  <div>
    {{ message }}
  </div>
</template>

<script>
export default {
  name: "Grandson",
  // 通过inject声明属性
  inject: {
    message: "message"
  }
};
</script>

<style scoped></style>

注意:当父辈组件和子孙组件同时注入一个provide值,比如都叫message属性,那么通过inject声明的message属性会优先使用离它最近的上一级组件provide中的message属性的值。

需要值得注意的是,通过provide/inject传递的值的来源不如Prop那么清晰,所以推荐使用inject注入值的时候最好注释一下该值是从哪个组件中传递进来的。

5. eventBus

eventBus的使用有一定的复杂性,我自己也是没有进行深入研究,但是就我大致浏览了一下后发现并没有Vuex好用,具体可以百度搜索一下相关的教程。

6. Vuex

Vuex虽然解决了复杂情况的值传递问题,但是它无法用来制作和封装高阶组件,也就是你在一个项目中使用Vuex做出来的组件,放到另一个项目中无法立即使用,还得一步一步的去注册Vuex。如果你想要封装一个高阶组件,那么Vuex肯定不是一个好选择。

因为Vuex的复杂性,篇幅有限,就不在这里进行多讲了,有兴趣的可以直接参考官方Vuex文档

7. Vue router

使用路由进行传值也是在日常开发中经常会用到的方式,路由传参一般有两种方式,一种是使用http://xxx/user/:id这种方式,一种是http://xxx/user?id=xxx这种方式,这两种方式应用场景都非常多,而就我个人来讲,我比较喜欢`?id=xxx`这种方式。

因为使用第一种方式你得到Vue router的路由声明中先进行path: '/user/:id'这种形式的声明,并且你直接访问/user就无法再正常进行访问,而第二种?id的方式则不需要在Vue router中进行声明。

第一种:

const routes = [
  {
    // 首先需要在路由中进行声明
    path: "/:id",
    name: "Home",
    component: Home
  }
];

在组件中获取到该值的方法为:

this.$route.params.id // 获取方式

还有一种更简便的方法,则在路由设置中将props设置为true

const routes = [
  {
    path: "/:id",
    name: "Home",
    component: Home,
    props: true
  }
];

在组件中就可以直接通过Prop获取id的值:

<template>
  <div class="home">
    {{ id }}
  </div>
</template>

<script>
export default {
  name: "Home",
  props: {
    id: {
      type: String
    }
  }
};
</script>

第二种

this.$route.query.id // 获取方式

8. 浏览器存储

通过浏览器提供的Local Storage、Session Storage、Cookie、IndexedDB这些方式将数据存入到本地,然后在需要用到的组件中取出也可以完成组件中的值传递。

但是要用这些方式来实现组件之中的值传递会有一些问题,如果大量的数据都是通过它们进行传递,那么会导致数据的结果变得无法预估,最常通过这种方式来传递的值就是token,使用这种方式传递token,还可以解决浏览器关闭后token丢失的问题,因为使用它们进行传递token时,可以设置token有效时间。

9. 最后

主流的值传递方式大概就这么多,可能还有很多非主流的方式。不过无论是使用哪种值传递,在写代码的时候最好考虑清楚,先问一问使用这种方式在后期是否利于维护,写代码远远不仅为了当下的爽快,你要想想以后如果别人接手你的代码是否能够不花费太多的功夫就能看得懂。

尤其是组件之间的值传递,因为IDE工具可能还没有智能到每一种值传递都可以自动帮你找到来源,大多数值传递通过IDE工具都是无法找到来源的,这就给后期维护代码增添了工作量,所以还是那句话,如果是过于隐式的值传递(比如$refs这种方式),一定要将值的来源在数据中写清楚。

参考文章:

Vue组件通信方式居然有这么多?你了解几种