一篇文章看懂Vue.js的11种传值通信方式

2,313 阅读9分钟

面试的时候,也算是常考的一道题目了,而且,在日常的开发中,对于组件的封装,尤其是在ui组件库中,会用到很多,下面,就来详细的了解下,通过这篇文章的学习,可以提升项目中组件封装的灵活性,可维护性,话不多说,先从大致的通信方式分类说起,然后依次非常详细地介绍,看完整篇文章,你能大致熟悉vue中的各种组件通信机制,然后就能够封装更加高阶的组件啦!

通信目录

  • props
  • $emit
  • $attrs & $listeners
  • provide & inject
  • vuex
  • Observable
  • eventBus
  • $refs
  • slot-scope & v-slot
  • scopedSlots
  • $parent & $children & $root

1.props

  • 基本使用

props是父组件传子组件的传参方式,可以看到父组件中传入了一个parentCount变量,通过prop传递给了子组件,子组件拿到的count就是通过prop传递过来的值,所以子组件中显示的1

// Parent.vue
<template>
    <div>
        <Child :count="parentCount" />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        data(){
            return{
                parentCount:1
            }
        }
    }
</script>

//Child.vue
<template>
    <div>{{count}}</div>
</template>

<script>
    export default {
        name: "Child",
        props:{
            count:Number
        }
    }
</script>
  • 类型限定

可以看到上面的代码中,给props中的count变量设定了Number的类型限定,如果改为String类型,可以看到如下的报错 prop传值的类型检测

可以设置的类型有如下几种类型
  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Symbol
  • Function
  • Promise

前面的基本基本类型就不详细讲解了,主要对于Function写一个实际的案例,还是按照上面的案例,做一个点击数字+1的方法吧,也是通过父传子的props方式

//Parent.vue
<template>
    <div>
        <Child :parentFunction="addCount" :count="parentCount" />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        data(){
            return{
                parentCount:1
            }
        },
        methods:{
            addCount(){
                this.parentCount++;
            }
        }
    }
</script>

//Child.vue
<template>
    <div @click="parentFunction">{{count}}</div>
</template>

<script>
    export default {
        name: "Child",
        props:{
            count:Number,
            parentFunction:Function
        }
    }
</script>
  • 类型限定-进阶

不仅能传变量,还能传递方法,能对于变量的数据类型加以控制,当然,props远比你想象的更强大,他不仅可以传入一个字符串,数组,还能传入一个对象,在对象中可以进行设置必填,设置默认值,自定义校验等操作

export default {
    name: "Child",
    props:{
         //你可以让count不仅可以传数值型,还能传入字符串
        count:[Number,String],   
        //设置必填
        count:{
            type:Number,
            require:true 
        }, 
        //设置默认值
        count:{
            type:Number,
            default:100  
        },
        //设置带有默认值的对象,对象或数组默认值必须从一个工厂函数获取
        user:{           
            type:Object,
            default: ()=>{return {name:'jj'}}
        },
        // 自定义校验,这个值必须匹配下列字符串中的一个
        user:{
            type:String,
            validate: (value)=>{return ['bob','jack','mary'].findIndex(value)!==-1}
        }
    }
}
  • 单向数据流

prop还有一个特点,是单向数据流,你可以理解为自来水从上流至下,父组件中值的变化能传递到子组件中更新,反之,子组件中的更新是不会触发父组件的更新的,这样可以防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

//Parent.vue
<template>
    <div>
        <div>父级别{{parentCount}}</div>
        <Child :count="parentCount" />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        data(){
            return{
                parentCount:1
            }
        },
        methods:{

        }
    }
</script>

//Child.vue
<template>
    <div>{{count}}
        <button @click="addCount">测试</button>
    </div>
</template>

<script>
export default {
    name: "Child",
    props:{
        count:Number,
    },
    methods:{
        addCount(){
            this.count+=1;
        }
    }
}
</script>

上面的子组件中,点击按钮,改变count,子组件的数值会进行累加,但是会发生报错,同时,父组件的value值并不会发生更新,这就是因为props是单向数据流的,至于如何实现双向数据流,让子组件的更新能同时更新父组件,在接下来的讲解中,我会揭开它的面纱 props的单向数据流

2. $emit

  • 基本使用

$emit是子传父的通信方式,官方说法是触发当前实例上的事件。附加参数都会传给监听器回调,$emit接收两个参数,第一个是事件名,你可以理解为click,change等事件监听的事件名,然后呢,这个事件名是绑定在父组件当中的,第二个参数就是传递的参数啦,话不多说,上个栗子瞅瞅

//Parent.vue
<template>
    <div>
        <Child @childListener="getChildValue" />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        data(){
            return{

            }
        },
        methods:{
            getChildValue(value){
                console.log(value);
            }
        }
    }
</script>

//Child.vue
<template>
    <div>
        <button @click="addCount">测试</button>
    </div>
</template>

<script>
export default {
    name: "Child",
    methods:{
        addCount(){
            this.$emit('childListener','test');
        }
    }
}
</script>

子组件点击提交的时候,相当于触发了父组件的自定义事件,父组件能够在自定义事件中获取到子组件通过$emit传递的值,所以,当子组件的按钮点击的时候,控制台会打印出test

  • 父子组件双向绑定

  • 1.props结合$emit方式实现双向绑定

现在,让我们再次回到之前描述的单向数据流的场景中,由于有的时候可能因为项目需要,确实需要实现双向的数据流绑定,可以看到依旧是单向数据流的方式,父组件的parentCount通过子组件的props进行值的传递,子组件点击按钮的时候,触发父组件的自定义事件,改变父组件的值(进行累加操作),然后父组件通过props,将更新传递到子组件,实现了父子组件的双向数据绑定。

//Parent.vue
<template>
    <div>
        <div>父级别{{parentCount}}</div>
        <Child @update:count="getValue" :count="parentCount" />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        data(){
            return{
                parentCount:1
            }
        },
        methods:{
            getValue(value){
                this.parentCount=value;
            }
        }
    }
</script>

//Child.vue
<template>
    <div>{{count}}
        <button @click="addCount">测试</button>
    </div>
</template>

<script>
export default {
    name: "Child",
    props:{
        count:Number,
    },
    methods:{
        addCount(){
            this.$emit('update:count',this.count+1);
        }
    }
}
</script>
  • 2.使用.sync修饰符

你可以觉得上面的写法有点长,那么,你可以使用一个vue 的语法糖(指在不影响功能的情况下,添加某种方法实现同样的效果,从而方便程序开发,例如v-bindv-on,可以改写为:以及@),那么,我们下面就用.sync修饰符来重写父组件吧,子组件的写法依旧不变,实际运行发现效果是一样的

<template>
    <div>
        <div>父级别{{parentCount}}</div>
        <Child :count.sync="parentCount" />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        data(){
            return{
                parentCount:1
            }
        },
    }
</script>

3.$attr & $listener

  • 基本使用

首先引用官网的概念,

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

$listeners包含了父作用域中的(不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners"传入内部组件——在创建更高层次的组件时非常有用。

让我们来看个例子

//Parent.vue
<template>
    <div>
        <div>父级别{{parentCount}}</div>
        <Child @click="handleOne" @change.native="handleTwo" :count="parentCount" :name="username" />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        data(){
            return{
                parentCount:1,
                username:'cooldream'
            }
        },
        methods:{
            handleOne(){
                alert('test');
            },
            handleTwo(){

            }
        }
    }
</script>

//Child.vue
<template>
    <div>
        <button>测试</button>
    </div>
</template>

<script>
export default {
    name: "Child",
    props:{
        count:Number,
    },
    mounted(){
        console.log(this.$attrs);
        console.log(this.$listeners);
    },
    methods:{

    }
}
</script>

让我们来康康控制台的输出,可以看到的是,$attrs主要是收集父组件的属性,毕竟正如其名嘛,如果我猜的不错全写就是attributes,而$listeners就是相当于eventListeners,那就是收集父组件的事件呗,两兄弟各负其职,分工明确。虽然自己的工作职责明确了,但是管辖范围也可以从中看出,只有没有在子组件中的props注册的父组件属性,才能被$attrs监听到,而没有.native修饰符的事件,才能被$listeners所监听到。 控制台输出

与此同时,调用this.$attrs.name可以拿到值为cooldream,并且,调用this.$listeners.click()可以触发父组件中绑定click事件的方法,即案例中的handleOne
  • inheritAttrs

还是上面的案例,我们来看一下html结构,可以看到我们的name由于没有在props中进行注册,编译之后的代码会把这些个属性都当成原始属性对待,添加到 html 原生标签上 暴露的name变量

我们来给子组件修改一下,添加了一个inheritAttrs:false,可以看到name属性被隐藏了

<script>
export default {
    name: "Child",
    inheritAttrs:false,
    props:{
        count:Number,
    },
    ......
}
</script>
被隐藏的name属性
被隐藏的name属性

4.provide & inject

  • 基本使用

首先引用官方的一段话就是

provideinject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。值得一提的是,provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

说白了就是在父组件中注册了provide之后,在所有的子组件,子子组件,子子孙孙,都可以注册inject拿到父组件中的provide中的东西,简直是一笔财富,子子孙孙无穷尽,所有子子孙孙都能拥有的遗产啊,下面就来看看如何注册自己的财产,以及子子孙孙怎么拿到这笔遗产吧,为了证实我们的设想,这里我们新增一个子组件,现在的结构是Parent.vue=>Child.vue=>ChildPlus.vue,可以看到,无论是在Child.vue还是在ChildPlus.vue中,只要inject中注册了name,都可以访问到provide中的name值,也就是说只要是子组件,都可以访问,无论层级都多深。

//Parent.vue
<template>
    <div>
        <Child />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        provide: {
            name: "cooldream"
        },
    }
</script>

//Child.vue
<template>
    <div>
        <ChildPlus />
    </div>
</template>

<script>
    import ChildPlus from "./ChildPlus";

    export default {
        name: "Child",
        inject:['name'],
        components:{
            ChildPlus
        },
        created() {
            console.log(this.name);
        }
    }
</script>

//ChildPlus.vue
<template>
    <div></div>
</template>

<script>
    export default {
        name: "ChildPlus",
        inject:['name'],
        created(){
            console.log(this.name);
        }
    }
</script>
  • 使用provide & inject实现不白屏的刷新

上面的案例中,只是实现了provide继承变量,其实,他还可以继承方法,用一个变量控制<router-view>的渲染,将他设为false,就会页面重新渲染,然后在nextTick的回调完成后,再将页面进行显示,相比于原生的js刷新页面,他不会白屏闪一下,用户体验质量会高很多。

//App.vue
<template>
  <div id="app">
      <router-view v-if="isRouterAlive"></router-view>
  </div>
</template>

<script>
  export default {
    name:'app',
    provide(){
      return{
        reload:this.reload
      }
    },
    data(){
      return{
        isRouterAlive:true
      }
    },
    methods:{
      reload(){
        this.isRouterAlive=false;
        this.$nextTick(()=>{
          this.isRouterAlive=true;
        })
      }
    }
  }
</script>

//Parent.vue
<template>
    <div></div>
</template>

<script>
    export default {
        name: "Parent",
        inject:['reload'],
        methods:{
            //刷新页面
            flashPage(){
                this.reload();
            }
        }
    }
</script>
  • v-if和v-show的区别

这里捎带提一下上面为什么是要使用v-if,而不是v-show,首先来看看两者的区别

v-if在切换过程中条件块内的事件监听器和子组件都会被适当地销毁和重建,但它也是惰性的,如果在初始渲染条件为假的时候,则说明都不做,只有当条件第一次为真的时候,才会开始渲染,它的本质其实就是display:none;
v-show不管初始条件是真是假,都会被渲染,如果切换频繁,那么用v-show,如果很少改变,就用v-if,它的本质其实就是visibility:hidden;
所以,上面就需要使用v-if,因为刷新页面的主要目的就是要重新渲染页面,而v-show并不会重新渲染,只是类似捉迷藏似的,很简单的控制显示和隐藏
  • nextTick()

那么,上面用到的nextTick()究竟又是干啥的呢,先来看看官方的解释

Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

其实说白了就是你可以理解成DOM更新完成之后的一个回调,只有当DOM更新完成之后,再将isRouterAlive设置为true,将<router-view>中的内容再次显示出来,更多关于nextTick()的相关内容这边也不再做拓展啦,想看的可以点击这里,www.jianshu.com/p/a7550c0e1…进行阅读

Observable

  • 基本概念

如果项目还不够复杂,那么这种时候,可以使用vue2.6以上新增的Vue.Observable() API来构建全局变量,可以应对一些简单的跨组件数据状态共享

  • 基本使用

可以构建一个store.js,创建基本的state以及mutation。一个是变量的定义,一个是变量的修改。

//store.js
import Vue from 'vue';

export let store =Vue.observable({count:0,name:'李四'});
export let mutations={
    setCount(count){
        store.count=count;
    },
    changeName(name){
        store.name=name;
    }
}

在需要的使用的文件中进行引入使用

import {store,mutations} from '@/store'

export default{
    computed(){
        count(){
            return store.count
        },
        name(){
            return store.name
        }
    },
    methods:{
        setCount:mutations.setCount,
        changeName:mutations.changeName    
    }
}

Vuex

  • 基本概念

vuex可以理解为整个Vue程序中的全局变量,但他和以前概念中的全局变量又有所不同,他也是响应式的,而且,你不能直接修改vuex中的变量,只能通过显式地commit=>mutation来进行数据的改变。

  • 五大模块

  • State => state里面存放的是变量,如果你要注册全局变量,写这里

  • Getter => getters相当于是state的计算属性,如果你需要将变量的值进行计算,然后输出,写这里

  • Mutation => 修改store中的变量的方法,如果你要改变变量的值,就写这里

  • Action => actions提交的是mutations,相当于就是改变变量的方法的重写,但是,actions是可以进行异步操作的

  • Module => 将整个Vuex模块化,主要的特点就是namespaced,所有的访问都要经过命名空间

  • 基本定义

首先在src文件目录下新建store文件夹,在store文件夹中新建module文件夹以及index.js,然后在module中新建自己的功能模块的js文件,例如我这边新建了一个user.js,专门存放用户相关的全局变量,所以目录如下

├─ src    //主文件
|  ├─ store       //vuex全局变量文件夹
|  |  |- index.js      //store主文件
|  |  └─ module     //store模块文件夹
|  |  |  └─ user.js      //存放user相关的全局变量
|  ├─ main.js 

接下来,在相应的文件夹中写入以下内容

//index.js
import Vue from 'vue'
import Vuex from 'vuex'

import user from './modules/user'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    user
  }
})

//user.js
const state = ({      //state里面存放的是变量,如果你要注册全局变量,写这里
  username:'',   
});

const getters = {                //getters相当于是state的计算属性,如果你需要将变量的值进行计算,然后输出,写这里
  fullName(state){
    return state.username + '大王';
  }}
;

const mutations = {       //修改store中的变量的方法,如果你要改变变量的值,就写这里
  SET_username(state, value) {
    state.username = value;
  },
};

const actions = {            //actions提交的是mutations,相当于就是改变变量的方法的重写,但是,actions是可以进行异步操作的
  setUsername(content) {
    content.commit('SET_username');
  }
};

export default{
  namespaced:true,
  state,
  getters,
  mutations,
  actions
};

//main.js
import Vue from 'vue'
import App from './App'
import store from './store/index'

new Vue({
  el: '#app',
  store,
  components: { App },
  template: '<App/>'
});

在上面的代码中,已经实现了vuex的模块化,定义State,Getter,Mutation,Action等所有基本的功能。

  • 页面中的使用

  • 直接使用
  • state => this.$store.state.user.username
  • getter => this.$store.getters.user.fullName
  • mutation => this.$store.commit('user/SET_username','cooldream') 此时的username值为cooldream
  • mutation => this.$store.dispatch('user/SET_username') action未找到传值的方法
  • 使用辅助函数
<script>
  import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'

    export default {
        name: "Parent",
        computed:{
            ...mapState({
                username:state=>state.user.username   //用this.username即可访问
            }),
            ...mapGetters({
                fullName:'user/fullName'    //用this.fullName可访问
            })
        },
        methods:{
          ...mapMutations({
              setName:'user/SET_username'    //this.setName('cooldream');   即可将值变为cooldream
          }),
          ...mapActions({
              asyncSetName:'user/setUsername'    //this.asyncSetName();   即可调用commit('user/setUsername')
          })
        },
    }
</script>

上面大致罗列了vuex的所有基本使用方法,使用全局变量,同样可以实现vue组件之间的通信,还是挺方便的

EventBus

  • 基本概念

EventBus又称为事件总线,它就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,你可以理解为现实生活中的中介,一般项目小的时候可以推荐使用EventBus,但是,中大型项目还是更加推荐使用Vuex

  • 基本使用

它主要分为两种使用方式,局部和全局

局部:你可以通过新建一个eventBus.js,它根本不需要任何DOM,只需要它创建的实例方法即可。 import Vue from 'vue' export const EventBus = new Vue()

全局:在main.js中将EventBus挂载到Vue实例中即可实现全局 Vue.prototype.$EventBus = new Vue()

  • 局部使用

我们先来聊聊第一种局部使用的方式,注意,这里用父子组件只是为了方便查看效果,两个毫无关系的组件也可以使用EventBus的方式进行传值,值得注意的是,我们是父组件mounted生命周期,而子组件created生命周期中执行,如果反转,将无效果,所以,一定要先开启监听器,再发送请求,先后顺序应该是$on=>$emit

//Parent.vue
<template>
    <div>
        <Child />
    </div>
</template>

<script>
    import {EventBus} from "../utils/eventBus";
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            // eslint-disable-next-line vue/no-unused-components
            Child
        },
        mounted(){
            EventBus.$emit('sendMessage','cooldream');
        },
    }
</script>

//Child.vue
<template>
    <div>{{username}}</div>
</template>

<script>
    import {EventBus} from "../utils/eventBus";

    export default {
        name: "Child",
        data(){
            return{
                username:''
            }
        },
        created(){
            EventBus.$on('sendMessage',callback=>this.username=callback);
        }
    }
</script>

所以,可以得到的结论就是 // 发送消息 EventBus.$emit(event: string, args(payload1,…))

// 监听接收消息 EventBus.$on(event: string, callback(payload1,…))

  • 全局使用的方式如下

//main.js
let EventBus = new Vue();

Object.defineProperties(Vue.prototype, {
  $bus: {
    get: function () {
      return EventBus
    }
  }
});
//Parent.vue
<template>
    <div>
        <Child />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            // eslint-disable-next-line vue/no-unused-components
            Child
        },
        mounted(){
            this.$bus.$emit('sendMessage','cooldream');
        },
    }
</script>

//Child.vue
<template>
    <div>{{username}}</div>
</template>

<script>
    import {EventBus} from "../utils/eventBus";

    export default {
        name: "Child",
        data(){
            return{
                username:''
            }
        },
        created(){
            this.$bus.$on('sendMessage',callback=>this.username=callback);
        }
    }
</script>

采用全局的方式同样可以达到相同的效果

  • 注意点

值得注意的是,由于Vue是SPA(单页面富应用),如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去。还要就是如果业务有反复操作的页面,EventBus在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理EventBus在项目中的关系。通常会用到,在vue页面销毁时,同时移除EventBus事件监听,用过定时器出过问题的应该都深有印象,全局的东西,在页面跳转之后是依然存在的,所以确实有销毁的必要。上面的案例中可以使用Event.$off('sendMessage',{})或者this.$bus.$off('sendMessage',{})来清空,当我们绑定的监听器过多,想一次全部清空的时候可以使用Event.$off()或者this.$bus.$off()来完成。其实上面所说的就是大家耳熟能详的Pub/Sub(发布/订阅者模式)。$emit负责发布,$on负责订阅。

$refs

使用$refs同样可以拿到值和方法,当然仅限于父组件取子组件中的值,但是可以深度遍历,取孙子组件的值,孙子的孙子等,我们可以在子组件中注册refs,输出查看值

//Parent.vue
<template>
    <div>
        <Child ref="childrenComponent" />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            // eslint-disable-next-line vue/no-unused-components
            Child
        },
        mounted(){
            console.log(this.$refs.childrenComponent);
        },
    }
</script>

//Child.vue
<template>
    <div></div>
</template>

<script>
    export default {
        name: "Child",
        data(){
            return{
                username:'cooldream'
            }
        },
        methods:{
            hiName(){
                alert(this.username);
            }
        }
    }
</script>

可以看到子组件中的username的值,以及子组件中定义的hiName方法 子组件中的值和方法

接下来我们可以用方法访问变量的值以及调用方法

console.log(this.$refs.childrenComponent.username);
this.$refs.childrenComponent.hiName();

接下来我们看看如何取孙子组件的值

//Parent.vue
<template>
    <div>
        <Child ref="childrenComponent" />
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            // eslint-disable-next-line vue/no-unused-components
            Child
        },
        mounted(){
            console.log(this.$refs.childrenComponent);
        },
    }
</script>

//Child.vue
<template>
    <div><ChildPlus></ChildPlus></div>
</template>

<script>
    import ChildPlus from "./ChildPlus";

    export default {
        name: "Child",
        components:{
            // eslint-disable-next-line vue/no-unused-components
            ChildPlus
        },
    }
</script>

//ChildPlus.vue
<template>
    <div></div>
</template>

<script>
    export default {
        name: "ChildPlus",
        data(){
            return{
                username:'jackDemon'
            }
        }
    }
</script>

可以看到,我们在输出结果中的$children[0],看到了孙子组件中定义的变量,可以通过this.$refs.childrenComponent.$children[0].username访问到孙子组件中的变量。这里值得提到的一点是,ref绑定的值是可以重复的。如果一个模板中,绑定了5个相同值的ref,那么,获取这个$refs的时候,你会拿到一个长度为5的数组列表,没试过的童鞋可以试试哦。 image.png

slot-scope & v-slot

一般用过element-uitable组件的人都对这个slot-scope比较熟悉,在表格的模板中可以拿到当前行的数据

<el-table-column prop="name" label="报价人" width="165px">
  <template slot-scope="scope">
    <div>{{scope.row.username}}</div>
  </template>
</el-table-column>

不过,slotslot-scope已经要被整合了,新标准就是v-slot,所以,接下来还是着重讲讲v-slot的方式吧,拥抱新技术

  • 普通插槽

还记得上面讲过的props吗,就是父组件传给子组件变量,方法等,相当于是父传子js代码,那么,现在这个插槽,就相当于是父传子html代码。正如他的名字,插槽,父组件有值就用父组件的,没有就用子组件中的默认值,插槽插上了,那就显示插上的内容,没有插上,就显示默认值。其实就是那么简单。案例还是上面的Parent.vue & Child.vue,具体的组件引入我就不重复写了,代码如下,可以看到最后显示的内容是Header,因为父组件传递给了子组件html内容

//Parent.vue
<Child>
    <template>
        Header
    </template>
</Child>

//Child.vue
<div>
    <slot>defaultContent</slot>
</div>
  • 具名插槽

具名插槽和普通插槽最大的区别在于,给普通插槽赋予了名字,这就是最大的一点区别,这就有点类似ES6中新出的特性,解构赋值。现在我们来改写上面的案例,新增加一个明明为header的插槽,并且,用v-slot:header的方式给传入的html代码进行命名,可以看到成功地将名字为header的插槽替换了,所以输出结果为Header defaultContent

//Parent.vue
<Child>
    <template v-slot:header>
        Header
    </template>
</Child>

//Child.vue
<div>
    <slot name="header">defaultHeader</slot>
    <slot>defaultContent</slot>
</div>

介绍完了具名插槽以后,值得注意的一点是,普通插槽其实也是具名插槽,它的名字为default,我们来改写第一个案例的代码,可以发现运行效果是一样的,输出结果为Header

//Parent.vue
<Child>
    <template v-slot:default>
        Header
    </template>
</Child>

//Child.vue
<div>
    <slot>defaultContent</slot>
</div>

所以,第一个案例的效果相当于

//Parent.vue
<Child>
    <template v-slot:default>
        Header
    </template>
</Child>

//Child.vue
<div>
    <slot name="default">defaultContent</slot>
</div>

插槽就像锁一样,传入的html代码就像钥匙,一把钥匙,就能开所有的锁,如果有两个名为header的插槽,一个名为header的传入模板,那么,最终会显示两次header模板,例如下面的代码,输出结果为Header Header default

//Parent.vue
<Child>
    <template v-slot:header>
        Header
    </template>
</Child>

//Child.vue
<div>
    <slot name="header">defaultContent</slot>
    <slot name="header">defaultContent</slot>
    <slot>default</slot>
</div>
  • 作用域插槽

上面主要是讲了下插槽的基础使用,现在要讲的作用域插槽,就和组件通信有关啦,是父组件拿子组件中的信息。可以看到我们在插槽上动态绑定了data,值为user对象,那么,在父组件引用中的v-slot值.data就可以访问到子组件中的user对象的值了。所以父组件中的输出结果就是jack 18。也就是说element-ui的table组件用的就是:row

//Parent.vue
<Child>
    <template v-slot="scope">
        {{scope.data.name}}
        {{scope.data.age}}
    </template>
</Child>

//Child.vue
<template>
    <div>
        <slot :data="user">default</slot>
    </div>
</template>

<script>
    export default {
        name: "Child",
        data(){
            return{
                user:{
                    name:'jack',
                    age:18
                }
            }
        }
    }
</script>

值得注意的是,默认插槽的缩写语法不能与具名插槽混用,下面的代码将会报错,报错如下

//Parent.vue
<Child>
    <template v-slot="scope">
        {{scope.data.name}}
        <template v-slot:other="other">
            {{other.data.age}}
        </template>
    </template>
</Child>

//Child.vue
<div>
    <slot :data="user">default</slot>
    <slot name="other" :data="user"></slot>
</div>
报错信息
报错信息

所以应该改为下面的这种方式

<Child>
    <template v-slot="scope">
        {{scope.data.name}}
    </template>
    <template v-slot:other="other">
        {{other.data.age}}
    </template>
</Child>
  • 动态名称插槽

通过[ ]将变量名称括起来,可以实现动态名称的插槽,配合条件渲染,选择相应的具名插槽,能让组件的封装更加地灵活,因为args=other,所以,输出结果为default jack

//Parent.vue
<template>
    <div>
        <Child>
            <template v-slot:[args]="scope">
                {{scope.data.name}}
            </template>
        </Child>
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        data(){
            return{
                args:'other'
            }
        }
    }
</script>

//Child.vue
<template>
    <div>
        <slot :data="user">default</slot>
        <slot name="other" :data="user"></slot>
    </div>
</template>

<script>
    export default {
        name: "Child",
        data(){
            return{
                user:{
                    name:'jack',
                    age:18
                },
            }
        }
    }
</script>
  • 具名插槽简写(语法糖)

v-slot:other => #other,值得注意的是,v-slot => v-slot:default,但是# !=> v-slot:default,使用语法糖必须加上名称,即#default => v-slot:default

  • $scopedSlots

还记得上面的v-slot吗,他们有很多相同点与不同点,我们先对他们进行一下比较

相同点:都是作用域插槽
不同点: v-slot是模板语法,即写在<template>中的语言 | $scopedSlots是编程式语法,即写在render() || Jsx的语言
首先来看下一个使用v-slot写的案例
//Parent.vue
<template>
    <div>
        <Child>
            <template #other="scope">
                {{scope.data.name}}
            </template>
        </Child>
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
    }
</script>

//Child.vue
<template>
    <div>
        <slot name="other" :data="user"></slot>
    </div>
</template>

<script>
    export default {
        name: "Child",
        data(){
            return{
                user:{
                    name:'jack',
                    age:18
                },
            }
        },
    }
</script>
  • render()方式使用$scopedSlots来改写,下面的代码运行以后和上面的效果是一样的,总结而言render函数就是h('标签名字',{标签属性},[标签内容])

//Parent.vue
<template>
    <div>
        <Child>
            <template #other="scope">
                {{scope.data.name}}
            </template>
        </Child>
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        render(h){
            return h('div',{},[
                h('Child',{scopedSlots:{
                    other: props=>{
                        return h('',{},[props.data.name])
                    }
                    }},)
            ])
        }
    }
</script>

//Child.vue
<script>
    export default {
        name: "Child",
        data(){
            return{
                user:{
                    name:'jack',
                    age:18
                },
            }
        },
        render(h){
            return h('div',{},[
                this.$scopedSlots.other({data:this.user})
            ])
        }
    }
</script>
  • Jsx方式使用$scopedSlots来改写

Jsx方式我就不再写案例演示了,具体的可以查看这里juejin.cn/post/684490…

  • $parent & $children & $root

$parent 写在子组件中,用于获取父组件实例,可无限向上遍历 $children 写在父组件中,用于获取子组件实例,可无限向上遍历 $root 写在任意组件中,用于获取Vue实例,它的$children就是App.vue实例

$parent用于子组件中拿到父组件中的实例,可以多层级地遍历当前子组件中的父组件实例,先来看下下面的例子,还是Parent.vue & Child.vue,所以,按照层级顺序应该是App.vue => Parent.vue => Child.vue

//Parent.vue
<template>
    <div>
        <Child></Child>
    </div>
</template>

<script>
    import Child from "../components/Child";

    export default {
        name: "Parent",
        components:{
            Child
        },
        data(){
            return{
                username:'jack'
            }
        },
        methods:{
            sayName(){
                alert(this.username);
            }
        }
    }
</script>

//Child.vue
<template>
    <div></div>
</template>

<script>
    export default {
        name: "Child",
        mounted(){
            console.log(this.$parent);
        }
    }
</script>

我们可以在输出内容中找到Parent.vue中的变量和方法,然后可以在当前的实例中找到$parent,而这个实例就是App.vue,而$children同理,是写在父组件中获取子组件实例的,同样可以无限向下遍历实例。这里,我们就不再写案例演示了。而$root是用于获取根组件实例的,例如无论是写在Parent.vue中还是Child.vue中,都可以获取到App.vue的实例

我们可以看看$root输出的内容,App.vue的实例可以在$children中访问,App.vue中的变量和方法也是在这里 image.png

以上就是我所理解的vue中的所有组件通信方式,阅读完所有的内容,我相信还是很有帮助的。文中有很多可能讲解的不是很详细,具体深入的可以自行百度查看,但是,基本的案例,都是我一个个在实际项目中运行过的,希望能对大家有所帮助。

参考:Vue.js 父子组件通信的1212种方式 vue官网 Vue事件总线(EventBus)使用详细介绍 Vue.js 你需要知道的 v-slot (译)

本文使用 mdnice 排版