VUE.js组件通信精髓归纳(基础篇)

5,148 阅读14分钟

前言

文章涉及的内容可能不全面,但量很多,需要慢慢看。我花了很长的时间整理,用心分享心得,希望对大家有所帮助。但是难免会有打字的错误或理解的错误点,希望发现的可以邮箱告诉我ghui_master@163.com,我会及时的进行修改,只希望对你有所帮助,谢谢。

vue 何为组件化?

我们可以很直观的将一个复杂的页面分割成若干个独立组件

每个组件包含自己的逻辑和样式再将这些独立组件组合完成一个复杂的页面。

将页面中重复的的功能抽离出来封装成一个单独的组件,在任何需要的地方使用该组件即可;

这样既减少了 逻辑复杂度 , 提高代码的可复用程度和可维护性;

页面是组件的容器,组件自由组合形成完整的界面,当不需要某个组件时,或者想要替换某个组件时,可以随时进 行替换和删除,而不影响整个应用的运行。

每个组件都是一个 Vue 的实例,那么这个组件也有自己的生命周期,并且也有 data、computed、methods、watch这些属性,每个组件都有自己私有的数据;还可以接受来自上层组件传入的数据;

组件化开发的好处

  • 提高开发效率
  • 方便可复用性
  • 便于协同开发
  • 更容易被管理和维护
  • 每一个VUE组件都是一个独立的个体(独立的VM实例):DATA是独立的(不同组件的DATA应该互不干扰)、有完整的生命周期、方法等也是独立的
  • 能够实现组件的嵌套:需要掌握组件之间的信息通信
  • ....

VUE中的组件

  • vue中的组件是自定义的标签,可以扩展原生的html元素,封装可重用的代码
  • 一个 自定义标签 vue 就会把他看成一个组件 (除w3c规定以外的标签), 自定义标签原本没有实际意义,但是vue会给这些标签赋予一定的意义

全局组件 & 局部组件

  • 全局组件:无需单独引用或者配置,直接在大组件中调取全局组件即可

    可以声明一次在任何地方使用

    一般在写插件时全局组件使用的多一些

    vue.filters 它也是放到全局中,不用声明直接用

    全局组件是声明在 vue类 上的

    <!-- IMPORT CSS-->
    <body>
    <div id="app">
        <h3 v-text="title"></h3>
        <my-handlesome></my-handlesome>
        <!--
        我希望 my-handlesome 实现效果和下面的div实现同样的效果
    
        <div> 我很帅气!!!</div>
        -->
        </div>
        <!-- IMPORT JS-->
        <script src="./20191007/node_modules/vue/dist/vue.js"></script>
        <script>
            /*
            * 全局组件:无需单独引用或者配置,直接在大组件中调取全局组件即可
            *  Vue.component(componentName,options)
    	   *     + options可以使用的有VM实例具备的大部分(DATA、METHODS、生命周期函数...)
    	   *     + 每调用一次组件都是创建一个单独的VUE实例(VueComponent -> Vue)
    	   *
            * 组件命名的规范:
            *     1.组件名不要带有大写  多个单词用 — 中划线分隔  kebab-case
    	   *     2.只要组件名和定义名字相同是可以的 (只有首字母可以大写) PasalCase
            *     3.html采用短横线隔开命名法js中转驼峰也是可以的
            * */
            Vue.component('my-handlesome',{ //=> 一个对象可以成一个组件
                /* 相当于用  template 中的 div 标签替换 我们自定义的 my-handlesome 标签*/
                template:'<div>{{msg}}</div>',
                //=> 组件是中data数据必须是一个函数 ,返回一个实例(对象)作为组件的数据
                data(){
                    return{
                        msg:'我很帅气!!!'
                    }
                }
            });
            let vm = new Vue({
               el:'#app',
                data:{
                   title:"你自认为自己颜值???"
                }
            });
        </script>
    </body>
    
  • 局部组件 :1- 创建 2- 注册 3- 引用

    必须告诉这个组建属于谁

    局部组件是声明在 实例 vm 上的

    组件是相互独立的 不能直接跨作用域 实例(vm)也是一个组件, 组件中拥有生命周期函数

    <!-- IMPORT CSS-->
    <body>
    <div id="app">
        <h3 v-text="title"></h3>
        <handelesome1></handelesome1>
        <handelesome2></handelesome2>
    
        </div>
        <!-- IMPORT JS-->
        <script src="./20191007/node_modules/vue/dist/vue.js"></script>
        <script>
            /*
            *  局部组件-使用三部曲:
            *     1. 创建组件
            *     2. 注册组件
            *     3. 引用组件
            *  组件是相互独立的  不能直接跨作用域 实例(vm)也是一个组件, 组件中拥有生命周期函数
            *
            * */
            let obj ={ face:'丑到爆' }; //=> 如果组件共用了数据,会导致数据同时更新 (=错误写法=) (但是组件 特点 独立性)
            /*
             子组件不能直接使用父组件的数据 (组件之间的数据交互)
             组件理论上可以无限嵌套
             */
    
            //=> 1. 创建这个组件
            let handelesome1 ={
                template:`<div @click="fn">1.帅气 {{face}} {{a}}</div>`,
                /* face='颜值爆表' === @click="fn"*/
                data(){ //=> 这里的data 必须是一个函数
                    return obj
                },
                methods:{
                    fn(){
                        //=> this 为当前的组件
                        this.face = '颜值爆表'
                    }
                }
    
            } ;
            let handelesome2 ={
                template:'<div>2.霸气 {{face}}{{a}}</div>',
                data(){
                    return obj
                }
            } ;
    
            let vm = new Vue({
               el:'#app',
                data:{ //=> 只有这里的data是 对象
                   title:"你自认为自己颜值???",
                    //=> 这里的a 属性 在 局部组件中调用引发报错
                    //=>
                    a:1
    
    
                },
    
                components:{
                    //=> 2. 注册这个组件  ES6名字一样写一个
                    handelesome1,
                    handelesome2
                }
            });
        </script>
    
    </body>
    

嵌套组件

  • 1- 创建 2- 注册 3- 引用
  • 如果要在一个组件使用另一个组件,
    • 1.先保证使用的组件得是真实存在,
    • 2.在需要引用这个组件的实例上通过components注册这个组件,
    • 3.组件需要在父级的模板中通过标签的形式引入
<body>
<div id="app">

</div>

<!-- IMPORT JS-->
<script src="./20191007/node_modules/vue/dist/vue.js"></script>
<script>
   /*
   最终页面渲染效果
   parent
   son
   grandson
   */
   
   //=> 首先创建三个 组件 顺序颠倒过来  父亲 儿子 孙子   、
     
   /*
   * 如果要在一个组件使用另一个组件,
   *     1.先保证使用的组件得是真实存在,
   *     2.在需要引用这个组件的实例上通过components注册这个组件,
   *     3.组件需要在父级的模板中通过标签的形式引入
   * */
   //=> 孙子
   let grandson = { template:'<div>grandson</div>'};
   //=> 儿子
   let son = { template:'<div>son <grandson></grandson></div>',
       components:{
           grandson
       }
   };
    //=> 父亲
    let parent = {
        template:'<div>parent <son></son></div>',
        components: {
            son
        }
    };
    let vm = new Vue({
        el:'#app',
        //=> 在此处注册组件
        template:"<parent></parent>",
        components:{
            parent
        },
        data:{

        }
    });

</script>
</body>

父传子-props--属性传递

  • props:[]方式传递属性 以及 对象 的方式传递属性
  • 对象属性传递的参数校验
    • type:[] 校验类型
    • default:0, 设置默认值
    • required: true, 设置是必须传递 属性,但是不能和default同时使用
    • validator(val){ } 自定义校验器
<body>
<div id="app">
    <!--父亲基于胡子语法取自己的 money 属性 -->
    父亲:{{money}}
    <!--
        当前组件的属性 = 父级的值
        给儿子加了一个m属性,属性对应的数据是属于父亲的  传递给儿子,可以传递多个
    -->
    <child :m="money"></child> <!--如果想传的是一个数字就加 : 不然是字符串-->
    <!--<child> </child> //=>这样才是没有传 而不是 :m="" -->
</div>
<!-- IMPORT JS-->
<script src="./20191007/node_modules/vue/dist/vue.js"></script>
<script>
    <!--vm 组件也是根实例-->
    //=> 父传递子 属性传递
    let vm = new Vue({ //=> parent
        el:'#app',
        data:{
            money:400
        },
        components:{
            child:{
                /*
                 template:'<div>儿子 {{money}}</div>'
                 儿子 组件取 父亲的 money 属性
                 属性没有拿到还引发了报错
                 因为 两个组件间作用域是独立的
                 
                 */
                 
                props:{ //=> 对象的形式可以对传的值进行检验 ,校验时不能阻断代码执行,只是阻断而已
                    m:{ //=> 校验属性的类型 不带 : 号得到的是字符串money  :m='1' :m='true'

                        type:[String,Function,Boolean,Array,Number],
                      /*  default:0, //=> 可以给m设置默认值,如果不传默认调用 default*/
                        required: true, //=> 此属性是必须传递,但是不能和default同时使用
                        validator(val){ //=>  val===money第一个参数是当前传递的值,返回true表示通过 false 反之
                            return val>300//=> 自定义校验器 

                        }
                    }
                },
                 //=> 基于 props:[]属性 儿子这里要接受一下父亲的’money‘
               /* props:['m'], 
               //=> this.m = 100;会在当前 子组件实例上 声明一个 m的属性,存的父亲传过来的money
                    也和data一样会影响视图渲染吗,但是子组件不能基于this.xxx来直接修改属性
                    
               */
                template:'<div>儿子:{{m}}</div>'
            }
        }
    });
</script>
</body>

基于ref实现父子组件信息通信

  • ref 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,基于此可以快速获取和操作子组件中的数据
  • parent和children是获取组件和子组件的实例,只不过$children是一个数组集合,需要我们记住组件顺序才可以

VUE--发布emit  订阅on

订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象

  1. 创建一个实例,相当于传建一个任务队列
  2. vm.$on(自定义事件,要执行的方法) 把自定义事件和方法订阅 到任务队列中;值得注意的是:我们调取子组件的时候,基于 @xxx='func也相当于给子组件所在实例的任务队列订阅一个方法
  3. vm.$emit(自定义事件,需要传参的信息)把指定自定义事件中的 方法触发执行,可以给方法传递对应的信息

vue的单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程:父 beforeUpdate -> 父 updated

  • 销毁过程:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

有两种常见的试图改变一个 prop 的情形 :

  • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值
  • 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app">
  <son :msg="pmsg"></son>
</div>

<script src="vue.js"></script>
<script>
  let son = {
    data() {
      return {
        hello: 'xxxx'
      }
    },
    props: ['msg', 'changePMSG'],
    template: '<span>{{msg}} <button @click="fn">dddd</button></span>',
    methods: {
      fn() {
        this.msg = 1233; // props 中的数据也会代理到子组件的实例身上,可以直接通过 this 访问
      }
    }
  };

  let vm = new Vue({
    el: '#app',
    data: {
      pmsg: 'msg from parent'
    },
    methods: {
      changeMsg() {
        this.pmsg = 123445
      }
    },
    components: {
      son
    }
  });
  // 1. 子组件中的数据不能全是写死的,而是有一部分从父组件传递过来的
  // 2. 为了把父组件的数据传递给子组件,子组件在标签上动态绑定一个属性,这个属性绑定父组件的数据,并且在子组件的props中注册这个属性
  // 3. 子组件如果想使用父组件的数据,就使用对应的 props 就可以(父传子用props)

  // 单向数据流:数据只能通过父组件传递给子组件,而不能直接从子组件传给父组件,子组件也不能直接修改父组件的数据;当父组件的数据发生改变之后,子组件的收到的数据也会跟着变化;如上面的例子,直接修改从父组件中的数据会引发Vue的报错;
  // 如果子组件想修改父组件的数据,只能通知父组件,让父组件修改数据;
</script>
</body>
</html>

子传父--发布$emit 通知

  • 基于 发布订阅模式 on 及emit
  • 单向数据流来实现子通知父
<body>

<div id="app">
    父亲:{{money}}
    <!--
    儿子里面绑定了一个事件 child.$on('child-msg', things) 绑定的事叫做 child-msg ,事情主体是父亲的 things
    此时我们已经在儿子上绑定了一个child-msg ,当我们点击的时候让child-msg触发,触发时可以传参,
     -->

    <child :m="money" @child-msg="things"></child>

</div>
<!--
    父亲绑定一些事件,儿子触发这个事件将参数传递过去 ,
    单项数据流, 父亲数据刷新,儿子就刷新
-->
<!-- IMPORT JS-->
<script src="./20191007/node_modules/vue/dist/vue.js"></script>

<script>
    //=>
    let vm = new Vue({ //=> parent
        el:'#app',
        data:{
            money:400
        },
        methods:{
            //=> 儿子要触发这个事件,并且传一个参数 , 将此次事件绑定给

            things(val){ //=> 相当于 $on('xxx',things)

                //=>儿子通知父亲多给点钱  当父亲接收到过来的800 更新自己的价格
                //=> this为父亲
                this.money=val;
            }
        },
        components:{
            child:{
                props:['m'],

                template:'<div>儿子:{{m}}  <button @click="getMoney()">多要钱</button></div>',
                methods: {
                    getMoney(){
                        /*
                         this为当前的 child
                         当点击儿子组件时触发的事件child-msg ,
                         当child-msg事件触发的时候,会触发 things
                         并且将参数 800 传给 父亲的things 的 val
                         */
                        this.$emit('child-msg',800);//=> 触发自己的自定义事件,让父亲的方法执行 ,方法是父亲的,属性永远是当前组件的
                    }
                }

            }
        }
    });

</script>
</body>

事件总线 -- EventBus

什么是事件总线?

  • 每个组件都是一个 Vue 的实例,相互之间不能互通数据;

  • 要修改数据一定要通知,所以找一个第三方,让第三方监听事件,在事件触发时执行对应的修改数据的操作,这个第三方就是事件总线;

事件总线的用法

  • 创建一个空的 Vue 实例;let eventBus = new Vue();

  • eventBus.$on(自定义事件名, 事件函数) 监听事件

  • eventBus.$emit(事件名) 触发事件

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>

<div id="app">
  <prev></prev>
  <next></next>
</div>

<script src="vue.js"></script>
<script>
  // 每个组件都是一个 Vue的实例,相互之间不能互通数据;
  // 要修改数据一定要通知,所以找一个第三方,让第三方监听事件,在事件触发时执行对应的修改数据的操作;
  // eventBus.$on(自定义事件名, 事件函数) 监听事件
  // eventBus.$emit(事件名) 触发事件

  let eventBus = new Vue(); // 这第三方就是一个空的 Vue 实例,叫做事件总线 EventBus
  let prev = {
    data() {
      return {
        color: 'green'
      }
    },
    created() {
      eventBus.$on('changeRed', this.toRed) // 监听 changeRed 事件,当事件触发时,会执行 this.toRed 方法
    },
    methods: {
      toRed(x) {
        console.log(x); x 是事件触发时,传递的数据
        this.color = 'red';
      }
    },
    template: `<div :style="{background: color}">{{color}}</div>`
  };
  let next = {
    data () {
      return {
        color: '红色'
      }
    },
    methods: {
      red() {
        eventBus.$emit('changeRed', 'hahaha')
      }
    },
    template: `<div><button @click="red">变红</button></div>`
  };

  let vm = new Vue({
    el: '#app',
    data: {},
    components: {
      prev,
      next
    }
  });

  // 兄弟组件之间通信,通过 EventBus,谁的数据需要被改变,谁监听事件,谁发起改变谁触发事件;例如本例中,next 要修改 prev 的数据,所以在 prev 的 created 中 eventBus.$on() 监听事件,而 next 发起改变,所以在 next 中 $emit() 事件;

  // 哥哥改弟弟同理;

</script>
</body>
</html>

插槽 slot

插槽是什么?

当引用组件时,我们可以向组件的标签中嵌入内容。这些内容可以嵌入到子组件中,但是需要使用插槽机制即,slot;

如何使用插槽

  • 首先在创建子组件时需要声明一个 slot 标签占位;

  • 在组件标签中嵌入内容

具名slot和匿名slot

  • 匿名 slot,在声明 slot 时不写 name 属性,嵌入在组件标签中没有写 slot 属性的标签都会放在匿名 slot 中

  • 具名 slot,在声明 slot 时要写上 name 属性;嵌入在组件标签中的内容需要指定 slot=“slot的名字”

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div id="app">
    <panel>
        <div>这是默认的内容</div>
        <p>hahahah </p>
        <div slot="header">这是个头</div>
        <div slot="body">主体</div>
        <div slot="footer">尾部</div>
    </panel>
</div>

<template id="tpl">
    <div>
        <slot></slot>
        <slot name="header"></slot>
        <slot name="body"></slot>
        <slot name="footer"></slot>
    </div>
</template>

<script src="vue.js"></script>
<script>
    let panel = {
        template: '#tpl',
        data() {
            return {
                x: 1
            }
        }
    };

    let vm = new Vue({
        el: '#app',
        data: {},
        components: {
            panel
        }
    });

    // 如果要想子组件中嵌入内容,需要使用插槽 slot;并且需要在子组件中提前用 slot 标签占位;
    // slot 分为匿名 slot 和具名 slot
</script>

</body>
</html>

基于 provide 和 inject 实现祖先与后代的通信

  • 祖先组件基于provide注册需要供后代组件使用的数据
{
    provide:{ //=>对象或者返回对象的函数都可以(属性值如果是data中的数据,则必须使用函数的方法进行处理)
        name:'zhufeng',
        year:10
    },
    ...
}
  • 后代组件基于inject声明需要使用的数据并调取使用
{
    inject:['name'],
    methods:{
        func(){
            let name=this.name;
        }
    }
}