Elemtn-ui radio-group

3,195 阅读2分钟

Element-ui radio-group

887dceb55a87972f49bb97cc76c09a7.png

剖析

<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))
            
        }

    }

}

总结

  • prop传值时加冒号和不加v-binde的区别,官方文档
  • mixin混入,将组件公共的数据抽离出来,官方文档