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 servr 实际执行的是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 获取到数据了
⑨ 获取子组件或者子标签的引用,父组件可以通过 $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 对象并进行收发数据通信了。