vue——组件

组件是什么?

组件是可复用的 Vue 实例,且带有一个名字。

组件的注册与使用

组件与vue实例一样,需要注册,才可以使用,注册有全局注册和局部注册俩种方式

1、全局注册
<div id="app">
    <my-component></my-component>   // 使用组件
</div>
<script>
//全局注册组件,必须在实例创建前注册,注册后,任何vue实例都可以使用
Vue.component('my-component',{   // 'my-component' 就是组件的名字,推荐写法为小写加减号分隔
    template: '<p>我是组件的内容</p>'    // 组件的具体内容,外层必须用一个标签包裹一下
})
new Vue({
    el: '#app'
})
</script>
2、局部注册
<div id="app">
    <my-component></my-component>
</div>
<script>
    new Vue({
        el: '#app',
        components: {   // 在实例中,使用components选项局部注册,注册后只有在该实例下有效
            'my-component': {
                template: '<p>这里是组件的内容</p>'
            }
        }
    })
</script>

以上代码渲染后的结果都为:

<div id="app">
  <p>我是组件的内容</p>
</div>
3、组件的嵌套
<div id="app">
    <my-component></my-component>
</div>
<script>
    new Vue({
        el: '#app',
        components: {
            'my-component': {
                template: '<p>hello <my-component2></my-component2></p>',
                components: {   // 在这里,可以继续使用components选项注册组件
                    'my-component2': {
                        template: '<span>tom</span>'
                    }
                }
            }
        }
    })
</script>

渲染后的结果为:

<p>
  hello 
  <span>tom</span>
</p>
4、组件中定义数据

组件中,也可以使用vue实例中的那些选项,只是这里data的定义方式有一点点不同。

<div id="app">
    <my-component></my-component>
</div>
<script>
    var app = new Vue({
        el: '#app',
        components: {
            'my-component': {
                template: '<p>{{message}}</p>',
                data: function () {   // 定义一个函数,内部返回一个对象,对象中定义数据
                    return {
                        message: 'hello'
                    }
                }
            }
        }
    })
</script>

组件的通信

组件不仅仅是要把模板的内容进行复用,更重要的是组件间要进行通信。

1.1、props——父组件向子组件传递数据

通常父组件的模板中包含子组件,父组件要正向的向子组件传递数据或者参数,子组件接收到后根据参数的不同来渲染不同的内容或者执行操作,这个正向传递数据的过程就是通过props来实现的。

<div id="app">
    <my-component :message="message"></my-component>   // 使用 ':message'的形式传递父组件中的data,不加冒号,就是传递字符串message
</div>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            message: 'hello~'
        },
        components: {
            'my-component': {
                template: '<p>{{message}}</p>',
                props: ['message']   // 使用props选项用数组(也可对象)的方式接收传递过来的数据,在template,computed,method中都可使用
            }
        }
    })
</script>
1.2、props的实际使用方式

通过props传递数据是单向的,也就是说,父组件的数据变化了,子组件跟着变化,这没问题,但是呢,反过来,则不可以,vue不推荐直接修改prop的值(尽管可以实现子组件数据的变化),以下俩种使用方式比较常见。

1.2.1、使用data保存

第一种就是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改,这时可以在组件data内再声明一个数据,引用父组件的prop。

<div id="app">
    <my-component :message-par="message"></my-component>
</div>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            message: 'hello~'
        },
        components: {
            'my-component': {
                template: '<p><button @click="messageSon=\'hi\'">change</button>{{messageSon}}</p>',   // 直接修改没问题
                props: ['messagePar'],
                data: function () {
                    return {
                        messageSon: this.messagePar   // 在data中定义一个数据引用prop
                    }
                }
            }
        }
    })
</script>
1.2.2、使用computed保存

这种情况就是prop作为需要被转变的原始值传入,可以用计算属性。

<div id="app">
    <my-component :width="100"></my-component>
</div>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            message: 'hello~'
        },
        components: {
            'my-component': {
                template: '<p :style="style">123</p>',
                props: ['width'],
                computed: {
                    style: function () {
                        return {
                            width: this.width + 'px'
                        }
                    }
                }
            }
        }
    })
</script>
1.3 prop的验证

上面所说的props选项的值都是一个数组,其实当prop需要验证时,就需要使用对象写法。一般,当你的组件需要提供给别人使用时,推荐都使用数据验证,比如某个数据必须是数字类型,否则在控制台弹出警告(必须引入的是vue包为开发版本)。

Vue.component('my-component',{
        ...,
        props: {
            propA: Number,   // 必须是数字类型
            propB: [String, Number],   // 必须是字符串或者数字类型
            propC: {   // 必须是布尔值,如果没传的话默认为true
                type: Boolean,
                default: true
            },
            propD: {    // 必须是数字类型,且必传
                type: Number,
                required: true
            },
            propE: {   // 如果是数组或者对象,必须以一个函数来返回
                type: Array,   
                default: function () {
                    return [];
                }
            },
            propF: {   // 自定义一个验证函数
                validator: function (value) {
                    return value > 10;
                }
            }

        }
    })

例子:

<div id="app">
    <my-component :message="message"></my-component>
</div>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            message: 'hi~'
        },
        components: {
            'my-component': {
                template: '<p>{{message}}</p>',
                props: {
                    message: Number   // 定义了number类型
                }
            }
        }
    })
</script>

以上代码虽然可以渲染,但会在控制台报错。

2.1、$emit 子组件向父组件传递数据

props是不能把数据传递给父组件的,这里有一种方法,就是,子组件用emit()来触发事件,父组件用on()来监听子组件的事件。

<div id="app">
    <p>{{total}}</p>
    <my-component @increment="changeTotal" @reduce="changeTotal"></my-component>   // 2、父组件根据子组件的事件名来接收,并做出响应
</div>
<script>
    Vue.component('my-component',{
        template: '\
            <div>\
                <button @click="handleIncrement">增加</button>\
                <button @click="handleReduce">减少</button>\
            </div>',
        data: function(){
            return {
                counter: 0
            }
        },
        methods: {
            handleIncrement:function () {
                this.counter ++;
                this.$emit('increment', this.counter);   // 1、触发事件,并传递值 'increment'是定义事件的名称
            },
            handleReduce:function () {
                this.counter --;
                this.$emit('reduce', this.counter);
            }
        }
    })
    var vm = new Vue({
        el: '#app',
        data: {
            total: 0
        },
        methods: {
            changeTotal: function (value) {   // 3、具体响应,将子组件传递过来的值进行处理
                this.total = value;
            }
        }
    })
</script>

效果:


2.2、$emit的语法糖
<div id="app">
    <p>{{total}}</p>
    <my-component v-model="total"></my-component>   // 直接用v-model绑定一个父组件的数据
</div>
<script>
    Vue.component('my-component',{
        template: '\
            <div>\
                <button @click="handleReduce">减少</button>\
            </div>',
        data: function(){
            return {
                counter: 0
            }
        },
        methods: {
            handleReduce:function () {
                this.counter --;
                this.$emit('input', this.counter);   // $emit()的事件名改为'input'
            }
        }
    })
    var vm = new Vue({
        el: '#app',
        data: {
            total: 0
        }
    })
</script>

仍然是点击减一的效果,这里简洁了许多。

2.3、v-model实现自定义的双向绑定的表单输入组件
<div id="app">
    <p>{{total}}</p>
    <my-component v-model="total"></my-component>
    <button @click="total--">-1</button>
</div>
<script>
    Vue.component('my-component',{
        template: '<input type="text" :value="value" @input="handleInput" />',
        props: ['value'],
        methods: {
            handleInput:function (event) {
                this.$emit('input',event.target.value);
            }
        }
    })
    var vm = new Vue({
        el: '#app',
        data: {
            total: 0
        }
    })
</script>

效果:



按钮点击,和子组件文本框输入都可以使父子组件的数据同时变化。

3.1、中央事件总线(bus) —— 非父子组件的通信

这种方式巧妙而轻量的实现了任何组件间的通信,包括父子,兄弟,跨级。比如兄弟组件间通信:

<div id="app">
    <component-aaa></component-aaa>
    <component-bbb></component-bbb>
</div>
<script>
    var bus = new Vue();   // 1、首先创建一个空的vue实例
    var vm = new Vue({
        el: '#app',
        components: {
            'component-aaa': {
                template: '<button @click="handleMessage">click me</button>',
                data: function () {
                    return {
                        message: '我是来自子组件aaa的内容~'
                    }
                },
                methods: {
                    handleMessage:function () {
                        bus.$emit('on-message',this.message);   // 3、组件aaa中按钮点击把事件'on-message'发出去,同时携带相应数据
                    }
                }
            },
            'component-bbb':{
                template: '<p>{{message}}</p>',
                data: function(){
                    return {
                        message: '我是来自子组件bbb的内容~'
                    }
                },
                mounted: function () {
                    var that = this;
                    bus.$on('on-message', function (value) {   // 2、在组件bbb中监听来自bus的事件'on-message'
                        that.message = value;
                    })
                }
            }
        }
    })
</script>
4.1、父链和子组件索引

除了中央事件总线bus外,还有俩种方法可以实现组件间通信:父链和子组件索引。

4.2、父链

在子组件中,使用this.parent可以直接访问该组件的父实例或组件,父组件也可以通过this.children访问它所有的子组件,而且可以递归向上或向下无限访问。

<div id="app">
    <p>{{message}}</p>
    <my-component></my-component>
</div>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            message: 123
        },
        components: {
            'my-component': {
                template: '<button @click="handleMessage">click me</button>',
                methods: {
                    handleMessage: function () {
                        this.$parent.message = 456;   // 通过父链直接修改父组件的数据
                    }
                }
            }
        }
    })
</script>

还是推荐使用props或者$emit()的方式通信。

4.3、子组件索引

当子组件较多时,通过this.$children来一一遍历出我们需要的一个组件实例是比较困难的,Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称:

<div id="app">
    <button @click="handleMessage">click me</button>
    <my-component ref="comA"></my-component>   // 在父组件模板中子组件标签上,使用ref指定一个名称
</div>
<script>
    var vm = new Vue({
        el: '#app',
        methods: {
            handleMessage: function () {
                this.$refs.comA.message = 456;   // 父组件内部,通过 this.$refs来访问指定名称的子组件的
            }
        },
        components: {
            'my-component': {
                template: '<p>{{message}}</p>',
                data: function () {
                    return {
                        message: 123
                    }
                }
            }
        }
    })
</script>

slot —— 分发内容

1、单个slot

在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的<slot>标签及它的内容。

<div id="app">
    <my-component>
        <p>hello</p>
        <p>world</p>
    </my-component>
</div>
<script>
    Vue.component('my-component',{
       template: '<div><slot><p>如果没有内容填充,我将作为默认的内容出现。</p></slot></div>'
    })
    var vm = new Vue({
        el: '#app'
    })
</script>

// 渲染后
<div>
  <p>hello</p>
  <p>world</p>
</div>

子组件my-component的模板内定义了一个slot元素,并且用一个<p>作为默认的内容,在父组件没有使用slot时,会渲染这段默认的文本,如果写入了slot,那就会替换整个<slot>。

2、具名slot

给slot元素指定一个name后可以分发多个内容,具名slot可以与单个slot共存:

<div id="app">
    <my-component>
        <p slot="header">我是头部内容</p>
        <p>我是导航内容</p>
        <p>我是主体内容</p>
        <p slot="footer">我是底部内容</p>
    </my-component>
</div>
<script>
    Vue.component('my-component',{
        template: '\
            <div>\
                <div class="header"><slot name="header"></slot></div>\
                <div class="main"><slot></slot></div>\
                <div class="footer"><slot name="footer"></slot></div>\
            </div>'
    })
    var vm = new Vue({
        el: '#app'
    })
</script>

//  渲染后
<div>
  <div class="header">
    <p>我是头部内容</p>
  </div>
  <div class="main"> 
    <p>我是导航内容</p>
    <p>我是主体内容</p>
  </div> 
  <div class="footer">
    <p>我是底部内容</p>
  </div>
</div>

子组件内声明了3个slot元素,其中在<div class="main"></div>内的slot没有使用name特性,它将作为默认slot出现,父组件没有slot特性的元素与内容都将出现在这里。如果没有指定默认的匿名slot元素,父组件内多余的内容片断都将被抛弃。

3、作用域插槽

作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染元素。基本用法:

<div id="app">
    <my-component>
        <template scope="props">
            <p>{{prop.message}}</p>
        </template>
    </my-component>
</div>
<script>
    var vm = new Vue({
        el: '#app',
        components: {
            'my-component': {
                template: '<div><slot :message="message"></slot></div>',
                data: function () {
                    return {
                        message: 123
                    }
                }
            }
        }
    })
</script>

// 渲染后
<div>
  <p>123</p>
</div>

作用域插槽最具代表性的例子就是列表组件,比如:

<div id="app">
    <my-component :fruits="fruits">
        <template scope="props">
            <li :style="{backgroundColor:(props.index%2===0?'skyblue':'pink')}">{{props.fruitName}}</li>
        </template>
    </my-component>
</div>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            fruits: ['香蕉','苹果','梨子','西瓜','菠萝','水蜜桃']
        },
        components: {
            'my-component': {
                template: '<ul><slot v-for="(item, index) in fruits" :fruitName="item" :index="index"></slot></ul>',
                props: ['fruits']
            }
        }
    })
</script>

效果:



作用域插槽的使用场景是既可以复用子组件的slot,又可以使用slot内容不一致。

其他

1、$nextTick —— 异步更新队列

现在有一个需求,有一个div ,默认用v-if 将它隐藏,点击一个按钮后,改变v-if 的
值,让它显示出来,同时拿到这个div 的文本内容。如果v-if 的值是false ,直接去获取div 的内容
是获取不到的, 因为此时div 还没有被创建出来,那么应该在点击按钮后,改变v -if 的值为true,
div 才会被创建,此时再去获取,示例代码如下:

<div id="app">
    <div id="con" v-if="flag">这是一段文本内容</div>
    <button @click="fn">click me</button>
</div>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            flag: false
        },
        methods: {
            fn: function () {
                this.flag = true;
                var conText = document.getElementById("con").innerHTML;
                console.log(conText);
            }
        }
    })
</script>

然而,在点击按钮之后,控制台却报错了,Cannot read property 'innerHTML' of null,意思就是获取不到div元素,这里就涉及Vue一个重要的概念,异步更新队列。

Vue 在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一事件循环
中发生的所有数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM 操作。然后,
在下一个事件循环tick 中, Vue 刷新队列井执行实际(己去重的)工作。所以如果你用一个for 循
环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制, DOM 就要重绘100
次,这固然是一个很大的开销。

知道了Vue 异步更新DOM 的原理,上面示例的报错也就不难理解了。事实上,在执行
this.flag = true时, div 仍然还是没有被创建出来,直到下一个Vue 事件循环时,才开始创建。
$nextTick 就是用来知道什么时候DOM 更新完成的,所以上面的示例代码需要修改为:

<div id="app">
    <div id="con" v-if="flag">这是一段文本内容</div>
    <button @click="fn">click me</button>
</div>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            flag: false
        },
        methods: {
            fn: function () {
                this.flag = true;
                this.$nextTick(function () {
                    var conText = document.getElementById("con").innerHTML;
                    console.log(conText);
                })
            }
        }
    })
</script>
2、x-Templates

这个功能就是如果组件模板内容太过于复杂,不想拼接字符串,所以提供了一种方法,具体使用如下:

<div id="app">
    <my-components></my-components>
</div>
<script type="text/x-template" id="my-components">
    <div>
        <p>123</p>
        <p>456</p>
        <p>789</p>
    </div>
</script>
<script>
    var vm = new Vue({
        el: '#app',
        components: {
            'my-components': {
                template: '#my-components'
            }
        }
    })
</script>

//  渲染后
<div id="app">
  <div>
    <p>123</p> 
    <p>456</p> 
    <p>789</p>
  </div>
</div>
3、watch —— 监听

watch选项用来监听某个prop或者data的改变,当它们发生改变时,就会触发watch配置的函数,从而完成我们的业务逻辑。

<div id="app">
    <p>{{message}}</p>
    <button @click="fn">click me</button>
</div>
<script>
    var vm = new Vue({
        el: '#app',
        data: {
            message: 123
        },
        methods: {
            fn: function () {
                this.message = 456;
            }
        },
        watch: {
            // 监听了message,如果发生改变,就会触发这个函数,参数1为改变后的值,参数2为改变前的值
            message: function (newValue,oddValue) {  
                console.log(newValue);   // 456
                console.log(oddValue);   // 123
            }
        }
    })
</script>

实战

1、数字输入框组件
<div id="app">
    <input-number :max="10" :min="0" v-model="value"></input-number>
</div>
<script>
    function isValueNumber(value){
        return (/(^-?[0-9]+\.{1}\d+$)|(^-?[1-9][0-9]*$)|(^-?0{1}$)/).test(value);
    }
    Vue.component('input-number', {
        template: '\
            <div>\
                <input type="text" :value="currentValue" @change="handleChange" />\
                <button @click="handleDown">-1</button>\
                <button @click="handleUp">+1</button>\
            </div>',
        props: {
            max: {
                type: Number,
                default: Infinity
            },
            min: {
                type: Number,
                default: -Infinity
            },
            value: {
                type: Number,
                default: 0
            }
        },
        data: function () {
            return {
                currentValue: this.value
            }
        },
        methods: {
            handleDown: function () {
                if(this.currentValue<=this.min) return;
                this.currentValue --;
            },
            handleUp: function () {
                if(this.currentValue>=this.max) return;
                this.currentValue ++;
            },
            handleChange: function (event) {
                var val = event.target.value.trim();
                if(isValueNumber(val)){
                    if(val>this.max){
                        this.currentValue = this.max;
                    }else if(val<this.min){
                        this.currentValue = this.min;
                    }else{
                        this.currentValue = val;
                    }
                }else{
                    event.target.value = this.currentValue;
                }
            },
            updateValue: function (val) {
                if(val > this.max){
                    this.currentValue = this.max;
                }else if(val < this.min){
                    this.currentValue = this.min;
                }else{
                    this.currentValue = val;
                }
            }
        },
        watch: {
            currentValue: function(val){
                this.$emit('input',val);
            },
            value: function (val) {
                this.updateValue(val);
            }
        },
        mounted: function () {
            this.updateValue(this.value);
        }
    })
    var vm = new Vue({
        el: '#app',
        data: {
            value: 5
        },
        methods: {
            changeValue: function () {
                this.value ++;
            }
        }
    })
</script>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容