组件通信
1、父组件 => 子组件
- 属性props
// child props: { msg: String } // parent <HelloWorld msg="Welcome to Your Vue.js App"/>
- 引用refs --- 特别适用于想要操作某些DOM结构时,refs就是这些DOM节点
// parent <HelloWorld ref="hw"/> this.$refs.hw.xx = 'xxx'
this.$refs 需要在mounted 中 或之后调用,因为父组件先于子组件挂载,在父组件 created 的时候,子组件还没挂上去,所以访问不到
- 子组件chidren
// parent this.$children[0].xx = 'xxx'
子元素不保证顺序,需要小心使用
2、子组件 => 父组件 自定义事件
// child this.$emit('add', good) // parent <Cart @add="cartAdd($event)"></Cart>
3、兄弟组件:通过共同祖辈组件
通过共同的祖辈组件搭桥,$parent 或 $root
// brother1 this.$parent.$on('foo', handle) // brother2 this.$parent.$emit('foo')
4、祖先和后代之间
由于嵌套层数过多,传递props不切实际,vue提供了 provide/indect API完成该任务
- provide / inject:能够实现祖先给后代传值
// ancestor provide() { return {foo: 'foo'} } // descendant inject: ['foo']
注意:provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序中。我们更多会在开源组件库中见到。
但是,反过来想要后代给组件传值,这种方案就不行了,子组件如果想改的话,需要组件在provide中注入 setFoo 的函数,子组件通过setFoo来修改 foo 的值
provide的时候,完全可以把当前组件(祖先组件)注入,直接传入 this 就行了
5、任意两个组件之间:事件总线 或 vuex
- 事件总线:创建一个 Bus 类负责事件派发、监听 和 回调管理
// Bus:事件派发、监听和回调管理 class Bus { constructor() { this.callbacks = {}; } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach(cb => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus(); // 使用 // child1 this.$bus.$on("foo", handle); // child2 this.$bus.$emit("foo");
简单点的写法: Vue.prototype.$bus = new Vue()
这里的 new Vue() 是一个新的、干净的vue实例,只在做监听、派发消息的事情。因为Vue已经实现上述 Bus 的接口了
- vuex:创建唯一的全局数据管理者 store,通过它管理数据并通知组件状态变更
插槽
插槽语法是Vue实现的内容分发API,用于复合组件开发。该技术在通用组件库开发中有大量应用。
Vue 2.6.0 之后采用全新的 v-slot 语法取代之前的 slot、slot-scope
匿名插槽
// child <div> <slot></slot> </div> // parent <comp>hello</comp>
具名插槽
// child <div> <slot></slot> <slot name="content"></slot> </div> // parent <Comp2> // 默认插槽用default做参数 <template v-slot:default>具名插槽</template> // 具名插槽用插槽名做参数 <template v-slot:content>内容...</template> </Comp2>
作用域插槽
// comp3 <div> <slot :foo="foo"></slot> </div> // parent <Comp3> // 把v-slot的值指定为作用域上下文对象 <template v-slot:default="ctx"> 来自子组件的数据:{{ctx.foo}} </template> </Comp3>
相当于子组件存有一个值,父组件可以从 v-slot 获取后,进行相应的操作
另一种写法--对象的解构
// comp3 <div> <slot :foo="foo"></slot> </div> // parent <Comp3> // 把v-slot的值指定为作用域上下文对象 <template v-slot:default="{foo}"> 来自子组件的数据:{{foo}} </template> </Comp3>
组件化实战
实现 Form、FormItem、Input
- Input
- 双向绑定:@input、:value
- 派发校验事件
<template> <div> <!-- 自定义组件要实现 v-model 必须实现 :value @inpit --> <!-- $attrs存储的是props之外的部分,这里用v-bind把这些东西展开 --> <input :value="value" @input="onInput" v-bind="$attrs"/> </div> </template> <script> export default { inheritAttrs: false, // 避免顶层容器继承属性---<div>上面就不会有 type=password 属性了 props: { value: { type: String, default: '' } }, methods: { onInput(e) { // 通知父组件数值变化 this.$emit('input', e.target.value)
// 通知FormItem校验 this.$parent.$emit('validate'); } }, } </script>
- 实现KFormItem
- 任务1:给Inoput预留插槽 - slot
- 任务2:能够展示label 和校验信息
- 任务3:能够进行校验
数据校验,思路:校验发生在FormItem,它需要知道何时校验(让Input通知它),还需要知道怎么校验(注入校验规则)
任务1:Input通知校验
onInput(e){ // $parent 指 FormItem this.$parent.$emit('validate') }
任务2:FormItem 监听校验通知,获取规则并执行校验
安装校验库:npm i async-validator -S
<template> <div> <label v-if="label">{{label}}</label> <slot></slot> <!-- 校验信息 --> <p v-if="errorMessage"> {{errorMessage}} </p> </div> </template> <script> import Schema from 'async-validator' export default { inject: ['form'], props:{ label: { type: String, default: '' }, prop: String }, data(){ return{ errorMessage: '' } }, methods: { validate() { // 执行组件校验 // 1、获取校验规则 const rules = this.form.rules[this.prop] // 2、获取数据 const value = this.form.model[this.prop] // 3、执行校验 const desc = { [this.prop]: rules } const schema = new Schema(desc) // 参数1是值,参数2是校验错误对象数组 // 返回的是Promise<boolean> return schema.validate({[this.prop]: value}, errors => { if(errors){ // 有错 this.errorMessage = errors[0].message } else { // 没错,就清除错误信息 this.errorMessage = '' } }) } }, mounted() { // 监听校验事件,并执行监听 this.$on('validate', () => { this.validate() }) } } </script>
- Form
- 给FormItem留插槽
- 设置数据和校验规则
- 全局校验
template> <div> <slot></slot> </div> </template> <script> export default { provide() { return { form: this // 传递Form实例给后代,比如FormItem用来校验 } }, props: { model: { type: Object, required: true }, rules: Object }, methods: { validate(cb) { // map 的结果是若干 Promise数组 const tasks = this.$children .filter(item => item.prop) .map(item => item.validate()) // 所有任务必须全部成功才算校验成功 Promise.all(tasks).then(() => { cb(true) }).catch(() => { cb(false) }) } }, } </script>
使用如下:index.vue
<template> <div> <KForm :model="model" :rules="rules" ref="loginForm"> <KFormItem label="用户名" prop="username"> <KInput v-model="model.username"></KInput> </KFormItem> <KFormItem label="密码" prop="password"> <KInput v-model="model.password" type="password"></KInput> </KFormItem> <KFormItem> <button @click="onLogin">登录</button> </KFormItem> </KForm> {{model}} </div> </template> <script> import KInput from './KInput' import KFormItem from './KFormItem' import KForm from './KForm' export default { data() { return { model: { username: 'tom', password: '' }, rules: { username: [{required: true, message: '用户名必填'}], password: [{required: true, message: '密码必填'}] } } }, methods: { onLogin() { this.$refs.loginForm.validate((isValid) => { if(isValid) { alert('登录!!!') }else{ alert('有错!!!') } }) } }, components: { KInput, KFormItem, KForm } } </script>
inheritAttrs 2.4.0 新增
类型:boolean 默认:true
默认情况下父作用域的不被认作props的特性绑定将会“回退”,且作为普通的 HTML 特性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 为 false,这些默认行为将会被丢掉。而通过实例属性 $arrts 可以让这些特性生效,且可以通过 v-bind 显式绑定到非根元素上。
注意:这个选项不影响 class 和 style 绑定
组件由上至下广播
可以解决 组件之间通信 父组件向子组件派发事件时,如果父子之间还有其他组件导致无法通信的问题 :$children 的派发事件
function boardcast(eventName, data) { this.$children.forEach(child => { // 子元素触发$emit child.$emit(eventName, data) if(child.$children.length) { // 递归调用,通过call修改this指向 child boardcast.call(child, eventName, data) } }) } Vue.prototype.$boardcast = function(eventName, data) { boardcast.call(this, eventName, data) }
elementUI的做法
// 事件广播 function broadcast(componentName, eventName, params){ // 遍历所有的children this.$children.forEach(child => { var name = child.$options.componentName; if(name === componentName){ // 如果找到了 componentName 组件,则让child自己给自己派发事件 // 然后子组件里面就可以自己监听这个事件了 child.$emit.apply(child, [eventName].concat(params)) }else{ // 如果没找到,接着找,继续调用broadcast给子组件 broadcast.apply(child, [componentName, eventName].concat([params])) } }) }
由下而上广播
Vue.prototype.$dispatch = function(eventName, data) { let parent = this.$parent // 查找父元素 while(parent){ // 父元素用$emit触发 parent.$emit(eventName, data) // 递归查找父元素 parent = parent.$parent } }
elementUI的做法
// 自下而上的事件派发 dispatch(componentName, eventName, params) { var parent = this.$parent || this.$root var name = parent.$options.componentName // 当parent存在,并且 name存在且name不等于componentName时进行循环 while(parent && (!name || name !== componentName)){ parent = parent.$parent if(parent){ name = parent.$options.componentName } } if(parent){ parent.$emit.apply(parent, [eventName].concat(params)) } } broadcast(conponentName, eventName, params) { broadcast.call(this, componentName, eventName, params) }
v-model 和 .sync 的异同
// v-model 是语法糖 <Input v-model="username"> // 默认等效于下面这行 <Input :value="username" @input="username=$event"> // 但是你也可以通过设置model选项修改默认行为, Checkbox.vue { model: { prop: 'checked', event: 'change' } } // 上面这样设置会导致上级使用 v-model 时行为变化,相当于 <Checkbox :checked="model.remember" @change="model.remember = $event"></Checkbox>
场景:v-model 通常用于表单控件,它有默认行为,同时属性名和事件名均可在子组件定义
// sync 修饰符添加于 v2.4,类似于 v-model,它能用于修改传递到子组件的属性,如果像下面这样写 <Input :value.sync="username"> // 等效于下面这行,那么和v-model的区别只有事件名称的变化 <Input :value="username" @update:value="username=$event"> // 这里绑定属性名称更改,相应的属性名也会变化 <Input :foo="username" @update:foo="username=$event">
场景:父组件传递的属性子组件想修改
所以sync修饰符的控制能力都在父级,事件名称也相对固定 update:xx
习惯上表单元素用 v-model
实现弹窗组件
弹窗这类组件的特点是它们在当前vue实例之外独立存在,通常挂载于body;它们是通过JS动态创建的,不需要在任何组件中声明,常见使用姿势:
this.$create(Notice, { title: 'xx', message: 'xx', duration: 1000 }).show()
create
create函数用于动态创建指定组件实例并挂载至body
// 创建指定组件实例,并挂载到body上 import Vue from 'vue'; export default function create(Component, props) { // 0、先创建vue实例,这里先不挂载 $mount() const vm = new Vue({ render(h) { // render方法提供给我们一个h函数(createElement函数的别名),它可以渲染VNode return h(Component, {props}) } }).$mount() // 1、上面的vm帮我们创建组件实例 // 2、通过$children获取该组件实例 // console.log(vm.$root) const comp = vm.$children[0] // 3、追加至body document.body.appendChild(vm.$el) // 4、清理函数 comp.remove = () => { document.body.removeChild(vm.$el) vm.$destroy() } // 5、返回组件实例 return comp }
创建通知组件 Notice.vue
<template> <div v-if="isShow"> <h3>{{title}}</h3> <p>{{message}}</p> </div> </template> <script> export default { props: { title:{ type: String, default: '' }, message:{ type: String, default: '' }, duration: { type: Number, default: 1000 } }, data() { return { isShow: false } }, methods: { show(){ this.isShow = true setTimeout(() => { this.hide() }, this.duration) }, hide(){ this.isShow = false this.remove() } }, } </script>
使用方法:
const notice = create(Notice, { title: 'xxx', message: 'blabla~~', duration: 1000 }) notice.show()