Vue-12种组件传值

203 阅读5分钟

vue中的12种组件通讯方式

vue中父子组件传值

vue中的父子组件传值,值得注意的是要遵守单向数据流原则。所谓单向数据流原则,简单的说就是父组件的数据可以传递给子组件,子组件也可以正常获取并使用由父组件传过来的数据;但是,子组件中不能直接修改父组件传过来的数据,必须要向父组件传递一个事件来父组件需要修改数据,即通过子组件的操作,在父组件中修改数据;这就是单项数据流。

1.父传子

父组件的数据、方法传递给子组件,子组件通过props接收,可以是数组、对象。

实现代码如下:

子组件:

<template>
  <div>
     <p>我是{{msg.name}},我今年{{msg.age}}岁了</p>
  </div>
</template>

<script>
export default {
  //在子组件中注册props,并使用父组件中传递过来的数据
        props:{
          msg:{
            required:true,
            type:Object
          }
        }
}
</script>

父组件:

<template>
  <div>
    <son :msg="list" ></son>
  </div>
</template>

<script>
//  父组件中引入子组件,并传入子组件内需要的值 
import son from './components/son.vue';

export default {
  components: { son },
  data() {
    return {
       //定义需要传入的值
      list: {
        name: "小明",
        age: 18,
      },
    };
  },
};
</script>

2子传父

  1.  this.$emit('')可以触发一个自定义的事件

实现代码如下:

子组件:

<template>
  <button @click="bt"> 我是子组件  </button>
</template>

<script>
export default {
  data(){
    return{
      say:'hello,你好啊'
    }
  },
    methods: {
      bt() {
        this.$emit('fn',this.say)
      },
    },
}
</script>

父组件:

<template>
  <div>
     <p>我是父组件 {{ msg }}</p>
<son @fn='btn'></son>
  </div>
</template>
<script>
import son from './components/son.vue';

export default {
  data(){
    return{
        msg:''
    }
  },
  components: { son },

  methods: {
    btn(obj) {
      this.msg = obj
    },
  },
  
 
};
</script>

3 v-model 实现父子组件传值

v-model是语法糖, v-model等价于 给一个input框提供了 :value属性以及 @input事件,但是如果每次使用input框,都需要提供value和input事件比较麻烦,所以使用v-model

实现代码:

子组件:

<template>
  <div>
    我是子组件:
    <input type="text" :value="value" @input="changeVal" />
  </div>
</template>
<script>
export default {
  name: "child",
  props: ["value"],
  methods: {
    changeVal(e) {
      this.$emit("input", e.target.value);
    },
  },
};
</script>

父组件:

<template>
  <div class="parent">
    <h1>我是父组件:{ { msg }}</h1>
    <child v-model="msg"></child>
  </div>
</template>
<script>
import child from "./child";
export default {
  name: "parent",
  components: {
    child,
  },
  data() {
    return {
      msg: "hello!",
    };
  },
};
</script>

4.sync 修饰符 实现父子组件传值

.sync 修饰符 是vue 2.3.0+ 新增的特性(但是在vue3.0中被废弃)

实现代码如下

子组件:(以 update:myPropName 的模式触发事件)

<template>
  <div>
    我是子组件:
    <input type="text" :value="msg" @input="changeValFn" />
  </div>
</template>
<script>
export default {
  name: 'child',
  props: ['msg'],
  methods: {
    changeValFn(e) {
      this.$emit('update:msg', e.target.value)
    },
  },
}
</script>

父组件:(以v-bind:msg.sync="msg"的模式传递/接收数据)

<template>
  <div class="parent">
    <h1>我是父组件:{ { msg }}</h1>
    <!-- <child v-bind:msg.sync="msg"></child> -->
    <child :msg.sync="msg"></child>
  </div>
</template>
<script>
import child from "./child";
export default {
  name: "parent",
  components: {
    child,
  },
  data() {
    return {
      msg: "hello!",
    };
  },
};
</script>

5 兄弟组件通信

EventBus 第一种方法:

第一步: 在父组件中给要传值的兄弟两个都绑定要传的变量。

<app-user-detail :age="age"></app-user-detail>
<app-user-edit :age="age" @editAge="changeAge"></app-user-edit>

第二步: 在要传值得组件中接受变量和绑定触发事件

<template>
    <div>
        <h3>用户编辑页面</h3>
        <p>编辑用户</p>
        <p>年龄: {{ age }}</p>
        <p><button @click="changeAge">编辑年龄</button></p>
    </div>
</template>
<script>
export default {
    props: ["age"],
    methods: {
        changeAge: function() {
        this.$emit('editAge', 20)   // 触发自定义事件并传值
        }
    }
}
</script>
```

第三步: 在父组件中绑定要传组件中的自定义事件

export default {
    data() {
        return {
            name: 'zhuli',
            age: 10,
        }
    },
    components: {
        'app-user-detail': UserDetail,
        'app-user-edit': UserEdit,
    },
    methods: {
        changeAge: function(age) {
            this.age = age
        },
    }
}

总结: 当要传值的组件改变了父组件的变量,父组件又可以把改变的值传值所绑定变量的组件,实现兄弟间传值。

第二种方式: 和上面子组件传值父组件一样,使用一个函数,再函数中传参使用。

小结: 这两种方式的缺点是嵌套层级深,不好处理。

第三种方式: 后续应该使用vuex

第四种方式: 思想: 既然要借助父级,那么可不可不定义一个全局事件

第一步: 在入口文件main.js里暴露一个vue实例

export const EventBus = new Vue() // 暴露一个vue实例

第二步: 在要传值的文件里导入vue实例模块,age使用自己的变量,再定义一个全局触发事件函数,触发事件函数绑定在一个button上

<template>
    <div>
        <h3>用户编辑页面</h3>
        <p>编辑用户</p>
        <p>年龄: {{ age }}</p>
        <p><button @click="changeAge">编辑年龄</button></p>
    </div>
</template>
<script>
import { EventBus } from "../main.js"
// 导入模块
export default {
 // props: ["age"],
 data() {
     return {
         age: 10,
     }
    },
  methods: {
      changeAge: function() {
          this.age = 20
          EventBus.$emit("editAge", this.age)
          // 触发全局事件 并且把改变后的值传入事件函数
          // console.log(EventBus)
      }
    }
}
</script>

第三步: 在要被传入值得组件中也导入vue实例模块,也不使用父组件中传过来个age,自己重新定义,创建一个初始化的钩子函数,再使用created钩子函数中使用传值组件的全局定义事件。

import { EventBus } from "../main.js"
export default {
    data() {
        return {
            name : this.myName,
            age: 10
        }
    },
    methods: {
        changeParentName: function() {
            this.$emit('changeParentName', 'xiaohong')
            // this.$emit('')触发自定义事件
        },
     created() {
         EventBus.$on('editAge', (age) => { // 使用$on去绑定事件
             this.age = age // 使用es6写法,this刚好指向父级
         })
     }
}

总结: 现在兄弟之间传值,都没有通过父组件

6 provide / inject

//祖先组件
import { provide } from 'vue';

 const { separtor = '>'} = defineProps<{separtor?:string}>()
// 祖先级别的传值
 provide('separtor',separtor)
//后代组件
import { inject } from 'vue';

defineProps<{to?:string}>()

const separtor = inject('separtor')

7ref传值

父组件可以通过 ref 主动获取子组件的属性或者调用子组件的方法

// Child.vue
export default {
    data(){
        return {
            name:"oldCode"
        }
    },
    methods:{
        someMethod(msg){
            console.log(msg)
        }
    }
}

// Parent.vue
<template>
    <child ref="child"></child>
</template>
<script>
export default {
    mounted(){
        const child = this.$refs.child
        console.log(child.name) 
        child.someMethod("调用了子组件的方法")
    }
}
</script>

8 $attrs / $listeners

当要和一个嵌套很深的组件进行通信时,如果使用 prop 和 events 就会显的十分繁琐,中间的组件只起到了一个中转站的作用,像下面这样:

<!--父组件-->
  <parent-component :message="message">我是父组件</parent-component>
<!--子组件-->
  <child-component :message="message">我是子组件</child-component>
<!--孙子组件-->
  <grand-child-component :message="message">我是孙子组件</grand-child-component>

当要传递的数据很多时,就需要在中间的每个组件都重复写很多遍,反过来从后代组件向祖先组件使用 events 传递也会有同样的问题。使用 $attrs 和 $listeners 就可以简化这样的写法。

$attrs 会包含父组件中没有被 prop 接收的所有属性(不包含class 和 style 属性),可以通过 v-bind="$attrs" 直接将这些属性传入内部组件。

$listeners 会包含所有父组件中的 v-on 事件监听器 (不包含 .native 修饰器的) ,可以通过 v-on="$listeners" 传入内部组件。

下面以父组件和孙子组件的通信为例介绍它们的使用:

<!--父组件 parent.vue-->
<template>
    <child :name="name" :message="message" @sayHello="sayHello"></child>
</template>
<script>
export default {
    inheritAttrs: false,
    data() {
        return {
            name: '通信',
            message: 'Hi',
        }
    },
    methods: {
        sayHello(mes) {
            console.log('mes', mes) // => "hello"
        },
    },
}
</script>
<!--子组件 child.vue-->
<template>
    <grandchild v-bind="$attrs" v-on="$listeners"></grandchild>
</template>
<script>
export default {
    data() {
        return {}
    },
    props: {
        name,
    },
}
</script>
<!--孙子组件 grand-child.vue-->
<template>
</template>
<script>
export default {
    created() {
        this.$emit('sayHello', 'hello')
    },
}
</script>

9$children / $parent

$parent 属性可以用来从一个子组件访问父组件的实例,$children 属性 可以获取当前实例的直接子组件。

看起来使用 $parent 比使用prop传值更加简单灵活,可以随时获取父组件的数据或方法,又不像使用 prop 那样需要提前定义好。但使用 $parent 会导致父组件数据变更后,很难去定位这个变更是从哪里发起的,所以在绝大多数情况下,不推荐使用。

在有些场景下,两个组件之间可能是父子关系,也可能是更多层嵌套的祖孙关系,这时就可以使用 $parent

下面是 element ui 中的组件 el-radio-group 和 组件 el-radio 使用示例:

<template>
  <el-radio-group v-model="radio1">
    <el-radio :label="3">备选项</el-radio>
    <component-1>
        <el-radio :label="3">备选项</el-radio>
    </component-1>
  </el-radio-group>
</template>

<script>
  export default {
    data () {
      return {
        radio2: 3
      };
    }
  }
</script>

在 el-radio-group 和 组件 el-radio 通信中, 组件 el-radio 的 value 值需要和 el-radio-group的 v-model 的值进行“绑定”,我们就可以在 el-radio 内借助 $parent 来访问到 el-radio-group 的实例,来获取到 el-radio-group 中 v-model 绑定的值。

下面是获取 el-radio 组件中获取 el-radio-group 实例的源码:

// el-radio组件
    let parent = this.$parent;
    while (parent) {
        if (parent.$options.componentName !== 'ElRadioGroup') {
            parent = parent.$parent;
        } else {
            this._radioGroup = parent; // this._radioGroup 为组件 el-radio-group 的实例
        }
    }

10 通过 $root 访问根实例

通过 $root,任何组件都可以获取当前组件树的根 Vue 实例,通过维护根实例上的 data,就可以实现组件间的数据共享。

//main.js 根实例
new Vue({
    el: '#app',
    store,
    router,
    // 根实例的 data 属性,维护通用的数据
    data: function () {
        return {
            author: ''
        }
    },
    components: { App },
    template: '<App/>',
});


<!--组件A-->
<script>
export default {
    created() {
        this.$root.author = '于是乎'
    }
}
</script>


<!--组件B-->
<template>
    <div><span>本文作者</span>{{ $root.author }}</div>
</template>

11Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。对一个中大型单页应用来说是不二之选。

使用 Vuex 并不代表就要把所有的状态放入 Vuex 管理,这样做会让代码变的冗长,无法直观的看出要做什么。对于严格属于组件私有的状态还是应该在组件内部管理更好。

对于小型的项目,通信十分简单,这时使用 Vuex 反而会显得冗余和繁琐,这种情况最好不要使用 Vuex,可以自己在项目中实现简单的 Store。

//store.js
var store = {
  debug: true,
  state: {
    author: 'yushihu!'
  },
  setAuthorAction (newValue) {
    if (this.debug) console.log('setAuthorAction triggered with', newValue)
    this.state.author = newValue
  },
  deleteAuthorAction () {
    if (this.debug) console.log('deleteAuthorAction triggered')
    this.state.author = ''
  }
}

和 Vuex 一样,store 中 state 的改变都由 store 内部的 action 来触发,并且能够通过 log 保留触发的痕迹。这种方式十分适合在不需要使用 Vuex 的小项目中应用。

12 插槽 slot 与子组件传值

在实际项目中确实有遇到插槽 后备内容 动态显示的情况,所以这里要补充一下插槽 后备内容 是如何与子组件进行通信的。

插槽后备内容是指:写在父组件中,包含在子组件标签里的,与子组件中的 slot 对应。

<template>
  <child-component>
    我是插槽的后备内容
  </child-component>
</template>
复制代码

比如这里有一个含有 slotcurrent-user 组件,它的模版结构是这样的:

<!-- 子组件 current-user.vue -->
<template>
  <div>
    <div>current-user组件</div>
    <slot>插槽里默认显示:{{user.firstName}}</slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {
        firstName: "zhao",
        lastName: "xinglei"
      }
    }
  }
}
</script>
复制代码

它的父组件是这样的:

<!-- 父组件 Users.vue -->
<template>
  <div>
    <div>我是Users组件</div>
    <current-user>
      我是插槽里的后备内容: {{user.lastName}}(我想显示为子组件中 user.lastName )
    </current-user>
  </div>
</template>

<script>
import CurrentUser from './Current-User.vue'
export default {
  components: {
    CurrentUser
  }
}
</script>
复制代码

我们看到,在父组件 Users 中,为子组件 current-user 提供的后备内容中,想要显示子组件定义的 user.firstName 是不能做到的。

官网中提供一个指令 v-slot,它与 props 结合使用从而达到插槽后备内容与子组件通信的目的。

我们首先需要在子组件的 slot 中传递一个 props(这个props 叫做插槽props),这里我们起名叫 user

<!-- 子组件 current-user.vue -->
<template>
  <div>
    <div>current-user组件</div>
    <slot :user="user">
      插槽里默认显示:{{user.firstName}}
    </slot>
  </div>
</template>
复制代码

在父组件中,包含插槽后备内容的子组件标签上我们绑定一个 v-slot 指令,像这样:

<template>
  <div>
    <div>我是Users组件</div>
    <!-- slotProps里的内容就是子组件传递过来的 props -->
    <!-- "user": { "firstName": "zhao", "lastName": "xinglei" } -->
    <current-user v-slot="slotProps">
      {{slotProps}}
    </current-user>
  </div>
</template>
复制代码

最后渲染出来的结果为:

作用域插槽

官网给这种插槽起名叫做作用域插槽

总结

1. 组件之间传值无非就是通过属性、事件和操作 Vue 实例进行的。
2. 操作实例进行组件件通信,实例属性 $root、$parent、$children 分别对应了根实例、父实例、子实例。
3  ref 子组件引用,在操作表单元素时会应用的到。
4. Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,简单的应用不要使用 Vuex。