前言

虽然Vue3出来很久并且非常成熟了,但市面上的Vue项目中Vue2依然还是占半壁江山的。小弟当年刚入行时接触就是Vue2项目,后来Vue3流行之后慢慢开始在Vue3Vue2项目中反复穿插。因此想通过整合最简单易懂的事例全面总结一下Vue2的传参方式。

先直入主题列出有哪些传参方式,下面再通过事例一一讲解。

  • props(父传子)
  • $emitv-on (子传父)
  • EventBus (兄弟传参)
  • .syncupdate: (父子双向)
  • v-model (父子双向)
  • ref
  • $children$parent
  • $attrs$listeners (爷孙双向)
  • provideinject (多层传参)
  • Vuex (全局)
  • Vue.prototype (全局)
  • 路由
  • 浏览器缓存 (全局)
  • window (全局)
  • $root (顶层)
  • slot(父传子)

一、props(父传子)

思路简述:父组件直接用冒号:绑定变量,然后子组件通过props接收父组件传过来的内容。

父组件代码: 核心代码在第3行,直接用:message="message"传参。

<template>
  <div>
    <child  :message="message" />
  </div>
</template>

<script>
import child  from './child .vue';
export default {
  components: {
    child 
  },
  data() {
    return {
      message: '这是父组件传过去的'
    };
  }
};
</script>

子组件代码:props接收消息后可以直接使用,如下第3行和第16行中直接使用

注意: props有两种接收方法,如下第9行注释的就是简写用法,此用法不能设置默认值。

<template>
  <div>
    <p>接收到的消息: {{ message }}</p>
  </div>
</template>

<script>
  export default {
    //props:['message'],
    props: {
      message: {
        type: String,
        default: '', // 这里能设置默认值,如果父组件没有传参,默认值会生效
      },
    },
    mounted() {
      console.log(this.message);
    },
  };
</script>

注意: 此传参方式是单向的,即子组件接收到父组件的数据后,是不能直接修改props接收的数据,否则会直接报错。

二、$emit与v-on (子传父)

思路简述: 子组件通过$emit触发父组件的指定方法并且在此方法中携带任意参数,父组件通过在被触发的方法中拿到携带的参数完成子传父。

语法:$emit(方法名,参数)

子组件代码: 核心代码在第11行,触发方法并且携带参数。

<template>
  <div>
    <button @click="sendParent">点击发送数据给父组件</button>
  </div>
</template>

<script>
  export default {
    methods: {
      sendParent() {
        this.$emit('my-event', '这是传递的参数');
      },
    },
  };
</script>

父组件代码: 核心代码在第3行触发事件,获取到子组件的传参。

<template>
  <div>
    <child v-on:my-event="childEvent" />
    // 或者用@简写代替v-on
    <child @my-event="childEvent" />
  </div>
</template>

<script>
  import child from './child.vue';
  export default {
    components: {
      child,
    },
    methods: {
      childEvent(item) {
        console.log('接收到子组件的传参:', item);
      },
    },
  };
</script>

三、EventBus (兄弟传参)

思路简述: 先创建一个全局的事件总线Bus(可以随意命名),并挂载在Vue.prototype上。

然后兄弟组件A通过$emit发送参数,兄弟组件B通过$on接收参数。

  • 有两种使用方法,下面分别讲解。

方法一: 直接挂载全局事件总线,全局直接使用不需要额外引入。

  • 先在项目的入口文件中(main.jsmain.ts)创建全局事件Bus并且挂载在Vue.prototype*
import Vue from 'vue';

const Bus = new Vue();

Vue.prototype.$Bus = Bus;

兄弟组件A代码: 通过this.$Bus.$emit(方法名,参数)发送参数。

<template>
  <div>
    <button @click="sendSibling">点击发送内容给兄弟组件</button>
  </div>
</template>

<script>
  export default {
    methods: {
      sendSibling() {
        this.$Bus.$emit('my-event', '参数');
      },
    },
  };
</script>

兄弟组件B代码: 通过this.$Bus.$on(对应$emit的方法,本地方法)触发本地的方法,从而接收参数。

<template>
  <div> 我是兄弟组件B </div>
</template>

<script>
  export default {
    created() {
      this.$Bus.$on('my-event', this.handleMessage);
    },
    beforeDestroy() {
      this.$Bus.$off('my-event', this.handleMessage);
    },
    methods: {
      handleMessage(message) {
        console.log('来自兄弟的参数:', message);
      },
    },
  };
</script>

注意: 如上第10-12行所示,在组件销毁前要在 beforeDestroy 生命周期中使用$off移除移除$on的事件监听器,防止避免内存泄漏影响性能。如下所示

方法二: 不创建全局事件总线,单独开一个文件,哪里需要就哪里引用。

  • 创建一个单独文件命名为Bus.js(可以自由命名)
import Vue from "vue"

export default new Vue()

兄弟组件A代码: 先引入Bus.js文件,然后通过Bus.$emit(方法名,参数)发送参数。

<template>
  <div>
    <button @click="sendSibling">点击发送内容给兄弟组件</button>
  </div>
</template>

<script>
  import Bus from './Bus.js';
  export default {
    methods: {
      sendSibling() {
        Bus.$emit('my-event', '参数');
      },
    },
  };
</script>

兄弟组件B代码: 先引入Bus.js文件,然后通过Bus.$on(对应$emit的方法,本地方法)触发本地的方法,从而接收参数。同样也需要使用$off销毁事件监听。

<template>
  <div> 我是兄弟组件B </div>
</template>

<script>
  import Bus from './Bus.js';
  export default {
    created() {
      Bus.$on('my-event', this.handleMessage);
    },
    beforeDestroy() {
      Bus.$off('my-event', this.handleMessage);
    },
    methods: {
      handleMessage(message) {
        console.log('来自兄弟的参数:', message);
      },
    },
  };
</script>

四、.sync与update: (父子双向)

思路简述:.sync其实是一个语法糖, 配合子组件用this.$emit('update:绑定的属性名', 方法)修改父组件属性, 能解决props只能单向传递的问题。

父组件代码: 核心代码在第3行,比普通的父传子多使用了.sync修饰符。

<template>
  <div>
    <chile :myprop.sync="myData" />
  </div>
</template>

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

export default {
  components: {
    chile
  },
  data() {
    return {
      myData: '父组件数据'
    };
  }
};
</script>

子组件代码: 核心代码是第14行,通过this.$emit同步修改父组件内容。

<template>
  <div>
    <button @click="updateData">点击子修改父传过来的数据</button>
  </div>
</template>

<script>
  export default {
    props: {
      myprop: String,
    },
    methods: {
      updateData() {
        this.$emit('update:myprop', 新内容);
      },
    },
  };
</script>

注意: 使用.sync修饰符时,this.$emit里面总是以 update: 开头,后面接要修改的属性名称。

五、v-model (父子双向)

思路简述:v-model最常用于表单,它其实是一个语法糖,并且和上面.sync有点类似。v-model本质上是v-bind:value@input组件效果。通过v-bind:value绑定数据父传子,通过@input触发对应事件子传父从而实现双向绑定。

父组件代码: 直接用v-model绑定要传给子组件的参数,当子组件触发 input 事件时父组件myData会同步更新。

<template>
  <div>
    <child v-model="myData" />
  </div>
</template>

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

export default {
  components: {
    child
  },
  data() {
    return {
      myData: '天天鸭'
    };
  }
};
</script>

子组件代码:input输入框的内容发生变化时,就会触发 @input 事件,然后this.$emit同步修改父组件的值。

<template>
  <div>
    <input :value="childData" @input="handleChange" />
  </div>
</template>

<script>
export default {
  model: {
    prop: 'myProp',
    event: 'input'
  },
  props: {
    myProp: String
  },
  data() {
    return {
      childData: this.myProp
    };
  },
  methods: {
    handleChange(event) {
      this.childData = event.target.value;
      this.$emit('input', this.childData);
    }
  }
};
</script>

注意: 在子组件当中,必须要定义model来指定 props 和事件名称(名称默认为 input)。

六、ref

思路讲解: ref主要用来访问子组件的方法和属性,是直接操纵DOM的方式。主要用法是在子组件上绑定一个ref,然后父组件用this.$refs直接访问子组件的方法。

父组件代码: 子组件上用ref="refChild"绑定一个ref,然后用this.$refs.refChild获取到子组件实例,能获取到子组件属性和操纵子组件方法。

<template>
  <div>
    <child ref="refChild" />
  </div>
</template>

<script>
  import child from './child.vue';
  export default {
    components: {
      child,
    },
    mounted() {
      let childObj = this.$refs.refChild;
      console.log(childObj.name); // 直接获取到子组件的属性内容 打印出来:天天鸭
      childObj.childMethod('参数'); // 触发子组件的方法
    },
  };
</script>

子组件代码:

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

<script>
  export default {
    data() {
      return {
        name: '天天鸭',
      };
    },
    methods: {
      childMethod(val) {
        console.log(val);
      },
    },
  };
</script>

七、$children与$parent

简述: $children$parentVue用于访问子组件实例和父组件实例的特殊属性。其中$children能获取所有子组件实例但不能获取孙子的,而$parent获取当前组件的父组件实例。

父组件代码: 直接使用this.$children即可获取。

注意: 获取到的实例可能为空,因此需要判空。并且如果有多个子组件时返回的是一个数组,所以需要通过下标确认对应的子组件数据。

<template>
  <div>
    <child />
    <button @click="getChildMethod">调用子组件方法</button>
  </div>
</template>

<script>
  import child from './child.vue';
  export default {
    components: {
      child,
    },
    methods: {
      getChildMethod() {
       // 判空,然后用下标获取
        if (this.$children.length > 0) {
          this.$children[0].childMethod();  // 使用子组件方法
          this.$children[0].name;    // 使用子组件属性
        }
      },
    },
  };
</script>

子组件代码: 类似地,父组件也是同样用法,但区别是返回的不是数组而且一个对象,能直接使用。

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

<script>
  export default {
    mounted(){
        this.$parent.parMethod()
        this.$parent.name
    }
  };
</script>

八、$attrs与$listeners (爷孙双向)

简述: $attrs$listeners相当于是使用在父亲组件上的一个中转站。 $attrs用于将props外的数据从爷组件传递给孙组件的,而$listeners用于从孙组件中触发爷组件中事件达到传参效果。

下面把$attrs$listeners分开讲解更易于理解。

$attrs使用流程代码:

(1)爷组件代码: 类似父传子,正常用冒号绑定属性传参。

<GrandParent :name=name></GrandParent>

(2)父组件代码$attrs作用在父组件,意思是把props之外属性全部传递给到孙子。

注意:如果这里父组件用props接收了name属性,那么用$attrs无法传递到孙子组件,因为只能传递props之外属性。

<Parent v-bind="$attrs"></Parent>

(3)孙组件代码:类似父传子,正常用popos接收爷组件传过来的参数。

<template>
  <div> 爷组件传来的:{{ name }} </div>
</template>
<script>
  export default {
    props: {
      name: {
        default: String,
      },
    },
  };
</script>

$listeners使用流程代码:

(1)孙组件代码 直接this.$emit类似子传父

<template>
  <div>
    <el-button @click="update">点击孙传爷</el-button>
  </div>
</template>

<script>
  export default {
    methods: {
      update() {
        this.$emit('my-event', '孙传给爷的数据');
      },
    },
  };
</script>

(2)父组件代码: $listeners作用在父组件。

<Parent v-bind="$listeners"></Parent>

(3)爷组件代码: 类似子传父中的父组件,触发对应孙子组件this.$emit中的my-event事件接收到参数。

<template>
  <div>
    <Parent @my-event="getMyEvent" />
  </div>
</template>

<script>
  import Parent from './Parent.vue';
  export default {
    components: {
      Parent,
    },
    methods: {
      getMyEvent(val) {
        console.log('爷组件接收到的数据:', val);
      },
    },
  };
</script>

这里感觉算两种(爷传孙与孙传爷)传参方式了,但由于都是类似中转站效果,所以放一起说比较好理解。

九、provide与inject (多层传参)

简述: provideinject无论多少层组件都能传参。顶层组件通过provide传参,下面所有组件都能用inject接收,而且子组件也能通过方法给顶层组件传参。

顶层组件代码: 核心代码在第8行的provide()中,可以传递常量、变量和方法。

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

<script>
  export default {
    provide() {
      return {
        name: '天天鸭',
        age: this.age,
        myMethod: this.myMethod,
      };
    },
    data() {
      return {
        age: '18',
      };
    },
    methods: {
      myMethod(data) {
        console.log('收到来自某个孙子的数据:', data);
      },
    },
  };
</script>

子孙组件代码: 核心代码在第10行接收参数, 除了能接收顶层参数外,还能通过参考第13行的用法,通过顶层给到的方法传参给顶层组件。

<template>
  <div>
    <el-button @click="myMethod('我是孙子组件')">我是孙子组件</el-button>
    这是顶层传下来的参数: {{ name }}
  </div>
</template>

<script>
  export default {
    inject: ['name', 'age', 'myMethod'],
    methods: {
      myMethod(data) {
        this.myMethod(data); // 传参给顶层祖先组件
      },
    },
  };
</script>

十、Vuex (全局)

有针对性写过对应的文章,可以直接跳转细看:对比学习vuex和pinia用法

十一、Vue.prototype (全局)

简述:能用Vue.prototype把任何属性和方法挂载在Vue实例了,让所有Vue实例共用。

(1)挂载属性 直接往Vue.prototype挂载即可

Vue.prototype.$testName = '天天鸭';

(2)挂载方法 直接往Vue.prototype挂载即可

Vue.prototype.$testMethod = function(val) {
  console.log(val);
};

调用: 直接在任何页面用this调用

this.$appName;

this.$testMethod('参数');

十二、浏览器缓存

简述: localStoragesessionStorage:主要是浏览器用来持久化存储的,这算是用的不多,但也是必用的一种通信方式。两者区别如下

sessionStorage(临时存储):最大空间5M,为每一个数据源维持一个存储区域,但只在浏览器打开期间存在,关闭后数据会不会消失,包括页面重新加载。

localStorage(长期存储):最大空间5M,与 sessionStorage 一样,但是哪怕浏览器关闭后,数据依然会一直存在,除非手动删除。

具体用法如下所示:

// 存储
sessionStorage.setItem('key', 'value');
localStorage.setItem('key', 'value');

// 获取
let valueFromSessionStorage = sessionStorage.getItem('key');
let valueFromLocalStorage = localStorage.getItem('key');

// 删除
sessionStorage.removeItem('key');
localStorage.removeItem('key');

// 清空所有
sessionStorage.clear();
localStorage.clear();

注意: 存储的数据只能是字符串形式,因此如果要存储对象或者数组,则需要使用 JSON.stringify 来转换后再存储,读取后用 JSON.parse还原。

十三、window (全局)

简述: 直接用语法 window.age = '18' 定义然后全局通用即可。(此方式是存放在内存刷新会清空)

注意:在 Vue 应用中,虽然可以直接将属性挂载到 window 对象上实现全局通用,但并推荐,因为这可能会出现命名冲突、导致代码难以维护

添加属性和方法:直接定义,可以是属性也可以是对象

window.Obj = { test: '挂载对象' }
window.name = '天天鸭'

使用:

console.log( window.Obj); 
console.log( window.name);

十四、路由

简述: Vue在路由跳转时携带参数其实也很常用的方式,下面汇总一下三种路由传参。

(1)通过 params 传参 跳转页面用法:

this.$router.push({name:"index", params:{id}})

目标页面接收参数:

this.$route.params.id

(2)通过 query 传参

跳转页面用法:有几种方式

this.$router.push({ name:"index", query:{id}})

this.$router.push({ path:"/index", query:{id}}) 

this.$router.push('/index?name='+obj.name+'&age='+obj.age)

目标页面接收参数:

this.$route.query.id

(3)通过动态路由传参

注意: 如果用动态路由传参需要对路由进行配置;并且参数会在 url 中显示。 如下所示,在path后面要配置:name/:age.

{ 
  path: '/about/:name/:age' ,  
  name: 'About', 
  component() => import('@/views/About.vue') 
}

跳转页面用法:

this.$router.push('/about/'+obj.name+'/'+obj.age)

目标页面接收参数:

this.$route.params

十五、$root (顶层)

简述: 可以通过 $root 访问到整个 Vue 树的根实例,也就是可以使用 $root 来访问全局的属性或者修改全局的属性。

示例:在main.js文件中定义一个globalName属性,可以全局使用。

import App from './App.vue';
import Vue from 'vue';

new Vue({
  el: '#app',
  render: h => h(App),
  data: {
    globalName: '天天鸭'
  }
});

在下层任意组件使用或者修改内容

<template>
  <div>
    {{ globalName }}
  </div>
</template>

<script>
export default {
  mounted() {
    this.$root.globalName = '修改数据';
  },
};
</script>

十六、slot(父传子)

简述: 通过插槽也是可以传递参数,这也是很多人忽略的一种方式。父组件可以通过插槽向子组件传递参数,然后子组件拿到参数进行渲染。

下面主要讲解具名插槽和默认插槽两种使用方式。

(1)具名插槽用法

子组件代码:slot定义一个名叫header的插槽。

<template>
  <div>
    <slot name="header"></slot>
  </div>
</template>

<script>
export default {
};
</script>

父组件代码:v-slot:header向子组件的header插槽传递内容。这边传递什么那边就会在对应区域显示什么。

<template>
  <div>
    <child>
        <template v-slot:header>
          <h1>这是插槽的内容</h1>
        </template>
    </child>
  </div>
</template>

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

export default {
  components: {
    child
  }
};
</script>

(2)无参数传递的默认插槽

子组件代码:slot定义插槽时不需要指定name名称。

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
export default {
};
</script>

父组件代码: 不需要指定插槽名称,只要在组件中间填写内容就会渲染在默认插槽中。

<template>
  <div>
    <child>
      <p>默认插槽中的内容。</p>
    </child>
  </div>
</template>

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

export default {
  components: {
    child
  }
};
</script>