Vue全家桶学习笔记(三)——组件

539 阅读7分钟

写在前面

最近看尚硅谷的视频有点懵逼了,直接跳过了组件来讲用CLI搭建项目了,找到了coderwhy老师的vue教学视频,好好学了一下组件。

组件的创建及使用步骤

  1. 创建组件构造器对象 Vue.extend()
  2. 注册组件
  3. 使用组件 同样也是一个例子,先在js中创建一个组件构造器和注册该组件(注意到template的字符串是模板字符串的形式,而且template的内容最外部必须至少要有一个包裹元素):
    //创建组件构造器对象 Vue.extend()
    const cpnC = Vue.extend({
        template: `  
        <div>
        <h2>我是标题</h2>
        <p>我是内容</p>
        </div>
    `
    })
    //注册组件 Vue.component(全局注册)
    Vue.component('my-cpn', cpnC)
    new Vue({
        el: '#app',
    })

之后就可以在HTML中使用该组件了,这样看来组件应该算是HTML的语法糖,增加了复用率,减少了广大CV开发者的工作。

<body>
    <div id="app">
        <my-cpn></my-cpn>
    </div>
</body>

全局组件和局部组件

刚刚的例子可以看到是使用Vue的函数对象方法注册组件,这样注册的组件是全局组件,该组件在任何Vue实例中都可以被解析。
另外一种注册方式是局部注册,该注册方式只能在指定Vue实例的解析元素中使用:

    const cpnC = Vue.extend({
        template: `  
        <div>
        <h2>我是标题</h2>
        <p>我是内容</p>
        </div>
    `
    new Vue({
        el: '#demo',
        components: {
            cpn: cpnC,
        }

父子组件

组件内部是也可以注册组件的,这样在父组件内部的template也可以使用已经注册了的子组件。真·奥义·套娃:

        //组件1
        const cpnC1 = Vue.extend({
            template: `
                    <div>
                        <h2>组件1</h2>
                        <p>组件1的内容</p>
                    </div>
            `
        })
        //组件2,里面注册了组件1
        const cpnC2 = Vue.extend({
            template: `
                    <div>
                        <h2>组件2</h2>
                        <p>组件2的内容</p>
                        <cpnc1></cpnc1>
                    </div>
                    
            `,
            components: {
                cpnc1: cpnC1,
            }
        })
        //el为#app的Vue实例,注册了组件2
        new Vue({
            el: '#app',
            components: {
                cpnc2: cpnC2,
            }
        })

在div#app中使用组件2和组件1(注意html标签不支持驼峰命名法,大写字母会被识别成小写字母):

    <div id="app">
        <cpnc2></cpnc2>
        <cpnc1></cpnc1>
    </div>

在这里由于组件1并没有在Vue实例中注册,也没有在全局中注册,因此无法被解析。其实该Vue实例也可以看成是一个根组件(root),它就是无尽套娃的起点。

注册组件的语法糖

前面注册组件的步骤还是有些繁琐的,现在已经很少能看到这种注册方式了,语法糖的写法如下:

    //注册组件 Vue.component(全局注册)
    Vue.component('my-cpn', {
        template: `  
        <div>
        <h2>我是标题</h2>
        <p>我是内容</p>
        </div>
    `
    })

语法糖的写法省略了组件构造器的创建步骤,直接来了一个二合一,但是底层还是调用Vue.extend()实现的功能。

组件模板(template)抽离

注册组件的语法糖写法确实写起来轻松了,但是把html内容都塞到一个地方看起来有些混乱,尤其是存在父子组件嵌套的关系下。
我们可以在html中用template标签写我们组件模板的内容,加上id进行对应:

    <template id="cpn">
        <div>
            <h2>我是标题</h2>
            <p>我是内容</p>
        </div>
    </template>
    //全局注册
    Vue.component('cpn', {
        template: '#cpn'
    })
    //局部注册
    new Vue({
        el: '#demo',
        components: {
            cpn: {
                template: '#cpn'
            }
        }
    })

组件的data函数

上面这些写法写出来的html元素虽然能够重复利用,但是都是静态的,不符合Vue的响应式特点,我们能不能在组件中设置响应式数据呢?yes!我们可以在组件中配置data函数(注意!不同于实例中的配置,不是一个对象嗷。),该函数的返回值是一个对象,以及methods方法等。如下注册了一个加减器的组件并在html中使用了该组件:

    new Vue({
        el: '#demo',
        components: {
            cpn: {
                template: '#cpn',
                data() {
                    return {
                        counter: 0
                    }
                },
                methods: {
                    inc() {
                        this.counter++
                    },
                    dec() {
                        this.counter--
                    }
                }
            }

        }
    })
    <template id="cpn">
        <div>
            <h2>当前计数:{{counter}}</h2>
            <button @click='inc'>+</button>
            <button @click='dec'>-</button>
        </div>
    </template>
    <div id="demo">
        <cpn></cpn>
    </div>

那么为神魔这个data属性要设置成函数呢?我们看这样的场景:这个加减器我们需要重复使用,那么多个加减器中的counter属性是不是会共享值呢?我们来试试:

image.png

每个加减器组件的counter值都是独立不影响的!这是因为每次使用组件会调用data函数,每一次调用返回的新对象都是存储在不同内存空间之中的,相互之间不会产生影响。

父子组件通信

  1. 父组件通过props向子组件传递信息
  2. 子组件通过自定义事件向父组件传递信息
  3. 父组件访问子组件: childrenorchildren or refs
  4. 子组件访问父组件: parentandparent and root

父传子

在子组件中配置props(props可以使多种形式,以数组为例),props中的元素是被传递变量在子组件中的属性名的字符串,子组件通过v-bind绑定被传递变量,之后在子组件中就能通过vue的语法进行操作被传递变量:

<body>
    <template id="cpn1">
        <div>
            <h2>{{cmsg}}</h2>
            <h2 v-for="(item,index) in cmovies" :key='item'>{{item}}</h2>
        </div>
    </template>

    <div id="demo">
        <cpn1 :cmsg='message' :cmovies='movies'></cpn1>
    </div>
</body>
<script>

    const cpn1 = {
        template: '#cpn1',
        props: ['cmsg', 'cmovies']
    }
    //注册组件 Vue.component
    new Vue({
        el: '#demo',
        data: {
            message: 'hello',
            movies: ['Batman', 'Superman', 'Wonderwoman']
        },
        components: {
            cpn1
        }
    })
</script>

当然,props的配置不止于数组,我们也可以配置对象,可以以对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型,当然也可以给这些prop提供默认值以及required属性:

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
  //提供了默认值的prop
  movies:{
      type:String,
      default:'Hello',
      required:true // 在传props时,该prop必须要被传入
  }
  msgs:{
      type:Array,
      default(){
          return []
      }, // 当类型指定为数组或对象时,default属性必须是带返回值的函数
      required:true // 在传props时,该prop必须要被传入
  }  
  
}

子传父

当子组件向父组件传递数据时需要用到自定义事件。自定义事件的使用步骤可以归纳为如下:

  1. 子组件中的原生事件会向父组件抛出自定义事件(this.$emit('myevent',[**args]))
  2. 父组件接收到自定义事件(记得别用驼峰命名),调用对应的事件回调函数
  3. 回调函数可以接收到从子组件传过来的数据(可选)并进行处理 举个例子:
<body>
    <!-- 子组件模板 -->
    <template id="cpn1">
        <div>
            <button v-for="(item,index) in categories" @click='btnClick(item)'>{{item.name}}</button>
        </div>
    </template>
    <!--  父组件模板 -->
    <div id="demo">
        <cpn1 @my-click='cpnClick'></cpn1>
    </div>
</body>
<script>
    //子组件
    const cpn1 = {
        template: '#cpn1',
        data() {
            return {
                categories: [
                    { id: 1, name: '手机家电' },
                    { id: 2, name: '实用工具' },
                    { id: 3, name: '计生用品' },
                    { id: 4, name: '运动护具' },

                ]
            }
        },
        methods: {
            btnClick(item) {
                this.$emit('my-click', item)
            }
        }
    }
    //父组件
    new Vue({
        el: '#demo',
        components: {
            cpn1
        },
        methods: {
            cpnClick(item) {
                alert(item.name)
            }
        }
    })
</script>

父访子

简单的案例:

<body>
    <template id="cpn">
        <div></div>
    </template>
    <div id="demo">
        <cpn></cpn>
        <cpn ref="cpn2"></cpn>
        <button @click='btnClick1'>父组件按钮--操控组件1</button>
        <button @click='btnClick2'>父组件按钮--操控组件2</button>

    </div>
</body>
<script>
    //注册组件 Vue.component
    const cpnC = {
        template: '#cpn',
        methods: {
            showMessage() {
                alert('子组件的显示函数!')
            }
        }

    }
    new Vue({
        el: '#demo',
        components: {
            cpn: cpnC,
        },
        methods: {
            btnClick1() {
                this.$children[0].showMessage()
            },
            btnClick2() {
                this.$refs.cpn2.showMessage()
            }
        }
    })
</script>

子访父

和父访子的使用差不多,在子组件里面用 this.parent可以访问到父组件对象,用this.parent 可以访问到父组件对象,用 this.root 可以访问到根组件对象。但实际开发中不推荐用这个功能,因为子组件如果依赖父组件,则该子组件的复用性会变差。

插槽(slot)

何为插槽?插槽可以扩展设备的功能:比如笔记本的USB插槽、网线插槽。Vue的插槽同样用来强化组件的扩展性,利用slot,抽取共性,预留不同(设置插槽)

插槽的基本使用

也是一个小案例:

<body>
    <template id="cpn">
        <div>
            <h2>我是标题</h2>
            <p>我是内容</p>
            <slot>
                <p>我是插槽默认值</p>
            </slot>
        </div>
    </template>

    <div id="demo">
        <cpn></cpn>
        <cpn>
            <button>按钮嗷</button>
        </cpn>
        <cpn>
            <button>按钮嗷</button>
            <button>按钮嗷</button>
            <input type="text" placeholder="文本框">
        </cpn>
    </div>
    <script>
        //创建组件构造器对象 Vue.extend()
        const cpnC = Vue.extend({
            template: '#cpn'
        })
        //注册组件 Vue.component

        new Vue({
            el: '#demo',
            components: {
                cpn: cpnC,
            }
        })
    </script>
</body>

具名插槽

可以看到,一个插槽位置可以插任意个元素,而且可以给插槽一个默认值。当一个组件有多个插槽时,如果我们仍然像上面那么写,则所有插槽都会被替换相同内容,所以我们需要给插槽命名(具名插槽):

        <template id="cpn1">
            <div>
                <slot name="l">
                    </name=>>
                    <span></span>
                </slot>
                <slot name='m'>
                    <span></span>
                </slot>
                <slot name='r'>
                    <span></span>
                </slot>
            </div>
        </template>
    
        <cpn1>
            <span slot="l">改了左边</span>
            <span slot="m">改了中间</span>
            <span slot="r">改了右边</span>
        </cpn1>

编译作用域

类似于js中的作用域,在html中使用了vue的数据时,我们也可以通过作用域来确定vue数据的作用范围,假设一对父子组件,在父子组件中都设置了isShow属性,一个为true,一个为false,那么如下的情况,该子组件会不会显示呢:

    <!-- 父组件 -->
    <div id="demo">
    <!-- 父组件作用域 -->
        <cpn v-show='isShow'><!-- 子组件作用域 --></cpn>
    </div>

这里,由于vshow所处作用域是父组件的作用域,因此isShow的值为true。

作用域插槽

父组件替换插槽的标签,标签内容由子组件提供。v-slot是新语法,Vue3.0之后旧语法(slot-scope)被抛弃了。v-slot在使用时推荐写在template标签下

<!-- 默认插槽-->
<foo v-slot="{ msg }">
  {{ msg }}
</foo>

<!--具名插槽 -->
<foo>
  <template v-slot:one="{ msg }">
    {{ msg }}
  </template>
</foo>

动态组件

这部分的内容在掘金上有一些教程,其中这篇文章比较深入分析了动态组件的原理,奈何现在水平不够,不太能看懂,先马着,日后再看深入剖析Vue源码