参考: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.一个真正的input2.一个能接 “单独选项”的 slot3.一个label负责美化input框数据交互层面:本质 input有两个数据 <input :checked="xx" :disabled=""/>1.v-model:==> prop:valuevalue改变了 要 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><inputtype="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:provaldata:{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 = inputvalv-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.当用户点击切换选择的时候,会更新multi1.如果组件内没有本地值,那么直接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组件?
===》 问题转换为 在子组件怎么找到跨级最近组件?===》子:<inputv-if="hasGroup"type="checkbox":disabled="disabeld":value="label"v-model="checkdata" #之所以使用的是本地state是方便得到本地的用户交互带来的数据更新#如果直接用prop则没有这个效果?@change="change"/>change事件if (this.group) {# 从该组件出发 找到 parent,然后修改parent组件上绑定的 v-modelthis.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-checkboxthis.children.forEach(child => {// 因为v-model = modelchild.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<inputtype='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 没有就维护本地的updatethis.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><inputv-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的checkdatathis.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><inputtype="checkbox":disabled="disabled":checked="currentValue"@change="change"/></span><slot></slot></label>
<input>、<slot>都是包裹在一个<label>元素内的,这样做的好处是,当点击<slot>里的文字时,<input>选框也会被触发,否则只有点击那个小框才会触发,那样不太容易选中,影响用户体验。
====》 本来是用label 的for 和input的id绑定的,用包裹的形式可以不用绑定了好棒
