這是我第20篇簡(jiǎn)書。
1、組件通信
(1)父子通信
① props-$emit
② $refs
短信驗(yàn)證碼、圖形驗(yàn)證碼組件我經(jīng)常用$refs
this.$refs.captcha= 'xxx'
③ 子組件$children[0]
并不保證順序,所以從來(lái)不用這個(gè)方法,除非只有一個(gè)子組件。
// parent
this.$children[0].xx = 'xxx'
(2)兄弟通信
通過(guò)共同的祖輩組件搭橋,$parent或$root。
$root 和 $parent 都能夠?qū)崿F(xiàn)訪問(wèn)父組件的屬性和方法,兩者的區(qū)別在于,如果存在多級(jí)子組件,通過(guò)$parent訪問(wèn)得到的是它最近的一級(jí)的父組件,通過(guò)$root得到的是它的根父組件。
brother1:
this.$parent.$emit('foo');
brother2:
this.$parent.$on('foo', handle)
(3)祖先與后代通信
用于組件庫(kù)的開(kāi)發(fā),只能祖先給后代傳值.。
這時(shí)用props屬性就會(huì)嵌套太多props,不是很合適。
祖先: provide() {
return {hi: 'hello 后代'}
}
后代:inject:['hi']
-----------------------------------
可直接返回this,子組件直接拿祖先的數(shù)據(jù):
祖先:
provide() {
return {hi: this}
},
data() {
return {
grandfa:'dxl'
}
}
后代:
<p>{{hi.grandfa}}</p>
inject:['hi']
(4)任意兩個(gè)組件之間通信:事件總線 或 vue
事件總線:創(chuàng)建一個(gè)Bus類負(fù)責(zé)事件派發(fā)、監(jiān)聽(tīng)和回調(diào)管理
// Bus:事件派發(fā)、監(jiān)聽(tīng)和回調(diào)管理
class Bus{
constructor(){
// {
// eventName1:[fn1,fn2],
// eventName2:[fn3,fn4],
// }
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()
以上自定義bus類實(shí)現(xiàn)觀察者模式
或者直接用vue實(shí)例即可。
Vue.prototype.$bus = new Vue()
// child1
this.$bus.$on('foo', handle)
// child2
this.$bus.$emit('foo')
(5)$attrs和$listeners(基本被Vuex替代了)
-
$listeners:
包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監(jiān)聽(tīng)器。它可以通過(guò) v-on=”$listeners” 傳入內(nèi)部組件——在創(chuàng)建更高層次的組件時(shí)非常有用。 -
$attrs:
包含了父作用域中不被認(rèn)為 (且不預(yù)期為) props 的特性綁定 (class 和 style 除外)。當(dāng)一個(gè)組件沒(méi)有聲明任何 props 時(shí),這里會(huì)包含所有父作用域的綁定 (class 和 style 除外),并且可以通過(guò) v-bind=”$attrs” 傳入內(nèi)部組件——在創(chuàng)建更高層次的組件時(shí)非常有用。
舉例一:
想象一下,你打算封裝一個(gè)自定義input組件——MyInput,需要從父組件傳入type,placeholder,title等多個(gè)html元素的原生屬性。此時(shí)你的MyInput組件props如下:
props:['type','placeholder','title',...]
很繁瑣不是嗎?$attrs專門為了解決這種問(wèn)題而誕生,這個(gè)屬性允許你在使用自定義組件時(shí)更像是使用原生html元素。比如:
// 父組件
<my-input placeholder="請(qǐng)輸入你的姓名" type="text" title="姓名" v-model="name"/>
// 子組件
<template>
<div>
<label>姓名:</label>
<input v-bind="$attrs" :value="value" @input="$emit('input',$event.target.value)"/>
</div>
</template>
<script>
export default {
inheritAttrs:false,
props:['value']
}
</script>
讓MyInput組件實(shí)現(xiàn)focus事件:
// 父組件
<my-input @focus="focus" placeholder="請(qǐng)輸入你的姓名" type="text" title="姓名" v-model="name"/>
// 子組件
<template>
<div>
<input v-bind="$attrsAll" v-on="$listenserAll"/>
</div>
</template>
<script>
export default {
inheritAttrs:false,
props:['value'],
computed:{
$attrsAll() {
return {
value: this.value,
...this.$attrs
}
},
$listenserAll(){
return Object.assign({},
this.$listeners,
{input:(event) =>
this.$emit('input',event.target.value)
}
)
}
}
}
</script>
舉例二:
三個(gè)組件:Grandfa、Father、Son
Grandfa => Son: (爺爺給孫子傳值)
通過(guò)$attrs傳值
// Grandfa 傳了一個(gè)靜態(tài)placeholder值: 請(qǐng)輸入
<div id="app">
{{value}}
<wrapper v-on:focus="onFocus" v-bind:value="value" v-on:input="onFocus" placeholder="請(qǐng)輸入">
</wrapper>
</div>
// Father
Vue.component("Wrapper",{
template:`
<div>
<son v-bind="$attrs" v-on="$listeners"></son>
</div>
`
});
// Son
Vue.component("son",{
template:`
<div>
<button @click="handleClick">sonbutton</button>
<input type="text" v-bind="$attrs" v-on="rewriteListener">
</div>
`,
});
Son => Grandfa: (孫子通知爺爺)
computed: {
rewriteListener() {
const vm = this;
return Object.assign({},
this.$listeners,
{
input: (event) =>
vm.$emit("input", event.target.value)
}
)
}
}
2、內(nèi)容分發(fā)slot插槽
插槽語(yǔ)法是Vue實(shí)現(xiàn)的內(nèi)容分發(fā)API,用于復(fù)合組件開(kāi)發(fā),在通用組件庫(kù)開(kāi)發(fā)中大量應(yīng)用。
(注:Vue 2.6.0之后采用全新v-slot語(yǔ)法取代之前的slot、slot-scope)
(1)匿名插槽
// comp1
<div>
<slot></slot>
</div>
// parent
<Comp1>hello</Comp1>
(2)具名插槽
// comp2
<div>
<slot></slot>
<slot name="content"></slot>
</div>
// parent
<Comp2>
<!-- 默認(rèn)插槽用default做參數(shù) -->
<template v-slot:default>匿名插槽</template>
<!-- 具名插槽用插槽名做參數(shù) -->
<template v-slot:content>具名插槽的內(nèi)容...</template>
</Comp2>
(3)作用域插槽
以上兩個(gè)子組件的插槽值只能由父組件決定安排,但是我們的實(shí)際業(yè)務(wù)中,往往是兒子安排老子...這時(shí)就要用到作用域插槽。
// comp3
// 子組件決定值是'your name is XXX'
<div>
<slot :foo="your name is defaultFoo"></slot>
<slot name="content" :foo="your name is contentFoo"></slot>
</div>
// parent
<Comp3>
<!-- 把v-slot的值指定為作用域上下文對(duì)象 -->
<template v-slot:default="slotProps">
來(lái)自子組件數(shù)據(jù):{{slotProps.foo}}
</template>
// slotProps這個(gè)命名可以隨便起,或者直接解構(gòu):
<template v-slot:content="{foo}">
來(lái)自子組件數(shù)據(jù):{{foo}}
</template>
</Comp3>
3、sync修飾符
sync修飾符添加于v2.4,類似于v-model,它能?于修改傳遞到?組件的屬性,可以簡(jiǎn)化子組件通知父元素更新傳入?yún)?shù)這個(gè)動(dòng)作的代碼邏輯。
場(chǎng)景:?組件傳遞的屬性?組件想修改
所以sync修飾符的控制能?都在?級(jí),事件名稱也相對(duì)固定update:xx。
// 父組件將value傳給子組件并使用.sync修飾符。
<Input :value.sync="model.username">
<!-- 等效于下?這?,那么和v-model的區(qū)別只有事件名稱的變化 -->
<Input :value="username" @update:value="username=$event">
<!-- 這?綁定屬性名稱更改,相應(yīng)的屬性名也會(huì)變化 -->
<Input :foo="username" @update:foo="username=$event">
<!-- 綁定對(duì)象 -->
<my-com v-bind.sync="obj1"></my-com>
// 子組件觸發(fā)事件:
this.$emit('update:obj1', "it is new key by my-com");
4、實(shí)戰(zhàn)1:自定義表單組件
做幾個(gè)自定義組件來(lái)更好的鞏固知識(shí)。


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 label="記住密碼" prop="password">
<KCheckBox v-model="model.remember"></KCheckBox>
<KCheckBox :checked="model.remember" @change="model.remember = $event"></KCheckBox>
</KFormItem>
<KFormItem>
<button @click="onLogin">登錄</button>
</KFormItem>
</KForm>
{{model}}
</div>
</template>
<script>
import KInput from "./KInput.vue";
import KCheckBox from "./KCheckBox.vue";
import KFormItem from "./KFormItem.vue";
import KForm from "./KForm.vue";
import Notice from "../Notice";
import create from "@/utils/create";
export default {
components: {
KInput,
KFormItem,
KForm,
KCheckBox
},
data() {
return {
model: {
username: "tom",
password: "",
remember: false
},
rules: {
username: [{ required: true, message: "用戶名必填" }],
password: [{ required: true, message: "密碼必填" }]
}
};
},
methods: {
onLogin() {
// 創(chuàng)建彈窗實(shí)例
let notice;
this.$refs.loginForm.validate(isValid => {
<!--彈窗組件在第5點(diǎn)講解 -->
notice = create(Notice, {
title: "xxx",
message: isValid ? "登錄成功!" : "有錯(cuò)??!",
duration: 3000
});
notice.show();
});
}
}
};
</script>
KInput.vue:
- 重點(diǎn):
v-bind="$attrs
把父組件的type="password"傳了過(guò)來(lái),但是此時(shí)會(huì)影響到div,這時(shí)就要用到inheritAttrs,將其設(shè)為false避免頂層容器繼承屬性。 - 實(shí)現(xiàn):
雙向綁定::value、@input
派發(fā)校驗(yàn)事件
<template>
<div>
<!-- 自定義組件要實(shí)現(xiàn)v-model必須實(shí)現(xiàn):value, @input -->
<!-- $attrs存儲(chǔ)的是props之外的部分:------{type:'password'} -->
<input :value="value" @input="onInput" v-bind="$attrs">
</div>
</template>
<script>
export default {
inheritAttrs: false, // 避免頂層容器繼承屬性
props: {
value: {
type: String,
default: ''
}
},
methods: {
onInput(e) {
// 通知父組件數(shù)值變化
this.$emit('input', e.target.value);
// 通知FormItem校驗(yàn)
// 此處用$parent派發(fā)事件不夠健壯,因?yàn)槿绻谇短滓粚訕?biāo)簽就派發(fā)不到了。
// 可看下elementUI form表單的源碼,在下面
this.$parent.$emit('validate');
}
},
}
</script>
<style lang="scss" scoped>
</style>
elementUI form表單input部分源碼:
// 派發(fā),就是子組件向父組件派發(fā)事件
dispatch (componentName, eventName, params) {
// 獲取當(dāng)前組件的父組件
var parent = this.$parent || this.$root
// 拿到父組件名稱
var name = parent.$options.componentName
// 通過(guò)循環(huán)的方式不斷向父組件查找目標(biāo)組件
while (parent && (!name || name !== componentName)) {
parent = parent.$parent
if (parent) {
name = parent.$options.componentName
}
}
// 當(dāng)循環(huán)結(jié)束,證明目標(biāo)父組件已找到(如果存在),就通知父組件觸發(fā)相應(yīng)事件
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params))
}
},
KCheckBox.vue:
還可以通過(guò)設(shè)置model選項(xiàng)修改默認(rèn)行為:
<template>
<div>
<input type="checkbox" :checked="checked" @change="onChange"/>
</div>
</template>
<script>
export default {
props: {
checked: {
type: Boolean,
default: false
}
},
model: {
prop: "checked",
event: "change"
},
methods: {
onChange(e) {
this.$emit('change', e.target.checked)
}
},
};
</script>
KFormItem.vue:
- 實(shí)現(xiàn):
給Input預(yù)留插槽 - slot
能夠展示label和校驗(yàn)信息
能夠進(jìn)行校驗(yàn)
<template>
<div>
<label v-if="label">{{label}}</label>
<slot></slot>
<!-- 校驗(yàn)信息 -->
<p v-if="errorMessage">{{errorMessage}}</p>
</div>
</template>
<script>
import Schema from "async-validator";
export default {
data() {
return {
errorMessage: ""
};
},
inject: ["form"],
props: {
label: {
type: String,
default: ""
},
prop: String
},
mounted() {
// 監(jiān)聽(tīng)校驗(yàn)事件、并執(zhí)行監(jiān)聽(tīng)
this.$on("validate", () => {
this.validate();
});
},
methods: {
validate() {
// 執(zhí)行組件校驗(yàn)
// 1.獲取校驗(yàn)規(guī)則
const rules = this.form.rules[this.prop];
// 2.獲取數(shù)據(jù)
const value = this.form.model[this.prop];
// 3.執(zhí)行校驗(yàn)
const desc = {
[this.prop]: rules
};
const schema = new Schema(desc);
// 參數(shù)1是值,參數(shù)2是校驗(yàn)錯(cuò)誤對(duì)象數(shù)組
// 返回的Promise<boolean>
return schema.validate({ [this.prop]: value }, errors => {
if (errors) {
// 有錯(cuò)
this.errorMessage = errors[0].message;
} else {
// 沒(méi)錯(cuò),清除錯(cuò)誤信息
this.errorMessage = "";
}
});
}
}
};
</script>
5、實(shí)戰(zhàn)2:彈窗類組件
彈窗類組件的特點(diǎn):
- 在當(dāng)前vue實(shí)例之外獨(dú)立存在,通常掛載與body
- 通過(guò)js動(dòng)態(tài)創(chuàng)建,不需要在任何組件中聲明
----------- create.js:--------------
import Vue from 'vue'
// 創(chuàng)建指定組件實(shí)例并掛載于body上
/**
* Component 組件
* props 屬性值
*/
export default function create(Component, props) {
// 0. 先創(chuàng)建vue實(shí)例
const vm = new Vue({
// render方法提供給我們一個(gè)h函數(shù),它可以渲染VNode
render(h) {
return h(Component, {props})
}
}).$mount(); // 更新操作
// 1. 上面vm幫我們創(chuàng)建組件實(shí)例
// 2. 通過(guò)$children獲取該組件實(shí)例
cosole.log(vm.$root);
const comp = vm.$children[0];
// 3.追加至body
document.body.appendChild(vm.$el);
// 4.清理函數(shù)
comp.remove = () => {
document.body.removeChild(vm.$el);
vm.$destroy();
}
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: ""
}
},
data() {
return {
isShow: false
};
},
methods: {
show() {
this.isShow = true;
setTimeout(() => {
this.hide()
}, this.duration);
},
hide() {
this.isShow = false;
this.remove();
(對(duì)應(yīng)create.js里的remove清理函數(shù))
}
}
};
</script>
未完待續(xù) 。。。