1.初识Vue组件

Vue中的组件是页面中的一部分,通过层层拼装,最终形成了一个完整的组件。这也是目前前端最流行的开发方式。下面是Vue3官方给出的一张图,通过图片能清楚的了解到什么是Vue中的组件。

p04_00.png

图的左边是一个网页,网页分为了头部、左侧主体和右侧边栏。这时候你用组件的概念,就可以先把这个网页分为三个大的组件,分别是头部、左侧和右侧。然后再根据每个大组件里的内容和功能,作更加详细的组件划分。

这样就可以把一个复杂的大页面,分解成一个个可以复用的小页面。

Vue中的组件可以分为:根组件、全局组件、局部组件。

1.1.根组件

  1. <body>
  2. <div id="app"></div>
  3. <script src="https://unpkg.com/vue@next"></script>
  4. <script>
  5. let app = Vue.createApp({}); //Vue实例
  6. let vm = app.mount('#app') //根组件
  7. </script>
  8. </body>
  • Vue.createApp({}) 实际是建立一个Vue实例。
  • Vue.createApp({}).mount(‘#app’) 将Vue的实例挂载到一个DOM节点上,就成为一个根组件。

1.2.全局组件

直接挂载到Vue实例上的组件就是一个全局组件。 全局组件可以应用在任何一个根组件中。

<body>
    <div id="app1">
        <h3>app1</h3>
        <mycomponent></mycomponent>
        <mycomponent></mycomponent>
    </div>
    <div id="app2">
        <h3>app2</h3>
        <mycomponent></mycomponent>
        <mycomponent></mycomponent>
    </div>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        //创建Vue实例
        let app = Vue.createApp({});
        //在Vue实例上创建一个mycomponent全局组件
        app.component('mycomponent', {
            template: '<p>我是全局组件</p>'
        });

        //全局组件可以应用在任何一个根组件中
        //app.mount('#app1');
        app.mount('#app2');
    </script>
</body>

使用vue.component()注册组件,需要提供2个参数:组件的标签名和组件构造器。组件构造器中的template中设置组件的html模板。

因为组件是可复用的vue实例,所以它们也能有data、computed、watch、methods以及生命周期钩子等。

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

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        //创建Vue实例
        let app = Vue.createApp({});
        //在Vue实例上创建一个mycomponent全局组件
        app.component('mycomponent', {
            template: `<div>
                          <h3>我是全局组件</h3>
                          {{num}} <button @click="add">加</button>
                       </div>`,
            data() {
                return {
                    num: 1
                }
            },
            methods: {
                add() {
                    this.num++;
                }
            }
        });
        app.mount('#app');
    </script>
</body>

组件模板的内容,可以写在一对反引号中(``),这样就可以不使用字符串拼接的形式了。

1.3.局部组件

全局组件可以在根组件的任何地方使用。

但一旦定义了全局组件,就会占用系统资源。因为即使是你并不使用某个组件,它仍然会被包含在最终的构建结果中。这就造成了用户下载的JavaScript的无谓的增加。

声明在根组件上的组件就是一个局部组件。局部组件只能应用在根组件中。而且,局部组件只有在使用时才会耗费系统资源。

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

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: `<div>
                          <h3>我是局部组件</h3>
                          {{num}} <button @click="add">加</button>
                       </div>`,
                    data() {
                        return {
                            num: 1
                        }
                    },
                    methods: {
                        add() {
                            this.num++;
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>

1.4.组件模板

如果组件中的template内容过多,那么可以使用组件模板来声明template中的内容。

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

    <!-- 组件模板 -->
    <template id="mytemplate">
        <div>
            <h3>我是局部组件</h3>
            {{num}} <button @click="add">加</button>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            num: 1
                        }
                    },
                    methods: {
                        add() {
                            this.num++;
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>

2.父子组件

当我们继续在组件中写组件,形成组件嵌套的时候,就是我们所说的父子组件了。

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

    <template id="mytemplate">
        <div>
            <h3>我是父组件</h3>
            <!-- 在父组件中使用子组件 -->
            <subcomponents></subcomponents>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            num: 1
                        }
                    },
                    methods: {
                        add() {
                            this.num++;
                        }
                    },
                    components:{          //声明子组件
                        subcomponents:{
                            template:`<div>我是子组件</div>`
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>

3.组件之间的通信

组件与组件之间是可以互相通信的。包括父子组件之间、兄弟组件之间等等,都可以互相通信。
下面只讨论父子组件之间通信问题。

3.1.子组件获取父组件数据

3.1.1.数据传递选项prop

在vue中,组件实例的作用域是孤立的,默认情况下,父子组件的数据是不能共享的,也就是说,子组件是不能直接访问父组件的数据的。为此,vue给我们提供了一个数据传递的选项prop,用来将父组件的数据传递给子组件。具体使用如下:

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

    <template id="mytemplate">
        <div>
            <h3>我是父组件</h3>
            <!-- 在父组件中使用子组件 -->
            <subcomponents msg="hello world!"></subcomponents>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            num: 1
                        }
                    },
                    components:{          //声明子组件
                        subcomponents:{
                            template:`<div>我是子组件,我能获取父组件传递的数据:{{msg}}</div>`,
                            props:['msg']
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>

上面实例中,子组件获取父组件传递的数据的步骤为:

  1. 在子组件标签中,声明 msg 属性,属性值即为父组件向子组件传递的值。
  2. 在子组件中,使用props选项,声明接收父组件向子组件传递值的载体,即 ‘msg’ 。
  3. 子组件中就可以使用 msg 获取父组件向子组件传递的值了。

也可以使用 v-bind 绑定子组件标签属性,这样就可以将父组件data数据传递个子组件了。

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

    <template id="mytemplate">
        <div>
            <h3>我是父组件</h3>
            <!-- 在父组件中使用子组件 -->
            <subcomponents :msg="welcome"></subcomponents>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            welcome: 'hello world!'
                        }
                    },
                    components:{          //声明子组件
                        subcomponents:{
                            template:`<div>我是子组件,我能获取父组件传递的数据:{{msg}}</div>`,
                            props:['msg']
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>

3.1.2.传值校验

我们希望组件之间传递数据时,可以对数据的类型、是否必填等进行校验,这就是参数校验功能。

只要将 props 采用对象形式就可以实现参数校验:

components:{          //声明子组件
    subcomponents:{
        template:`<div>我是子组件,我能获取父组件传递的数据:{{msg}}</div>`,
        props:{
            msg:{
                type:String,
                required: true
            }
            //或者简写
            //msg:String
        }
    }
}
  • type:表示参数类型。值:String、Boolean、Array、Object、Function、Symbol
  • required:表示是否必填。值:true、false

3.1.3.单向数据流

父组件向子组件传递数据是单向的。也就是说:子组件可以接收并使用父组件传递过来的数据,但子组件不能修改此数据。

这样做是为了防止子组件意外改变父组件的状态,从而导致父组件的行为异常。

<script src="https://unpkg.com/vue@next"></script>
<script>
    //...
    subcomponents:{
        template:`<div>
                       我是子组件,我能获取父组件传递的数据:{{msg}} 
                       <button @click="update">更改</button>
                   </div>`,
        props:['msg'],
         methods: {
             update(){
                  this.msg = 'haha';
             }
         }
    }
    //...
</script>
  • 当子组件试图修改父组件传递值时,就会出现警告,表示 msg 是只读的:
vue@next:1568 [Vue warn]: Attempting to mutate prop "msg". Props are readonly

3.2.父组件获取子组件数据

和上面不一样的是,父组件想要获取子组件的数据时,需要子组件通过emit主动将自己的数据发送给父组件。

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

    <template id="mytemplate">
        <div>
            <h3>我是父组件,接收子组件传递过来的数据:{{msg}}</h3>
            <subcomponents @childmsg="get" :msg="welcome"></subcomponents>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            welcome: 'hello world!',
                            msg:''
                        }
                    },
                    methods: {
                        get(msg) {
                            this.msg = msg;
                        }
                    },
                    components: {          //声明子组件
                        subcomponents: {
                            template: `<div>
                                           我是子组件,我能获取父组件传递的数据:{{msg}}
                                           <input type="button" value="给父组件发数据" @click="send">
                                       </div>`,
                            props: ['msg'],
                            data() {
                                return {
                                    message: '我是子组件的数据'
                                }
                            },
                            methods: {
                                send() {
                                    this.$emit('childmsg', this.message);
                                }
                            }
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>
  • 首先,我们需要在子组件中触发一个主动发送数据的事件。上面的例子中是一个点击事件send
  • 其次,在点击事件中使用emit方法,这个emit接收两个参数:传递数据的事件和需要传递的数据。 这个传递数据的事件也是自定义的;
  • 然后在父组件中引用子组件,并在引用的子组件中使用on监听上一步传递数据的事件。上面的例子中是childmsg;
  • 最后在父组件中使用这个事件,这个事件带有一个参数,就是从子组件发送过来的数据。

3.3.多级组件通信

下面是一个多级组件之间的传值:

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

    <template id="mytemplate">
        <div>
            <subcomponents :num="num"></subcomponents>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        document.getElementById
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            num: 10
                        }
                    },
                    components: {          //声明子组件
                        subcomponents: {
                            template: `<subsubcomponents :num="num"/>`,
                            props: ['num'],
                            components: {
                                subsubcomponents: {
                                    template: `<div>孙子组件:{{num}}</div>`,
                                    props: ['num']
                                }
                            }
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>
  • 上面实例中,父组件经过三级传递,将数据传递给孙子组件。

那么,如果此时有更多级组件嵌套,那么传递数据将非常麻烦。而且,父组件其实只想将数据传递给孙子组件,但却不得不通过子组件接力传递。

所以,Vue提供了 provide/inject 来解决这个问题。

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

    <template id="mytemplate">
        <div>
            <subcomponents :num="num"></subcomponents>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            num: 10
                        }
                    },
                    provide() {
                        return {
                            num: this.num
                        }
                    },
                    components: {          //声明子组件
                        subcomponents: {
                            template: `<subsubcomponents/>`,
                            components: {
                                subsubcomponents: {
                                    template: `<div>孙子组件:{{num}}</div>`,
                                    inject: ['num']
                                }
                            }
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>
  • 上面实例中,使用 provide 来声明要传递的数据
  • 在孙子组件中使用inject 来接收数据。这样,就可以越过子组件进行数据传递了。

4.slot插槽

父组件不但可以向子组件传递数据,还能向子组件分发内容(就是向子组件传递html内容)。

官方解释:Vue 实现了一套内容分发的 API,将 slot 元素作为承载分发内容的出口。

4.1.基本用法

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

    <template id="mytemplate">
        <div>
            <subcomponents>
                <p>父组件向子组件分发的内容</p>
            </subcomponents>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            num
                        }
                    },
                    components:{          //声明子组件
                        subcomponents:{
                            template:`<div>
                                          <p>我是子组件</p>
                                          <slot></slot>
                                      </div>`,
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>
  • 在父组件中,使用子组件标签时,标签中将会添加需要分发给子组件的内容,也就是一段html代码。
  • 在子组件中,就可以使用slot标签来显示父组件分发的内容。

值得注意的是,官网中有这样一句话:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

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

    <template id="mytemplate">
        <div>
            <subcomponents>
                <p>{{msg}}</p>
            </subcomponents>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            msg:'父组件向子组件分发的内容'
                        }
                    },
                    components:{          //声明子组件
                        subcomponents:{
                            template:`<div>
                                          <p>我是子组件</p>
                                          <slot></slot>
                                      </div>`,
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>
  • 上面实例中,父组件会访问自己的 msg 数据去解析插槽内容。之后才分发给子组件。
  • 并不是在子组件中去访问父组件的 msg 数据。

4.2.slot默认内容

如果父组件没有提供插槽内容,那么子组件可以在 slot标签中书写插槽的默认内容。

<template id="mytemplate">
    <div>
        <subcomponents></subcomponents>
    </div>
</template>

subcomponents:{
    template:`<div>
                  <p>我是子组件</p>
                  <slot>插槽默认内容</slot>
              </div>`,
}
  • 当父组件没有提供插槽内容时,子组件将使用自己的默认内容显示。

4.2.具名插槽

有时,父组件需要分发多个内容,并且这些内容需要在子组件的不同位置进行显示。

比如:在实际应用中,一个组件的头部和尾部是共通的,那么头部和尾部就应该由父组件,统一分发给所有子组件。此时可以使用具名插槽。

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

    <template id="mytemplate">
        <div>
            <subcomponents>
                <template v-slot:header>
                    <div>头部内容</div>
                </template>
                <template v-slot:footer>
                    <div>尾部内容</div>
                </template>
            </subcomponents>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            msg:'父组件向子组件分发的内容'
                        }
                    },
                    components:{          //声明子组件
                        subcomponents:{
                            template:`<div>
                                          <slot name="header">
                                              <div>默认头部内容</div>
                                          </slot>
                                          <p>我是子组件</p>
                                          <slot name="footer">
                                              <div>默认尾部部内容</div>
                                          </slot>
                                      </div>`,
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>
  • 在父组件中,使用 template 标签声明具名插槽,并取名。
  • 在子组件中,通过插槽名决定内容的显示位置。
  • v-slot:header 可以简写为 #header。

总结:插槽是父子组件关系中,插槽在子组件中展示位置以及展示内容的操作手段。父组件决定展示内容,子组件决定展示位置。

5.动态组件

5.1.基本用法

有时,我们可能需要根据状态来决定使用那个组件,比如下面的例子:

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

    <template id="mytemplate">
        <div>
            <!-- <component :is="componentname"></component>-->
            <subcomponents1 v-if="componentname=='subcomponents1'"></subcomponents1>
            <subcomponents2 v-if="componentname=='subcomponents2'"></subcomponents2>
            <button @click="change">切换组件</button>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {
                            componentname: 'subcomponents1'
                        }
                    },
                    components:{          //声明子组件
                        subcomponents1:{
                            template:`<input type="text">`,
                        },
                        subcomponents2:{
                            template:`<textarea></textarea>`,
                        }
                    },
                    methods:{
                        change(){
                            if(this.componentname=='subcomponents1'){
                                this.componentname='subcomponents2';
                            }else{
                                this.componentname='subcomponents1';
                            }
                        }
                    }
                }
            }
        }).mount('#app');
    </script>
</body>
  • 上面实例中,通过在子组件上使用 v-if 来判断 componentname 的状态,从而实现对子组件的切换使用。

但是上面的写法有些麻烦,所以Vue提供了动态组件用于实现上面的功能。

<template id="mytemplate">
    <div>
        <component :is="componentname"></component>
        <button @click="change">切换组件</button>
    </div>
</template>
  • 使用 component 标签中的 is 属性,它会根据子组件名来动态的切换子组件的显示。

5.2.保持组件状态

上面实例中,当子组件切换时,子组件状态将会被重置(比如:文本框中输入的数据不能保持)。

vue提供了 keep-alive 标签让我们保持子组件状态。它能够将子组件状态缓存起来,在子组件显示时在恢复其状态。

<template id="mytemplate">
    <div>
        <keep-alive>
            <component :is="componentname"></component>
        </keep-alive>
        <button @click="change">切换组件</button>
    </div>
</template>
  • 当子组件切换时,子组件状态将会被保持。

6.异步组件

在实际开发中,一个应用可能会非常复杂。它可能会由很多组件组成。如果在应用启动时就加载所有组件,势必会造成效率低下。因此,正确的方式应该是按需加载。也就是先加载必要组件,然后根据需求在加载其它组件。

为了实现这个需求,Vue为我们提供了异步组件。

官网解释:在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

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

    <template id="mytemplate">
        <div>
            <subcomponents1></subcomponents1>
            <subcomponents2></subcomponents2>
        </div>
    </template>

    <script src="https://unpkg.com/vue@next"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {}
                    },
                    components:{          //声明子组件
                        subcomponents1:{
                            template:`<div>第一个组件</div>`,
                        },
                         subcomponents2:Vue.defineAsyncComponent(()=>{
                            return new Promise((resolve,reject)=>{
                                setTimeout(() => {
                                    resolve({
                                        template:`<div>第二个组件</div>`,
                                    });
                                }, 3000);
                            })
                        })
                    }
                }
            }
        }).mount('#app');
    </script>
</body>
  • 上面代码中声明了两个子组件,一个是同步组件,一个是异步组件。
  • 使用 Vue.defineAsyncComponent 来声明异步组件,并且它要求返回一个Promise对象。
  • 代码运行后,先加载第一个子组件,3秒钟后加载第二个组件。