Vue(4)之 组件

1、组件简介

前面介绍的 Vue.js 知识都是“开胃菜”,现在才是“正餐”环节。

组件是 Vue.js 最核心的功能,在前端应用程序中可以采取模块化的开发,实现可重用、可扩展。组件是带有名字的可复用的 Vue 实例,因此在根 Vue 实例中的各个选项在组件中也一样可以使用,唯一的例外是 el 选项,这是只用于根实例的特有选项。

组件系统让我们可以独立可复用的小组件来构建大型应用,几乎任意类型应用的界面都可以抽象为一个组件树
在这里插入图片描述

2、全局组件与局部组件

定义与自定义指令、过滤器方法类似,这里就不在复述了,直接看用例

// 定义一个名为 ButtonCounter 的新组件
Vue.component('ButtonCounter', {
<!-- 下面两种方式都可以 ES6语法支持方法简写 -->
<!--   data: function () { -->
  data() {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

之前在 Vue 的根实例中定义数据对象时,一直采用的是如下形式:

data: {
  count: 0
}

而在注册组件时,data 选项是一个函数(也必须是一个函数)。这是因为组件是可复用的 Vue 实例,如果依然采用之前根实例的数据定义形式,那么所有复用的组件将共享同一份数据。采用函数定义方式,那么每个组件实例都将拥有自己的一份返回对象的独立拷贝,每复用一次组件,data函数就执行一次,从而返回一个新的数据对象

2.1、使用组件

  • 组件必须用一个根元素来包裹,且只能有一个根元素。
<div id="components-demo">
    <h2>组件</h2>
    <ButtonCounter></ButtonCounter>
</div>

let vm = new Vue({
    el: '#components-demo'
})

不过上面的代码并不能正常执行,这是因为 HTML 并不区分元素和属性的大小写,所以<ButtonCounter>被解析成<buttoncounter>,而我们注册时的名称为ButtonCounter,这就导致找不到组件而报错。

解决办法就是:采用 kebab-case 命名来引用组件

// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
  data() {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

<div id="components-demo">
    <h2>组件</h2>
    <!--   此处不要把组件当做自闭和元素来使用,即:<button-counter/>   -->
    <!--   在非DOM模板中没有这个限制,相反,还鼓励将没有内容的组件作为自闭和元素来使用   -->
    <button-counter></button-counter>
</div>

2.2、组件构造器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<div id="components-demo">
    <h2>组件</h2>
    <button-counter></button-counter>
</div>

<script src="VueJs/vue.js"></script>
<script>
    let Mycomponent = Vue.extend({
        data() {
            return {
                count: 0
            }
        },
        template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
    })
    Vue.component('button-counter', Mycomponent)
    let vm = new Vue({
        el: '#components-demo'
    })
</script>
</body>
</html>

2.3、注册局部组件

let vm = new Vue({
    el: '#components-demo',
    components: {
        'button-counter': {
            data() {
                return {
                    count: 0
                }
            },
            template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
        }
    }
})

let Mycomponent = Vue.extend({
    data() {
        return {
            count: 0
        }
    },
    template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
let vm = new Vue({
    el: '#components-demo',
    components: {
        'button-counter': Mycomponent
    }
})

2.4、例外

组件注册时采用的是 PascalCase (首字母大写)命名,就可以采用 kebab-case 命名来引用。

<div id="components-demo">
    <h2>组件</h2>
    <button-counter></button-counter>
</div>

<script src="VueJs/vue.js"></script>
<script>
    let Mycomponent = Vue.extend({
        data() {
            return {
                count: 0
            }
        },
        template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
    })
    // Vue.component('button-counter', Mycomponent)
    let vm = new Vue({
        el: '#components-demo',
        components: {
            ButtonCounter: Mycomponent
        }
    })
</script>

3、使用 Prop 向子组件传递数据

组件是当做自定义元素来使用的,而元素一般是有属性的,同样,组件也可以有属性。

首先需要在组件内注册一些自定义的属性,称之为 prop,这些 prop 是放在组件的 props 选项中定义的;
之后,在使用组件时,就可以把这些 prop 的名字作为元素的属性名来使用,通过属性向组件传递数据,这些数据将作为组件实例的属性被使用。

<body>
<div id="app">
    <h2>组件: props</h2>
    <!--  定义的 PostItem 被 HTML解析改为 post-item  -->
    <!--  定义的 postTitle 被 HTML解析改为 post-title  -->
    <post-item post-title="Vue.js 无难事"></post-item>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('PostItem', {
        // 声明 props
        props: ['postTitle'],
        // postTitle 就像 data 中定义的数据属性一样,在该组件中可以如 “this.postTitle” 这样使用
        template: '<h3>{{ postTitle }}</h3>'
    })
    let vm = new Vue({
        el: '#app'
    })
</script>

3.1、字符串模板,直接使用命名与自闭和元素

在字符串模板中,除了各种命名可以直接使用外,组件还可以当做自闭和元素来使用。

<div id="app">
    <h2>组件: props</h2>
    <post-item post-title="PostItem: Vue.js 无难事"></post-item>
    <post-list></post-list>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('PostItem', {
        // 声明 props
        props: ['postTitle'],
        // postTitle 就像 data 中定义的数据属性一样,在该组件中可以如 “this.postTitle” 这样使用
        template: '<h3>{{ postTitle }}</h3>'
    });
    Vue.component('PostList', {
        // 在字符串模板中可以直接使用 PascalCase 命名的组件名,和 camelCase 命名的 prop 名
        template: '<div><PostItem postTitle="PostList: Vue.js 无难事" /></div>'
    });
    let vm = new Vue({
        el: '#app'
    })
</script>

3.2、多参数传递

普通多参数:

<div id="app">
    <h2>组件: props</h2>
    <post-list></post-list>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('PostItem', {
        // 声明 props,多参数通常都是传入一个对象,避免参数过多
        props: ['author', 'title', 'content'],
        // postTitle 就像 data 中定义的数据属性一样,在该组件中可以如 “this.postTitle” 这样使用
        template:
            '<div>' +
            '   <h3>{{ title }}</h3>' +
            '   <p>作者:{{ author }}</p>' +
            '   <p>{{ content }}</p>' +
            '</div>'
    });
    Vue.component('PostList', {
        data() {
            return {
                author: '田七',
                title: 'Vue.js 无难事',
                content: '这本书还不错'
            }
        },
        // 在字符串模板中可以直接使用 PascalCase 命名的组件名,和 camelCase 命名的 prop 名
        template: '<div><PostItem :author=author :title=title :content=content /></div>'
    });
    let vm = new Vue({
        el: '#app'
    })
</script>


修改后:

<div id="app">
    <h2>组件: props</h2>
    <post-list></post-list>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('PostItem', {
        // 声明 props,多参数通常都是传入一个对象,避免参数过多
        props: ['post'],
        // postTitle 就像 data 中定义的数据属性一样,在该组件中可以如 “this.postTitle” 这样使用
        template:
            '<div>' +
            '   <h3>{{ post.title }}</h3>' +
            '   <p>作者:{{ post.author }}</p>' +
            '   <p>{{ post.content }}</p>' +
            '</div>'
    });
    Vue.component('PostList', {
        data() {
            return {
                post: {
                    author: '田七',
                    title: 'Vue.js 无难事',
                    content: '这本书还不错'
                }
            }
        },
        // 在字符串模板中可以直接使用 PascalCase 命名的组件名,和 camelCase 命名的 prop 名
        template: '<div><PostItem :post=post /></div>'
    });
    let vm = new Vue({
        el: '#app'
    })
</script>

3.3、单项数据流

通过 prop 传递的数据是单向的,父组件的属性变化会向下传递给子组件,但是返回来不行,这可以防止子组件意外修改父组件的状态,从而导致应用程序的数据流难以理解。
如果我们尝试在子组件中修改 prop,那么 Vue 会在浏览器控制台给出告警

<div id="app">
    <h2>组件: props</h2>
    <p>父级内容修改前:{{ content }}</p>
    <!--   子组件通过 methods 修改 content 值   -->
    <post-item :title=title :author=author :content=content></post-item>
    <p>父级内容修改后:{{ content }}</p>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('PostItem', {
        // 声明 props,多参数通常都是传入一个对象,避免参数过多
        props: ['author', 'title', 'content'],
        template:
            '<div>' +
            '   <h3>{{ title }}</h3>' +
            '   <p>{{ author }}</p>' +
            '   <p v-bind="contentNew()">{{ content }}</p>' +
            '</div>',
        methods: {
            contentNew() {
                this.content = '内容:' + this.content;
            }
        }
    });
    let vm = new Vue({
        el: '#app',
        data: {
            title: "Vue.js前端学习",
            author: "张三",
            content: "肾宝,味道好急撩"
        }
    })
</script>

为了解决上面的问题,我们可以采用两种方式

3.3.1、方式一,使用data,创建一个本地的变量来储存

props: ['author', 'title', 'content'],
data() {
    return {
        contentNew: '内容:' + this.content,
    }
}

3.3.2、方式二,使用计算属性

props: ['author', 'title', 'content'],
computed: {
    contentNew() {
        return '内容:' + this.content;
    }
},

3.3.3、注意

如果 prop 是一个数组对象类型,那么父组件也会随子组件改变

<div id="app">
    <h2>组件: props</h2>
    <p>父级内容修改前:{{ post.content }}</p>
    <post-item :post=post></post-item>
    <p>父级内容修改后:{{ post.content }}</p>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('PostItem', {
        // 声明 props,多参数通常都是传入一个对象,避免参数过多
        props: ['post'],
        methods: {
            contentNew() {
                this.post.content = '内容:' + this.post.content;
            }
        },
        template:
            '<div>' +
            '   <h3>{{ post.title }}</h3>' +
            '   <p>{{ post.author }}</p>' +
            '   <p v-bind="contentNew()">{{ post.content }}</p>' +
            // '   <p>{{ contentNew }}</p>' +
            '</div>'
    });
    let vm = new Vue({
        el: '#app',
        data: {
            post: {
                title: "Vue.js前端学习",
                author: "张三",
                content: "肾宝,味道好急撩"
            }
        }
    })
</script>

3.4、prop 验证

当开发一个通用组件时,我们希望父组件通过 prop 传递的数据类型符合要求。
为此。Vue.js 也提供了 prop 的验证机制,在定义 props 选项时,使用一个带验证需求的对象来代替之前一直使用的字符串数组(props: ['author', 'title', 'content']

Vue.component('MyComponent', {
        props: {
            // 基本检查类型(null 和 undefined 会通过任何类型检查)
            age: Number,
            // 多个可能类型
            tel: [String, Number],
            // 必填的字符串
            username: {
                type: String,
                required: true
            },
            // 带有默认值的数字
            sizeOfPage: {
                type: Number,
                default: 10
            },
            // 带有默认值的对象
            greeting: {
                type: Object,
                // 对象或数组默认值必须从一个工厂函数获取
                default() {
                    return { message: 'hello' }
                }
            },
            // 自定义验证函数
            info: {
                validator(value) {
                    // 这个值必须匹配下列字符串中的一个
                    return ['Success', 'Warning', 'Danger'].indexOf(value) !== -1
                }
            }
        }
    });

当 prop 验证失败时,在开发版本下,Vue会在控制台抛出一个告警。

验证的 type 也可以是下列原生构造函数中的一个:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Data
  • Function
  • Symbol
  • 自定义的构造函数(下面案例)
function Person(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
}

Vue.component('BlogPost', {
    props: {
        // 验收 author 的值是否是通过 new Person 创建的
        author: Person
    }
});

3.4.1、验证通过案例

<div id="app">
    <blog-post :author="author"></blog-post>
</div>

<script src="VueJs/vue.js"></script>
<script>
    function Person(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
    Vue.component('BlogPost', {
        props: {
            // 验收 author 的值是否是通过 new Person 创建的
            author: Person
        },
        template:
            '<div>' +
            '   <p>{{ author.firstName }}</p>' +
            '   <p>{{ author.lastName }}</p>' +
            '</div>'
    });
    let vm = new Vue({
        el: '#app',
        data: {
            author: new Person('张', 'three')
        }
    })
</script>

3.4.2、验证失败案例

<div id="app">
    <blog-post :author="author"></blog-post>
</div>

<script src="VueJs/vue.js"></script>
<script>
    function Person(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
    Vue.component('BlogPost', {
        props: {
            // 验收 author 的值是否是通过 new Person 创建的
            author: Person
        },
        template:
            '<div>' +
            '   <p>{{ author.firstName }}</p>' +
            '   <p>{{ author.lastName }}</p>' +
            '</div>'
    });
    let vm = new Vue({
        el: '#app',
        data: {
            author: '张 three'
        }
    })
</script>

3.5、非 prop 的属性

在使用组件时,组件的使用者可能会向组件传入未定义的 prop 属性值。这也是允许的,组件可以接受任意的属性,而外部设置的属性会被添加到组件的根元素上

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .child {
            background-color: hotpink;
        }

        .parent {
            opacity: 0.5;
        }
    </style>
</head>

<body>
<div id="app">
    <my-input class="parent" type="text"></my-input>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('MyInput', {
        template: '<input class="child" type="checkbox">'
    });
    let vm = new Vue({
        el: '#app',
    })
</script>
</body>
</html>

注意:
只有 classstyle 属性的会合并其他属性,外部提供的值会覆盖内部设置的值。

如果不想被外部属性覆盖组件内设置的值,可以在组件选项中设置 inheritAttrs: false

Vue.component('MyInput', {
    inheritAttrs: false,
    template: '<input class="child" type="checkbox">'
});

4、监听子组件事件

前面介绍了父组件如何通过 prop 向子组件传递数据,反过来,子组件如何向父组件通信呢?
在 Vue.js 中,这是通过自定义事件来实现的,子组件使用 $emit() 方法触发事件,父组件使用 v-on 指令监听子组件的自定义事件。

// evenName: 事件名
// args: 事件传递的参数
vm.$emit( evenName, [...args] )
<div id="app">
    <child @greet="sayHello"></child>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('child', {
        props: [],
        data() {
            return {
                name: '张三'
            }
        },
        methods: {
            handleChick() {
                this.$emit('greet', this.name)
            }
        },
        template: `<button @click="handleChick">开始欢迎</button>`
    });
    new Vue({
        el: '#app',
        methods: {
            sayHello(name) {
                alert("Hello, " + name)
            }
        }
    });
</script>

4.1、案例:点赞

<div id="app">
    <post-list></post-list>
</div>

<script src="vue.js"></script>
<script>
    // 父组件
    Vue.component('PostList', {
        data() {
            return {
                posts: [
                    {id: 1, title: '《Spring Boot实践》', author: '张三', date: '2019-10-21 20:10:15', vote: 0},
                    {id: 2, title: '《Vue.js入门》', author: '李四', date: '2019-10-10 09:15:11', vote: 0},
                    {id: 3, title: '《Python数据分析》', author: '王五', date: '2019-11-11 15:22:03', vote: 0}
                ]
            }
        },
        methods: {
            // 自定义事件vote的事件处理器方法
            handleVote(post) {
                post.vote = ++post.vote
                return post
            }
        },
        template: `
          <div>
          <ul>
            <PostListItem
                v-for="post in posts"
                :key="post.id"
                :post="post"
                @vote="handleVote(post)"/>
          </ul>
          </div>`
    });

    // 子组件
    Vue.component('PostListItem', {
        methods: {
            handleVote() {
                // 触发自定义事件
                this.$emit('vote');
            }
        },
        props: ['post'],
        template: `
          <li>
          <p>
            <span>标题:{{ post.title }} | 发帖人:{{ post.author }} | 发帖时间:{{ post.date }} | 点赞数:{{ post.vote }}</span>
            <button @click="handleVote">赞</button>
          </p>
          </li>
        `
    });

    let vm = new Vue({
        el: '#app'
    });
</script>

5、将原生事件绑定到组件

在组件上也可以监听原生事件,在使用 v-on 命令时,添加一个 .native 修饰符即可。如:

<base-input @focus.native="onFocus"></base-input>

这种方式最终是在组件的根元素上添加了 focus(聚焦)事件的监听,如果组件模板的根元素是 <input> ,那没有问题,但是如果不是,就有问题了。如:

Vue.component('MyInput', {
    template: `
        <label>
            {{ label }}
            <input class="child">
        </lavel>`
})

根元素是 <label> ,相当于在<label>上添加了 focus 事件监听器,这时,父级的 .native 监听器将静默失败,它不会报错,但是 onFocus 处理函数不会被如期被调用
为了解决这个问题,Vue.js 提供了一个 $listeners 属性,它是一个对象,里面包含了作用在这个组件上的所有监听器,如:

{
    focus(event) {...},
    input(value) {...},
    ...
}

有了 $listeners 属性,就可以使用 v-on="$listeners" 将组件上的所有事件监听器发送到特定的子元素。对于需要那些使用 v-model 的元素(如 <input> )来说,可以为这些监听器创建一个新的计算属性,如下面:

<div id="app">
    <my-input :label="title" v-model="msg" @focus="onFocus"></my-input>
    <p>{{ msg }}</p>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('MyInput', {
        inheritAttrs: false,
        // 父级传入数据:title -> label;msg -> value
        props: ['label', 'value'],
        data() {
            return {}
        },
        computed: {
            inputListeners() {
                let vm = this
                // 将所有的对象合并为一个新对象
                return Object.assign({},
                    // 从父级添加的所有监控
                    this.$listeners,
                    // 添加自定义监控器或覆写一些监听器的行为
                    {
                        // 确保组件和 v-model 一起工作
                        input(event) {
                            vm.$emit('input', event.target.value)
                        }
                    }
                )
            }
        },
        template: `
          <label>
          {{ label }}
          <input
              v-bind="$attrs"
              :value="value"
              v-on="inputListeners">
          </label>`
    });
    new Vue({
        el: '#app',
        data: {
            title: '输入框:',
            msg: '请输入'
        },
        methods: {
            onFocus() {
                console.log("不要摸人家么,好痒,臭流氓!")
            }
        }
    });
</script>

5.1、.sync 修饰符

在某些情况下,可能需要对一个组件的 prop 进行双向绑定,Vue.js 推进以 update: myPropName 模式触发事件来实现。例如:

<div id="app">
    <span>父组件计数值:{{ counter }}</span>
    <!--    <child :val="counter" @update:val="addCounter"></child>-->
    <!--  $event:自定义事件的附加参数  -->
    <child :val="counter" @update:val="counter = $event"></child>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('child', {
        props: {
            val: {
                type: Number,
                default: 0
            }
        },
        data() {
            return {
                count: this.val
            }
        },
        methods: {
            handleChick() {
                this.$emit('update:val', ++this.count)
            }
        },
        template: `
          <div>
          <span>子组件计数值:{{ val }}</span>
          <button @click="handleChick">增加计数</button>
          </div>`
    });
    new Vue({
        el: '#app',
        data: {
            counter: 0
        },
        methods: {
            addCounter(val) {
                return this.counter = val;
            }
        }
    });
</script>

为了方便起见,Vue.js 为了上述这种模式提供了一个缩写,即 .sync 修饰符(在 v-bind 指令上使用),修改如下:

<!-- 修改前 -->
<child :val="counter" @update:val="counter = $event"></child>
<!-- 修改后 -->
<child :val.sync="counter"></child>

当用一个对象同时设置多个 prop 的时候,也可以将 .sync 修饰符和 v-bind 一起使用:

<text-document v-bind.sync="doc"></text-document>

这里会把 doc 对象中的每一个属性作为一个单独的 prop 传进去,然后为每个属性添加 v-on:update 监听器。

<body>
<div id="app">
    <span>父组件 post:{{ post.title }} | {{ post.author }} | {{ post.time }} | {{ post.vote }} | {{ post.price }}</span>
    <child v-bind.sync="post"></child>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('child', {
        props: {
            title: { type: String },
            author: { type: String },
            time: { type: String },
            vote: { type: Number },
            price: { type: Number },
        },
        data() {
            return {
                title: this.vote,
                author: this.vote,
                time: this.vote,
                vote: this.vote,
                price: this.price
            }
        },
        methods: {
            handleChick() {
                this.$emit('update:vote', this.vote += 1)
                this.$emit('update:price', this.price += 4)
            }
        },
        template: `
          <div>
          <span>子组件 post:{{ title }} | {{ author }} | {{ time }} | {{ vote }} | {{ price }}</span>
          <br>
          <button @click="handleChick">增加计数</button>
          </div>`
    });
    new Vue({
        el: '#app',
        data: {
            post: {
                title: '《Spring Boot 从入门到入土》',
                author: '张三',
                time: '2021年05月16日00:05:50',
                vote: 0,
                price: 0
            }
        },
        methods: {}
    });
</script>

6、自定义组件的 v-model

6.1、model 选项

6.1.1、方法一

某些输入类型(如复选框和单选按键)可能希望将 value 属性用于其他目的,或者想要改变触发数据同步的默认 input 事件,这可以通过组件的 model 选项来实现。

<div id="app">
    <my-checkbox v-model="isAgree" value="同意协议"></my-checkbox>
    <p>{{ isAgree }}</p>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('MyCheckbox', {
        props: {
            // value prop 可以用于不同的目的
            value: String,
            // 使用 checked 替换 value 作为 prop
            checked: {
                type: Boolean,
                default: false
            }
        },
        model: {
            // 使用 checked 替换 value 作为 prop
            // isAgree 的值将会传入名为 checked 的 prop
            // 当触发 change 事件并附带一个新值的时候,isAgree 属性将会更新
            prop: 'checked',
            event: 'change'
        },
        methods: {},
        template: `
          <div>
              <input
                  type="checkbox"
                  :checked="checked"
                  @change="$emit('change', $event.target.checked)">
              <label>{{ value }}</label>
          </div>`
    });
    new Vue({
        el: '#app',
        data: {
            isAgree: false,
        }
    });
</script>

6.1.2、方法二(采用上面 2 的方法)

我尝试将 input 输入与单项合并成一个组件,但是失败了,原因在于 v-model 接收的 value 值不对,如果后面我尝试成功,再来更新方法三。

<div id="app">
    <my-checkbox :label="title" v-model="isAgree" @focus="onFocus"></my-checkbox>
    <p>{{ msg }}</p>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('MyCheckbox', {
        inheritAttrs: false,
        // 父级传入数据:title -> label;isAgree -> value
        props: {
            label: String,
            value: {
                type: Boolean,
                default: false
            }
        },
        model: { 
            // 这个是必要的,不添加返回值为 event 属性,而不是 Boolean 属性。
            // [Vue warn]: Invalid prop: type check failed for prop "value". Expected Boolean, got Event 
            prop: 'value',
            event: 'change'
        },
        computed: {
            changeListeners() {
                let vm = this
                // 将所有的对象合并为一个新对象
                return Object.assign({},
                    // 从父级添加的所有监控
                    this.$listeners,
                    // 添加自定义监控器或覆写一些监听器的行为
                    {
                        // 确保组件和 v-model 一起工作
                        change(event) {
                            vm.$emit('change', event.target.checked)
                        }
                    }
                )
            }
        },
        template: `
          <label>
          <input
              type="checkbox"
              :checkde="value"
              v-on="changeListeners">
          {{ label }}
          </label>`
    });
    new Vue({
        el: '#app',
        data: {
            title: '同意协议',
            isAgree: false
        },
        methods: {
            onFocus() {
                console.log("不要摸人家么,好痒,臭流氓!")
            }
        }
    });
</script>

7、实例:combobox

有的时候需要用到组合框(combobox),下面演示一个从下拉框中选择一项,文本输入框中会显示所选内容,并向父组件传递输入框内的值。

<div id="app">
    <my-combobox
            :label="selectLabel"
            :list="selectList"
            v-model="selectVal">
    </my-combobox>
    <p>选中的值:{{ selectVal }}</p>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('MyCombobox', {
        inheritAttrs: false,
        props: {
            label: String,
            list: Attr,
            value: String
        },
        computed: {
            inputListeners() {
                let vm = this
                return Object.assign({},
                    this.$listeners,
                    {
                        input(event) {
                            vm.$emit('input', event.target.value)
                        }
                    }
                )
            }
        },
        template: `
          <div>
          <h3>{{ label }}</h3>
          输入值传递给父组件:<input :value="value" v-on="inputListeners">
          <br>
          输入值不传递给父组件:<input :value="value">
          <br>
          <select v-model="value" v-on="inputListeners">
            <option disabled value="">请选择</option>
            <option v-for="item in list" :value="item">{{ item }}</option>
          </select>
          </div>`
    });
    new Vue({
        el: '#app',
        data: {
            selectLabel: '喜欢哪一句?',
            selectList: ['云想衣裳花想容', '春风拂槛露华浓', '若非群玉山头见', '会向瑶台月下逢'],
            selectVal: ''
        }
    });
</script>

8、使用插槽分发内容

组件是当做自定义元素来使用的,元素可以有属性和内容

  • 属性:通过组件定义的 prop 来接收属性值
  • 内容:通过 <slot> 元素来解决

此外,插槽(slot)也可以作为父子组件之间通信另一种实现方式

例:

Vue.component('greeting', {
    template: `<div><slot></slot></div>`
});

在组件模板中,<div>元素内部使用一个<slot>元素,可以把这个元素理解为占位符。

<greeting>Hello, Vue.js</greeting>

在组件渲染时,这个内容会被置组件内部的<slot>元素。最终渲染成:

<div>Hello, Vue.js</div>

8.1、编译作用域

如果想通过插槽向组件传递动态数据,例如:

<greeting>Hello, {{ name }}</greeting>

那么要清楚一点,name 是在父组件的作用域下解析的,而不是 greeting 组件的作用域。name 必须存在于父组件的 data 选项中。

要记住:父组件模板中的所有内容都是在父级作用域内编译;子组件模板中的所有内容都是在子作用域内编译

<div id="app">
    <greeting>Hello, {{ name }}</greeting>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('greeting', {
        template: `<div><slot></slot></div>`
    });
    new Vue({
        el: '#app',
        data: {
            name: '张三'
        }
    });
</script>

8.2、缺省内容

在组件内部使用 <slot> 元素时,可以给该元素指定一个内容,以防止组件的使用者没有给该组件传递内容。例如,一个用作提交的组件 <submit-button> 的模板内容如下:

<div id="app">
    <submit-button></submit-button>
    <submit-button>注册</submit-button>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('SubmitButton', {
        template: `
            <button type="submit">
                <!-- 如果不输入内容,默认显示“提交” -->
                <slot>提交</slot>
            </button>`
    });
    new Vue({
        el: '#app'
    });
</script>

8.3、命名插槽

在开发组件时,可能会需要用到多个插槽。例如,有一个布局插件 <base-layout> 它的模板内容需要如下的形式:

<div id="app">
    <base-layout>
        <template v-slot:header>
            <h1>这里是页头部分,如导航栏</h1>
        </template>

        <p>主要内容段落</p>
        <p>另一个段落</p>

<!--        <template v-slot:default>-->
<!--            <p>没有使用 name 属性的 slot 元素具有隐含的名称 default</p>-->
<!--            <p>没有使用 name 属性的 slot 元素使用方式只能与上面二选一</p>-->
<!--        </template>-->

<!--     v-slot的缩写为:#     -->
        <template #footer>
            <h2>这里是页脚部分,如联系信息,友情链接等</h2>
        </template>
    </base-layout>
</div>

<script src="vue.js"></script>
<script>
    Vue.component('BaseLayout', {
        template: `
            <div calss="container">
                <header>
                    <slot name="header"></slot>
                </header>
                <main>
                    <slot></slot>
                </main>
                <footer>
                    <slot name="footer"></slot>
                </footer>
            </div>`
    });
    new Vue({
        el: '#app'
    });
</script>

8.4、作用域插槽

前面介绍过,在父级作用域下,在插槽的内容中是无法访问到子组件的数据属性的,但有时候需要在父级的插槽内容中访问子组件的数据,为此,可以在子组件的 <slot> 元素上使用 v-bind 指令绑定一个 prop

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('MyButton', {
        inheritAttrs: false,
        data() {
            return {
                titles: {
                    login: '登录',
                    register: '注册'
                }
            }
        },
        template: `
          <button>
              <slot v-bind:values="titles">
                {{ titles.login }}
              </slot>
          </button>`
    });
    let vm = new Vue({
        el: '#app',
    })
</script>

这个按键的名称可以在“登录”和“注册”之间切换,为了让父组件可以访问 titles,在 <slot> 元素上使用 v-bind 指令绑定一个 values 属性,成为 插槽 prop,这个prop不需要在props选项中声明。

在父级作用域下使用该组件时,可以给 v-slot 指令一个值来定义组件提供的插槽 prop的名字。

<div id="app">
    <my-button>
        <template v-slot:default="slotProps">
            {{ slotProps.values.register }}
        </template>
    </my-button>
</div>

slotProps 这个名字可以随便取,代表的是包含组件内所有插槽 prop的一个对象。

在上面的例子中,父级作用域只是给了默认插槽提供了内容,在这种情况下,可以简写:

<div id="app">
    <my-button v-slot:default="slotProps">
        {{ slotProps.values.register }}
    </my-button>
</div>

还可以进一步简写:

<div id="app">
    <my-button v-slot="slotProps">
        {{ slotProps.values.register }}
    </my-button>
</div>

**注意:**如果有多个插槽,则不建议使用简写,防止作用域不明确

8.4.1、案例:多插槽,多插槽prop

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>案例:多插槽,多插槽prop</title>
</head>

<body>
<div id="app">
    <my-button>
        <template #user="userProps">
            用户:<input :value="userProps.user">
        </template>

        <template #prwd="prwdProps">
            密码:<input :value="prwdProps.user">
        </template>

        <template #default="slotProps">
            {{ slotProps.values.register }}
            {{ slotProps.msg }}
        </template>
    </my-button>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('MyButton', {
        inheritAttrs: false,
        data() {
            return {
                userName: '张三',
                userPrwd: 'KJkieh4163874',
                titles: {
                    login: '登录',
                    register: '注册'
                },
                msg: 'success'
            }
        },
        template: `
          <div>
              <tr>
                <td>
                  <slot name="user" :user="userName"></slot>
                </td>
              </tr>
              <tr>
                <td>
                  <slot name="prwd" :user="userPrwd"></slot>
                </td>
              </tr>
              <br>
              <button>
                <slot :values="titles" :msg="msg">
                  {{ titles.login }}
                </slot>
              </button>
          </div>
        `
    });
    let vm = new Vue({
        el: '#app',
    })
</script>
</body>
</html>

8.4.2、作用域插槽工作原理

作用域插槽的内部工作原理是将插槽内容包装到传递单个参数的函数中来工作的。

function (slotProps) {
    // 插槽内容
}

这就意味着 v-slot 的值实际上可以是任何能作为函数定义中的参数的 JS 表达式。所有在支持 ES6 的环境下,可以使用 解构语法 来提取特定的 插槽 prop

<template #default="slotProps">
    {{ slotProps.values.register }}
    {{ slotProps.msg }}
</template>

可以改为:

<!-- 使用对象解构 与 重命名 -->
<template #default="{ values, msg:context }">
    {{ values.register }}
    {{ context }}
</template>

9、动态组件

在页面应用程序中,经常会遇到多标签页面,在 Vue.js 中,可以通过动态组件来实现。组件的动态切换是通过在 <component> 元素上使用 is 属性来实现。

<div id="app">
    <button
            v-for="tab in tabs"
            :key="tab.title"
            :class="['tab-button', { active: currentTab === tab.title }]"
            @click="currentTab = tab.title">
        {{ tab.displayName }}
    </button>
    <component
            :is="currentTabComponent"
            class="tab">
    </component>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('TabIntroduce', {
        data() {
            return {
                content: 'Vue.js 从入门到入土'
            }
        },
        template: `<div><input v-model="content"></div>`
    });
    Vue.component('TabComment', {
        template: `<div>这书还不错</div>`
    });
    Vue.component('TabQa', {
        template: `<div>有人看过这本书么?怎么样</div>`
    });
    let vm = new Vue({
        el: '#app',
        data: {
            // 当前展示页
            currentTab: 'introduce',
            // 页签列表,方便 v-for 循环展示
            tabs: [
                {title: 'introduce', displayName: '图书简介'},
                {title: 'comment', displayName: '图书评价'},
                {title: 'qa', displayName: '图书问答'},
            ],
        },
        computed: {
            // 计算属性,用于切换页签
            currentTabComponent() {
                return 'tab-' + this.currentTab
            }
        }
    })
</script>

我们使用后会发现,我们每个页签当点击时,都会重新创建一个新的,并不会保留之前的操作,为了解决这个问题,我们可以添加一个 <keep-alive> 标签:

    <keep-alive>
        <component
                :is="currentTabComponent"
                class="tab">
        </component>
    </keep-alive>

这样就不会每次都创建一个新的组件,而是复用。

10、异步组件(暂时用不到,略过)

11、组件的生命周期

生命周期得几个钩子方法的应用,比如 渲染等待时的等待动画等。当前不太重要,略过。

12、混入

当前不重要,略

13、杂项

**注意:**如果数据需要在多个组件中访问,并且能够响应更新,可以考虑官方推荐真正状态管理解决方案Vuex

13.1、组件通信的其他方式

总结一下前面介绍的组件通信的三种方式:

  • 父组件通过 prop 向子组件传递数据
  • 子组件通过自定义事件向父组件发起通知或进行数据传递
  • 子组件通过<slot>元素充当占位符,获取父组件分发的内容;也可以在子组件的<slot>元素上使用 v-bind 指令绑定一个插槽 prop,向父组件提供数据

这里介绍组件通信的其他实现方式。

13.1.1、访问根实例

在每个 new Vue 实例的子组件中,都可以通过 $root 属性来访问根实例。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<div id="app">
    <child></child>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('child', {
        methods: {
            accessRoot() {
                console.log('单价:' + this.$root.price)
                console.log('总结:' + this.$root.totalPrice)
                console.log(this.$root.hello())
            }
        },
        template: `<button @click="accessRoot">访问根实例</button>`
    })

    new Vue({
        el: '#app',
        data: {
            price: 98
        },
        computed: {
            totalPrice() {
                return this.price * 10
            }
        },
        methods: {
            hello() {
                return 'Hello, Vue.js'
            }
        }
    })
</script>
</body>
</html>

13.1.2、访问父组件实例

$root 类似,$parent 属性用于在一个子组件中访问父组件实例,这可以代替父组件通过 prop 向子组件传递数据的方式。

注意: 这里 $parent 不能访问 父组件的父组件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<div id="app">
    <parent></parent>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('child', {
        methods: {
            accessRoot() {
                console.log('单价:' + this.$parent.price)
                console.log('总结:' + this.$parent.totalPrice)
                console.log(this.$parent.hello())
                console.log(this.$root.hello())
            }
        },
        template: `<button @click="accessRoot">访问父组件实例</button>`
    })

    Vue.component('parent', {
        data() {
            return {
                price: 98
            }
        },
        computed: {
            totalPrice() {
                return this.price * 10
            }
        },
        methods: {
            hello() {
                return 'Hello, Vue.js 我是父节点内容'
            }
        },
        template: `<child></child>`
    })

    new Vue({
        el: '#app',
        methods: {
            hello() {
                return 'Hello, Vue.js 我是根节点内容'
            }
        }
    })
</script>
</body>
</html>

13.1.3、访问子组件实例或子元素

父组件要访问子组件实例或子元素,可以给子组件或子元素添加一个特殊的属性 ref,为子组件或子元素分配一个印有 ID ,然后父组件就可以通过 $refs 属性来访问子组件实例或子元素。

注意: $refs 属性只在组件渲染完成之后生效,并且它们是 非响应式 的。要避免在模板和计算属性中访问 $refs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<div id="app">
    <parent></parent>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('child', {
        data() {
            return {
                message: 'Hello Vue.js'
            }
        },
        template: `<p>{{ message }}</p>`
    })

    Vue.component('parent', {
        mounted() {
            // 访问子元素 <input> ,让其具有焦点
            this.$refs.inputElement.focus()
            // 访问子组件 <child> 的 message 数据属性
            console.log(this.$refs.childComponent.message)
        },
        template: `
            <div>
                无焦点:<input><br>
                有焦点:<input ref="inputElement"><br>
                <child ref="childComponent"></child>
            </div>`
    })

    new Vue({
        el: '#app',
    })
</script>
</body>
</html>

13.1.4、依赖注入

  • $root 属性用于访问根实例
  • $parent 属性用于访问父组件实例

但是如果组件嵌套的层级不确定,某个组件的数据或方法需要被后代组件所访问,又该如何实现呢。
这时需要用到两个新的实例选项:provideinject

  • provide:选项允许我们制定要提供给后代组件的数据或方法
  • inject:在后代组件中使用 inject 选项来接收要添加到该实例中的特定属性

优点: 父组件不用管传递给哪个子组件,子组件不用管哪个父组件注入的
缺点: 它将应用程序中的组件和它们当前的组织方式耦合起来,使重构变得更加困难。此外,所提供的属性也是 非响应式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<div id="app">
    <parent></parent>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('child', {
        // 接收 父组件传递来的数据属性和方法
        inject: ['message', 'hello'],
        mounted() {
            // 使用父组件传递的方法
            this.hello('张三在子组件中使用')
        },
        template: `<p>子组件访问父组件传递的数据属性:{{ message }}</p>`
    })

    Vue.component('parent', {
        methods: {
            sayHello(name) {
                console.log('Hello, ' + name + ' 父组件传递的方法')
            }
        },
        provide() {
            return {
                // 数据属性 message 和 sayHello 方法可供后代组件访问
                message: 'Hello, 我是父组件传递的数据属性',
                hello: this.sayHello
            }
        },
        template: `<child></child>`
    })

    new Vue({
        el: '#app',
    })
</script>
</body>
</html>

13.1.5、手动监听事件

第 4 节,已经讲过 $emit 的用法,它用于出发当前实例的事件,触发的事件可以被 v-on 指令监听。除此之外,Vue还提供了一下三种事件方法,让我们能够以编程的方式手动对自定义事件进行处理。

  • $on(eventName, eventHandler)

监听当前实例上的自定义事件,事件可以有 vm.$emit 触发

  • $once(eventName, eventHandler)

监听一个自定义事件,但是只触发一次。一旦触发,监听器就会被删除

  • $off(eventName, eventHandler)

删除自定义事件监听

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<div id="app">
    <child></child>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('child', {
        created() {
            // 监听当前实例的 greet 事件
            this.$on('greet', function () {
                this.$parent.sayHello()
            })
            // 监听当前实例的 greetOnce 事件,仅一次
            this.$once('greetOnce', function () {
                this.$parent.sayOnce()
            })
        },
        beforeDestroy() {
            // 实例销毁前 删除 greet 事件的所有监听器
            this.$off('greet')
        },
        methods: {
            handleClick() {
                // 触发 自定义事件 greet
                this.$emit('greet')
            },
            handleOnceClick() {
                // 触发 自定义事件 greet
                this.$emit('greetOnce')
            }
        },
        template: `
          <div>
          <button @click="handleClick">实例销毁前,都可触发</button>
          <br>
          <button @click="handleOnceClick">只监听一次</button>
          </div>
        `
    })

    new Vue({
        el: '#app',
        methods: {
            sayHello() {
                alert('Hello, $on 监听 greet 事件触发,实例销毁前都可以触发')
            },
            sayOnce() {
                alert('Hello, $once 监听 greetOnce 事件触发,第二次点击不在触发事件')
            }
        }
    })
</script>
</body>
</html>

13.1.6、递归组件

组件可以在自己的模板中递归调用自己,但这需要使用 name 选项来为组件指定一个内部调用的名称。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<div id="app">
    <category-component :list="categories"></category-component>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('CategoryComponent', {
        name: 'catComp',
        props: {
            list: {
                type: Array
            }
        },
        template: `
          <ul>
          <template v-if="list">
            <li v-for="cat in list">
              {{ cat.name }}
              <catComp :list="cat.children"/>
            </li>
          </template>
          </ul>
        `
    })

    new Vue({
        el: '#app',
        data: {
            categories: [
                {
                    name: '程序设计',
                    children: [
                        { name: 'Java', 
                         children: [ 
                            {name: 'Java SE'}, 
                            {name: 'Java EE'} 
                            ]
                        }, 
                        { name: 'C++' }
                    ]
                },
                {
                    name: '前端框架',
                    children: [ 
                        {name: 'Vue.js'}, 
                        {name: 'React'} 
                    ]
                }
            ]
        }
    })
</script>
</body>
</html>

13.1.7、内联模板(自定义模板)

内联模板是在子组件上使用的一个特殊属性 inline-template,然后这个组件将使用其元素的内容作为模板,而不是将其作为要发布的内容,这使得模板的编写更加灵活。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<div id="app">
    <button-counter inline-template>
        <button @click="count++">You clicked me {{ count }} times.</button>
    </button-counter>
</div>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('ButtonCounter', {
        data() {
            return {
                count: 0
            }
        }
        // 这里没有 template
    })

    new Vue({
        el: '#app'
    })
</script>
</body>
</html>

13.1.8、X-Temlate(自定义模板)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
<div id="app">
    <button-counter></button-counter>
</div>

<script id="btn-counter" type="text/x-template">
    <button @click="count++">You clicked me {{ count }} times.</button>
</script>

<script src="VueJs/vue.js"></script>
<script>
    Vue.component('ButtonCounter', {
        data() {
            return {
                count: 0
            }
        },
        template: '#btn-counter'
    })

    new Vue({
        el: '#app'
    })
</script>
</body>
</html>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值