21

Vue组件基础与通信

一、vue cli脚手架

① vue cli 简介与安装

vue cli 3.0之前安装的是vue-cli模块,vue cli 3.0之后安装的是@vue/cli模块。
如果已经全局安装了旧版本的 vue-cli , 那么需要先卸载vue-cli,再全局安装@vue/cli
虽然安装的是vue cli,但是执行的命令仍然是vue
npm uninstall vue-cli -g // 卸载旧版本脚手架

npm install -g @vue/cli // 安装新版本脚手架

vue --version // 检测是否安装成功

② vue cli 的简单使用

  • 通过create命令创建一个由 vue-cli-service 提供支持的新项目。即会通过vue-cli-service去启动vue项目,从package.json文件中可以看到npm run serve实际执行的是vue-cli-service serve
vue create vue-test // 在当前目录下创建一个vue-test项目,通过问卷的方式选择好需要安装的模块后会自动安装并初始化vue项目

cd vue-test // 进入项目根目录下

npm run serve // 启动vue项目
  • 通过vue serve或者vue build直接启动vue项目,执行vue serve命令的时候可以指定一个.vue文件或者.js文件作为启动入口,如果没有指定启动入口文件,那么就会自动在当前目录下自动寻找main.js、index.js、App.vue 或 app.vue入口文件。
如果入口文件是.js文件,那么其中必须创建一个Vue实例,并且必须添加el属性,且其属性值必须为"#app",并且添加render属性渲染一个Vue组件以便能够显示组件内容。
如果入口文件是.vue文件,那么可以直接渲染,.vue文件中的template里面必须要有一个根元素标签,不限于<div>,但是不需要添加id为app的根元素,即根元素内容可以任意书写

// main.js

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

const vm = new Vue({
    el: "#app", // 固定为#app,否则无法显示App组件内容
    render: h => h(App)
});
export default vm; // 可以不对外暴露

二、父子组件通信

如果父组件要向子组件传递数据,那么只需要在子组件中添加一个props属性,其属性值可以是一个数组,数组元素为一个字符串,即父组件给子组件传递数据时所使用的名称,直接在子组件标签中当作元素的一个属性名使用;props属性值也可以是一个对象,对象的属性名为父组件给子组件传递数据时所使用的名称,属性值为一个对象,用于控制父组件传递数据的类型、默认值
// 父组件 Parent.vue

<template>
    <div>
        Parent父组件: {{firstName}}
        <!--如果子组件的属性名前不带冒号,则传递是原字符串;如果带冒号则传递的是当前组件中该属性名对应的值,即this.firstName的值;-->
        <Son :value="firstName"></Son> 
        <!-- 如果该属性名在该组件中不存在对应的值,那么虽然不会报错,但是子组件获取的值将为null,即获取不到值 -->
        <Son :value="firstName1"></Son> <!--父组件上不存在firstName1属性,子组件无法获取到value对应的值 -->
    </div>
</template>

// 子组件 Son.vue

<template>
    <div>
        Son组件: {{value}}
        <button @click="change">修改子组件数据</button>
    </div>
</template>
<script>
export default {
    mounted () {
        console.log(this.value); // props中定义的属性名也会添加到vue实例上,可以直接通过vue组件实例获取到 
    },
    props: ["value"], // Son组件上定义了一个value属性,用于接收来自父组件传递过来的数据,可以子组件中可以直接使用
    // props: { // 对象的形式
    //     value: {
    //         type: String // 只能传递字符串类型,String是大写
    //     }
    // }
    methods: {
        change() {
            this.value = "zhang"; // 直接修改父组件的值是不允许的
        }
    }
}
</script>
注意,上面子组件中添加了一个按钮用于修改value的属性值,由于Vue规定子组件不能直接去修改父组件的值,所以会报错。即父子组件中的数据是单向的。子组件中如果确实想要修改父组件传递过来的数据,那么可以在子组件中定义一个新的变量,将父组件传递过来的数据保存起来,这个时候子组件去修改这个新的变量对应的值就可以了,但是这样子组件修改的值不会同步到父组件中,因为子组件修改的是自己的数据了

三、父子组件数据同步

① 可以在父组件中使用子组件的时候,在子组件标签上监听一个事件,这个事件名可以任意,比如input事件,这样当子组件内部发射该事件后,子组件就能监听到该事件就可以直接调用父组件中的方法进行处理,如:
//父组件 Parent.vue

<!-- 在子组件上监听一个input事件,但是其事件处理函数是父组件中的函数 -->
<Son :value="firstName" @input="change"></Son> 

// 子组件,Son.vue

 <button @click="change">修改父组件数据</button>
 methods: {
        change() {
            this.$emit("input", "zhang");// 子组件内部发射一个input事件
        }
 }
在父组件使用子组件的时候,给子组件标签上添加了@input="change",子组件渲染的时候,就会在子组件上监听该事件,就相当于 子组件.$on("input", change),即子组件监听到input事件后会触发父组件上change函数的执行,所以该种方式其实就是利用父子组件单向数据流特性,由子组件发起事件,通过修改父组件中的数据来实现父子组件数据的同步,本质就是修改父组件数据

② 父组件向子组件传递数据的时候,使用sync修饰符来修饰子组件上用于接收父组件数据的变量名,同时子组件内部发射一个update:value事件也可以实现父子组件数据的同步更新,如:
// 父组件 Parent.vue

<Son :value.sync="firstName"></Son> 
<!-- <Son :value="firstName" @update:value="change"></Son>  --><!--二者等价-->

// 子组件 Son.vue

methods: {
        change() {
            this.$emit("update:value", "zhang"); // 子组件发射update:value事件
        }
}
sync修饰符其实就是在子组件上绑定值的同时监听了@update:value事件,是一种语法糖,但是子组件上必须发射固定名称的update:value事件才会起作用
需要注意的是,这里所谓的固定名称中value是不固定的,value只是一个子组件用于接收父组件数据时所定义的变量(属性),比如子组件上定义的用于接收父组件的变量是surname,那么子组件就要发射"update:surname"事件了,父组件传递数据的时候就要使用:surname.sync="firstName"了

③ 如果子组件上定义的用于接收父组件数据的属性(变量)是value,并且在子组件中监听的是@input事件,那么我们可以直接简写成v-model,因为v-model实际就是绑定value的值并且监听@input事件,如:
// 父组件 Parent.vue

<Son v-model="firstName"></Son>

// 子组件 Son.vue

methods: {
        change() {
            this.$emit("input", "zhang");// 必须是发射input事件
        }
    }
sync和v-model都能实现父子组件数据的同步,但是v-model相对比较局限,属性名必须是value,事件名必须是input,而sync修饰符属性名可以任意。

④ 如果是三级组件通信,该如何处理?比如父组件与孙子组件通信。
同样,我们也可以利用单向数据流的原理,我们只要能够改变父组件上的数据,那么儿子组件和孙子组件上的数据都会进行相应的修改了,而前面父子组件通信的时候是通过子组件发射一个input事件来调用父组件的方法去改变父组件上的数据的,由于孙子组件直接发射一个input事件,父组件上是监听不到,因为父组件上监听的是子组件内部发射的input事件,但是我们可以通过孙子组件的$parent属性获取到子组件,然后通过子组件去发射input事件,那么父组件就能监听到input事件了,从而改变父组件中的数据,实现三级组件通信,如:
// 孙子组件 Grandson.vue

 <div>
        Grandson组件: {{value}}
        <button @click="changeParent">修改我的父组件数据</button>
 </div>
 methods: {
        changeParent() {
            this.$parent.$emit("input", "Wang");// 通过孙子组件的父组件去发射input事件
        }
 }   

⑤ 如果是多级组件通信呢?我也可以通过前面三级组件通信原理,我们只要遍历当前组件的所有祖先组件,然后让所有祖先组件都发射一个input事件,这样一层一层发射input事件,那么最终顶层的父组件肯定能够收到一个input事件从而改变顶层父组件的数据,实现多级组件之间的通信。如:
// main.js 在Vue原型对象上添加一个$dispatch方法,方便后辈组件调用

Vue.prototype.$dispatch = function (eventName, data) { // 切勿使用箭头函数
    let parent = this.$parent; // 获取调用$dispatch方法的父组件
    while(parent) { 遍历祖先组件,一层一层发射相应的事件
        parent.$emit(eventName, data);
        parent = parent.$parent;
    }
}

// 后辈组件

methods: {
        changeParent() {
            this.$dispatch("input", "Wang"); // 派发一个input事件,其祖先组件都会发生input事件
        }
    }

⑥ 有后辈组件向祖先组件派发事件,自然有祖先组件向后辈组件广播事件,所谓广播就是祖先组件通知监听了某个事件的组件都执行一下对应的事件函数,如:
// main.js 在Vue原型对象上添加一个$broadcast方法,方便所有组件调用

Vue.prototype.$broadcast = function(eventName, data) {
    const broadcast = (children) => { // 递归调用broadcast方法
        children.forEach((child) => { // 遍历子组件,每个子组件都发射一个指定的事件
            child.$emit(eventName, data);
            if (child.$children) { // 如果子组件上还有子组件
                broadcast(child.$children) // 递归调用
            }
        });
    }
    broadcast(this.$children);
}

v-bind和\$attrs、v-on和\$listenners的用法
当我们在组件内部并没有定义props属性来接收父组件传递过来的数据时,这些非prop声明的属性将会原封不动的添加到组件渲染后的html标签上,如:
// Parent.vue父组件

<!--父组件传递了 value、name、age三个属性给Son组件-->
<Son :value="firstName" @input="change" :name="name" :age="age"></Son> 

Son.vue 子组件

export default {
    props: ["value"] // 子组件上只定义了一个value属性用于接收父组件上的数据
}

子组件渲染完成后,除value属性外,name和age属性都原封不动添加到了html标签上,如:

<div name="Si" age="18">
    Son组件: Li
</div>
如果不想这些父组件传递过来的非prop属性出现在html标签上,那么可以在子组件上添加一个inheritAttrs属性,并且属性值设置为false
export default {
    inheritAttrs: false, // 不在html标签上继承非prop属性
    props: ["value"] // 子组件上只定义了一个value属性用于接收父组件上的数据
}
需要注意的是虽然设置了inheritAttrs为false,但是子组件上还是具有\$attrs属性的,通过\$attrs属性还是可以获取到那些来自父组件传递过来的非prop属性的,\$attrs为一个对象,对象属性名为非prop属性名,如上子组件上的$attrs值为{name: "Si", age: 18}

如果v-bind不绑定属性,直接赋值一个对象的时候,那么其会将对象的属性名当作组件的属性名,将对象的属性值传递给组件,如:
// Son.vue

<Grandson :value="value" v-bind="{name: 'Si', age: 18 }"></Grandson>
<!--其等价于-->
<Grandson :value="value" name='Si' age=18></Grandson>
所以如果父组件传递给子组件的数据,子组件不想用(子组件并未定义相应的props属性进行接收),但是孙子组件想用,那么可以通过v-bind直接传递给孙子组件,如:
// Son.vue
<Grandson :value="value" v-bind="$attrs"></Grandson>
同样的,还有$listeners,其包含的是
父作用域中不含.native修饰的事件监听器,如上面例子,在父作用域中的Son组件上监听了input事件,那么Son组件内就可以通过$listeners.input()执行input的事件函数。
当然也可以通过v-on="$listeners"传递给孙子组件使用
其主要区别就是: \$attrs是组件上属性的集合,$listeners是组件上方法(事件)的集合

⑧ provide和inject,提供和注入实现祖先组件和后代组件之间通信。

可以通过provide()提供一个对象数据到父组件上,然后其后代组件就可以通过inject将祖先组件上的数据注入到后代组件中

// Parent.vue

export default {
    provide() { // 提供一个数据到祖先组件上
        return {
            money: 1000000
        }
    }
}

// Grandson.vue

export default {
    inject: ["money"] // 在后代组件中注入提供到祖先组件上的数据
}
// Grandson组件就可以直接通过this.money获取到数据了

Provide和inject的原理就是,父组件通过配置一个provide属性,值可以是一个对象或者一个返回对象的函数,父组件初始化的时候就会拿到provide属性值,如果是函数则执行拿到返回值,并且将这个值赋值到父组件实例的_provided属性上。子组件通过配置一个inject属性,值为一个数组包含了父组件上provide对象中的key,子组件渲染的时候,就会拿到inject属性值,进而拿到其中的key,然后到父组件上的_provided属性中取值,取到对应的值后再将其定义到子组件上去。

⑨ 获取子组件或者子标签的引用,父组件可以通过$refs属性获取到添加了ref属性的子组件或者子标签对象,然后进行相应的操作,如:

<Son :value="firstName" @input="change" ref="son"></Son>
export default {
    mounted() {
        this.$refs.son.say(); // 通过$refs获取到<Son>组件,然后调用其say方法
    }
}

⑩ 通过eventBus进行通信,所谓eventBus就是一个公共的Vue实例,所有组件都通过这个公共的Vue实例进行发射和监听事件,如:

Vue.prototype.$bus = new Vue(); // 将这个eventBus对象暴露到原型上方便调用
所有组件都可以获取到这个$bus对象并进行收发数据通信了。

JS_Even_JS
2.6k 声望3.7k 粉丝

前端工程师