参考:https://juejin.im/book/5bc844166fb9a05cd676ebca
分析
<i-checkbox v-model="single">单独选项</i-checkbox>
<i-checkbox-group v-model="multiple">
<i-checkbox label="option1">选项 1</i-checkbox>
<i-checkbox label="option2">选项 2</i-checkbox>
<i-checkbox label="option3">选项 3</i-checkbox>
<i-checkbox label="option4">选项 4</i-checkbox>
</i-checkbox-group>
两种用法,对应单选和多选
有两个个技术难点:
- Checkbox 要同时支持单独使用和组合使用的场景;(==》 判断是否有父组件CheckboxGroup
- CheckboxGroup 和 Checkbox 内可能嵌套其它的布局组件,所以就涉及到找理想中的父组件可能实际位于祖先位次上;(===> 从该组件出发,向上or向下找任意组件实例
以上是作者总结的两个难点,于我可不止这两个呢
1、首先从组件的api设计上,就值得深究。
由组件api初探组件内部必要的设计:
# 组件使用者
<i-checkbox v-model="single">单独选项</i-checkbox>等价于
<i-checkbox :value="single" @input="xx">单独选项</i-checkbox>
# checkbox内部
<script>
export default{
props:{
value: {
type: [String, Number, Boolean],
default: false
}
},
methods:{
change(){
const checked = event.target.checked;
this.$emit('input', checked);
}
}
}
</script>
===> 单选,是input type=checkbox v-model=single 的进化版
所以组件内部
dom层面会有
1.一个真正的input
2.一个能接 “单独选项”的 slot
3.一个label负责美化input框
数据交互层面:
本质 input有两个数据 <input :checked="xx" :disabled=""/>
1.v-model:
==> prop:value
value改变了 要 this.$emit('input',val)
数据的改变:
1、input框接受用户的交互,当用户改变input框之后 需要将事件派发给组件的使用者
更改v-model的single
===》
1.组件本地不维护数据,input的checked 的数据来源于prop,
那么响应用户动作的时候,就派发事件给上层,上层改变prop,从而联动引起组件内部数据改变
2.组件内部维护数据,data.curval = prop.value;
响应用户动作的时候 直接修改 this.curval = event.target.value;
然后再通知prop的更改;
同时,对于上层的prop更新,组件内部采用监听prop.value改变,以此手动维护本地数据的更新;
2. prop层的改变
1.如果组件内部不维护数据,那么直接更改prop即可
2.如果组件内部维护了数据,那么需要组件内部监听prop的改变
综上,有两种设计组件内部的方式,木偶组件 or 有本地state的组件
组件的dom都是这样的
<div>
<span>
<input
type="checkbox"
:disabled="disabled"
:checked="=>" currentValue or value
@change="change"==> this.$emit('input',xx),this.$emit('on-change',xxx)
/>
</span>
<slot></slot>
</div>
vue下的v-model在表单组件上的特殊表现 会给组件的设计上带来什么优势?
探究一下vue的特殊组件更新
怎么维护model值的双向绑定
<i-checkbox v-model="single">选项 1</i-checkbox>
组件内部
way1:
<input type="checkbox" value="Jack" v-model="propVal">
prop:propVal
===> 这样是不行的,因为改变prop值了
way2:
本地data转一下
<input type="checkbox" value="Jack" v-model="stateval">
prop:proval
data:{
stateval:propval// 初始值为propval
}
way3:
<input type="checkbox" :value="stateval" @input="inputchange">
way3的过于底层,虽然更自由,但是好像和v-model的使用更占优势?(即使用v-model也不会损伤自由
way2此时当前input的value对应的数据源:见下图
由图可知,v-model可以解决右边两个更新维护的问题,
所以现在维护prop的话,监听prop的变化, 然后手动维护v-model的值与prop的统一;
而回归到vue的特殊处理上,v-model的值与value的值相交辉映 绑定约束的感觉;
value的值和v-model的值一致则被选中;
弊端:
这样设计最大的有点恶心的地方就是需要监听prop的变化再手动维护stateval
抛弃了prop改变本地数据自动改变的优势;
vue原生支持的:
<div id='example-3'>
<input type="checkbox" value="Jack" v-model="checkedNames">
<input type="checkbox" value="Job" v-model="checkedNames">
<input type="checkbox" value="Tom" v-model="checkedNames">
<span>Checked names: {{ checkedNames }}</span>
</div>
v-model最大的好处是 一个数据源 维护了多个input的变化
且当父级是v-model时候
<i-checkbox v-model="single">选项 1</i-checkbox>
组件内部数据改变通知组件使用者的时候,只需组件内部this.emit('input',value)即可
组件使用者无需再监听 this.on('input',func)==> this.state = inputval
v-model会帮你处理
此时当前input的value对应的数据源:
checkbox-group的深入分析
核心:group的v-model 与 check的v-model是统一的,我们要做的所有工作都围绕着手动统一这两个变量来
首次渲染与用户change时更新group的值
<i-checkbox-group v-model="multiple">
<i-checkbox label="option1">选项 1</i-checkbox>
<i-checkbox label="option2">选项 2</i-checkbox>
<i-checkbox label="option3">选项 3</i-checkbox>
<i-checkbox label="option4">选项 4</i-checkbox>
</i-checkbox-group>
当是多选组件的时候,想获得的数据主要是 multi维护的那个数组变量 muti = ['option1',‘option2’]
这里维护的是选择的字段,而不是选择的value;
数据交互解析:
1.当首次渲染的时候,multi里的数据会下发到checkbox的各个组件内部,初始化组件状态
2.当用户点击切换选择的时候,上层的multi也得得到更新
3.当v-model的mult变化的时候,下面的组件也会跟着变化;
设计:
1.当首次渲染的时候,multi里的数据会下发到checkbox的各个组件内部,初始化组件状态
===》checkbox渲染的时候 会判断有没有group,如果有 那么初始值就由group获得
===> way1.得到group的value,然后更新自己
way2.将更新的权限放在父级,父级统一遍历以自己的value 更新子组件 的值
更新子组件的方法 还是调用子组件的update值;
2.当用户点击切换选择的时候,会更新multi
1.如果组件内没有本地值,那么直接emit更新;但此时 group是对应的不是父组件的关系;而是跨级关系
根据vue的表单特殊处理,我们知道 input value="" v-model="" 通过v-model可以屏蔽input元素
的响应细节;===》 emit的时候直接 this.emit('input',value)即可
===》 本地是 v-model 的设计
2.本地组件有值,v-model。 :value="label" 即组件使用者指定的字段
【关键是 v-model这个值 是本地新值 还是 直接用group的值?】
v-model=""
【无论用什么值都涉及到 怎么把跨组件的值 统一起来?】goup内部与checkbox内部是 跨组件的关系;
所以,用prop也没什么优势,因为 prop更新之后,checkbox并不会自动更新;
(即prop无法直接作为父组件的传值 直接给check组件用;
综上,使用way2,即将更新组件的权限放在group,然后父group调用子check方法更新子的v-model数据,
子的v-model数据使用本地state维护;
这里的父与子 即 group 与旗下众多的 check组件的关系
虽然在vue层面他们看起来更像是插槽关系,在vue组件层面他们并不能通过 prop传值
但在dom树层面上,他们就是父子;
group找子check===> context.$children
check找父group===> context.$parent
梳理一下上面的数据交互逻辑:
1、首次渲染的时候,multi里的数据会下发到checkbox的各个组件内部,初始化组件状态
checkbox渲染的时候 会判断有没有group,如果有 那么初始值就由group获得 将更新的操作放在父级group,父级group统一遍历 以自己的value 更新子组件 (更新子组件的方法:调用子组件的update方法; 这一点vue的双向绑定很像,dep统一遍历调用watcher的更新方法
2、当用户点击切换选择的时候,会更新multi
本地组件有值,v-model。 :value=”label” 即组件使用者指定的字段; v-model维护一个本地的新值 (check与group内部是跨组件的关系,并不能通过prop传值。所以用prop继承group的值也不划算。这样的话还 需要在组件使用者的地方做一个数据的中转。使用不友好) 子的v-model数据使用本地state维护,当用户change的时候,手动向group组件传值通知更新;
问题转换为 在子组件怎么找到跨级最近组件?即check组件内部向上找到group组件内部 反过来一样,group组件内部怎么找到他下面的每一个check组件?
===》 问题转换为 在子组件怎么找到跨级最近组件?===》
子:
<input
v-if="hasGroup"
type="checkbox"
:disabled="disabeld"
:value="label"
v-model="checkdata" #之所以使用的是本地state是方便得到本地的用户交互带来的数据更新
#如果直接用prop则没有这个效果?
@change="change"
/>
change事件
if (this.group) {
# 从该组件出发 找到 parent,然后修改parent组件上绑定的 v-model
this.parent.change(this.model);
}
父:(
change (newModel) {
this.$emit('input',newModel);
}
3.当v-model的mult变化的时候,下面的组件也会跟着变化;
对于group组件内部,props:value
监听props.value的变化,一旦group的value变化则更新下面子组件的value
父:
watch: {
value(){
this.updateModel();
}
},
updateModel(){
this.children = findComponentsDownward(this, 'iCheckbox');
if(this.children){
const { value } = this;//value:prop 把父的值更新下放到i-checkbox
this.children.forEach(child => {
// 因为v-model = model
child.checkdata = value;
#因为子组件v-model绑定的是本地state 所以可以这样直接修改
#如果是v-model绑定的是prop则不能修改
即一旦决定了这样的数据交互方式,即父会越俎代庖,那么子的相关数据就必须为state值。
而不是不可改的prop
})
}
},
实践
i-checkbox
使用者
<i-checkbox v-model="single" trueValue='yes' falseValue='no'>单选框</i-checkbox>
<script>
import iCheckbox from './checkbox.vue';
export default {
components: {
iCheckbox
},
data () {
return {
single: 'no',
}
},
}
</script>
组件内部
<template>
<label>
<span>
-- !hasGroup
<input
type='checkbox'
:disabled="disabeld"
:checked="curval"
@change="change"
/>
</span>
<slot></slot>
</label>
</template>
<script>
import { findComponentUpward } from '../util.js';
export default {
name:'iCheckbox',
props:{
disabeld: {
type:[Boolean, String, Number],
default:false
},
value: {
type:[Boolean, String, Number],
default:false
},
trueValue: {
type:[Boolean, String, Number],
default:true
},
falseValue: {
type:[Boolean, String, Number],
default:false
}
},
data(){
return{
curval: this.value,
parent: null
}
},
mounted() {
// 往上查找 findComponentUpward 是否有i-group 没有就维护本地的update
this.update();
},
methods:{
change(event){
const checked = event.target.checked;
this.curval = checked ? this.trueValue : this.falseValue;
this.$emit('input',this.curval);
},
update(){
this.curval = this.value === this.trueValue;
}
},
watch: {
// 因为是本地维护了变量,prop的更新并不会直接触发本地的什么变动
// 所以需要监听prop的变化以此来依据prop的变化手动维护本地state的赋值更新
// 至于 group下的check更新则是另一条更新路径
value(newval){
if(newval === this.trueValue || newval === this.falseValue){
this.update();
}
else {
throw 'value should be truevalue or falsevalue';
}
}
},
}
</script>
i-checkbox-group
<i-checkbox-group v-model="multiple">
<i-checkbox label="option1">选项 1</i-checkbox>
<i-checkbox label="option2">选项 2</i-checkbox>
<i-checkbox label="option3">选项 3</i-checkbox>
<i-checkbox label="option4">选项 4</i-checkbox>
</i-checkbox-group>
multiple: ['option1','option3'],
check内部
- 首次渲染,初始化值
当checkbox接受用户交互的时候 ,怎么更新?
checkArr=['jack','tom'] 只要当前input的value 数组有一项满足 则被checked
<input type="checkbox" value="Jack" v-model="checkArr">
那么意味着 用户交互改变的时候 checkbox本地的数据要变,同时,也要更新group的那个数组变化
group数据变化的时候,也要更新到每一个checkbox的变化
<template>
<label>
<span>
<input
v-if="hasGroup"
type="checkbox"
:value="label"
v-model="checkdata"
@change="change"
/>
</span>
<slot></slot>
</label>
</template>
<script>
import { findComponentUpward } from '../util.js';
export default {
name:'iCheckbox',
props:{
label: {
type:[Boolean, String, Number]
}
},
data(){
return{
hasGroup: false,
checkdata: [],
parent: null
}
},
mounted() {
const parent = findComponentUpward(this,'iCheckboxGroup');
if(parent){
this.hasGroup = true;
this.parent = parent;
// 由group统计更新check本地v-model的checkdata
this.parent.updateChild();
}
},
methods:{
change(event){
// 多选的更新 本地触发 再更新 group的
if(this.hasGroup) {
this.parent.changeChild(this.checkdata);
return ;
}
},
update(){
this.curval = this.value === this.trueValue;
}
}
}
</script>
checkgroup
<template>
<div>
<slot></slot>
</div>
</template>
<script>
import { findComponentsDownward } from '../util.js';
export default {
name:'iCheckboxGroup',
props:{
value: {
type:Array,
default(){
return [];
}
}
},
methods: {
// check 初次渲染 this.parent.updateChild()
// group的值去更新check的v-model值
updateChild() {
const children = findComponentsDownward(this,'iCheckbox');
if(children && children.length){
children.forEach(child => {
child.checkdata = this.value;
})
}
},
// check 接受用户改变的时候
// this.parent.changeChild(this.checkdata);
changeChild(newval) {
// 通知改变i-iCheckboxGroup的v-model 子组件就自然改变了
this.$emit('input', newval);
}
},
mounted () {// group 渲染的时候 要 更新child
// child 初次渲染的时候 也调用父 使得更新自己
// 那岂不是更新了两遍?
this.updateChild();
},
watch: {
value () {
this.updateChild();
}
}
}
</script>
评论里的疑问
checkbox
组件初始化时父组件是group
就调用父级updateModel
方法更新勾选状态,这个感觉并不是必须的,反而的感觉更会消耗性能。初始化一个checkbox
就执行一次 ,checkbox
上百个就得执行上百次。 讨论:
因为动态增加?动态删除的话是不会接触到mounted这个生命周期的 而且group和check都在mounted的时候维护了一个check的遍历更新,怎么觉得有点冗余? ===》 动态增加:此时group已经加载了;check又会多一个,以check的mounted调用group的遍历更新这是说的过去的;
除此以外考虑,当check集成到form表单的时候,
-- checkbox 的 change方法 --
if (this.group) {
this.parent.change(this.model);
}
else {
// 派给 组件使用者
this.$emit('on-change', value);
// 给表单
this.dispatch('iFormItem', 'on-form-change', value);
}
-- group的 change方法 --
this.$emit('input', data);
this.$emit('on-change', data);
this.dispatch('iFormItem', 'on-form-change', data);
值得一学的小技巧
label
<label>
<span>
<input
type="checkbox"
:disabled="disabled"
:checked="currentValue"
@change="change"
/>
</span>
<slot></slot>
</label>
<input>
、<slot>
都是包裹在一个<label>
元素内的,这样做的好处是,当点击<slot>
里的文字时,<input>
选框也会被触发,否则只有点击那个小框才会触发,那样不太容易选中,影响用户体验。
====》 本来是用label 的for 和input的id绑定的,用包裹的形式可以不用绑定了好棒