Vue.js第3课-深入理解Vue组件(part01)

一、组件使用中的细节

1、使用 is 解决 h5 标签上的一些小 bug

<div id="app">
    <table>
        <tbody>
            <row></row>
            <row></row>
            <row></row>
        </tbody>
    </table>
</div>
<script>
    Vue.component("row", {
        template: "<tr><td>this is row</td></tr>"
    })
    var app = new Vue({
        el: "#app",
    })
</script>

面代码中,我们创建了一个全局组件 row,并设置了模板 template,然后将这个组件添加到 table 的 tbody 中,这是一个正常的使用组件的方法,但是打开页面,可以看到模板正常显示,结构却不对,tr 渲染到了 tbody 之外。

这是因为在 h5 规范中,正确结构应该是 table > tbody > tr > td,但是现在将组件 row 加到了 tbody 中,所以元素的结构发生了错乱。

接下来看一下如何使用 is 来解决 h5 标签上的 bug,在 tbody 中还是用 tr 标签,将组件的名称放到 is 中去,意思是将 tr 解析为 row 组件。同样在使用 ul 或其他这样标签的时候,也建议用这种方式,以免出现兼容性的错误。

<div id="app">
    <table>
        <tbody>
            <tr is="row"></tr>
            <tr is="row"></tr>
            <tr is="row"></tr>
        </tbody>
    </table>
</div>
<script>
    Vue.component("row", {
        template: "<tr><td>this is row</td></tr>"
    })
    var app = new Vue({
        el: "#app",
    })
</script>

2、子组件里定义 data 必须是一个函数

将上面一段代码修改一下,把 td 中的内容放到子组件的 data 中进行存储,看是否能成功渲染出来:

<div id="app">
    <table>
        <tbody>
            <tr is="row"></tr>
            <tr is="row"></tr>
            <tr is="row"></tr>
        </tbody>
    </table>
</div>
<script>
    Vue.component("row", {
        template: "<tr><td>{{content}}</td></tr>",
        data : {
            content : "this is row"
        }
    })
    var app = new Vue({
        el: "#app",
    })
</script>

打开页面,发现报错了,是因为组件中的 data 必须是一个 function,不能是一个对象。

再修改一下代码,在 function 中返回 content 的值:

<div id="app">
    <table>
        <tbody>
            <tr is="row"></tr>
            <tr is="row"></tr>
            <tr is="row"></tr>
        </tbody>
    </table>
</div>
<script>
    Vue.component("row", {
        template: "<tr><td>{{content}}</td></tr>",
        data: function () {
            return {
                content: "this is row"
            }
        }
    })
    var app = new Vue({
        el: "#app",
    })
</script>

此时再刷新页面,可以看到数据能正常的显示。

总结:在子组件里定义 data 的时候,data 必须是个函数,而不能是一个对象,之所以这么设计,是因为一个子组件不像是根组件,只会被调用一次,他可能在不同的地方被调用多次(在例子中我们调用了三次),每一个子组件的数据不希望和其他子组件产生冲突,也就是说每一个子组件都应该有自己的数据,如上例子,每个 row 对应的数据对应的是各自的数据,而不应该共享一套数据,通过一个函数返回对象的目的,就是为了让每一个子组件都有一个独立的数据存储,这样就不会出现多个子组件互相影响的情况。

3、ref(引用) 的使用

Vue 不建议我们在代码里操作 DOM,但是在处理一些极其发杂的动画效果,你不操作 DOM,光靠 Vue 的数据绑定,有的时候处理不了这样的情况,所以在有一些必要的情况下,还真就得操作 DOM。那么,在 Vue 中如何操作DOM 呢?需要通过 ref 这种引用的形式来获取 DOM并操作。

首先给 div 标签起一个的名字,比如 hello,接着,我的需求是这样的:点击 Div 的时候,把里边的内容打印出来。

在这里,因为给 div 起了一个引用的名字叫 ref,所以当被点击的时候,可以通过

this.$refs.hello

来获取该 DOM 元素,他指的是整个 Vue 实例里所有的引用(可以在 Vue 事例中再添加一个 DOM 元素,并给他设置 ref,打印一下 this.$refs,可以看到这个 DOM 的 ref 值也显示在其中),所有的引用中有一个名叫 hello 的引用,他指向的就是这个 Div 所指向的 DOM 节点,然后就可以通过 innerHMTL 来获取 DOM 元素中的内容了。

<div id="app">
    <div ref="hello" @click="clickFun">Hello World!</div>
</div>
<script>
    var app = new Vue({
        el: "#app",
        methods: {
            clickFun: function () {
                console.log(this.$refs.hello); // <div>Hello World!</div>
                console.log(this.$refs.hello.innerHTML); // Hello World!
            }
        }
    })
</script>

到这里会发现,这些之和 DOM 节点有关系,和组件并没有关系,实际上不是这样的,现在的 DIV 是一个标签,在标签上加一个 ref,通过

this.$refs

获取到这个引用的时候,你获取到的是一个 DOM 元素,那假设,这是一个组件呢?如果一个组件上加了 ref,通过 this.$refs.hello 获取到的是什么呢?其实,你获取到的是这个组件的引用,接下来通过一个计数器的例子再来理解一下 ref。

<div id="app">
    <!-- 在父级的组件上使用 counter 这个子组件,如果没有问题,点击他么就会增加。 -->
    <counter ref="one" @change="clickChange"></counter>
    <counter ref="two" @change="clickChange"></counter>
    <!-- 现在想实现一个功能,在福组件中加一个 div 标签,在这里想显示两个 counter 的总数,也就是对两个 counter 进行求和。 -->
    <div>{{total}}</div>
</div>
<script>
    // 创建子组件 counter
    Vue.component("counter", {
        template: "<div @click='clickFun'>{{number}}</div>",
        data: function () {
            return {
                number: 0
            }
        },
        methods: {
            // 因为在子组件中绑定了一个点击事件 clickFun,所以该事件就应该写到子组件中的 methods 中。
            clickFun: function () {
                this.number++;
                // 当子组件数据发生变化的时候,向外触发一个事件(涉及到子组件向父组件传值的内容,可以回顾下第一章),将改事件添加到组件 counter 中,然后去父组件中的 methods 中添加一个 clickChange 方法。
                this.$emit("change");
            }
        }
    })
    var app = new Vue({
        el: "#app",
        data: {
            total: 0
        },
        methods: {
            clickChange: function () {
                console.log('change');
                // 此时可以发现,点击两个子组件 counter 都可以触发 change 方法,现在只要实现一个求和的功能就行了,接下来就要用到 ref 了。

                // 给两个子组件都起一个引用(ref)的名字,例如 “ref="one"” 和 “ref="two"”,不管哪个组件发生了改变,最外层的根实例的 clickChange 方法都会被执行。
                console.log(this.$refs.one);
                console.log(this.$refs.two);

                // 在父组件中添加一个 DOM 来存放 total 的值,在 data 中给 total 一个默认值 0,通过 ref 获取两个 counter 的 number 值然后计算和并复制给 total。
                this.total = this.$refs.one.number + this.$refs.two.number;
            }
        }
    })
</script>

这个时候,一个 counter 求和的工作就做完了。

总结:当 ref 是写到一个标签上的时候,通过

this.$refs.名字

获取到的内容实际上的标签对应的 dom 元素。当在一个组件上写 ref,通过 “this.$refs.名字”获取到的内容实际上 counter 子组件的一个引用。

二、父子组件的数据传递

上一节学习 ref 的时候,讲解了子组件如何通过事件触发的形式向父组件传递数据,这一节将会系统的借助计数器(counter)这个例子来讲解父子组件之间更多的数据传递方式。

<div id="app">
    <counter :count="1"></counter>
    <counter :count="2"></counter>
</div>
<script>
    var counter = {
        props: ['count'],
        template: "<div @click='clickFun'>{{count}}</div>",
        methods: {
            clickFun: function () {
                this.count++
            }
        }
    };
    var app = new Vue({
        el: "#app",
        components: {
            counter: counter
        }
    });
</script>

上面代码,我们先创建一个局部组件 counter,可以在模板中先写死一个数据 0,之后在父组件中通过 components 来注册这个组件,这样就可以在父组件的模板里使用这个组件了。

首先我们要讲父组件怎么向子组件传递数据,父组件通过属性的形式来传递数据,可以在子组件中写一个 count='0',或者是 :count='0',加冒号的话,传递给子组件的 0 或 1,就变成一个数字了,如果不加冒号,传给子组件的 0 或 1 实际上就是一个字符串,因为如果加了冒号,后面双引号里的内容实际上就是一个 js 表达式了,不是字符串了,所以他就是一个数字类型了。

父组件通过属性的形式向 counter 这个子组件传递了一个名字叫做 count 的属性,那子组件要接收一下,才能使用这个数据,怎么接受呢?要在这个局部组件中写一个 props,指的是子组件需要接收父组件传递过来的什么内容?要接收一个 count 这样一个属性的内容,写了 props 后,就可以直接在 template 中通过 {{}} 来引用父组件传递过来的数据了。

在 Vue 中,父组件向子组件传值,都是通过属性的形式来传递的,接下来,我们要把子组件累加的这个功能给他做上去,在子组件 template 中绑定一个点击事件 clickFun,每一次被点击了,执行 count++,到页面上,发现确实好用,但是在控制台有报错,警告不要直接修改父组件传递过来的数据。

这是因为在 Vue 中有一个单项数据流的概念,也就是父组件可以通过属性向子组件传递参数,传递的参数可以随便的进行修改,这是没问题的,也就是父组件可以随意的向子组件传递参数,但是子组件绝对不能反过来,去修改父组件传递过来的参数,举例来说,如果父组件自身有一个属性是3,过一会又变成4传递给子组件,这是没有任何问题的,但是传递给子组件,子组件接收到数据后,子组件只能用这个数据,不能去修改,之所以 Vue 中有这个单项数据流的概念,原因在于,一旦你的子组件接收的这个 count 不是一个基础类型的数据,而是一个类似 object 的引用类型数据,在子组件里改变了一些传递过来的数据内容,有可能接收的这个引用型的数据还被其他的子组件做使用,这样的话,你这个子组件改变了数据,不仅仅影响了自己的组件,还有可能对其他的子组件造成影响,所以 Vue 这个单项数据流,你子组件不能改变父组件的数据,那如果确实要改变这个 count 的这个值该怎么办?

可以在这个子组件中定义一个 data,return 一个对象,可以在这个对象中定义一个名叫 number 的属性,它的初始值是 this.count,也就是,我从父组件接收到一个 count 这样的数据,把 count 数据复制了一份,放到子组件自己的 data 里,这样的话,在下面,我就不用 count 这个内容了,取而代之,我用我自己的 number,当自身被点击的时候,也不去加 count 了,去加自己的 number。

<div id="app">
    <counter :count="1"></counter>
    <counter :count="2"></counter>
</div>
<script>
    var counter = {
        props: ['count'],
        template: "<div @click='clickFun'>{{number}}</div>",
        data: function () {
            return {
                number: this.count
            }
        },
        methods: {
            clickFun: function () {
                this.number++
            }
        }
    };
    var app = new Vue({
        el: "#app",
        components: {
            counter: counter
        }
    });
</script>

接着继续完善功能,接下来看子组件向父组件传递数据,之前已经讲过了,通过 $.emit() 来传递,现在来更深入的讲解一下。

<div id="app">
    <counter @change="changeFun" :count="1"></counter>
    <counter @change="changeFun" :count="2"></counter>
    <div>{{total}}</div>
</div>
<script>
    var counter = {
        props: ['count'],
        template: "<div @click='clickFun'>{{number}}</div>",
        data: function () {
            return {
                number: this.count
            }
        },
        methods: {
            clickFun: function () {
                this.number = this.number + 2
                this.$emit('change', 2);
            }
        }
    };
    var app = new Vue({
        el: "#app",
        data: {
            total: 3
        },
        components: {
            counter: counter
        },
        methods: {
            changeFun: function (step) {
                console.log(step);
                this.total += step
            }
        }
    });
</script>

在实例中添加一个放总数的 DOM,在根实例中 data 下可以先将 total 写死,因为两个 count 的初始值分别为 1,2,所以可将 total 先定死为 3,实际上这不是一个正确的写法,目前可以先来做个练习,到后面,可以通过计算属性,避免一些数据的冗余。到页面上看一下,1,2,3 没有问题。

子组件每一次点击的时候,他可以往外携带一些数据,或者说他向父组件传递一下内容,怎么传递呢?子组件向父组件传值,我们通过事件的形式,也就是在子组件被点击的时候,可以通过 this.emit() 向外触发一个 change 事件,emit() 中还可以传入第二个参数,或多个参数,“this.emit('change',1)” 意思是总数加 1,也可以把 “this.number++;” 改为 “this.number = this.number + 2;”,同时,emit 中第二个参数也要修改为 2,,它的效果就是,每点击一次,就加2,然后需要告诉父组件,每次改变,都增加2,所以在父组件 methods 中添加一个子组件改变数据的方法 clickChange,给他传递一个参数 step,指每一次增加多少,因为子组件中 $emit 传入的是 2,所以 step 也就是 2。

三、组件参数校验与非 props 特性

1、组件的参数校验

接下来先看一个未经过校验的例子。首先创建一个名字叫做 child 的子组件,在父组件中去调用这个子组件,直接用 child 就可以了,此时去页面上看,child 就可以显示出来了。

<div id="app">
    <child></child>
</div>
<script>
    Vue.component("child", {
        template: "<div>Child</div>"
    })
    var app = new Vue({
        el: "#app",

    })
</script>
1.1、参数的校验

有的时候父组件需要往子组件里传递参数,例如我们一般可以在父组件里写一个 content="Hello World!",通过这种形式,父组件向子组件传递参数,那么组件的校验指的是什么?你父组件向子组件传递了一个内容,那子组件有权对这个内容做一些约束,这些约束我们就可以把它叫做参数的校验。父组件传递 content,子组件势必就要接收 content,我们先把 content 写到 props 里面,写完之后,就可以在模板里通过插值表达式使用这个 content 了。

<div id="app">
    <child content="Hello World!"></child>
</div>
<script>
    Vue.component('child', {
        props: ['content'],
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

下一步,就会有这样的需求,调用子组件传递的这个 content,我要做一些约束,例如你传递过来的 content 必须是一个字符串,如果有这样的需求该怎么办呢?如果要实现这样的需求,props 里就不写一个数组了,而是写一个对象,对象的键就是接收的参数的名字,叫做 content,可以写一个 String,这么去写的意思是:子组件接收到 content 这个属性必须是一个 String 字符串的类型,那现在父组件调用子组件,传递 content 的时候,“Hello World!”肯定是一个字符串,所以页面上此时不会有任何问题,我们修改一下父组件,让他传递一个数字,如果直接把 “Hello World!” 修改为“123”,那他依然传递的是一个字符串,回忆一下上一节的内容,如果传递的是数字,就要在属性前加冒号:

<div id="app">
    <child :content="123"></child>
</div>
<script>
    Vue.component('child', {
        props: {
            content: String
        },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

这个时候再到页面上看,content 内容虽然正常渲染了,但是却报了一个警告,说类型检测有问题,子组件希望 content 是一个字符串,但是父组件传递过来的却是 Number。

假设我希望传递过来的是一个数字,该怎么写呢?直接把 String 改为 Number 就可以了:

<div id="app">
    <child :content="123"></child>
</div>
<script>
    Vue.component('child', {
        props: {
            content: Number
        },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

此时页面就不会报错了。有时还会有这样的需求,我希望传过来的数据既可以是字符串,也可以是数组,这个时候,可以借助数组的语法,把 Number 和 String 放到一个数组里面,它的意思就是,子组件接收的 content 属性,要么是属性,要么是字符串,所以这个时候,传数字或字符串都不会报错。

<div id="app">
    <child content="Hello World!"></child>
    <child :content="123"></child>
</div>
<script>
    Vue.component('child', {
        props: {
            content: [Number, String]
        },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

假设传一个对象呢?

<div id="app">
    <child :content="{name :'liu'}"></child>
</div>
<script>
    Vue.component('child', {
        props: {
            content: [Number, String]
        },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

就会报错了,说我期待你传的是一个 Number 或 String,但是你传的却是一个 Object,所以就不对了。通过这个例子,就知道什么是组件参数校验了,也就是子组件接收什么参数,是有规则定义的,当然,这些规则还可以变得更复杂。

1.2、参数更复杂的校验

content 后面,不仅仅可以跟 Number,String 一个数组,实际上还可以跟一个对象。

1.2.1、 type 类型的校验

以写一个 type 加上 String,这个组件接收一个名叫 content 的属性,它的类型必须是 String。

<div id="app">
    <child :content="{name :'liu'}"></child>
</div>
<script>
    Vue.component('child', {
        props: {
            content: {
                type: String
            }
        },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

打开页面,会告诉我们传的必须是 String,但真正传入的却是一个 Object,所以会报出一个警告。

除了 type 还可以写一个 required。

1.2.2、required 接收的属性是否必传

required 的意思是,我这个子组件,接收 content 这个属性,这个属性是否是必传的,例如 required 如果设置为 true,如果在子组件中不传 content,看一下:

<div id="app">
    <child></child>
</div>
<script>
    Vue.component('child', {
        props: {
            content: {
                type: String,
                required: true
            }
        },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

页面报错,告诉你 content 必传,但是现在缺少了这个 content,如果把 required 改为 false,就不会报任何错误了。

除了写 required 之外,还可以写一个 default。

1.2.3、default 默认值的使用

default 可以随便写一些内容,比如“default value”。

<div id="app">
    <child></child>
</div>
<script>
    Vue.component('child', {
        props: {
            content: {
                type: String,
                required: false,
                default: 'default value'
            }
        },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

可以看到 default value 显示到页面上了。我们这个 child 组件主要接收一个属性,这个属性叫做 content,他不是必填的,也就是这个 content 可传可不传,但是假设你不传,他会使用一个 default 默认值,这个默认值就是 default value,所以你看,在父组件调用子组件的时候,没有传 content,那么子组件里 content 内容就是这个默认的 default value。假设父组件调用子组件的时候,传递了 content,等于一个 “Hello World!”,这个时候,默认值就不会生效了。

再来写一个更复杂的校验。

1.2.4、validator 验证器

要求 content 这个字符串的长度必须在某个长度之间,例如不能小于 5 位,可以借助 validator 的一个配置项来实现,在 validator 中配置一个函数,他会有个形参叫做 value,可以返回 value.length > 5,然后将 content 里的字符删除剩5个以内的字符。

<div id="app">
    <child content="He"></child>
</div>
<script>
    Vue.component('child', {
        props: {
            content: {
                type: String,
                required: false,
                default: 'default value',
                validator: function (value) {
                    return (value.length > 5);
                }
            }
        },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

意思是,我子组件要接受一个属性,属性的名字叫做 content,类型必须是一个字符串,同时我要对你传入的这个 content 通过校验器做一个校验,这个 value 就是指你传入的内容,我要求传入的这个人字符串长度必须大于5,上面代码中只传入了两个字符,validator 返回 false,所以校验不通过,所以页面报错了,如果大于5就不会报错。

以上就是组件校验的几个重点,接下来看一下非 props 特性。

2、非 props 特性

说到非 props 特性他一定和 props 特性相对应,先来看一下什么是 props 特性:

2.1、props 特性

就拿上边的例子来看,props 特性指的是当你的父组件使用子组件的时候,通过属性向子组件传值的时候,恰好子组件里声明了对父组件传递过来的属性的一个接收,也就是说,父组件调用子组件的时候,传递了 content,子组件恰好在 props 里又申明了这个 content,所以父子组件有一个对应关系,如果你这么去写这种形式的属性,我们就把他叫做 props 特性。

<div id="app">
    <child content="He"></child>
</div>
<script>
    Vue.component('child', {
        props: {
            content: {
                type: String,
            }
        },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

props 特性有什么样的特点呢?打开页面,看一下 DOM 结构。

可以看到,有一个 div,里边内容 "he",所以在子组件传递的 content 是不会在 DOM 标签中显示的。

props 还有一个特点,当父组件传递了 content,子组件接收了 content 之后,在子组件里就可以直接通过插值表达式,或者通过 this.content 去取得 content 里边的内容了,所以上边这么写,父组件传递了 content 过来,子组件就能把这个内容显示出来,这就叫做 props 特性。

下来看看什么叫做非 props 特性。

2.2、非 props 特性

非 props 特性指的是,父组件向子组件传递了一个属性,但是子组件并没有 props 这块内容,也就是说子组件并没有声明接收父组件传递过来的内容,如果是这一种情况,我们看一下页面的效果:

<div id="app">
    <child content="He"></child>
</div>
<script>
    Vue.component('child', {
        // props: {
        //     content: {
        //         type: String,
        //     }
        // },
        template: "<div>{{content}}</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

首先,页面就会报一个错误,说 content 没有被定义,无法使用,这是因为父组件向子组件传递了 content,但是这个时候子组件并没有去接子组件传过来的 content,你不去接收,子组件里就没法使用这个 content,一旦你用,就报错了,这是非 props 特性的第一个情况,就是如果你定义了一个非 props 特性,这个时候 content="he"就是一个非 props 特性。非 props 特性里子组件是没有办法获取到父组件的内容的,因为你压根就没有申明你要获取的内容,所以就没法用。

非 props 特性还有第二个特点,在 template 中不用插值表达式,可以直接写一个 Hello World!

<div id="app">
    <child content="He"></child>
</div>
<script>
    Vue.component('child', {
        // props: {
        //     content: {
        //         type: String,
        //     }
        // },
        template: "<div>Hello World!</div>"
    });
    var app = new Vue({
        el: "#app",
    });
</script>

如果现在使用的是一个非 props 特性,那么这个非 props 特性实际上是会显示在 DOM 的属性之中的,我们打开页面看一下:

可以看到,div 上面有一个 HTML 属性,上面写着 content="He",很明显,当我们申明一个非 props 特性,它的第二个特点是,这个属性会展示在子组件最外层的 DOM 标签的属性里面。

小结:props 特性要求父组件传,子组件接,然后可以在子组件里直接用父组件传过来的数据,同时 props 特性他不会把属性显示在你的 DOM 标签中,非 props 特性是父组件传,但是子组件不接,那么在子组件里就没法使用父组件传来的数据,同时非 props 特性对应的属性值,其实会显示在子组件最外层的 DOM 标签的属性里面。当然,实际的生产环境中,非 props 特性使用的场景并不是特别的多,这里简要的做一个了解就可以了。


长得好看的都会关注我的 o(≧v≦)o~~