剖析
<template>
<el-radio-group v-model="radio">
<el-radio :label="3">备选项</el-radio>
<el-radio :label="6">备选项</el-radio>
<el-radio :label="9">备选项</el-radio>
</el-radio-group>
</template>
<script>
export default {
data () {
return {
radio: 3
};
}
}
</script>
- 这里注意下,动态绑定v-bind和静态绑定的区别
> <y-radio label="1"></y-radio> 不加冒号, 是String类型 1+label=11(在组件y-radio中打印)
> <y-radio :label="1"></y-radio> 加冒号, 是Number类型 1+label=2
> 加冒号,表示这是一个 JavaScript 表达式而不是一个字符串
> 不加冒号,表示这是一个字符串
> 只有传递字符串常量时,不采用v-bind形式,其余情况均采用v-bind形式传递(不加v-bind时,vue就认为此时通过prop传递给组件的是字符串常量)
> Vue 为什么要这样设计呢?v-bind的设计有什么意义?
> 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。
> 这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
> prop 官方文档: https://cn.vuejs.org/v2/guide/components-props.html
测试代码
<template>
<div>
<p>raodio-group 静态绑定radio的label</p>
<y-radio-group v-model="radioGroupValue">
<y-radio label="1">男</y-radio>
<y-radio label="2">女</y-radio>
<y-radio label="3">未知</y-radio>
</y-radio-group>
<p>raodio-group 动态绑定radio的label</p>
<y-radio-group v-model="radioGroupValue">
<y-radio :label="1">男</y-radio>
<y-radio :label="2">女</y-radio>
<y-radio :label="3">未知</y-radio>
</y-radio-group>
</div>
</template>
<script>
export default {
data() {
return {
radioGroupValue: "1",
};
},
watch: {
radioGroupValue() {
console.log('父组件radioGroupValue', this.radioGroupValue)
}
}
};
</script>
组件 radio-group
<template>
<!-- radio-group, 有radio子组件
父组件会传递过来一个v-model即value和input事件
radio-group组件要把该value值传递给子组件radio
子组件radio中该值发生变化,要通知radio-group,radio-group再通过input事件通知父组件
如何把value值双向绑定给子组件呢?--
element-ui 2.15.1 通过组件radio向上追溯判断父组件是否是radio-group, 如果是的话, 则取radio-group的value值
-->
<div class="y-radio-group" role="rolegroup">
<!-- 并没有给子组件传递方法更新value值,那么子组件radio是如何通知父组件radio-group更新呢?(使用混入mixins的dispatch方法向上冒泡) -->
<slot></slot>
</div>
</template>
<script>
export default {
name: 'YRadioGroup',
componentName: 'YRadioGroup',
props: {
value: {},
},
created() {
/**
* this.$on 监听当前实例上的自定义事件。
* 事件可以由 this.$emit 触发。
* 回调函数会接收所有传入事件触发函数的额外参数。
* 事实上, 子组件radio在调用的父组件radio-group的handleChange方法, 在radio-group没有显示定义
*/
this.$on('handleChange', value => {
console.log('radio-group监听handleChange发生了变化', value);
// 通知父组件更新双向绑定的值
this.$emit('input', value);
})
},
mounted() {
console.log('变化了', this.value, this.$slots);
}
}
</script>
这里需要注意两点
- 父组件 radio-group 向子组件 radio 传递v-model绑定的值
- 子组件 radio 绑定的值更改后,需要通知父组件 radio-group 更新
组件 radio 修改
<template>
<label
class="y-radio"
:class="[
{'is-focus': focus},
{'is-checked': model === label}
]"
role="radio"
:aria-checked="model === label">
<span
class="y-radio__input"
:class="{
'is-checked': model === label
}">
<span class="y-radio__inner"></span>
<input
ref="radio"
class="y-radio__original"
:value="label"
type="radio"
aria-hidden="false"
v-model="model"
@focus="focus = true"
@blur="focus = false"
@change="handleChange" />
</span>
<span class="y-radio__label">
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</span>
</label>
</template>
<script>
import Emitter from '../../../src/mixins/emitter';
export default {
name: 'YRadio',
// 混入,增加dispath方法,将事件向上派发给父组件
mixins: [Emitter],
// 接收父组件传递的数据
props: {
value: {}, // 父组件v-model
label: {}, // 父组件label
},
computed: {
// 向上追溯父组件是否为radio-group,(不能通过name来判断,所以需要在radio-group定义componentName)
isGroup() {
let parent = this.$parent;
while(parent) {
// console.log('radio下的父组件', parent, parent.$options.componentName);
if(parent.$options.componentName !== 'YRadioGroup') {
parent = parent.$parent;
}else {
// 保留radio-group组件的数据
this._radioGroup = parent;
return true;
}
}
return false;
},
/**
* 如果是父组件radio-group下的radio, 则isGroup为true, model取radio-group组件上的value
* 如何去通知父组件radio-group更新value值呢?
*/
model: {
get() {
// return this.value;
return this.isGroup ? this._radioGroup.value : this.value;
},
set(val) {
console.log('model值发生了变化', val);
// 通知父组件更新v-model对应的value值
// this.$emit('input', val)
if(this.isGroup) {
// this.dispatch('YRadioGroup', 'input', [val]);
// this.dispatch 是混入进来的, 通知父组件radio-group更新
this.dispatch('YRadioGroup', 'input', [val]);
}else {
this.$emit('input', val);
}
// 同步到原生单选框的选中状态
this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
}
}
},
data() {
return {
focus: false,
}
},
methods: {
handleChange() {
// input改变后, 需要等视图的更新, 更新后方可获取该值, 为label对应的值
this.$nextTick(()=>{
console.log('handleChange', this.model);
// 这句话有什么意义, 通知父组件选中状态变化后的回调
this.$emit('change', this.model);
// 如果是父组件是radio-group组件, 则调用父组件的handleChange, 以更新model值
this.isGroup && this.dispatch('YRadioGroup', 'handleChange', this.model);
})
},
},
mounted() {
// console.log('111', this.$slots, Boolean(this.$slots.default), this.value, this.label, this.model === this.label);
// 测试: label值为1,如果加冒号,表示传入的是数字类型,1+1=1;如果不加冒号,表示传入的是字符串类型,1+1=11
// console.log('222', this.label, this.label + 1);
console.log('333', this.model)
}
}
</script>
混入 Emitter
/**
* mixin混入,可以混入到所使用的组件本身的实例对象,两个对象的融合
* 例如,radio组件使用了混入,且该混入对象中有radio组件没有的方法,则混入后radio组件便有了这个方法
* 使用场景:如果多个组件公用同一个方法或什么的,可以抽离出来,混入进去
* 官方文档: https://cn.vuejs.org/v2/guide/mixins.html
*/
export default {
methods: {
/**
* 实现子组件向父组件派发事件, 子组件通知父组件
* @param {*} componentName 组件名, 用来查找拥有该组件名的组件(当前的父组件)
* @param {*} eventName 事件名, 字符串类型
* @param {*} params 参数
*/
dispatch(componentName, eventName, params) {
// console.log('混入文件emitter', componentName, eventName, params, this);
let parent = this.$parent;
let name = parent.$options.componentName;
// 查找组件名为componentName的父组件
while(parent && (!name || name !== componentName)) {
parent = parent.$parent;
if(parent) {
name = parent.$options.componentName;
}
}
/**
* emit官方文档: https://cn.vuejs.org/v2/api/#vm-emit
* vm.$emit( eventName, […args] ), 接收的参数有两种形式, 一种是this.$emit(eventName, 参数), 一种是this.$emit([事件名, 参数])
* concat 连接两个数组 ['input'].concat(['1']) => ['input', '1'], ['input'].concat('1') => ['input', '1']
* 语法: arrayObject.concat(arrayX,arrayX,......,arrayX) 必需。该参数可以是具体的值,也可以是数组对象。可以是任意多个。
* 返回值: 返回一个新的数组。该数组是通过把所有 arrayX 参数添加到 arrayObject 中生成的。如果要进行 concat() 操作的参数是数组,那么添加的是数组中的元素,而不是数组。
* apply 修改this.$emit中的this指向
* this.$emit([事件名, 参数]) 会有什么样的效果?会报错的,不能这样写的
* 之所以可以这样写(this.$emit(parent, [eventName].concat(params))), 是因为js中修改this指向,如果使用apply方法接受数组形式的参数(call方法分别接受参数)
* 所以 this.$emit(parent, ['input', '1']) 会被译成 vm.$emit('input', '1') 这种形式
*/
console.log('父亲节点', parent, name, [eventName].concat('1'));
// apply 修改this.$emit中的this指向
this.$emit.apply(parent, [eventName].concat(params))
}
}
}