在表单输入组件节点上使用v-model

code_v-model、$attrs、$listeners

el: "#app-1",
data() {
    return {
        text: ''
    }
},
components: {
    'component-1': {
        template: '<input type="text">'
    }
}
<component-1 v-model="text"></component-1>

直接在表单组件的标签上使用v-model,没有任何作用。

clipboard.png

首先看看v-model如何在原生input元素上工作的。

data() {
    return {
        text: ''
    }
}
<input type="text" v-bind:value="text" v-on:input="text = $event.target.value">

上面的标签为v-model指令的完全形式,v-model是下面代码的语法糖,通过事件处理函数与数据绑定完成双向绑定。需要注意的是事件处理函数的内容是默认的。

v-bind:value="somedata"
v-on:input="somedata = $event.target.value"

clipboard.png

对比原生input元素,可以得出,输入组件上的v-model也是由事件处理与数据绑定组成。

data() {
    return {
        text_1: '',
        text_2: ''
    }
}
//......
'component-2-1': {
    template: '<input type="text" v-bind:value="value" v-on:input="eventHandler">',
    props: ['value'],
    methods: {
        eventHandler: function (event) {
            this.$emit('input', event.target.value)
        }
    }
}
<component-2-1 v-model="text_1"></component-2-1>
<component-2-1 v-bind:value="text_1" v-on:input="text_1 = arguments[0]"></component-2-1>

输入组件模板中,监听每次输入,每次输入时就触发input自定义事件,并以元素的值作为该事件负载;在父组件上监听这个input自定义事件,触发该事件时执行默认的方法,将事件负载传递给父组件自己的data;这里要关注默认的处理方法somedata = arguments[0],这个arguments[0]即是自定义事件的负载;若父组件数据修改了,会通过:value=props下发给子组件,因此子组件需要定义一个Propsvalue,子组件还要将value绑定在自己的input元素上,向其他使用该组件的地方传递变化。

变化的过程: 输入 - 子组件触发自定义事件 - 父组件监听到事件 - 根据负载修改数据 - 将修改数据下发给组件 - 引起其他位置的变化

对于子组件模板上,绑定自己原生input事件的方式,以上使用一个eventHandler事件处理方法。我们也可以在没有参数的v-on上绑定一个事件对象(在computed中定义),如下:

'component-2-2': {
    template: '<input type="text" v-bind:value="value" v-on="eventDict">',
    props: ['value'],
    computed: {
        eventDict: function () {
            return { input: event => this.$emit('input', event.target.value) }
        }
    }
}
<component-2-2 v-model="text_2"></component-2-2>
<component-2-2 v-bind:value="text_2" v-on:input="text_2 = arguments[0]"></component-2-2>

clipboard.png

子组件props.value用来传递变化给其他使用该输入组件的地方,如果没有它,只是单向的绑定。

使用$attrs、$listeners在多层次的组件结构中实现通信

如果在多层组件结构中,只是单纯的数据通信,那么使用$attrs、$listeners是最方便的。
在使用它们之前,先来看个选项inheritAttrs

data(){
    return {
        foo:'foo',
        bar:'bar'
    }
},
components:{
    'component-3':{
        props:['foo'],
        template:'<span>{{foo}}</span>',
    }
}
<dd><component-3 :foo="foo" :bar="bar"></component-3></dd>

在父组件中,我们绑定了两个Props,foobar,下发数据给子组件时,子组件只定义了foobar没地方传,组件渲染时它就留在了子组件的根元素上。

图片描述

如果我们不想在子组件根元素上保留这个属性,我们可以设置选项inheritAttrs为false。

inheritAttrs:false

图片描述

使用$attrs$listeners

new Vue({
    el: '#app-4',
    data() {
        return {
            firstData: 'firstData',
            secondData: 'secondData',
            thirdData: 'thirdData',
            fourthData: 'fourthData'
        }
    },
    methods: {
        firstEvent: function () {
            console.log('第一层组件触发的事件')
        },
        secondEvent: function () {
            console.log('第二层组件触发的事件')
        },
        thirdEvent:function(){
            console.log('第三层组件触发的事件')
        },
        fourthEvent:function(){
            console.log('第四层组件触发的事件')
        }
    },
    components: {
        'first': {
            'props': ['firstData'],
            template: '<div><h1>{{firstData}}</h1><second v-bind="$attrs" v-on="$listeners"></second></div>',
            mounted() {
                this.$emit('first');
            },
            inheritAttrs:false,
            components:{
                'second':{
                    'props':['secondData'],
                    template:'<div><h2>{{secondData}}</h2><third v-bind="$attrs" @fourth="eventHandler" v-on="$listeners"></third></div>',
                    mounted() {
                        this.$emit('second');
                    },
                    methods: {
                        eventHandler(payload){
                            $(this.$el).find('h2').after('<span>' + payload + '</span>')
                        }
                    },
                    inheritAttrs:false,
                    components:{
                        'third':{
                            'props':['thirdData'],
                            template:'<div><h3>{{thirdData}}</h3><fourth v-bind="$attrs" @fourth="eventHandler" v-on="$listeners"></fourth></div>',
                            mounted() {
                                this.$emit('third');
                            },
                            methods: {
                                eventHandler(payload){
                                    $(this.$el).find('h3').after('<span>' + payload + '</span>')
                                }
                            },
                            inheritAttrs:false,
                            components:{
                                'fourth':{
                                    'props':['fourthData'],
                                    template:'<div style="background: #9FEF4E;"><h4>{{fourthData}}</h4></div>',
                                    mounted() {
                                        this.$emit('fourth','负载在第四层上事件上的数据');
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
})
<first :first-data="firstData" :second-data="secondData" :third-data="thirdData" :fourth-data="fourthData" @first="firstEvent"
    @second="secondEvent" @third="thirdEvent" @fourth="fourthEvent"></first>

clipboard.png

$listeners上保存着所有低层组件触发的自定义事件,使用v-on="$listeners"将本层及以下层触发的事件传递给上一层,上一层中可以监听$listeners对象中所有的事件。整个过程即一个事件广播的形态。

clipboard.png

$attrs上保存着所有未被下发的上层组件中的数据,各层组件使用v-bind="$attrs"向下层组件传递下发的数据。$attrs好像一块数据蛋糕,被高层组件拿走的部分,低层组件无法再使用。配合inheritAttrs:false可以使那些用不到的数据属性,不会保留在组件的根元素上。

v-on="$listeners"向上广播事件,v-bind="$attrs"向下下发数据

父子组件的引用

完整代码
示例结果是这样:

clipboard.png

<div id="app-2">
    <dl>
        <dt class="first-dt">使用$parent和$refs在父子组件间传递数据</dt>
        <dd>
            <label>父组件</label>
            <input type="text" v-model="message" @input="handle">
            <span>{{message}}</span>
        </dd>
        <dd>
            <label>子组件</label>
            <component-21 ref="child"></component-21>
            <span>{{message}}</span>
        </dd>
    </dl>
</div>
new Vue({
    el:'#app-2',
    data:{
        message:''
    },
    methods:{
        handle(){
            this.$refs.child.message = this.message
        }
    },
    components:{
        'component-21':{
            template:'<input type="text" v-model="message" @input="handle"/>',
            data(){
                return {message:''}
            },
            methods:{
                handle(){
                    this.$parent.message = this.message 
                }
            }
        }
    },
    
})

在子组件中可以使用$parent获取父组件数据,也可以修改它。
在父组件中可以使用$refs获取子组件数据,也可以修改,之前必须在视图节点上给子组件取一个名字如:ref="child"(见组件引用 —— ref、$refs)。
就靠这两个属性,进行父子组件间的通信。

事件管理器Bus

通过该方法不仅可在任意组件内进行通信。创建一个全局的实例bus管理事件触发和监听

var bus = new Vue()

之后创建表单输入组件,在其输入事件中触发bus上的message事件,在created钩子中监听该事件。

Vue.component('component-a', {
    template:`<input type="text" v-model="message" @input="emitEvent"/>`,
    data(){
        return {message:''}
    },
    methods:{
        emitEvent(){
            bus.$emit('message',this.message)
        }
    },
    created(){
        var _this = this
        bus.$on('message', c_msg => {
            _this.message = c_msg
        })
    }
})
Vue.component('component-b', {
    template:`<input type="text" v-model="message" @input="emitEvent"/>`,
    data(){
        return {message:''}
    },
    methods:{
        emitEvent(){
            bus.$emit('message',this.message)
        }
    },
    created(){
        var _this = this
        bus.$on('message', c_msg => {
            _this.message = c_msg
        })
    }
    
})
new Vue({
    data:{
        message:''
    },
    methods:{
        emitEvent(){
            bus.$emit('message',this.message)
        }
    },
    created(){
        var _this = this
        bus.$on('message', c_msg => {
            _this.message = c_msg
        })
    }
}).$mount('#app-1')
<dl>
    <dt class="first-dt">使用全局View实例管理事件,在任意组件间传递数据</dt>
    <dd>
        <label>父组件</label>
        <input type="text" v-model="message" @input="emitEvent">
        <span>{{message}}</span>
    </dd>
    <dd>
        <label>子组件a</label>
        <component-a></component-a>
        <span>{{message}}</span>
    </dd>
    <dd>
        <label>子组件b</label>
        <component-b></component-b>
        <span>{{message}}</span>
    </dd>
</dl>

当一个组件输入时触发bus上的事件,所有组件都会监听到,并使用事件上的负载数据。

clipboard.png


yanniecheung
70 声望6 粉丝