慕课网Vue-去哪了4
第四章 深入理解Vue组件
一 组件使用的细节点
1. 使用is标签,解决标签渲染中的小bug
问题:创建一个table
<table>
<tbody>
<tr><td>1</td></tr>
<tr><td>2</td></tr>
<tr><td>3</td></tr>
</tbody>
</table>
审查元素:正常的表格逻辑关系
问题:希望table每一行的数据是一个子组件
<body> <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> </body>
审查元素:数据虽然已经显示出来,但是页面上的元素排列出错了(tr 已经和table并列了)
在html5规范里,table 里面是tbody ,tbody里面是tr ,现在使用了子组件row,所以浏览器解析row就出了问题。
解决:table里面仍然用 tr 标签,用is标签把row组件的内容显示在tr 里面。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>todolidt</title> <script src= './vue.js'></script> </head> <body> <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> </body> </html>
表示:虽然这写的是tr,实际上它是一个row组件。既保证tr 里面显示的是组件,又保证它符合h5编码规范,解决程序bug。
同样:ul li 有序列表 ,ol li无序列表,slect option 选择标签。
2. 在子组件定义data时候,data必须是一个函数而不是一个对象
<body> <div id="app"> <table> <tbody> <tr is="row"></tr> <tr is="row"></tr> <tr is="row"></tr> </tbody> </table> </div> <script> Vue.component('row',{ data(){ //函数、返回值是一个对象 data:function(){} return { content:"this is row" } }, template:"<tr><td>this is row</td></tr>" }) var app = new Vue({ el:"#app", //组件的使用要把实例挂载到页面上 data:{ //对象 } }) </script> </body>
因为:子组件不同于根组件只会被调用一次。每一个子组件都应该有自己对应的数据。
通过一个函数返回一个对象的目的,就是让每一个子组件都拥有一个独立的数据存储,不会出现多个子组件互相影响的情况。
3. Vue中的 ref
vue不建议在代码里面去操作dom,但是在处理一些复杂的动画效果,不操作dom而靠vue数据绑定,
有的时候处理不了这样的情况,就必须去操作dom。通过ref引用的形式进行dom操作。
方法: this.$refs.ref 从整个vue实例里的所有引用,找到一个引用名ref,ref对应的就是div的dom节点。
如果从组件标签里引用ref,ref获取到的是vue组件引用。
<div id="app"> <div ref="hello" @click="handleClick"> hello world</div> </div> <script> var app = new Vue({ el:"#app", methods:{ handleClick(){ alert(this.$refs.hello.innerHTML) } } }) </script>
计数器功能:(计算点击的总次数)
<body> <div id="app"> <counter ref="one" @change="handleChange"></counter> <counter ref="two" @change="handleChange"></counter> <div>{{total}}</div> </div> <script> Vue.component('counter',{ data(){ return { number:0 } }, template:"<div @click='handleClick'>{{number}}</div>", methods:{ handleClick(){ this.number++, this.$emit('change') } } }) var app = new Vue({ el:"#app", //组件的使用要把实例挂载到页面上 data:{ //对象 total:0 }, methods:{ handleChange(){ // this.total++ //不使用ref
// console.log(this.$refs.one) this.total=this.$refs.one.number+this.$refs.two.number } } }) </script> </body>
总结:
使用is标签,解决h5标签上的小bug。子组件定义data,data必须是函数。 ref 引用进行dom操作
二 父子组件的数据传递
1. 父组件向子组件传递数据
父组件通过属性向子组件传递数据
count= " 0 " //传给子组件的是字符串
:count= " 0 " //传给子组件的是数字, 它后面双引号里传递的是js表达式
子组件用props接收数据,才可以使用它
==》在vue中父组件向子组件传值,通过属性形式来传递的。
2. 单向数据流:父组件可以向子组件传递参数,父组件可以任意修改,子组件不可以修改父组件传递的数据
解释:当子组件接收的count并不是一个基础类型的数据,而是一个引用、对象形式数据,子组件改变了数据,
有可能接收的引用型数据还被其他子组件使用。这样的话,当前子组件改变的数据不仅仅影响了自身,还可能对其他子组件造成影响
解决:定义一个data ,初始值为this.count
<body> <div id="app"> <counter :count="0"></counter> <counter :count="1"></counter> </div> <script> var counter = { //局部组件 props:['count'], data(){ return { number: this.count } }, template:"<div @click='handleClick'>{{number}}</div>", methods:{ handleClick(){ this.number ++ } } } var app = new Vue({ el:"#app", components:{ //局部组件要在父组件注册 counter:counter }, data:{ //对象 } }) </script> </body>
3. 子组件向父组件传递数据
子组件向父组件传值通过触发事件$emit ,父组件通过监听执行方法。
同样按照单向流规定。(能执行页面,会报错)
计算上讲两个number数据之和:
<body> <div id="app"> <counter @inc="handleIncrease" :count="3"></counter> <counter @inc="handleIncrease" :count="2"></counter> <div>{{total}}</div> </div> <script> var counter = { //局部组件 props:['count'], data(){ return { number: this.count } }, template:"<div @click='handleClick'>{{number}}</div>", methods:{ handleClick(){ this.number =this.number +2; this.$emit('inc',2) } } } var app = new Vue({ el:"#app", components:{ //局部组件要在父组件注册 counter:counter }, data:{ //对象 total: 5 }, methods:{ handleIncrease(inc){ //alert(inc) this.total+=inc } } }) </script> </body>
三 组件参数校验与非props特性
1. 组件参数校验
父组件向子组件传递参数,子组件有权对该参数做一些约束。就是参数校验。
要求接收父组件的参数是字符串:
props 里面就可以不写数组,取而代之,写一个对象。
详解如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>todolidt</title> <script src= './vue.js'></script> </head> <body> <div id="app"> <child count="hello world"></child> <!-- <child :count="123"></child> 数字 --> <!-- <child :count="'123'"></child>字符串--> <!-- <child count="{a:1}"></child> 对象 --> </div> <script> Vue.component('child',{ // props:['count'], props:{ //count: String //子组件接收的count属性必须是string类型 //count:Number //传字符串,页面可以响应,会报错 //count:[Number,String] //通过数组形式,子组件接收的count要么是数值要么是字符串 count:{ //count后面也可以跟一个对象 type: String, required:false, //是否必传(ture/false),父组件一定要给子组件count值 default:'default value', //当count可传可不传时,如果没有传值,调用default默认值 validator:function(value){//validator配置项,形参value,字符串长度大于五否则报错 return (value.length>5) } } }, template:'<div>{{count}}</div>' }) var app = new Vue({ el:"#app", }) </script> </body> </html>
2. 非props特性
props特性:父组件使用子组件时,通过属性向子组件传值,恰好子组件里面声明了对父组件传递过来的属性的接收。
=》父组件调用子组件时候传递了count,子组件在props里面声明了count。父子组件有一个对应关系。这种形式就是props特性
特点: 1. count= "123" 这个属性的传递是不会在dom标签上显示的
2. 子组件接收了count后,在子组件中就可以通过{{插值表达式}}或者this.count 去取得count内容。
非props特性:父组件向子组件传递了一个属性,但是子组件并没有props声明接收内容
特点: 1. 报错,count没有被定义,无法使用。-- 子组件使用了没有声明的count就会报错
2. 如果使用的是非props特性,且子组件没有使用count, 那么这个属性就会显示在dom上(展示在子组件最外层的dom标签html属性里面)
四 给组件绑定原生事件
<body> <div id="root"> <child @click="handleClick"></child> </div> <script> Vue.component('child',{ template:'<div>Child</div>', }) var app = new Vue({ el:"#root", methods:{ handleClick:function(){ alert('click') } } }) </script> </body>
点击事件是没有执行的。也就是不会弹出click。
因为,当我们给一个组件绑定一个事件时,实际上这个事件绑定的是一个自定义事件。
也就是说我们真正鼠标点击时触发的事件,并不是我们绑定的click事件。
如果想触发这个自定义click事件:
1. 在子组件里,给div元素进行事件的绑定,这是真正的原生事件click,在子组件里methods中,写出方法,alert()。
这只能执行当前的click原生事件,外面的父组件上的click是无法打印的。(在子组件模板里div元素上绑定点击事件)
2. 想要触发自定义事件,只有通过子组件监听this.$emit('click')去触发自定义事件(类似向父组件传值,只是没有传递参数)
3. 为了避免这样麻烦的方法,有时候就想监听child组件的原生事件
=》添加事件修饰符native
五 非父子组件间的传值
1. 1和2之前的传值(父子组件)
2. 1和3之间的传值(1先把数据传给2,2再传给3,反过来同理),十分麻烦
3. 而3和3之间又怎么传值呢?
当我们的项目中,出现非常复杂的数据传递时,光靠Vue框架是解决不了这个问题的,于是我们需要引进一些其他的工具或者设计模式,
来帮我们解决Vue中复杂的组件传值问题。
非父子组件间的传值:两个组件之前进行传值,但是两个组件不具备父子关系(如1和3、3和3)
解决:1. 借助Vue官方提供的数据层框架,vuex (后面结合项目讲解)
2. 发布订阅模式(又称为:总线机制/Bus/观察者模式)
本节主要讲解如何使用总线机制来解决非父子组件传值:
点击child兄弟组件内容跟着改变
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>非父子组件间传值(Bus/发布订阅模式/总线/观察者模式)</title> <script src= './vue.js'></script> </head> <body> <div id="root"> <child content="Dell"></child> <child content="Lee"></child> </div> <script> Vue.prototype.bus= new Vue() //创建一个实例赋值给Vue.prototype.bus //Vue.prototype挂载了一个bus属性,这个属性指向一个vue实例 //只要之后调用new Vue 或者创建组件,每一个组件上都有bus属性 //因为每一个组件,或者一个vue实例,都是通过Vue这个类来创建的 //我们在Vue类的prototype上挂载了一个bus属性 //所以之后每一个通过这个类创建的对象(也就是vue实例上)上,都会有bus这个属性,都指向同一个vue实例 Vue.component('child',{ props:{ content:String }, data(){ return { selfContent:this.content } }, template:'<div @click="handleClick">{{selfContent}}</div>', methods:{ handleClick:function(){ //alert(this.content) this.bus.$emit('change',this.selfContent) //把我的内容传递给 另外一个组件this.bus.$emit() //this.bus -- 这个实例上挂载的bus //这个bus -- 又是一个vue实例,所以它就有$emit这个方法 // 现在就通过这个方法向外触发事件,并携带数据 } }, mounted(){ //实例挂载到页面上后执行的方法 var this_ =this //child组件 this.bus.$on('change',function(msg){//让这个组件监听bus的触发事件 //alert(msg);弹出两次 //在一个child里面触发事件的时候,其实这两个child都进行了同一个事件监听 this_.selfContent=msg //content不可以直接修改,单向流规则 //this 指的是child组件的bus属性 }) } }) var app = new Vue({ el:"#root", }) </script> </body> </html>
通过一个bus可以实现vue中两个非父子组件传值,当前的是兄弟节点,之后非父子非兄弟节点的使用也是一样的
六 在Vue中使用插槽
1. 插槽的使用场景
需求:子组件除了展示p标签之外。还要展示一段内容,这段内容并不是子组件决定的,而是父组件传递过来的。
以前:(通过属性父子组件传值)
<div id="root"> <child content='<p>Dell</p>'></child> </div> <script> Vue.component('child',{ props:['content'], template:`<div> <p>hello</p> <div v-html="this.content"></div> </div>` //ES6语法,允许换行 // p元素转义 v-html == innerHTML }) var app = new Vue({ el:"#root", }) </script>
1. 此时,<p>Dell</p>外层就多出一个div。 此时用template模板占位符 无法渲染。 (必须包裹一个div)
2. 如果用这种形式传递的内容很多时,虽然也能展示出来,但是在编译代码上变得很难阅读
===》当我们子组件的一部分内容,是根据父组件传递过来dom进行显示时,此时可以不用属性传值
===》vue提供了一个新的语法 插槽(slot)
<body> <div id="root"> <child> <p>DELL</p> <!-- 插槽 --> <p slot="header">Header</p> <!-- 具名插槽 --> <div slot="footer">Footer</div><!-- 具名插槽 --> <p>DELL</p> <!-- 插槽 --> </child> </div> <script> Vue.component('child',{ props:['content'], template:`<div> <slot name="header"><h1>default header</h2></slot> <p>hello</p> <slot>默认内容</slot> <p>world</p> <slot name="footer"></slot> </div>` }) /* 当不存在插槽时,显示默认内容 <slot></slot> 用以显示没有名字的所有插槽 <slot name="header">显示该名字的插槽,header可存在多个,也可显示多个
都可以有默认值 */ var app = new Vue({ el:"#root", }) </script> </body>
七 Vue中的作用域插槽
子组件循环遍历显示
<body> <div id="root"> <child></child> </div> <script> Vue.component('child',{ data(){ return { list:[1,2,3,4] } }, props:['content'], template:`<div> <ul> <li v-for="item of list">{{item}}</li> </ul> </div>` }) var app = new Vue({ el:"#root", }) </script> </body>
需求:child组件有可能在很多地方调用,我希望不同地方调用child组件时候。循环列表的样式不是由child决定而是由父组件决定
==》
<body> <div id="root"> <child> <template slot-scope="props"> <li>{{props.item}}</li> </template> <!-- 作用域插槽固定写法 --> </child> </div> <script> Vue.component('child',{ data(){ return { list:[1,2,3,4] } }, template:`<div> <ul> <slot v-for="item of list" :item= item></slot> </ul> </div>` }) var app = new Vue({ el:"#root", }) </script> </body>
子组件slot 做循环(传一个属性) ,外部做样式显示(使用属性)
父组件调用slot ,子组件接收slot 。
逻辑执行:首先,父组件调用子组件时,给子组件传了一个插槽,这个插槽叫做作用域插槽,作用域插槽必须以template开头结尾的内容。
同时,这个插槽要声明,我从子组件接收的数据要放在哪 (slot-scope)。
然后它要告诉子组件一个模板,用于给接收的数据以怎样的展示
个人总结:父组件调用子组件时,传递插槽,子组件接收使用插槽。
父组件会创建一个模板信息,在子组件使用插槽给创建的数据做循环把结果传递给父组件后,父组件会用这个模板显示传递的结果。
使用场景:当子组件做循环或者某一部分dom结构应该由外部传递进来时,用作用域插槽。
使用作用域插槽,子组件可以向父组件的插槽里面传数据,父组件在template里用slot-scope接收数据。
什么是插槽,插槽的使用,什么是作用域插槽:
子组件除了展示自身的内容外。还要展示一些父组件传递的元素内容,这个过程就是插槽。
父组件调用子组件时,会传递一个插槽。内容包含dom元素。子组件就接收使用插槽,显示到页面上。
父组件调用子组件时,仍然会传递一个插槽(作用域)。内部包含dom元素(dom节点和内容),而dom元素内容是由子组件传递过来的。
子组件使用插槽时,会用当前的数据做遍历后传递给父元素,然后显示到页面上。这就是作用域插槽。
八 动态组件与v-once指令
1. 动态组件
需求:点击button,子组件切换显示
通过绑定点击事件
<body> <div id="root"> <child-one v-if="type==='child-one'"> </child-one> <child-two v-if="type==='child-two'"> </child-two> <button @click="handleBtnClick">change</button> </div> <script> Vue.component('child-one',{ template:`<div>child-one</div>` }) Vue.component('child-two',{ template:`<div>child-two</div>` }) var app = new Vue({ el:"#root", data:{ type:'child-one' }, methods:{ handleBtnClick(){ this.type= this.type ==='child-one'? 'child-two':'child-one' } } }) </script> </body>
通过动态组件:
<child-one v-if="type==='child-one'"></child-one>
<child-two v-if="type==='child-two'"></child-two>
=》
<body> <div id="root"> <!-- <child-one v-if="type==='child-one'"> </child-one> <child-two v-if="type==='child-two'"> </child-two> --> <component :is="type"></component> <!-- vue自带的标签component,指动态组件 它有一个is属性,绑定一个数据 --> <button @click="handleBtnClick">change</button> </div> <script> Vue.component('child-one',{ template:`<div>child-one</div>` }) Vue.component('child-two',{ template:`<div>child-two</div>` }) var app = new Vue({ el:"#root", data:{ type:'child-one' }, methods:{ handleBtnClick(){ this.type= this.type ==='child-one'? 'child-two':'child-one' } } }) </script> </body>
动态组件:根据is里面数据的变化,自动加载不同组件
2. v-once指令
template:`<div v-once>child-one</div>`
使用v-if进行每一次切换时,实际上,Vue底层会判断这个组件已经不用了,取而代之用另一个组件,
就会把第一个组件销毁掉,去创建另一个组件(循环点击会频繁创建销毁)。
这种操作是耗费性能的,如果组件的内容每一次都一样,在组件上加 v-once指令。
当child-one第一次被渲染时,因为v-once这个指令,会直接放到内存里。
当点击切换时,并不需要重新创建child-one组件,而是从内存里直接拿出以前的child-one组件。
v-once 可以有效提供一些静态内容的展示效率。
另: vs code 区域块 注释 快捷键 alt + shift + a