Vue组件间通信方式详解


一、组件间通信的概念

开始之前,我们把组件间通信这个词进行拆分

  • 组件
  • 通信

都知道组件是vue最强大的功能之一,vue中每一个.vue我们都可以视之为一个组件通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的。广义上,任何信息的交通都是通信组件间通信即指组件(.vue)通过某种方式来传递信息以达到某个目的举个栗子我们在使用UI框架中的table组件,可能会往table组件中传入某些数据,这个本质就形成了组件之间的通信

二、组件间通信解决了什么

vue中,每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统。

三、组件间通信的分类

组件间通信的分类可以分成以下

  • 父子组件之间的通信
  • 兄弟组件之间的通信
  • 祖孙与后代组件之间的通信
  • 非关系组件间之间的通信

关系图:

img

四、组件间通信的方案

整理vue中8种常规的通信方案

  1. 通过 props 传递
  2. 通过 $emit 触发自定义事件
  3. 使用 ref
  4. EventBus
  5. $parent 或$root
  6. attrs 与 listeners
  7. Provide 与 Inject
  8. Vuex

props传递数据

img

  • 适用场景:父组件传递数据给子组件
  • 子组件设置props属性,定义接收父组件传递过来的参数
  • 父组件在使用子组件标签中通过字面量来传递值

Children.vue

props:{  
    // 字符串形式  
 name:String // 接收的类型参数  
    // 对象形式  
    age:{    
        type:Number, // 接收的类型为数值  
        defaule:18,  // 默认值为18  
       require:true // age属性必须传递  
    }  
}  

Father.vue组件

<Children name="jack" age=18 />  

$emit 触发自定义事件

  • 适用场景:子组件传递数据给父组件
  • 子组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
  • 父组件绑定监听器获取到子组件传递过来的参数

Children.vue

this.$emit('add', good)  

Father.vue

<Children @add="cartAdd($event)" />  

ref

  • 父组件在使用子组件的时候设置ref

  • 父组件通过设置子组件ref来获取数据

    • 如果ref用在子组件上,指向的是组件实例,可以理解为对子组件的索引,通过ref可能获取到在子组件里定义的属性和方法。

    • 如果ref在普通的 DOM 元素上使用,引用指向的就是 DOM 元素,通过$ref可能获取到该DOM 的属性集合,轻松访问到DOM元素,作用与JQ选择器类似。

<!-- 父组件 -->
   
   <template>
     <div>
       <h1>我是父组件!</h1>
       <child ref="msg"></child>
     </div>
   </template>
   
   <script>
     import child from '../components/child.vue'
     export default {
       components: {child},
       mounted: function () {
         console.log( this.$refs.msg);
         this.$refs.msg.getMessage('我是子组件一!')
       }
     }
   </script>

   <!-- 子组件 -->
    
   <template>
    <h3>{{message}}</h3>
   </template>
   <script>
    export default {
     data(){
      return{
       message:''
      }
     },
     methods:{
      getMessage(m){
       this.message=m;
      }
     }
    }
   </script>

prop和$ref之间的区别:

  1. prop 着重于数据的传递,它并不能调用子组件里的属性和方法。像创建文章组件时,自定义标题和内容这样的使用场景,最适合使用prop。
  2. ref 着重于索引,主要用来调用子组件里的属性和方法,其实并不擅长数据传递。而且ref用在dom元素的时候,能使到选择器的作用,这个功能比作为索引更常有用到。

EventBus

  • 使用场景:兄弟组件传值
  • 创建一个中央事件总线EventBus
  • 兄弟组件通过$emit触发自定义事件,$emit第二个参数为传递的数值
  • 另一个兄弟组件通过$on监听自定义事件
// 创建一个中央时间总线类  
class Bus {  
  constructor() {  
    this.callbacks = {};   // 存放事件的名字  
  }  
  $on(name, fn) {  
    this.callbacks[name] = this.callbacks[name] || [];  
    this.callbacks[name].push(fn);  
  }  
  $emit(name, args) {  
    if (this.callbacks[name]) {  
      this.callbacks[name].forEach((cb) => cb(args));  
    }  
  }  
}  
  
// main.js  
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上  
// 另一种方式  
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能  

在发送方组件中,使用 $emit 来触发一个自定义事件,并传递需要传递的数据:

import { EventBus } from './event-bus.js';

export default {
  methods: {
    sendData() {
      const data = 'Hello, EventBus!';
      EventBus.$emit('custom-event', data);
    }
  }
}

最后,在接收方组件中,使用 $on 来监听该自定义事件,并处理接收到的数据:

import { EventBus } from './event-bus.js';

export default {
  created() {
    EventBus.$on('custom-event', this.handleCustomEvent);
  },
  destroyed() {
    EventBus.$off('custom-event', this.handleCustomEvent);
  },
  methods: {
    handleCustomEvent(data) {
      console.log(data); // 输出:Hello, EventBus!
    }
  }
}

$parent 或 $root

  • 通过共同祖辈$parent或者$root搭建通信桥连

兄弟组件

this.$parent.on('add',this.add)

另一个兄弟组件

this.$parent.emit('add')

$attrs 与 $listeners

  • 适用场景:祖先传递数据给子孙
  • 设置批量向下传属性$attrs$listeners
    • $attrs:包含了父作用域中不被 prop 所识别(且获取)的特性绑定(class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定(class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
    • $listeners:包含了父作用域中的(不含 .native 修饰器的)v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。
  • 可以通过 v-bind="$attrs" 传⼊内部组件

首先,在父组件中,我们可以使用 $attrs 将父组件中所有非props属性传递给子组件,并使用 $listeners 将父组件中所有事件监听器传递给子组件:

<template>
  <div>
    <child-component v-bind="$attrs" v-on="$listeners"></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentData: '这是父组件的数据'
    };
  }
};
</script>

然后,在子组件中,我们可以使用 $attrs 来访问父组件传递的非props属性,使用 $listeners 来注册父组件传递的事件监听器:

<template>
  <div>
    <p>{{ $attrs.parentData }}</p>
    <button @click="$listeners.clickEvent">触发父组件事件</button>
  </div>
</template>

<script>
export default {
  mounted() {
    console.log(this.$attrs.parentData); // 输出:这是父组件的数据
  }
};
</script>

provide 与 inject

  • 在祖先组件定义provide属性,返回传递的值
  • 在后代组件通过inject接收组件传递过来的值
  1. 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。

    provide / inject API 主要解决了跨级组件间的通信问题,而不需要在每个中间组件中显式地传递。不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

    代码示例:

    首先,在父组件中,我们可以使用 provide 来提供数据给子孙组件:

    <template>
      <div>
        <child-component></child-component>
      </div>
    </template>
    
    <script>
    export default {
      provide: {
        parentData: '这是父组件的数据'
      }
    };
    </script>

    在上面的示例中,我们使用 provide 来提供一个名为 parentData 的数据给子孙组件。

    然后,在孙子组件中,我们可以使用 inject 来注入父组件提供的数据:

    <template>
      <div>
        <grandchild-component></grandchild-component>
      </div>
    </template>
    
    <script>
    export default {
      inject: ['parentData']
    };
    </script>

vuex

  • Vuex的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变 store 中的状态的唯一途径就是显式地提交(commitmutation。这样使得我们可以方便地跟踪每一个状态的变化。

小结

  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref
  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递
  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject
  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

Observable

Observable 翻译过来我们可以理解成可观察的

我们先来看一下其在Vue中的定义

Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象

返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器

Vue.observable({ count : 1})

其作用等同于

new vue({ count : 1})

Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象

Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的

使用场景

在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择

创建一个js文件

// 引入vue
import Vue from 'vue
// 创建state对象,使用observable让state对象可响应
export let state = Vue.observable({
  name: '张三',
  'age': 38
})
// 创建对应的方法
export let mutations = {
  changeName(name) {
    state.name = name
  },
  setAge(age) {
    state.age = age
  }
}

.vue文件中直接使用即可

<template>
  <div>
    姓名:{{ name }}
    年龄:{{ age }}
    <button @click="changeName('李四')">改变姓名</button>
    <button @click="setAge(18)">改变年龄</button>
  </div>
</template>
import { state, mutations } from '@/store
export default {
  // 在计算属性中拿到值
  computed: {
    name() {
      return state.name
    },
    age() {
      return state.age
    }
  },
  // 调用mutations里面的方法,更新数据
  methods: {
    changeName: mutations.changeName,
    setAge: mutations.setAge
  }
}

原理分析

源码位置:src\core\observer\index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 判断是否存在__ob__响应式属性
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 实例化Observer响应式对象
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that have this object as root $data

    constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
            if (hasProto) {
                protoAugment(value, arrayMethods)
            } else {
                copyAugment(value, arrayMethods, arrayKeys)
            }
            this.observeArray(value)
        } else {
            // 实例化对象是一个对象,进入walk方法
            this.walk(value)
        }
}

walk函数

walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍历key,通过defineReactive创建响应式对象
    for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
    }
}

defineReactive方法

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  // 接下来调用Object.defineProperty()给对象定义响应式属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 对观察者watchers进行通知,state就成了全局响应式对象
      dep.notify()
    }
  })
}

文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录