参考:https://juejin.im/book/5bc844166fb9a05cd676ebca

分析

  1. <i-checkbox v-model="single">单独选项</i-checkbox>
  2. <i-checkbox-group v-model="multiple">
  3. <i-checkbox label="option1">选项 1</i-checkbox>
  4. <i-checkbox label="option2">选项 2</i-checkbox>
  5. <i-checkbox label="option3">选项 3</i-checkbox>
  6. <i-checkbox label="option4">选项 4</i-checkbox>
  7. </i-checkbox-group>

两种用法,对应单选和多选
有两个个技术难点:

  • Checkbox 要同时支持单独使用和组合使用的场景;(==》 判断是否有父组件CheckboxGroup
  • CheckboxGroup 和 Checkbox 内可能嵌套其它的布局组件,所以就涉及到找理想中的父组件可能实际位于祖先位次上;(===> 从该组件出发,向上or向下找任意组件实例

以上是作者总结的两个难点,于我可不止这两个呢
1、首先从组件的api设计上,就值得深究。

由组件api初探组件内部必要的设计:

  1. # 组件使用者
  2. <i-checkbox v-model="single">单独选项</i-checkbox>等价于
  3. <i-checkbox :value="single" @input="xx">单独选项</i-checkbox>
  4. # checkbox内部
  5. <script>
  6. export default{
  7. props:{
  8. value: {
  9. type: [String, Number, Boolean],
  10. default: false
  11. }
  12. },
  13. methods:{
  14. change(){
  15. const checked = event.target.checked;
  16. this.$emit('input', checked);
  17. }
  18. }
  19. }
  20. </script>
  21. ===> 单选,是input type=checkbox v-model=single 的进化版
  22. 所以组件内部
  23. dom层面会有
  24. 1.一个真正的input
  25. 2.一个能接 “单独选项”的 slot
  26. 3.一个label负责美化input框
  27. 数据交互层面:
  28. 本质 input有两个数据 <input :checked="xx" :disabled=""/>
  29. 1.v-model:
  30. ==> prop:value
  31. value改变了 要 this.$emit('input',val)
  32. 数据的改变:
  33. 1、input框接受用户的交互,当用户改变input框之后 需要将事件派发给组件的使用者
  34. 更改v-model的single
  35. ===》
  36. 1.组件本地不维护数据,input的checked 的数据来源于prop,
  37. 那么响应用户动作的时候,就派发事件给上层,上层改变prop,从而联动引起组件内部数据改变
  38. 2.组件内部维护数据,data.curval = prop.value;
  39. 响应用户动作的时候 直接修改 this.curval = event.target.value;
  40. 然后再通知prop的更改;
  41. 同时,对于上层的prop更新,组件内部采用监听prop.value改变,以此手动维护本地数据的更新;
  42. 2. prop层的改变
  43. 1.如果组件内部不维护数据,那么直接更改prop即可
  44. 2.如果组件内部维护了数据,那么需要组件内部监听prop的改变
  45. 综上,有两种设计组件内部的方式,木偶组件 or 有本地state的组件
  46. 组件的dom都是这样的
  47. <div>
  48. <span>
  49. <input
  50. type="checkbox"
  51. :disabled="disabled"
  52. :checked="=>" currentValue or value
  53. @change="change"==> this.$emit('input',xx),this.$emit('on-change',xxx)
  54. />
  55. </span>
  56. <slot></slot>
  57. </div>

vue下的v-model在表单组件上的特殊表现 会给组件的设计上带来什么优势?

  1. 探究一下vue的特殊组件更新
  2. 怎么维护model值的双向绑定
  3. <i-checkbox v-model="single">选项 1</i-checkbox>
  4. 组件内部
  5. way1:
  6. <input type="checkbox" value="Jack" v-model="propVal">
  7. prop:propVal
  8. ===> 这样是不行的,因为改变prop值了
  9. way2:
  10. 本地data转一下
  11. <input type="checkbox" value="Jack" v-model="stateval">
  12. prop:proval
  13. data:{
  14. stateval:propval// 初始值为propval
  15. }
  16. way3:
  17. <input type="checkbox" :value="stateval" @input="inputchange">
  18. way3的过于底层,虽然更自由,但是好像和v-model的使用更占优势?(即使用v-model也不会损伤自由
  19. way2此时当前input的value对应的数据源:见下图
  20. 由图可知,v-model可以解决右边两个更新维护的问题,
  21. 所以现在维护prop的话,监听prop的变化, 然后手动维护v-model的值与prop的统一;
  22. 而回归到vue的特殊处理上,v-model的值与value的值相交辉映 绑定约束的感觉;
  23. value的值和v-model的值一致则被选中;
  24. 弊端:
  25. 这样设计最大的有点恶心的地方就是需要监听prop的变化再手动维护stateval
  26. 抛弃了prop改变本地数据自动改变的优势;
  27. vue原生支持的:
  28. <div id='example-3'>
  29. <input type="checkbox" value="Jack" v-model="checkedNames">
  30. <input type="checkbox" value="Job" v-model="checkedNames">
  31. <input type="checkbox" value="Tom" v-model="checkedNames">
  32. <span>Checked names: {{ checkedNames }}</span>
  33. </div>
  34. v-model最大的好处是 一个数据源 维护了多个input的变化
  35. 且当父级是v-model时候
  36. <i-checkbox v-model="single">选项 1</i-checkbox>
  37. 组件内部数据改变通知组件使用者的时候,只需组件内部this.emit('input',value)即可
  38. 组件使用者无需再监听 this.on('input',func)==> this.state = inputval
  39. v-model会帮你处理

此时当前input的value对应的数据源:
image.png

checkbox-group的深入分析
核心:group的v-model 与 check的v-model是统一的,我们要做的所有工作都围绕着手动统一这两个变量来
首次渲染与用户change时更新group的值

  1. <i-checkbox-group v-model="multiple">
  2. <i-checkbox label="option1">选项 1</i-checkbox>
  3. <i-checkbox label="option2">选项 2</i-checkbox>
  4. <i-checkbox label="option3">选项 3</i-checkbox>
  5. <i-checkbox label="option4">选项 4</i-checkbox>
  6. </i-checkbox-group>
  7. 当是多选组件的时候,想获得的数据主要是 multi维护的那个数组变量 muti = ['option1',‘option2’]
  8. 这里维护的是选择的字段,而不是选择的value;
  9. 数据交互解析:
  10. 1.当首次渲染的时候,multi里的数据会下发到checkbox的各个组件内部,初始化组件状态
  11. 2.当用户点击切换选择的时候,上层的multi也得得到更新
  12. 3.当v-model的mult变化的时候,下面的组件也会跟着变化;
  13. 设计:
  14. 1.当首次渲染的时候,multi里的数据会下发到checkbox的各个组件内部,初始化组件状态
  15. ===》checkbox渲染的时候 会判断有没有group,如果有 那么初始值就由group获得
  16. ===> way1.得到group的value,然后更新自己
  17. way2.将更新的权限放在父级,父级统一遍历以自己的value 更新子组件 的值
  18. 更新子组件的方法 还是调用子组件的update值;
  19. 2.当用户点击切换选择的时候,会更新multi
  20. 1.如果组件内没有本地值,那么直接emit更新;但此时 group是对应的不是父组件的关系;而是跨级关系
  21. 根据vue的表单特殊处理,我们知道 input value="" v-model="" 通过v-model可以屏蔽input元素
  22. 的响应细节;===》 emit的时候直接 this.emit('input',value)即可
  23. ===》 本地是 v-model 的设计
  24. 2.本地组件有值,v-model。 :value="label" 即组件使用者指定的字段
  25. 【关键是 v-model这个值 是本地新值 还是 直接用group的值?】
  26. v-model=""
  27. 【无论用什么值都涉及到 怎么把跨组件的值 统一起来?】goup内部与checkbox内部是 跨组件的关系;
  28. 所以,用prop也没什么优势,因为 prop更新之后,checkbox并不会自动更新;
  29. (即prop无法直接作为父组件的传值 直接给check组件用;
  30. 综上,使用way2,即将更新组件的权限放在group,然后父group调用子check方法更新子的v-model数据,
  31. 子的v-model数据使用本地state维护;

image.png

这里的父与子 即 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组件?

  1. ===》 问题转换为 在子组件怎么找到跨级最近组件?===》
  2. 子:
  3. <input
  4. v-if="hasGroup"
  5. type="checkbox"
  6. :disabled="disabeld"
  7. :value="label"
  8. v-model="checkdata" #之所以使用的是本地state是方便得到本地的用户交互带来的数据更新
  9. #如果直接用prop则没有这个效果?
  10. @change="change"
  11. />
  12. change事件
  13. if (this.group) {
  14. # 从该组件出发 找到 parent,然后修改parent组件上绑定的 v-model
  15. this.parent.change(this.model);
  16. }
  17. 父:(
  18. change (newModel) {
  19. this.$emit('input',newModel);
  20. }
  21. 3.v-modelmult变化的时候,下面的组件也会跟着变化;
  22. 对于group组件内部,propsvalue
  23. 监听props.value的变化,一旦groupvalue变化则更新下面子组件的value
  24. 父:
  25. watch: {
  26. value(){
  27. this.updateModel();
  28. }
  29. },
  30. updateModel(){
  31. this.children = findComponentsDownward(this, 'iCheckbox');
  32. if(this.children){
  33. const { value } = this;//value:prop 把父的值更新下放到i-checkbox
  34. this.children.forEach(child => {
  35. // 因为v-model = model
  36. child.checkdata = value;
  37. #因为子组件v-model绑定的是本地state 所以可以这样直接修改
  38. #如果是v-model绑定的是prop则不能修改
  39. 即一旦决定了这样的数据交互方式,即父会越俎代庖,那么子的相关数据就必须为state值。
  40. 而不是不可改的prop
  41. })
  42. }
  43. },

实践

i-checkbox

使用者

  1. <i-checkbox v-model="single" trueValue='yes' falseValue='no'>单选框</i-checkbox>
  2. <script>
  3. import iCheckbox from './checkbox.vue';
  4. export default {
  5. components: {
  6. iCheckbox
  7. },
  8. data () {
  9. return {
  10. single: 'no',
  11. }
  12. },
  13. }
  14. </script>

组件内部

  1. <template>
  2. <label>
  3. <span>
  4. -- !hasGroup
  5. <input
  6. type='checkbox'
  7. :disabled="disabeld"
  8. :checked="curval"
  9. @change="change"
  10. />
  11. </span>
  12. <slot></slot>
  13. </label>
  14. </template>
  15. <script>
  16. import { findComponentUpward } from '../util.js';
  17. export default {
  18. name:'iCheckbox',
  19. props:{
  20. disabeld: {
  21. type:[Boolean, String, Number],
  22. default:false
  23. },
  24. value: {
  25. type:[Boolean, String, Number],
  26. default:false
  27. },
  28. trueValue: {
  29. type:[Boolean, String, Number],
  30. default:true
  31. },
  32. falseValue: {
  33. type:[Boolean, String, Number],
  34. default:false
  35. }
  36. },
  37. data(){
  38. return{
  39. curval: this.value,
  40. parent: null
  41. }
  42. },
  43. mounted() {
  44. // 往上查找 findComponentUpward 是否有i-group 没有就维护本地的update
  45. this.update();
  46. },
  47. methods:{
  48. change(event){
  49. const checked = event.target.checked;
  50. this.curval = checked ? this.trueValue : this.falseValue;
  51. this.$emit('input',this.curval);
  52. },
  53. update(){
  54. this.curval = this.value === this.trueValue;
  55. }
  56. },
  57. watch: {
  58. // 因为是本地维护了变量,prop的更新并不会直接触发本地的什么变动
  59. // 所以需要监听prop的变化以此来依据prop的变化手动维护本地state的赋值更新
  60. // 至于 group下的check更新则是另一条更新路径
  61. value(newval){
  62. if(newval === this.trueValue || newval === this.falseValue){
  63. this.update();
  64. }
  65. else {
  66. throw 'value should be truevalue or falsevalue';
  67. }
  68. }
  69. },
  70. }
  71. </script>

image.png

i-checkbox-group

  1. <i-checkbox-group v-model="multiple">
  2. <i-checkbox label="option1">选项 1</i-checkbox>
  3. <i-checkbox label="option2">选项 2</i-checkbox>
  4. <i-checkbox label="option3">选项 3</i-checkbox>
  5. <i-checkbox label="option4">选项 4</i-checkbox>
  6. </i-checkbox-group>
  7. multiple: ['option1','option3'],

check内部

  • 首次渲染,初始化值
  • 当checkbox接受用户交互的时候 ,怎么更新?

    1. checkArr=['jack','tom'] 只要当前input的value 数组有一项满足 则被checked
    2. <input type="checkbox" value="Jack" v-model="checkArr">

    那么意味着 用户交互改变的时候 checkbox本地的数据要变,同时,也要更新group的那个数组变化

  • group数据变化的时候,也要更新到每一个checkbox的变化

  1. <template>
  2. <label>
  3. <span>
  4. <input
  5. v-if="hasGroup"
  6. type="checkbox"
  7. :value="label"
  8. v-model="checkdata"
  9. @change="change"
  10. />
  11. </span>
  12. <slot></slot>
  13. </label>
  14. </template>
  15. <script>
  16. import { findComponentUpward } from '../util.js';
  17. export default {
  18. name:'iCheckbox',
  19. props:{
  20. label: {
  21. type:[Boolean, String, Number]
  22. }
  23. },
  24. data(){
  25. return{
  26. hasGroup: false,
  27. checkdata: [],
  28. parent: null
  29. }
  30. },
  31. mounted() {
  32. const parent = findComponentUpward(this,'iCheckboxGroup');
  33. if(parent){
  34. this.hasGroup = true;
  35. this.parent = parent;
  36. // 由group统计更新check本地v-model的checkdata
  37. this.parent.updateChild();
  38. }
  39. },
  40. methods:{
  41. change(event){
  42. // 多选的更新 本地触发 再更新 group的
  43. if(this.hasGroup) {
  44. this.parent.changeChild(this.checkdata);
  45. return ;
  46. }
  47. },
  48. update(){
  49. this.curval = this.value === this.trueValue;
  50. }
  51. }
  52. }
  53. </script>

checkgroup

  1. <template>
  2. <div>
  3. <slot></slot>
  4. </div>
  5. </template>
  6. <script>
  7. import { findComponentsDownward } from '../util.js';
  8. export default {
  9. name:'iCheckboxGroup',
  10. props:{
  11. value: {
  12. type:Array,
  13. default(){
  14. return [];
  15. }
  16. }
  17. },
  18. methods: {
  19. // check 初次渲染 this.parent.updateChild()
  20. // group的值去更新check的v-model值
  21. updateChild() {
  22. const children = findComponentsDownward(this,'iCheckbox');
  23. if(children && children.length){
  24. children.forEach(child => {
  25. child.checkdata = this.value;
  26. })
  27. }
  28. },
  29. // check 接受用户改变的时候
  30. // this.parent.changeChild(this.checkdata);
  31. changeChild(newval) {
  32. // 通知改变i-iCheckboxGroup的v-model 子组件就自然改变了
  33. this.$emit('input', newval);
  34. }
  35. },
  36. mounted () {// group 渲染的时候 要 更新child
  37. // child 初次渲染的时候 也调用父 使得更新自己
  38. // 那岂不是更新了两遍?
  39. this.updateChild();
  40. },
  41. watch: {
  42. value () {
  43. this.updateChild();
  44. }
  45. }
  46. }
  47. </script>

评论里的疑问

checkbox 组件初始化时父组件是 group 就调用父级 updateModel 方法更新勾选状态,这个感觉并不是必须的,反而的感觉更会消耗性能。初始化一个 checkbox 就执行一次 , checkbox 上百个就得执行上百次。 讨论:
因为动态增加?动态删除的话是不会接触到mounted这个生命周期的 而且group和check都在mounted的时候维护了一个check的遍历更新,怎么觉得有点冗余? ===》 动态增加:此时group已经加载了;check又会多一个,以check的mounted调用group的遍历更新这是说的过去的;

image.png

除此以外考虑,当check集成到form表单的时候,

  1. -- checkbox change方法 --
  2. if (this.group) {
  3. this.parent.change(this.model);
  4. }
  5. else {
  6. // 派给 组件使用者
  7. this.$emit('on-change', value);
  8. // 给表单
  9. this.dispatch('iFormItem', 'on-form-change', value);
  10. }
  11. -- group change方法 --
  12. this.$emit('input', data);
  13. this.$emit('on-change', data);
  14. this.dispatch('iFormItem', 'on-form-change', data);

值得一学的小技巧

label

  1. <label>
  2. <span>
  3. <input
  4. type="checkbox"
  5. :disabled="disabled"
  6. :checked="currentValue"
  7. @change="change"
  8. />
  9. </span>
  10. <slot></slot>
  11. </label>
  1. <input><slot> 都是包裹在一个 <label> 元素内的,这样做的好处是,当点击 <slot> 里的文字时,<input> 选框也会被触发,否则只有点击那个小框才会触发,那样不太容易选中,影响用户体验。

====》 本来是用label 的for 和input的id绑定的,用包裹的形式可以不用绑定了好棒

findAnyComponent