一、组件使用中的细节
1、使用 is 解决 h5 标签上的一些小 bug
面代码中,我们创建了一个全局组件 row,并设置了模板 template,然后将这个组件添加到 table 的 tbody 中,这是一个正常的使用组件的方法,但是打开页面,可以看到模板正常显示,结构却不对,tr 渲染到了 tbody 之外。
这是因为在 h5 规范中,正确结构应该是 table > tbody > tr > td,但是现在将组件 row 加到了 tbody 中,所以元素的结构发生了错乱。
接下来看一下如何使用 is 来解决 h5 标签上的 bug,在 tbody 中还是用 tr 标签,将组件的名称放到 is 中去,意思是将 tr 解析为 row 组件。同样在使用 ul 或其他这样标签的时候,也建议用这种方式,以免出现兼容性的错误。
2、子组件里定义 data 必须是一个函数
将上面一段代码修改一下,把 td 中的内容放到子组件的 data 中进行存储,看是否能成功渲染出来:
打开页面,发现报错了,是因为组件中的 data 必须是一个 function,不能是一个对象。
再修改一下代码,在 function 中返回 content 的值:
此时再刷新页面,可以看到数据能正常的显示。
总结:在子组件里定义 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 元素中的内容了。
Hello World!
到这里会发现,这些之和 DOM 节点有关系,和组件并没有关系,实际上不是这样的,现在的 DIV 是一个标签,在标签上加一个 ref,通过
this.$refs
获取到这个引用的时候,你获取到的是一个 DOM 元素,那假设,这是一个组件呢?如果一个组件上加了 ref,通过 this.$refs.hello 获取到的是什么呢?其实,你获取到的是这个组件的引用,接下来通过一个计数器的例子再来理解一下 ref。
{{total}}
这个时候,一个 counter 求和的工作就做完了。
总结:当 ref 是写到一个标签上的时候,通过
this.$refs.名字
获取到的内容实际上的标签对应的 dom 元素。当在一个组件上写 ref,通过 “this.$refs.名字”获取到的内容实际上 counter 子组件的一个引用。
二、父子组件的数据传递
上一节学习 ref 的时候,讲解了子组件如何通过事件触发的形式向父组件传递数据,这一节将会系统的借助计数器(counter)这个例子来讲解父子组件之间更多的数据传递方式。
上面代码,我们先创建一个局部组件 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。
接着继续完善功能,接下来看子组件向父组件传递数据,之前已经讲过了,通过 $.emit() 来传递,现在来更深入的讲解一下。
{{total}}
在实例中添加一个放总数的 DOM,在根实例中 data 下可以先将 total 写死,因为两个 count 的初始值分别为 1,2,所以可将 total 先定死为 3,实际上这不是一个正确的写法,目前可以先来做个练习,到后面,可以通过计算属性,避免一些数据的冗余。到页面上看一下,1,2,3 没有问题。
子组件每一次点击的时候,他可以往外携带一些数据,或者说他向父组件传递一下内容,怎么传递呢?子组件向父组件传值,我们通过事件的形式,也就是在子组件被点击的时候,可以通过 this.emit() 中还可以传入第二个参数,或多个参数,“this.emit 中第二个参数也要修改为 2,,它的效果就是,每点击一次,就加2,然后需要告诉父组件,每次改变,都增加2,所以在父组件 methods 中添加一个子组件改变数据的方法 clickChange,给他传递一个参数 step,指每一次增加多少,因为子组件中 $emit 传入的是 2,所以 step 也就是 2。
三、组件参数校验与非 props 特性
1、组件的参数校验
接下来先看一个未经过校验的例子。首先创建一个名字叫做 child 的子组件,在父组件中去调用这个子组件,直接用 child 就可以了,此时去页面上看,child 就可以显示出来了。
1.1、参数的校验
有的时候父组件需要往子组件里传递参数,例如我们一般可以在父组件里写一个 content="Hello World!",通过这种形式,父组件向子组件传递参数,那么组件的校验指的是什么?你父组件向子组件传递了一个内容,那子组件有权对这个内容做一些约束,这些约束我们就可以把它叫做参数的校验。父组件传递 content,子组件势必就要接收 content,我们先把 content 写到 props 里面,写完之后,就可以在模板里通过插值表达式使用这个 content 了。
下一步,就会有这样的需求,调用子组件传递的这个 content,我要做一些约束,例如你传递过来的 content 必须是一个字符串,如果有这样的需求该怎么办呢?如果要实现这样的需求,props 里就不写一个数组了,而是写一个对象,对象的键就是接收的参数的名字,叫做 content,可以写一个 String,这么去写的意思是:子组件接收到 content 这个属性必须是一个 String 字符串的类型,那现在父组件调用子组件,传递 content 的时候,“Hello World!”肯定是一个字符串,所以页面上此时不会有任何问题,我们修改一下父组件,让他传递一个数字,如果直接把 “Hello World!” 修改为“123”,那他依然传递的是一个字符串,回忆一下上一节的内容,如果传递的是数字,就要在属性前加冒号:
这个时候再到页面上看,content 内容虽然正常渲染了,但是却报了一个警告,说类型检测有问题,子组件希望 content 是一个字符串,但是父组件传递过来的却是 Number。
假设我希望传递过来的是一个数字,该怎么写呢?直接把 String 改为 Number 就可以了:
此时页面就不会报错了。有时还会有这样的需求,我希望传过来的数据既可以是字符串,也可以是数组,这个时候,可以借助数组的语法,把 Number 和 String 放到一个数组里面,它的意思就是,子组件接收的 content 属性,要么是属性,要么是字符串,所以这个时候,传数字或字符串都不会报错。
假设传一个对象呢?
就会报错了,说我期待你传的是一个 Number 或 String,但是你传的却是一个 Object,所以就不对了。通过这个例子,就知道什么是组件参数校验了,也就是子组件接收什么参数,是有规则定义的,当然,这些规则还可以变得更复杂。
1.2、参数更复杂的校验
content 后面,不仅仅可以跟 Number,String 一个数组,实际上还可以跟一个对象。
1.2.1、 type 类型的校验
以写一个 type 加上 String,这个组件接收一个名叫 content 的属性,它的类型必须是 String。
打开页面,会告诉我们传的必须是 String,但真正传入的却是一个 Object,所以会报出一个警告。
除了 type 还可以写一个 required。
1.2.2、required 接收的属性是否必传
required 的意思是,我这个子组件,接收 content 这个属性,这个属性是否是必传的,例如 required 如果设置为 true,如果在子组件中不传 content,看一下:
页面报错,告诉你 content 必传,但是现在缺少了这个 content,如果把 required 改为 false,就不会报任何错误了。
除了写 required 之外,还可以写一个 default。
1.2.3、default 默认值的使用
default 可以随便写一些内容,比如“default value”。
可以看到 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个以内的字符。
意思是,我子组件要接受一个属性,属性的名字叫做 content,类型必须是一个字符串,同时我要对你传入的这个 content 通过校验器做一个校验,这个 value 就是指你传入的内容,我要求传入的这个人字符串长度必须大于5,上面代码中只传入了两个字符,validator 返回 false,所以校验不通过,所以页面报错了,如果大于5就不会报错。
以上就是组件校验的几个重点,接下来看一下非 props 特性。
2、非 props 特性
说到非 props 特性他一定和 props 特性相对应,先来看一下什么是 props 特性:
2.1、props 特性
就拿上边的例子来看,props 特性指的是当你的父组件使用子组件的时候,通过属性向子组件传值的时候,恰好子组件里声明了对父组件传递过来的属性的一个接收,也就是说,父组件调用子组件的时候,传递了 content,子组件恰好在 props 里又申明了这个 content,所以父子组件有一个对应关系,如果你这么去写这种形式的属性,我们就把他叫做 props 特性。
props 特性有什么样的特点呢?打开页面,看一下 DOM 结构。
可以看到,有一个 div,里边内容 "he",所以在子组件传递的 content 是不会在 DOM 标签中显示的。
props 还有一个特点,当父组件传递了 content,子组件接收了 content 之后,在子组件里就可以直接通过插值表达式,或者通过 this.content 去取得 content 里边的内容了,所以上边这么写,父组件传递了 content 过来,子组件就能把这个内容显示出来,这就叫做 props 特性。
下来看看什么叫做非 props 特性。
2.2、非 props 特性
非 props 特性指的是,父组件向子组件传递了一个属性,但是子组件并没有 props 这块内容,也就是说子组件并没有声明接收父组件传递过来的内容,如果是这一种情况,我们看一下页面的效果:
首先,页面就会报一个错误,说 content 没有被定义,无法使用,这是因为父组件向子组件传递了 content,但是这个时候子组件并没有去接子组件传过来的 content,你不去接收,子组件里就没法使用这个 content,一旦你用,就报错了,这是非 props 特性的第一个情况,就是如果你定义了一个非 props 特性,这个时候 content="he"就是一个非 props 特性。非 props 特性里子组件是没有办法获取到父组件的内容的,因为你压根就没有申明你要获取的内容,所以就没法用。
非 props 特性还有第二个特点,在 template 中不用插值表达式,可以直接写一个 Hello World!
如果现在使用的是一个非 props 特性,那么这个非 props 特性实际上是会显示在 DOM 的属性之中的,我们打开页面看一下:
可以看到,div 上面有一个 HTML 属性,上面写着 content="He",很明显,当我们申明一个非 props 特性,它的第二个特点是,这个属性会展示在子组件最外层的 DOM 标签的属性里面。
小结:props 特性要求父组件传,子组件接,然后可以在子组件里直接用父组件传过来的数据,同时 props 特性他不会把属性显示在你的 DOM 标签中,非 props 特性是父组件传,但是子组件不接,那么在子组件里就没法使用父组件传来的数据,同时非 props 特性对应的属性值,其实会显示在子组件最外层的 DOM 标签的属性里面。当然,实际的生产环境中,非 props 特性使用的场景并不是特别的多,这里简要的做一个了解就可以了。