精华内容
下载资源
问答
  • 父子组件通信 根据vue中的文档可知, 组件的props属性用于接收父组件传递的信息. 而子组件想要向父组件传递信息, 可以使用$emit事件. 我们定义两个组件, 一个为父组件名为father, 另外一个为子组件child. 子组件通过...
  • 单页面应用组件通信有以下几种,这篇文章主要讲 Angular 通信 父组件 => 子组件 子组件 => 父组件 组件A = > 组件B 父组件 => 子组件 子组件 => 父组件 sibling => sibling @input @output setters ...
  • 主要介绍了Vue 之孙组件向爷组件通信的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
  • 主要给大家介绍了关于如何使用vuex实现兄弟组件通信的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
  • 在开发组件的时候,一定会遇到组件通信,比如点击一个图标出现弹窗和蒙层,这三个分别是不同的组件。管理他们之间的状态就成了问题。 props双向绑定 通过 sync 双向绑定,属性变化会同步到所有组件,这也是最简单...
  • 主要介绍了vue非父子组件通信问题及解决方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
  • 一提到两个非父子组件通信方法,有经验的 coder 肯定会说用 Vuex 啊,我个人建议不要为了用 Vuex 而用 Vuex,除非你的项目很大,耦合度很高,需要大量的储存一些 data,组件之间通信频繁。当然还是要根据自己的业务...
  • 本文实例讲述了vue组件通信传值操作。分享给大家供大家参考,具体如下: 父子组件通信: 子组件 <h3 click=alerrt> 我是子组件一 <span>{{parentMessage}} [removed] export default{ props: ['...
  • 1.父组件与子组件间的通信。 在 React 中,父组件可以向子组件通过传 props 的方式,向子组件进行通讯。 父组件 App.js import React, { Component } from 'react'; import './App.css'; import Child from './...
  • 主要为大家详细介绍了Vue非父子组件通信的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • 主要给大家介绍了关于react中组件通信的几种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
  • 组件通信

    2019-06-26 10:55:52
    组件通信 为什么要进行组件通信? 组件可以说是一个具有独立功能的整体,但是当我们要将这些组件拼接在一起时,这些组件相互之间要建立联系,这个联系我们就称之为通信 组件通信的方式有以下几种( 王者级 ) ...

    组件通信

    1. 为什么要进行组件通信?
      组件可以说是一个具有独立功能的整体,但是当我们要将这些组件拼接在一起时,这些组件相互之间要建立联系,这个联系我们就称之为通信

    2. 组件通信的方式有以下几种( 王者级 )

      1. 父子组件通信 : 使用props来实现

        1. 在父组件的模板中将数据用单项数据绑定的形式,绑定在子组件身上
           <Son :money = "money"/>
        
        1. 在子组件的配置项中可以使用一个props配置项来接收这个数据,接收时,props的取值可以是一个数组
           Vue.component('Son',{
              template: '#son',
              props: ['money']
           })
        
        1. 在子组件模板中,接收到的属性可以像全局变量一样直接使用
           <p> 父亲给了我  {{ money }}  钱  </p> 
        
      2. 子父组件通信
        自定义事件

        1. 在父组件的模板中,通过事件绑定的形式,绑定一个自定义事件在子组件身上
          • <Son @aa = “fn”/> //这边要注意: fn是要在父组件配置项methods中定义
        2. 在子组件的配置项methods中写一个事件处理程序,在事件处理程序中触发父组件绑定的自定义事件
         Vue.component('Son',{
              template: '#son',
              data () {
                return {
                  hongbao: 500
                }
              },
              methods: {
                giveFather () {
                  //如何进行父组件给子组件的自定义事件触发
                  this.$emit('give',this.hongbao)
                }
              }
            })
        
        1. 将子组件定义的事件处理程序 giveFather,绑定在子组件的按钮身上
         <template id="son">
           <div>
              <button @click = "giveFather"> give </button>
              <h3> 这里是son组件 </h3>
           </div>
        </template>
        
      3. 非父子组件通信

        • ref链: 可以实现非父子组件的通信,但是如果层级太多,就比较繁琐了 $attrs
        • bus事件总线
          1. 在其中一个组件的 挂载钩子函数 上 做事件的声明
          Vue.component('Sma',{
             template: '#small',
             data () {
               return {
                 flag: false
               }
             },
             mounted () { //当前组件挂载结束,也就是我们可以在页面当中看到真实dom
               // mounted这个钩子函数的触发条件是组件创建时会自动触发
               // 事件的声明
               var _this = this 
               bus.$on( 'aa',function () {
                 _this.flag = true
                 console.log( this )//这里是this指的是bus, 但是我们需要的this应该是Sma这个组件
               })
             }
           })
          
          1. 在另一个组件中 通过 bus.$emit(‘aa’)来触发这个自定义事件
      4. 多组件状态共享( 多个组件共用同一个数据 ) 大知识点( vuex )
        vuex

      接下来的几种方式是非常规手段,是你不得已才使用的,砸门是不推荐的

    问题: 为什么data要定义为一个函数?

    1. 组件是一个独立的个体,那么它应该拥有自己的数据,这个数据应该是一个独立的数据
    2. 也就是说这个数据应该有独立作用域,也就是有一个独立的使用范围,这个范围就是这个组件内
    3. js的最大特征是:函数式编程 , 而函数恰好提供了独立作用域

    问题: 为什么data要有返回值?返回值还是一个对象?

    1. 因为Vue是通过observer来观察data选项的,所有必须要有返回值
    2. 因为Vue要通过es5的Object.defineProperty属性对对象进行getter和setter设置

    app实例的手动挂载

      new Vue({
        
      }).$mount('#app')
    

    自定义事件

    1. 自定义的 通过 $on 定义 $emit触发

         var vm = new Vue({
         el: '#app'
         })
      
         // 自定义事件的定义( 发布 )
      
         // vm.$on(自定义事件的名称,自定义事件的事件处理程序)
      
         vm.$on( 'aa', function () {
         console.log( 'aa' )
         })
      
         //自定义事件的触发 ( 订阅 )
      
         // vm.$emit( 自定义事件的名称,自定义事件处理程序需要的参数1,参数2,参数3)
      
         vm.$emit( 'aa' )
      
    2. 通过绑定在组件身上定义,通过 $emit触发
      <Son @aa = “fn”/>

      使用: 子父通信

    组件的根元素必须有且仅有一个

    动态组件

    1. 什么是动态组件?
      可以改变的组件
    2. 使用
      通过 Vue 提供了一个 component + is 属性
    3. 动态组件指的就是 component这个组件
    4. 案例
      <div id="app">
         <button @click = "change"> 切换 </button>
         <keep-alive include="">
            <component :is = "type"></component>
         </keep-alive>
      </div>
      
        Vue.component('Aa',{
            template: '<div> Aa </div>'
         })
      
         Vue.component('Bb',{
            template: '<div> Bb </div>'
         })
      
         new Vue({
            data: {
               type: 'Aa'
            },
            methods: {
               change () {
               this.type = (this.type === 'Aa'?'Bb':'Aa')
               }
            }
         }).$mount('#app')
      
    5. Vue提供了一个叫做 keep-alive 的组件可以将我们的组件进行浏览器缓存,这样当我们切换组件时,就可以大大提高使用效率
    6. keep-alive也可以以属性的形式呈现,但是我们如果搭配component的话,建议使用组件的形式

    slot 插槽

    1. 作用/概念: 预先将将来要使用的内容进行保留
    2. 具名插槽: 给slot起个名字
    • 注意: 以上两种形式在 vue2.6以上被废弃
    • 为什么要 用 v-slot指令来代替呢?
      • 经具名插槽和作用域插槽进行统一
      • 要将这两个属性带有 vue的标志,并且符合vue两个最大的特性之一: 指令的概念
    展开全文
  • 组件通信 一般常见的组件之间的通信有以下几种情况,A和B,B和C,B和D之间都是父子关系,C和D之间是兄弟组件关系。 常用的通信手段有两种: 1.ref:给元素或组件注册引用信息 2.children:访问父级组件和子组件...
  • 主要介绍了vue子父组件通信的实现代码,非常不错,具有参考借鉴价值,需要的朋友参考下吧
  • 主要给大家总结介绍了关于vue中八种组件通信方式的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用vue具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
  • 主要给大家介绍了关于vue2利用Bus.js如何实现非父子组件通信的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧。
  • 子组件向父组件通信 子组件的button按钮绑定点击事件,事件方法名为sendToParent(), 该方法在子组件的methods中声明,实现功能this.$emit('cus-event',this.msg); 在父组件引入子组件,并给cus-event事件绑定doAction($...
  • 可以使用props将父组件的数据传给子组件。子组件在接受数据时要显示声明props。下面通过一个例子给大家介绍Vue中父组件向子组件通信的方法,需要的朋友参考下吧
  • 装依赖 npm install 启动 npm run serve 这是一个vue组件的几种通信方式的源码
  • react组件通信,父子组件通信,跨级组件通信

    父子组件通信

    顾名思义就是父组件和子组件之间进行通信交流。下面先看样例代码:

    // 父子组件通信
    import React from "react";
    
    // 下面为父子组件通信实例代码
    // 父组件
    class ParentSon extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          msg: "father",
          name: "partent",
          age: 60
        };
      }
    
      callback = (msg, name, age) => {
        this.setState({ msg });
        this.setState({ name });
        this.setState({ age });
      };
    
      render() {
        return (
          <div style={{padding: 30+'px'}}>
            <h1>{this.state.msg}</h1>
            <Child
              callback={this.callback}
              age={this.state.age}
              name={this.state.name}
            ></Child>
          </div>
        );
      }
    }
    
    // 子组件
    class Child extends React.Component{
      constructor(props){
        super(props);
        this.state={
          msg: 'I am son',
          name: 'son',
          age: 8
        };
        // 下面一行代码是为了解决非箭头函数使用时this的指向问题不能指向当前子组件自身
        // this.change=this.change.bind(this);
      }
      // change(){
      //   // console.log(this);
      //   this.props.callback(this.state.msg,this.state.name,this.state.age);
      // }
      change=()=>{
        this.props.callback(this.state.msg,this.state.name,this.state.age);
      }
    
      render(){
        return (
          <div>
            <div>{this.props.name}</div>
            <div>{this.props.age}</div>
            <button onClick={this.change}>点击</button>
          </div>
        )
      }
    }
    
    export default ParentSon;
    

    如上面代码所示,分别创建了父组件 ParentSon 和子组件 Child ,父组件里面定义了自己的state内容,并定义了一个 callback 方法,此方法用来改变父组件自身的state内容值的,然后render函数里面除了渲染自身state里的msg值之外,将自身的callback方法以及自身state里的name,age分别作为参数传给了子组件 Child。

    下面再继续看子组件内部,除了有自己的state之外,还有一个change方法,然后render函数里渲染了来自props里的值以及一个点击事件按钮,而这个按钮点击事件就会触发调用自身那个change事件,在进入change方法内部会看到,实际上,执行的就是props接收的callback方法,也就是从父组件传过来的那个callback,而这里的参数又是子组件自身state的内容值,到此,我们可以顺一下整个思路了。

    思路: 一开始渲染出的全是父组件的值(因为子组件显示的就是来自父组件的值)-----> 点击按钮 -----> 触发子组件change事件(实际接收的父组件callback方法)-----> 父组件callback被调用,父组件的state里的值都发生改变,此时已变成和子组件state里一样的值了 -----> state改变后的值,又通过props传给了子组件Child -----> 再一次渲染出来传过来的值,即和子组件一模一样的值,此时整个过程全部结束。

    跨级组件通信

    顾名思义就是互不相干且之间隔了好几级的一种通信交流方式。下面先看样例代码:

    // 跨级组件通信
    import React from "react";
    
    const ThemeContext = React.createContext("light");
    
    class ContextMess extends React.Component {
      static contextType = ThemeContext;
      render() {
        return (
          <ThemeContext.Provider value="dark">
            <div style={{padding: 30+'px'}}>
              <h3>{this.context}</h3>
              <Toolbar></Toolbar>
            </div>
          </ThemeContext.Provider>
        );
      }
    }
    
    class Toolbar extends React.Component {
      render() {
        return (
          <div>
            <ThemedButton></ThemedButton>
          </div>
        );
      }
    }
    
    class ThemedButton extends React.Component {
      static contextType = ThemeContext;
      render() {
        return (
          <div>
            <h1>{this.context}</h1>
            <ThemeContext.Consumer>
              {value => (<h6>{value}</h6>)}
            </ThemeContext.Consumer>
          </div>
        );
      }
    }
    
    export default ContextMess;
    
    

    从上面代码可以看出,有三个组件,ContextMess ,Toolbar ,ThemedButton ,ContextMess 为最外层组件,依次是Toolbar 组件,再是ThemedButton 组件,逐层嵌套。
    代码中主要使用了react中的Context技术点,使用了Context中的以下几个API:React.createContext,Context.Provider,Class.contextType,Context.Consumer。

    React.createContext: 创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

    Context.Provider: 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化;Provider 接收一个 value 属性,传递给消费组件;当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。

    Class.contextType: (代码中通过 static 来定义静态属性contextType,即组件(class)自己的属性,等同于class.contextType)挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

    Context.Consumer: 这里,React 组件也可以订阅到 context 变更。这能让你在函数式组件中完成订阅 context;这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。

    思路: 简单来说,那个‘light’一开始创建的就相当于要在其他组件使用的数据,在 ContextMess 组件中通过使用 contextType 获取 createContext 创建的对象,然后通过this.context来获取值,即“light”;除此之外,使用 Context.Provider API 通过value参数将当前值改为“dark”,然后不使用 props 传值方式,在二级组件中可以看到,并没有使用props接收值,最后在下一级组件通过 Class.contextType 和 this.context 方式读取值,除此之外还使用 Context.Consumer API 来获取最近的那个匹配的context值,即改变后的值“dark”。

    具体Context相关知识及API使用请点击:React中Context的学习使用
    展开全文
  • 3. Flutter有状态组件和组件通信 0. 一个计算器的简单例子 再学习过程中在本例子中用到了几个注意点进行笔记: Flutter有状态组件的定义方法 Flutter子组件和父组件之间的通信过程 动态组件生成的方法 关于定义回调...
  • Vue3组件通信总结

    千次阅读 多人点赞 2020-12-16 21:07:41
    后面会以开发几个简单form组件为例子来演示。 基本操作 这里先简单开发一个VInput的输入框组件组件就像一个函数,主要就是处理输入和输出。Vue3在setup函数上提供了两个参数,一个props,一个是context下面的emit...

    前言


    我们知道vue3Composition Api是它几个最大亮点之一,所以下文都是在setup中演示代码的实现。后面会以开发几个简单form组件为例子来演示。

    基本操作


    这里先简单开发一个VInput的输入框组件。组件就像一个函数,主要就是处理输入和输出。Vue3setup函数上提供了两个参数,一个props,一个是context下面的emit方法,分别来处理输入和输出。

    props

    现在VInput就是子组件,我需要它能够接受父级传递一个值,让它可以帮我做后续的逻辑处理在返回给父级。所以,这里需要最基本的一些父子通信方式v-bind,props。

    父级组件中

    <template>
       // 通过v-bind将数据想子组件传递
      <VInput :value="valueRef" />
    </template>
    
    const valueRef = ref('')
    

    VInput中

    <template>
      <input :value="value" type="text" />
    </template>
    
    <script lang="ts">
    import { defineComponent } from 'vue'
    
    export default defineComponent({
      name: 'VInput',
      props: {
        value: String
      },
      setup(props) {
        // 其他逻辑
        
        // 接受到这个值
        console.log(props.value)
        return {}
      }
    })
    </script>
    

    emit
    当我们在组件中接受参数,进行一些逻辑处理后,我们就需要将处理好的值,向外部进行一个返回,外部同时需要实现一个事件函数去接受。此时我就可以使用emit方法

    假设我们希望VInput组件返回给外部的是一个限制长度的字符串。此时外部就需要实现一个对应的事件函数去接收这个值,然后VInput内部通emit执行事件,将内部的处理好的值当做参数返回出去。

    VInput

    <template>
      <input :value="value" type="text" @input="onInput" ref="inputRef" />
    </template>
    
    <script lang="ts">
    import { defineComponent, ref } from 'vue'
    
    export default defineComponent({
      name: 'VInput',
      props: {
        value: String,
        maxLength: Number
      },
      setup(props, { emit }) {
         // Vue3中获取组件或者dom实例的一种方式
        const inputRef = ref()
        
        // 限制文字长度
        const limitLength = (value: string, maxLength: number) =>
          value.slice(0, maxLength)
        
        
        // 输入控制
        const controlled = (value: string) => {
          inputRef.value.value = value
        }
    
        const onInput = (e: any) => {
          let value = e.target.value
    
          if (typeof props.maxLength === 'number' && props.maxLength >= 0) {
            value = limitLength(value, props.maxLength)
          }
    
          controlled(value)
          
          // 向外部返回一个处理过的值
          emit('onInput', value)
        }
        return {
          onInput,
          inputRef
        }
      }
    })
    </script>
    

    父级组件

    <template>
      // 通过v-on向子组件传递一个函数,用户接受返回值
      <VInput :value="valueRef" :maxLength="10" @onInput="onInput" />
    </template>
    
    <script lang="ts">
    import { defineComponent, ref } from 'vue'
    import VInput from '@/components/VInput.vue'
    
    export default defineComponent({
      name: 'Demo',
      components: {
        VInput
      },
      setup() {
        const valueRef = ref('')
    
        const onInput = (value: string) => {
           // 接受子组件VInput返回的值
          console.log(value)
          // 改变对应的值
          valueRef.value = value
        }
    
        return {
          valueRef,
          onInput
        }
      }
    })
    </script>
    

    对于这种input的组件的使用,我猜大家肯定都不想在父级组件这么麻烦的去接收和改变一个值,所以vue是提供了v-model来更快捷的实现输入和输出。

    v-model
    通过Vue3的文档可以发现,这个指令的用法发生了一定的变化。在之前,我们要想实现一个自定义的非表单组件的双向绑定,需要通过xxxx.sync的这种语法来实现,如今这个指令已经被废除了,而是统一使用v-model这个指令。

    父级组件

    新的v-model 还可以支持多个数据的双向绑定。

    <template>
      <VBtn v-model:value="valueRef" v-model:keyword="keywordRef" />
    </template>
    复制代码
    

    自定义的非表单组件

    <template>
      <button @click="clickHandle">click</button>
    </template>
    
    export default defineComponent({
      name: 'VBtn',
      props: {
        value: String,
        keyword: String
      },
      setup(props, { emit }) {
         // 省略其他代码
         
         // 用户点击按钮
        const clickHandle = (e: any) => {
          // 省略其他代码
          
          // 修改对应的props的数据
          emit('update:value', value)
          emit('update:keyword', value + '123')
        }
        
        return {
          // ...
        }
      }
    })
    
    复制代码
    

    以上就是在Vue3中一些基本通信方式的API的介绍。在Vue3中一般都是采用Composition Api的形式开发,所以你会发现开发的时候不能在采用this.$xxx的方式去调用实例上的某个函数或者是属性。那些this.$parent,this.$children,this.$on,this.$emit等等都不能在使用了。

    那在Vue3中如何解决组件间那些通信的呢?咱们从简单到复杂的场景,一个个来分析。

    先来看一下,开发的三个form组件,组合起来的实际的用法是怎么样的:

    <template>
      <ValidateForm ref="validateFormRef1" :model="state" :rules="rules">
        <ValidateFormItem label="用户名" prop="keyword">
          <ValidateInput
            placeholder="请输入"
            required
            v-model:modelValue="state.keyword"
          />
        </ValidateFormItem>
        <ValidateFormItem label="密码" prop="password">
          <ValidateInput
            placeholder="请输入"
            required
            type="password"
            v-model:modelValue="state.password"
          />
        </ValidateFormItem>
      </ValidateForm>
      <button class="btn btn-primary" @click="submit(0)">提交</button>
    </template>
    复制代码
    

    所有组件的功能,是模仿Element UI去实现的。

    父传子


    父组件向子组件传递一个数据,可以用这两种方式:

    • v-bind
    • refs获取子组件内部某个函数,直接调用传参(这里简称refs方式)

    refs方式
    关于v-bind咱们就不细说了,在基本操作章节已经讲过其对应的使用方式了。这小节主要在中讲Vue3如何通过ref获取子组件实例并调用其身上的函数来对子组件进行传值。

    子组件

    <template>
      // 渲染从父级接受到的值
      <div>Son: {{ valueRef }}</div>
    </template>
    
    <script lang="ts">
    import { defineComponent, ref } from 'vue'
    
    export default defineComponent({
      name: 'Son',
      setup() {
        const valueRef = ref('')
        
        // 该函数可以接受父级传递一个参数,并修改valueRef的值
        const acceptValue = (value: string) => (valueRef.value = value)
    
        return {
          acceptValue,
          valueRef
        }
      }
    })
    </script>
    复制代码
    

    父组件

    <template>
      <div>sonRef</div>
      <button @click="sendValue">send</button>
      // 这里ref接受的字符串,要setup返回的ref类型的变量同名
      <Son ref="sonRef" />
    </template>
    
    <script lang="ts">
    import { defineComponent, ref } from 'vue'
    import Son from '@/components/Son.vue'
    
    export default defineComponent({
      name: 'Demo',
      components: {
        Son
      },
      setup() {
        // 如果ref初始值是一个空,可以用于接受一个实例
        // vue3中获取实例的方式和vue2略有不同
        const sonRef = ref()
    
        const sendValue = () => {
          // 可以拿到son组件实例,并调用其setup返回的所有信息
          console.log(sonRef.value)
          
          // 通过调用son组件实例的方法,向其传递数据
          sonRef.value.acceptValue('123456')
        }
    
        return {
          sonRef,
          sendValue
        }
      }
    })
    </script>
    复制代码
    

    这里可以看一下流程图:
    在这里插入图片描述
    Vue2中使用this.$refs,this.$children的方式很相似,都是通过拿到子组件实例,直接调用子组件身上的函数。方法千篇一律,不过在Vue3中没有了this这个黑盒。

    这里我们可以在控制台看一下这个sonRef.value是一个怎样的东西。

    图片

    可以发现,通过ref获取到的子组件实例上面可以拿到setup返回的所有变量和方法,同时还可以拿到其他的一些内部属性。我们可以看一下官方文档Vue 组合式 API的描述。

    在 Virtual DOM patch 算法中,如果一个 VNode 的 ref 对应一个渲染上下文中的 ref,则该 VNode对应的元素或组件实例将被分配给该 ref。这是在 Virtual DOM 的 mount / patch 过程中执行的,因此模板 ref仅在渲染初始化后才能访问。

    ref方式总结

    优点:

    • 父组件可以获取快速向确定存在的子组件传递数据
    • 传递的参数不受限制,传递方式比较灵活

    缺点:

    • ref获取的子组件必须确定存在的(不确定存在的情况:如插槽上子组件,v-if控制的子组件)
    • 子组件还需要实现接受参数的方法

    父传更深的后代


    一般往深度层级传递值,有这两种方式:

    • provide / inject
    • vuex

    provide / inject
    一看到“深”这个字,大家肯定第一想到的就Vue2中的provide / inject选项。没错,这套逻辑在vue3中同样适用,这两个选项变成了两个方法。

    provide允许我们向当前组件的所有后代组件,传递一份数据,所有后代组件能够通过inject这个方法来决定是否接受这份数据。

    大致的示意图如下:
    在这里插入图片描述

    实际应用场景

    主要应用的场景有两中,一种深度传递一个参数或者一个函数的时候,另一种是给插槽上不确定性的组件传参的时候。

    重点说一下给插槽上的组件传参。先实现一个最外层的ValidateForm组件,它主要负责接受一整个表单数据和整个表单数据的校验规则。其内部提供了一个插槽,用于放置一些不确定性的组件。还有一个ValidateFormItem组件可以接受一个字段名,通过这字段名准确知道需要校验哪个字段(tips:功能其实和element-ui类似)。

    组件化开发,需要将参数和功能进行解耦,所以我们这样来设计:

    • ValidateForm:model,rules,只管接受整份表单的数据和校验规则
    • ValidateFormItem:prop,只管接受字段名,只需知道自己需要验证哪一个字段
    <template>
      <ValidateForm ref="validateFormRef" :model="formData" :rules="rules">
        <ValidateFormItem label="用户名" prop="keyword">
          <!-- field组件 -->
        </ValidateFormItem>
        <ValidateFormItem label="密码" prop="password">
          <!-- field组件 -->
        </ValidateFormItem>
      </ValidateForm>
    </template>
    复制代码
    

    如果ValidateFormItem组件需要通过prop去效验某个字段,那它就需要拿到那份表单的数据,通过formData[prop]去取到那个字段的值,那这份formData从哪里来呢?首先不可能每写一个ValidateFormItem组件都传递一份。因为,实际开发中我们并不能确定在ValidateForm下要写多少个ValidateFormItem组件,如果每写一个都手动传递一份表单的数据,这些写起来就会多了很多冗余的代码而且也很麻烦。所以,就由ValidateForm这个组件独立接受并分发下来。

    ValidateForm

    所以我们需要ValidateForm来向下分发数据。

    <template>
      <form>
        <slot></slot>
      </form>
    </template>
    
    <script lang="ts">
    import { defineComponent, provide } from 'vue'
    
    export const modelKey = Symbol()
    export const rulesKey = Symbol()
    
    
    export default defineComponent({
      name: 'ValidateForm',
      props: {
        model: {
          type: Object
        },
        rules: {
          type: Object
        }
      },
      setup(props) {
        // 向后代发放数据
        provide(modelKey, props.model)
        provide(rulesKey, props.rules)
    
        return {}
      }
    })
    </script>
    复制代码
    

    ValidateFormItem

    ValidateFormItem接受上面传递的数据。

    <script lang="ts">
    import { defineComponent, reactive, inject, provide } from 'vue'
    import { modelKey, rulesKey } from './ValidateForm.vue'
    
    
    export default defineComponent({
      name: 'ValidateFormItem',
      props: {
        label: String,
        required: {
          type: Boolean,
          default: false
        },
        prop: String
      },
      setup(props) {
        // 接受ValidateForm传下来的数据
        const model = inject<any>(modelKey, ref({}))
        const rules = inject<any>(rulesKey, ref({}))
        
        // 根据props.prop在model和rules分别取出需要 校验的数据 和 校验的规则
        console.log(model[props.prop])
        console.log(rules[props.prop])
        // 数据校验的逻辑
    
        return {
          //...
        }
      }
    })
    </script>
    复制代码
    

    provide / inject总结

    在这篇文章Vue组件通信方式及其应用场景总结中,大佬对其的优缺点已经总结很好了。这里提一下它的缺点,就是不能解决兄弟组件的通信。

    vuex
    vuex一直以来是vue生态中一个解决不同层级组件数据共享的优质方案。不仅是在父传子中可以适用,在子传父,或者祖先传后代,后代传祖先,兄弟组件间都是一个非常好的方案。因为它是一个集中状态管理模式。其本质实现也是响应式的。这里只简单提一下Vue3中是如何使用的。

    创建一个store

    import { createStore } from 'vuex'
    
    export enum Mutarions {
      SET_COUNT = 'SET_COUNT'
    }
    
    export default createStore({
      state: {
        count: 231
      },
      getters: {
        count: state => state.count
      },
      mutations: {
        [Mutarions.SET_COUNT]: (state, num: number) => (state.count = num)
      }
    })
    复制代码
    

    父组件

    <template>
      <div>father</div>
    
      <Son ref="sonRef" />
    </template>
    
    <script lang="ts">
    import { defineComponent, ref } from 'vue'
    import Son from '@/components/Son.vue'
    import { useStore } from 'vuex'
    import { Mutarions } from '@/store/index'
    
    export default defineComponent({
      name: 'Father',
      components: {
        Son
      },
      setup() {
        const valueRef = ref(100)
    
        const store = useStore()
    
        store.commit(Mutarions.SET_COUNT, valueRef.value)
    
        return {}
      }
    })
    </script>
    复制代码
    

    子组件

    <template>
      <div>Son: {{ count }}</div>
    </template>
    
    <script lang="ts">
    import { defineComponent, computed } from 'vue'
    import { useStore } from 'vuex'
    
    export default defineComponent({
      name: 'Son',
      setup() {
        const store = useStore()
        const count = computed(() => store.getters.count)
    
        return {
          count
        }
      }
    })
    </script>
    复制代码
    

    子传父
    子级向父级传递数据,可以有这三种方式:

    • v-on
    • refs方式
    • 事件中心

    refs方式
    通过ref的方式向父级传递一个数据是同样适用的。具体思路:子组件内部实现一个函数,该函数可以返回一个值。父级组件通过ref取到子组件实例后调用该方法,得到需要的返回值。

    这里来看一下实际的应用场景,我们希望ValidateForm组件去验证下面所有的表单项,然后通过一个函数将组件内部的一个验证状态返回出去。

    父组件

    <template>
      <ValidateForm ref="validateFormRef" :model="formData" :rules="rules">
        <ValidateFormItem label="用户名" prop="keyword">
          <!-- field组件 -->
        </ValidateFormItem>
        <ValidateFormItem label="密码" prop="password">
          <!-- field组件 -->
        </ValidateFormItem>
      </ValidateForm>
    </template>
    
    <script lang="ts">
    import { defineComponent, ref } from 'vue'
    
    export default defineComponent({
      name: 'demo',
      setup() {
        // 省略部分代码
        
        const validateFormRef = ref()
        
        // 通过validate拿到ValidateForm组件内部的一个验证状态
        if (this.validateFormRef.validate()) {
          // 表单验证成功后,做后续的操作
        }
    
        return {
          validateFormRef
        }
      }
    })
    </script>
    复制代码
    

    ValidateForm

    <template>
      <form>
        <slot></slot>
      </form>
    </template>
    
    <script lang="ts">
    import { defineComponent } from 'vue'
    
    export default defineComponent({
      name: 'ValidateForm',
      setup() {
        const validate = async () => {
          let result = false
          // 调用插槽下所有ValidateFormItem组件内部的校验方法
          //(tips:至于如何调用,后面的事件中心会重点说)
          // 如果有一个校验方法返回的是false就直接返回false
          // 如果都为true就返回一个true
    
          return result
        }
    
        return {
          validate
        }
      }
    })
    </script>
    复制代码
    

    这里来看一下大致的流程图:

    图片
    通过该种方法还可以拿到子组件内部的数据,这就跟闭包函数一样的道理。

    事件中心
    这种通信方式为什么拿到这里来讲呢?因为我觉接下的实际案例用上事件中心这种方式会非常的恰当。在上一个小节中,我们留下来一个坑,那就是ValidateForm组件要去验证整个表单是否通过,就必须想办法让每个ValidateFormItem将内部的校验结果返回给它。

    首先会遇到两个问题

    1. ValidateForm下面的组件是通过插槽去挂载的,所以无法通过ref的方式去拿到每个子表单项的实例,所以就没办法拿到每个ValidateFormItem的验证状态了。
    2. 上面的章节中有一个图片,展示了通过ref拿到的组件实例。可以发现,你可以找到$parent属性,但是没有$children属性。这就很尴尬了,我们没办法像Vue2一样在ValidateForm中通过$children拿到每个子组件的实例。

    解决思路

    既然没有办法拿到插槽上的组件实例,那咱们就绕开它,通过一个事件中心的方式来解决。思路是这样的:

    1. ValidateForm实例初始化的时候,去创建一个事件中心Emitter实例,它可以注册一个事件,当这个事件被执行时可以接受一个函数,并存在一个队列中。
    2. 将这个Emitter通过provide传递给后代,保证这个事件中心在不同的ValidateForm组件中都是独立的。换句话说,就是如果写了多个ValidateForm,他们的事件中心不会相互干扰。
    3. ValidateFormItem中使用inject接收自己所在表单域的Emitter,在挂载的时候,执行Emitter上的事件,将自己的内部的validate函数,传递发送给ValidateForm,并由其将方法缓存在队列中。
    4. ValidateForm执行校验的时候,就可以执行队列中的所有校验函数,并得出校验结果。

    具体代码实现:
    先来实现一个Emitter事件中心的类

    import { EmitterHandles } from '@/type/utils'
    
    export class Emitter {
      // 存放事件函数
      private events: EmitterHandles = {}
    
      // 用于注册事件
      on(eventName: string, eventHandle: Function) {
        this.events[eventName] = eventHandle
      }
    
      // 删除事件
      off(eventName: string) {
        if (this.events[eventName]) {
          delete this.events[eventName]
        }
      }
    
      // 触发事件
      emit(eventName: string, ...rest: any[]) {
        if (this.events[eventName]) {
          this.events[eventName](...rest)
        }
      }
    }
    复制代码
    

    当事件中心实现好了,这里来完善一下ValidateForm的代码

    <script lang="ts">
    import { defineComponent, nextTick, provide } from 'vue'
    import { Emitter } from '@/utils/emitter'
    
    type ValidateFunc = () => boolean
    
    export const emitterKey = Symbol()
    export const modelKey = Symbol()
    export const rulesKey = Symbol()
    
    
    export default defineComponent({
      name: 'ValidateForm',
      props: {
        model: {
          type: Object
        },
        rules: {
          type: Object
        }
      },
      setup(props) {
        // 将表单数据和验证规则传递给后代
        provide(modelKey, props.model)
        provide(rulesKey, props.rules)
        
        // 创建事件中心的实例
        const emitter = new Emitter()
        // 将事件中心传递给后代
        provide(emitterKey, emitter)
    
        // 接受formItem组件返回的验证函数
        // 并且将其存起来
        emitter.on('acceptValidate', (validateFunc: ValidateFunc) => {
          validateList.push(validateFunc)
        })
       
        // 用于接受保存后代返回的验证方法
        const validateList: ValidateFunc[] = []
    
        // 验证所有数据的状态
        const validate = () => {
          // 执行每一个子表单发送过来的验证方法
         return validateList.map(fn => fn()).every(valid => valid)
        }
    
        return {
          validate
        }
      }
    })
    </script>
    复制代码
    

    ok,现在实现了validateForm的逻辑,我们再来写一下validateFormItem的逻辑

    <template>
      <div class="form-group">
        <label v-if="label" class=" col-form-label">{{ label }}</label>
        <slot></slot>
        <small v-if="error.isError" class="invalid-feedback">
          {{ error.errorMessage }}
        </small>
      </div>
    </template>
    
    <script lang="ts">
    import { Emitter } from '@/utils/emitter'
    import { defineComponent, reactive, inject, onMounted, provide } from 'vue'
    import { emitterProviderKey, modelKey, rulesKey } from './ValidateForm.vue'
    
    export default defineComponent({
      name: 'ValidateFormItem',
      props: {
        label: String,
        required: {
          type: Boolean,
          default: false
        },
        prop: String
      },
      setup(props) {
        // 接受Emitter事件中心
        const emitter = inject<Emitter>(emitterProviderKey)
        // 接受数据和校验规则
        const model = inject<any>(modelKey)
        const rules = inject<any>(rulesKey)
    
        const error = reactive({
          isError: false,
          errorMessage: ''
        })
        
        // 校验对应的字段数据
        const validateField = () => {
          const prop = props.prop
          if (prop && model && rules && rules[prop]) {
            const result = rules[prop].some((item: any) => {
              if (!item.validator(model[prop])) {
                console.warn(`${prop}:${item.message}`)
                error.isError = true
                error.errorMessage = item.message
                return true
              }
            })
            return !result
          }
          return true
        }
       
       
        // 当组件挂载的时候,将自身的校验函数发送给ValidateForm组件
        onMounted(() => {
          emitter && emitter.emit('acceptValidate', validateField)
        })
    
        return {
          error
        }
      }
    })
    </script>
    复制代码
    

    为了更详细的理解上面的过程,这里来画一个示意图:

    1. 注册事件,分发事件中心

    图片
    2. 执行事件,发送验证函数
    图片
    整个过程的总结就是,顶层组件创建和分发事件中心,并注册事件监听函数。后代组件执行该事件然后发送信息,顶层组件回收信息。

    Tips

    这里再提一点,在使用Emitter这个事件中心的时候,是在ValidateFormsetup中去创建并且去下发的,并不是使用一个全局的事件中心。就像大佬的这篇文章Vue组件通信方式及其应用场景总结中总结到的,事件总线的形式是有一个致命缺点的,如果一个页面上有多个公共组件,我们只要向其中的一个传递数据,但是每个公共组件都绑定了数据接受的方法,那就会出现混乱的情况。但是,我们的事件总线不是一个全局的,而是单个作用域里面的一个事件中心。

    因为事件中心是在当前组件内部创建,并使用provide向下发布的,这样就只有当前组件的后代才能使用这个事件中心。所以,就算一个面上写了多个ValidateForm,他们的校验都是独立的。

    <template>
      <ValidateForm ref="validateFormRef1" :model="formData1" :rules="rules">
        <ValidateFormItem label="用户名" prop="keyword">
          <!-- field组件 -->
        </ValidateFormItem>
        <ValidateFormItem label="密码" prop="password">
          <!-- field组件 -->
        </ValidateFormItem>
      </ValidateForm>
      
        <ValidateForm ref="validateFormRef2" :model="formData2" :rules="rules">
        <ValidateFormItem label="用户名" prop="keyword">
          <!-- field组件 -->
        </ValidateFormItem>
        <ValidateFormItem label="密码" prop="password">
          <!-- field组件 -->
        </ValidateFormItem>
      </ValidateForm>
    </template>
    复制代码
    

    示意图:
    图片
    事件中心总结

    优点:

    1. 可以解决Vue3不能使用this.$children的问题
    2. 可以灵活使用,不受组件层级的限制
    3. 这种通信方式不受框架的限制

    缺点:

    1. 需要控制好事件中心的作用范围
    2. 需要控制好事件名的规范

    事件中心进阶


    因为在Vue3Composition API中,vue的功能api更加的颗粒化。我们可以对事件中心进行一个自定义需求的改造。

    可以通过引入reactive, ref帮助我们的事件中心内部维护一个响应式的数据,可以实现当事件中心进行一定通信行为时,去更新对应的视图。还可以引入computed实现计算属性的功能。

    import { reactive, ref, computed } from 'vue'
    
    export class Emitter {
      // 响应式的数据中心
      private state = reactive({})
      private events: EmitterHandles = ref({})
      
      // 记录当前事件中心 事件的数量
      private eventLength = computed(() => Object.keys(events.value).length)
      
      // 省略部分代码
    }
    
    复制代码
    

    加入watch,watchEffect实现数据监听做出一定逻辑行为的功能。我认为Composition APIReact Hooks Api都是非常强大,因为它们允许我们将功能函数当成积木一样去任意组装成我们希望得到的应用程序。

    深层后代向顶层通信,兄弟通信


    我觉得其实其他的场景,其通信方式基本都差不多了,所谓千篇一律。后代向祖先传值,或者兄弟组件传值,都可以使用vuex或者是事件中心的方式。兄弟层级,或者相邻层级的,就可以使用ref,$parent等方式。

    如果有帮助,请给个赞与关注

    展开全文
  • 主要介绍了vue实现父子组件之间的通信以及兄弟组件通信功能,结合实例形式分析了vue.js组件通信相关操作技巧,需要的朋友可以参考下
  • Vue 3.0父子组件通信

    千次阅读 2020-09-23 18:05:57
    在Vue 3.0 发布以后,我们基于新的特性,来归纳一下父子组件通信的方式。并且检验一下Vue 2.0中常用的通信方式,如何在Vue 3.0中使用。本文列出了三种通信方式: 通过emit函数派发消息 父组件通过事件代理获取子...

    在Vue 3.0 发布以后,我们基于新的特性,来归纳一下父子组件通信的方式。并且检验一下Vue 2.0中常用的通信方式,如何在Vue 3.0中使用。本文列出了三种常用的通信方式:

    1. 子组件通过emit函数派发消息给父组件
    2. 父组件通过事件代理获取子组件内容
    3. 通过vuex实现父子组件数据共享
    4. 通过依赖注入实现父子组件数据共享

    设计和实现

    本文通过一个Tabs组件实例来说明父子组件的通信方式,用户可以将Tabs数据和初始默认选中的Tab项作为属性传入Tabs组件。组件中每个月份为一个Tab项,每个Tab项内包含当月的销量数据。

    点击Tabs项的链接,页面显示对应月份的销量数据。具体如下图所示:
    在这里插入图片描述

    根据上述需求,Tabs组件包括TabItem和TabContent两个子组件。

    • TabItem 显示月份信息
    • TabContent 显示指定月份的销量数据
    • Tabs,TabItem和TabContent共享当前点击的月份信息,即哪个Tabs项被选中。
    • 数据结构请参看附录

    最终实现的用法如下,data见附录:

    <tabs :data="data" :initialIdx="1"  >
    

    Tabs组件目录接构如下:

    • index.vue Tabs的最外层组件,可以接收用户传入的Tabs数据和默认选中的Tabs项
    • tab-item.vue Tabs的子组件,用于显示每个Tabs项的月份信息
    • tab-content.vue Tabs的子组件,用于显示每个Tabs项的销量数据

    基于emit函数实现父子组件通信

    父组件Tabs关键代码如下:

    <template>
      <div>
          <div class="Tabs">
              <tab-item
                v-for = "item in data"
                :key = "item.Id"
                :text = "item.Month"
                :curIdx = "CurrentIdx"
                @click-item = "clickItem"
              >
              </tab-item>
          </div>
          <tab-content :curIdx = "CurrentIdx" :data = "data"></tab-content>
      </div>
    </template>
    <script>
    import TabItem from "./tab-item";
    import TabContent from "./tab-content";
    import {onMounted, reactive, toRefs} from "vue";
    export default {
        name: "Tabs",
        props:{
            data:{
                type:Array,
                default:()=>[]
            },
            initialIdx: {
                type:Number,
                default:1
            }
        },
        components:{
            TabItem,
            TabContent
        },
        setup(){
            const state = reactive({CurrentIdx:0});
            onMounted(()=>{
                state.CurrentIdx = props.initialIdx;
            });
            const clickItem = (index)=>{
                state.CurrentIdx = index;
            };
            return {
                ...toRefs(state),
                clickItem
            };
        }
    }
    </script>
    
    • CurrentIdx定义为响应式数据,子组件通过curIdx属性获取当前选中的tab项。
    • onMounted是composition API,使用方式和Vue2.0的钩子函数类似。
    • …toRefs(state)将响应式数据平铺,以便于视图中绑定CurrentIdx。

    子组件TabItem关键代码如下:

    <template>
      <div class="tab-item" @click = "clickItem()">
          <a href="javascript:;" 
          :class = "{active: index === curIdx}"
          >{{text}}</a>
      </div>
    </template>
    <script>
    import {getCurrentInstance} from 'vue'
    export default {
        name: "TabItem",
        props: {
            text: String,
            curIdx: Number
        },
        setup(props,ctx){
            const instance = getCurrentInstance();
            const index = instance.vnode.key;
            const clickItem = ()=>{
                ctx.emit("click-item",index);
            }
            return {index, clickItem};
        }
    }
    </script>
    
    • 父组件Tabs在渲染的每个TabItem时,已经将key值传入,作为每个Tabs项的Id。通过Composition API “getCurrentInstance”获取当前组件实例,然后即可获取当前tab项的Id,这样做的好处是我们省去了定义其他属性获取Id的步骤。
    • 当点击TabItem时候,需要将父组件Tabs的响应式数据CurrentIdx进行修改,显然该步骤显然在TabItem组件中无法完成。
    • 因此通过自定义事件click-item,将当前Tabs项的Id通过emit函数传递给父组件Tabs,在Tabs中的clickItem函数中修改CurrentIdx。
    • 调用emit函数不同于Vue2.0的this.$emit方式,通过setup的第二个参数,上下文参数对象,调用emit函数。

    子组件TabContent关键代码如下:

    <template>
      <div class="tab-content">
          <h1>{{item.title}}</h1>
          <p>Sales: {{ item.content}}</p>
      </div>
    </template>
    
    <script>
    import {watch} from 'vue'
    export default {
        name: "TabContent",
        props:{
            curIdx:Number,
            data:{
                type: Array,
                default: ()=>[]
            },
        },
        setup(props){
            const item = {};
            watch(()=>{
                return props.curIdx;
            },(val)=>{
                const {Month,Sales} = props.data.filter(item=>item.Id === val)[0];
                item.title = Month;
                item.content = Sales;
            });
            return {item};
        }
    }
    </script>
    
    • TabContent通过属性接收到curIdx,如果该值发生变化,则需要重新查找指定月份的数据
    • 因此通过watch函数对curIdx进行监听,该函数包括两个函数参数,第一个函数返回要监听的数据,第二个函数在监听数据发生变化后执行。
    • watch函数的使用不同于Vue2.0,该函数也是Composition API,通过对象解构的方式引入。

    基于事件代理父子组件通信

    父组件Tabs关键代码如下:

    <template>
      <div>
          <div class="Tabs" @click = "clickItem($event)">
              <tab-item
                v-for = "item in data"
                :key = "item.Id"
                :text = "item.Month"
                :curIdx = "CurrentIdx"           
              >
              </tab-item>
          </div>
          <tab-content :curIdx = "CurrentIdx" :data = "data"></tab-content>
      </div>
    </template>
    <script>
    import TabItem from "./tab-item";
    import TabContent from "./tab-content";
    import {onMounted, reactive, toRefs} from "vue";
    export default {
        name: "Tabs",
        props:{
            data:{
                type:Array,
                default:()=>[]
            },
            initialIdx: {
                type:Number,
                default:1
            }
        },
        components:{
            TabItem,
            TabContent
        },
        setup(){
            const state = reactive({CurrentIdx:0});
            onMounted(()=>{
                state.CurrentIdx = props.initialIdx;
            });
            const clickItem = (e)=>{
                state.CurrentIdx = Number(e.target.getAttribute("date-index")) || 1;
            };
            return {
                ...toRefs(state),
                clickItem
            };
        }
    }
    </script>
    

    子组件TabItem关键代码如下:

    <template>
      <div class="tab-item" :data-index="index">
          <a href="javascript:;" 
          :class = "{active: index === curIdx}"
          >{{text}}</a>
      </div>
    </template>
    
    <script>
    import {getCurrentInstance} from 'vue'
    export default {
        name: "TabItem",
        props: {
            text: String,
            curIdx: Number
        },
        setup(props,ctx){
            const instance = getCurrentInstance();
            const index = instance.vnode.key;
            return {index};
        }
    }vuex
    </script>
    

    子组件TabContent关键代码同上。

    • 通过事件代理的方式获取当前选中Tabs项Id,因此将clickItem方法绑定在父组件Tabs上
    • TabItem不再需要绑定任何click事件,只需要将当前Tabs项的Id,绑定到data-index属性上,便于父组件通过事件代理函数的参数获取。
    • 在点击TabItem组件后,利用事件冒泡原理,触发Tabs的click方法,通过方法参数,获取当前的Tabs项Id。
    • 在点击事件的方法中,将父组件Tabs中的CurrentIdx的值修改为当前点击的Tabs项Id,该Id通过事件代理参数获取。

    基于vuex实现父子组件通信

    父组件Tabs关键代码如下

    <template>
      <div>
          <div class="Tabs" >
              <tab-item
                v-for = "item in data"
                :key = "item.Id"
                :text = "item.Month"
                :curIdx = "CurrentIdx"         
              >
              </tab-item>
          </div>
          <tab-content :curIdx = "CurrentIdx" :data = "data"></tab-content>
      </div>
    </template>
    <script>
    import TabItem from "./tab-item";
    import TabContent from "./tab-content";
    import {onMounted,computed} from "vue";
    import {useStore} from "vuex"
    export default {
        name: "Tabs",
        props:{
            data:{
                type:Array,
                default:()=>[]
            },
            initialIdx: {
                type:Number,
                default:1
            }
        },
        components:{
            TabItem,
            TabContent
        },
        setup(props,ctx){
            const store = useStore();
            const state = store.state;
            onMounted(()=>{
                ctx.commit("setCurIdx", props.initialIdx);
            });
            const CurrentIdx = computed(()=>{
                return store.state.curIdx;
            })
            return {CurrentIdx};
        }
    }
    </script>
    

    子组件TabItem关键代码如下:

    <template>
      <div class="tab-item" @click ="clickItem()">
          <a href="javascript:;" 
          :class = "{active: index === curIdx}"
          >{{text}}</a>
      </div>
    </template>
    
    <script>
    import {useStore} from "vuex"
    import {getCurrentInstance} from 'vue'
    export default {
        name: "TabItem",
        props: {
            text: String,
            curIdx: Number
        },
        setup(props,ctx){
            const store = useStore();
            const state = store.state;
            const instance = getCurrentInstance();
            const index = instance.vnode.key;
            const clickItem = ()=>{
                state.commit("setCurIdx",index);
            }
            return {index, clickItem};
        }
    }
    </script>
    

    store/index.js 代码如下:

    import {createStore} from "vuex"
    export default createStore({
        state: {curIdx: 1,},
        mutations:{
            setCurIdx(state,curIdx){
                state.curIdx = curIdx;
            }
        }
    });
    
    • 选中Tabs项的Id不再通过父组件进行管理,而是通过全局store对象进行管理。
    • clickItem函数放到TabItem中执行即可,子组件通过调用mutations中的setCurIdx方法,直接修改选中的TabItem的Id。
    • 父组件Tabs通过设置计算属性CurrentIdx,实现组件的联动。即无论store中的curIdx在那个组件被修改,父组件立刻就得知该变化。

    基于依赖注入的父子组件通信

    父组件Tabs关键代码如下:

    <template>
      <div>
        <div class="Tabs">
          <tab-item v-for="item in data" :key="item.Id" :text="item.Month" />
        </div>
        <tab-content :data="data" />
      </div>
    </template>
    
    <script>
    import TabItem from "./item";
    import TabContent from "./tabcontent";
    import { provide, reactive, toRefs } from "vue";
    export default {
      name: "TabsDI",
      props: {
        data: {
          type: Array,
          default: () => []
        },
        InitialIdx: { type: Number, default: 1 }
      },
      components: {
        TabItem,
        TabContent
      },
    
      setup(props) {
        const state = reactive({
          currentIdx: props.InitialIdx
        });
        const clickItem = index => {
          state.currentIdx = index;
        };
        const data = toRefs(state);
        provide("currentIdx", data.currentIdx);
        provide("clickItem", clickItem);
        return {
          ...data
        };
      }
    };
    </script>
    
    • 子组件TabItem和TabContent 不再需要通过属性获取当前选中项的Id
    • 父组件TabsDI通过provide方法将当前选中项currentIdx 和修改该Id的方法clickItem传递给子组件
    • 本例子通过reactive方法创建响应式数据,请注意const data = toRefs(state);这句不可以省略。state.currentIdx只是一个数值,必须通过该方法转换为响应式数据。否则组件无法监控器变化。

    子组件TabItem关键代码如下:

    <template>
      <div class="tab-item" @click="clickItem(index)">
        <a href="javascript:;" :class="{ active: curIdx === index }">{{ text }}</a>
      </div>
    </template>
    
    <script>
    import { getCurrentInstance, inject } from "vue";
    export default {
      name: "TabItem",
      props: {
        text: String
      },
      setup() {
        const instance = getCurrentInstance();
        const index = instance.vnode.key;
        const clickItem = inject("clickItem");
        const curIdx = inject("currentIdx");
    
        return { curIdx, index, clickItem };
      }
    };
    </script>
    
    • 子组件通过inject方法获取当前选中项的Id
    • 子组件通过inject方法获取修改父组件选中项的方法

    子组件TabContent关键代码如下:

    <template>
      <div class="tab-content">
        <h1>{{ item.title }}</h1>
        <p>Sales: {{ item.content }}</p>
      </div>
    </template>
    
    <script>
    import { inject, computed } from "vue";
    export default {
      name: "TabContent",
      props: {
        curIdx: {
          type: Number,
          default: 0
        },
        data: {
          type: Array,
          default: () => []
        },
        text: String
      },
      setup(props) {
        const val = inject("currentIdx");
        const item = computed(() => {
          const { Month, Sales } = props.data.filter(
            item => item.Id === val.value
          )[0];
          return {
            title: Month,
            content: Sales
          };
        });
        return { item };
      }
    };
    </script>
    
    • 通过inject方法获取当前选中项Id
    • 因为本例不再使用属性传值的方式获取当前选中项Id,所以不再使用watch方法监控属性变化
    • 由于当前选中项Id值currentIdx依然是响应式数据,因此采用computed方法监控器变化。如果变化则更新销量数据

    附录

    数据结构

    export default [
    	{Id:1,Month:"January",Sales:100000},
    	{Id:1,Month:"February",Sales:200000},
    	{Id:1,Month:"March",Sales:300000},
    	{Id:1,Month:"April",Sales:300000},
    	{Id:1,Month:"May",Sales:400000},
    ]
    
    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 372,435
精华内容 148,974
关键字:

组件通信