Vue2
v-model原理
v-model本质就是一个语法糖(一种编程语言的语法特性,允许以更简洁、易读的方式表达某些操作)
例如在输入框中,是value属性和input事件的合并
作用:实现数据的双向绑定
-
数据变,视图跟着变
-
视图变,数据跟着变
例:
$event可以获取事件的形参
<template><div id='app'><input v-model='msg' type='text'><!-- 相当于 --><!-- $event可以获取事件的形参,相当于e --><input :value='msg' @input='msg=$event.target.value' type='text'></div>
</template>
-
:value='msg'
表示数据变化视图会更新,但是视图变化数据并不会更新 -
@input="msg2=e.target.value"
将视图中的数据赋值给msg2
v-model = :value='msg'
+ @input="msg2=$event.target.value"
表单类组件封装
-
父传子:数据从父组件prop传递过来,v-model拆解绑定数据
-
子传父:监听输入,子传父传值给父组件修改
例:
子组件:
<template><div class="baseA"><select :value="selectId" @change="handleChange"><option value="101">北京</option><option value="102">上海</option><option value="103">武汉</option><option value="104">深圳</option><option value="105">广州</option></select></div>
</template><script>export default{props:{selectId:String},methods:{handleChange(e){console.log(e.target.value);this.$emit('change',e.target.value)}}}
</script><style scoped>.baseA{width: 200px;height: 200px;border: 1px solid black;margin-bottom: 20px;}
</style>
父组件:
<template><div class="App" id="app"><BaseA :selectId="selectId" @change="selectId=$event"></BaseA></div>
</template><script>
import BaseA from './components/BaseA.vue';export default{components:{BaseA},data(){return{selectId:'103'}}}
</script><style></style>
v-model简化代码
父组件通过v-model简化代码,实现子组件和父组件数据双向绑定
-
子组件:props通过value接收,事件触发input
-
父组件:v-model给组件直接绑数据
.sync修饰符
作用:可以实现子组件与父组件数据的双向绑定,简化代码
特点:prop属性名可以自定义,非固定的value
例:弹框类的基础组件,visible属性显示或隐藏;本质就是:属性名
和@update:属性名
组合
父组件
<template><div class="App" id="app"><button @click="isShow=true">退出</button><!-- :visible.sync = :visible + @update:visible --><BaseA :visible.sync="isShow"></BaseA></div>
</template><script>
import BaseA from './components/BaseA.vue';export default{components:{BaseA},data(){return{isShow:false}}}
</script><style></style>
子组件
<template><div class="baseA" v-show="visible"><div class="title"><span>温馨提示:</span><button @click="close">×</button></div><div class="content"><p>确认要退出本系统吗?</p></div><div class="foot"><button @click="close">确认</button><button @click="close">取消</button></div></div>
</template><script>export default{props:{visible:Boolean},methods:{close(){//由于:visible.sync = :visible + @update:visible 因此必须是update:visiblethis.$emit('update:visible',false)}}}</script><style scoped>.baseA{width: 200px;height: 200px;border: 1px solid black;margin-bottom: 20px;.title{vertical-align: middle;height: 25px;display: flex;span{font-size: 25px;font-weight: 700;}}}
</style>
ref和$refs
作用:利用ref或$refs获取dom元素或组件实例
特点:查找范围在当前组件内(更精确稳定)
document.querySelector('.box')
在整个页面中查找
获取DOM
步骤:
-
给目标标签添加ref属性 ,如
<div ref="chartRef"></div>
-
通过
this.$refs.xxx
获取目标标签mounted(){console.log(this.$refs.chartRef) }
例:使用echarts前需要安装npm install echarts
父组件
<template><div class="App" id="app"><BaseA ></BaseA></div>
</template><script>
import BaseA from './components/BaseA.vue';export default{components:{BaseA},data(){return{}}}
</script><style></style>
子组件
<template><div class="baseA" ref="myChart"></div>
</template><script>import * as echarts from 'echarts'export default{mounted(){const mychart = echarts.init(this.$refs.myChart)const option={xAxis: {type: 'category',data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']},yAxis: {type: 'value'},series: [{data: [120, 200, 150, 80, 70, 110, 130],type: 'bar'}]}mychart.setOption(option)}}</script><style scoped>.baseA{width: 800px;height: 500px;border: 1px solid black;margin-bottom: 20px;}
</style>
获取组件实例
步骤:
-
目标组件添加ref属性
<BaseForm ref="baseForm"></BaseForm>
-
通过
this.$refs.xxx
获取目标组件,就可以调用组件对象里面的方法this.$refs.baseForm.组件方法()
例:父组件调用子组件中封装好的方法
子组件
<template><div class="baseA" ><form action="#">账号:<input type="text" v-model="account"> <br>密码:<input type="text" v-model="password"></form></div>
</template><script>export default{data(){return{account:'',password:''}},methods:{//收集表单数据返回对象getValues(){return{account:this.account,password:this.password}},//重置表单数据resetValues(){this.account=''this.password=''}}}</script><style scoped>.baseA{width: 400px;height: 200px;border: 1px solid black;margin-bottom: 20px;}
</style>
父组件
<template><div class="App" id="app"><!-- 通过ref属性获取表单实例 --><BaseA ref="baseForm"></BaseA><button @click="handleGet">获取数据</button><button @click="handleReset">重置数据</button></div>
</template><script>
import BaseA from './components/BaseA.vue';export default{components:{BaseA},data(){return{}},methods:{handleGet(){console.log(this.$refs.baseForm.getValues());},handleReset(){this.$refs.baseForm.resetValues()}}}
</script><style></style>
Vue异步更新
场景:点击编辑,显示编辑框;让编辑框立刻获取焦点
this.isShowEdit=true //显示输入框
this.$refs.inp.focus() //获取焦点
但是当输入框显示之后立刻获取焦点是不可能的
原因:Vue是异步更新DOM
例:
methods:{handleEdit(){this.isShowEdit = trueconsole.log(this.$refs.inp) //undefined}
}
因此,需要等待DOM更新之后再去执行相关代码
$nextTick
作用:等待DOM更新之后,立刻触发执行此方法中的函数体
例:
this.$nextTick(()=>{this.$refs.inp.focus()
})
也可以通过setTimeout
来实现DOM更新之后执行相关代码,但是等待的时间并不准确
setTimeout(()=>{this.$refs.inp.focus()
},1000)
自定义指令
内置指令:
-
v-html
-
v-model
-
v-for
-
...
自定义指令即自己定义的指令,可以封装一些dom操作,扩展功能
(autofocus在safari浏览器中有兼容性)
语法:
-
全局注册:
Vue.directive('指令名',{"inserted"(el){//可以对el标签扩展额外功能el.focus()} })
-
局部注册:
directives:{"指令名":{inserted(){//可以对el标签扩展额外功能el.focus()}} }
使用:<input v-指令名 type="text">
全局注册指令
例:
main.js
import Vue from 'vue'
import App from './App.vue'Vue.config.productionTip = false//全局注册指令 指令名,指令对象
Vue.directive('focus',{//inserted会在指令所在的元素被插入到页面中触发inserted(el){ //el是指令所绑定的元素console.log(el);el.focus()}
})new Vue({render:(createElement)=>{return createElement(App)
}
}).$mount('#app')
App.vue
<template><div class="App" id="app"><h1>自定义指令</h1><input v-focus ref="inp" type="text"></div>
</template><script>export default{data(){return{}},methods:{},}
</script><style></style>
局部注册指令
仅在当前组件范围内使用
例:
<template><div class="App" id="app"><h1>自定义指令</h1><input v-focus ref="inp" type="text"></div>
</template><script>export default{directives:{focus:{inserted(el){ //el表示指令所绑定的元素el.focus()}}}}
</script><style></style>
自定义指令的值
在绑定指令时,可以通过 等号 的形式为指令绑定具体的参数值
如:<div v-color="color"> 嘀嘀嘀 </div>
通过binding.value可以拿到指令值,指令值修改会触发update函数
-
insert(){}:提供的是元素被添加到页面中的逻辑,无响应式
-
update(){}:可以监听指令值的变化,在指令被修改时触发,提供值变化后dom更新的逻辑;当data中的值被修改后视图也会随之改变
例:
<template><div class="App" id="app"><h1 v-color="color1">指令的值1</h1><h1 v-color="color2">指令的值2</h1></div>
</template><script>export default{data(){return{color1:'red',color2:'green'}},directives:{color:{inserted(el,binding){ //提供的是元素被添加到页面中的逻辑,无响应式console.log(el,binding.value);el.style.color=binding.value},update(el,binding){ //在指令被修改时触发,提供值变化后dom更新的逻辑;当data中的值被修改后视图也会随之改变el.style.color=binding.value}}}}
</script><style></style>
v-loading指令封装
实际开发中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态,导致用户体验不好
可以通过封装一个v-loading指令实现加载中的效果
-
本质loading是一个蒙层,盖在盒子上
-
数据请求中,开启loading状态,添加蒙层
-
数据请求完毕,关闭loading,移除蒙层
思路:
-
准备一个loading类,通过伪元素定位,设置宽高,实现蒙层
.loading:before{content:'';position:absolute;left:0;top:0;width:100%;height:100%;background:#fff url("./loading.gift") no-repeat center; }
-
开启关闭loading状态,本质只需要添加移除类即可
-
结合自定义指令的语法进行封装复用
例:
<template><div class="App" id="app" v-loading="isLoading"><ul><li v-for="item in list" :key="item.id"><div class="left"><div class="title"><div>{{item.title}}</div></div><div><span>{{item.source}}</span><span>{{item.time}}</span></div></div><div class="right"><img :src="item.img" alt=""></div></li></ul></div>
</template><script>
import axios from 'axios'export default{data(){return{list:[],isLoading:true}},async created(){const res = await axios.get('http://hmajax.itheima.net/api/news')setTimeout(()=>{this.list=res.data.datathis.isLoading=false},2000)},directives:{loading:{//设置默认状态inserted(el,binding){binding.value ? el.classList.add('loading') :el.classList.remove('loading') //数据变化视图并不会自动更新,需要使用update来监测数据的变化进而调整视图},//更新状态update(el,binding){binding.value ? el.classList.add('loading') :el.classList.remove('loading')}}}}
</script><style>.App{border: 1px solid gold;width: 850px;height: 1150px;li{list-style: none;display: flex;}.left{width: 500px;height: 230px;}.title{font-size: 25px;font-weight: 700;margin-bottom: 110px;}}.loading ::before{content: '';position: absolute;left: 0;top: 0;width: 100%;height: 100%;background: #fff url(../public/asset/01b49e578f38750000012e7e15a913.gif) no-repeat;z-index: 1000;}
</style>
插槽
插槽可以分为两类:
-
默认插槽:组件内定制一处结构
-
具名插槽:组件内定制多处结构
默认插槽
作用:让组件内部的一些结构支持自定义
组件的内容部分,不希望写死,希望使用的时候自定义,可通过插槽实现
基本语法:
-
组件内需要定制的结构部分改成
<slot></slot>
占位 -
使用组件时,将
<myDialog></Mydialog>
标签内部传入结构替换slot
例:
子组件
<template><div class="MyDialog" ><div class="header"><h3>友情提示</h3><p>×</p></div><div class="content"><!-- 1. 在需要定制的地方使用slot占位 --><slot></slot></div><div class="footer"><button>取消</button><button>确认</button></div></div>
</template><script>export default{}</script><style scoped>.MyDialog{width: 150px;height: 200px;background-color: #fff;.header{display: flex;justify-content: space-between;}.footer{margin-top: 20px;display: flex;justify-content: space-between;}}
</style>
父组件
<template><div class="App" id="app"><!-- 2. 在使用时,在使用的组件标签内部填入内容 --><MyDialog>你确认要删除吗?</MyDialog> <MyDialog>你确认要退出吗?</MyDialog> </div>
</template><script>import MyDialog from './components/MyDialog.vue'export default{components:{MyDialog}}
</script><style></style>
后备内容
通过插槽可以实现内容的定制,但是如果不传内容则是空白,因此需要给插槽显示默认的内容
语法:在<slot>
标签内放置内容,作为默认显示内容;如果外部使用组件时传入了内容则slot整体会被替换掉
例:
<template><div class="MyDialog" ><div class="header"><h3>友情提示</h3><p>×</p></div><div class="content"><!-- 1. 在需要定制的地方使用slot占位 --><slot>我是后备内容</slot></div><div class="footer"><button>取消</button><button>确认</button></div></div>
</template>
具名插槽
默认插槽只能完成一个定制位置
但是一个组件内有多处结构需要外部传入内容进行定制
一旦插槽取了名字就是具名插槽,只支持定向分发
语法:
-
子组件的多个slot标签使用name属性区分名字
<div class="header"><slot name="head"></slot> </div> <div class="content"><slot name="content"></slot> </div> <div class="footer"><slot name="footer"></slot> </div>
-
父组件使用template配合v-slot:名字 来分发对应标签
<MyDialog><template v-slot:head>标题</template><template v-slot:content>内容</template><template v-slot:footer><button>按钮</button></template> </MyDialog>
但是
v-slot:head
可以简写为#head
<MyDialog><template #head>标题</template><template #content>内容</template><template #footer><button>按钮</button></template> </MyDialog>
例:
子组件
<template><div class="MyDialog" ><div class="header"><slot name="header"></slot><p>×</p></div><div class="content"><!-- 1. 在需要定制的地方使用slot占位 --><slot name="content"></slot></div><div class="footer"><slot name="footer"></slot></div></div>
</template><script>export default{}</script><style scoped>.MyDialog{width: 180px;height: 200px;background-color: #fff;.header{display: flex;justify-content: space-between;}.footer{margin-top: 20px;display: flex;justify-content: space-between;}}
</style>
父组件
<template><div class="App" id="app"><!-- 2. 在使用时,在使用的组件标签内部填入内容 --><MyDialog><template #header><h3>友情提示</h3></template><template #content>你确认要退出吗?</template><template #footer><button>确认</button></template></MyDialog> <MyDialog><template #header><h3>警告</h3></template><template #content>你确认要注销该账号吗?</template><template #footer><button>确认</button><button>取消</button></template></MyDialog> </div>
</template><script>import MyDialog from './components/MyDialog.vue'export default{components:{MyDialog}}
</script><style></style>
作用域插槽
是插槽的一个传参语法,可以给插槽绑定数据(本质)
场景:封装表格组件:
-
父传子,动态渲染表格内容
-
利用默认插槽定制操作列
-
删除或查看都需要使用当前项的id,属于子组件内部的数据,可以通过作用域插槽传值绑定
使用步骤:
-
给slot标签以添加属性的方式传值
<slot :id="item.id" msg="测试文本"></slot>
-
所有添加的属性都会被收集到一个对象中
{id:3;msg:'测试文本'}
-
在template中使用
#插槽名="obj"
接收,默认插槽名为default如:
<MyTable :list="list"><template #default="obj"><button @click="del(obj.id)">删除</button></template> <MyTable>
例:
子组件
<template><div class="MyTable" ><thead><tr><th>序号</th><th>姓名</th><th>年纪</th><th>操作</th></tr></thead><tbody><tr v-for="(item,index) in data" :key="index"><td>{{index+1}}</td> <td>{{item.name}}</td><td>{{item.age}}</td><td><slot :row="item" msg="测试文本"></slot><!-- 会将所有的属性都添加到一个对象中 --><!-- {row:{id:2,name:'孙大名',age:19},msg:'测试文本'} --></td></tr></tbody> </div>
</template><script>export default{props:{data:Array}}</script><style scoped>.MyTable{width: 300px;height: 200px;td,th{border: 1px solid gainsboro;}thead{tr{background-color: slateblue;color: aliceblue;}}}
</style>
父组件
<template><div class="App" id="app"><MyTable :data="list"><!-- 由于此刻未取名为默认插槽 --><template #default="obj"><button @click="del(obj.row.id)">删除</button></template> </MyTable> <MyTable :data="list2"><!-- 直接进行解构 --><template #default="{row}"><button @click="show(row)">查看</button></template></MyTable> </div>
</template><script>import MyTable from './components/MyTable.vue'export default{components:{MyTable},data(){return{list:[{id:1,name:'张小花',age:18},{id:2,name:'孙大明',age:19},{id:3,name:'刘德忠',age:33}],list2:[{id:1,name:'赵晓云',age:18},{id:2,name:'刘蓓蓓',age:21},{id:3,name:'江晓琴',age:47}]}},methods:{del(id){this.list=this.list.filter(item=>item.id !== id)},show(row){alert(`姓名:${row.name};年纪:${row.age}`)}}}
</script><style></style>
综合案例-商品列表
封装两个组件:
-
MyTag:
-
双击显示输入框,输入框获取焦点
-
通过在main.js中封装全局指令,在组件元素中使用
v-focus
://将获取焦点封装为全局指令 Vue.directive('focus',{inserted(el){el.focus()} })
-
通过使用
$nextTick
等待dom更新后再获取焦点:<input ref="inp" /> //等待dom更新完毕再获取焦点 this.$nextTick(()=>{this.$refs.inp.focus() })
-
-
失去焦点,隐藏输入框
@blur="isEdit=false"
-
回显标签信息
-
回显的标签信息是由父组件传递过来的
-
通过
v-model
简化代码:是:value
+@input
组合
<MyTag v-model="tempText"></MyTag><input :value="value" @keyup.enter="handleEnter"/><script> export default{props:{value:String},methods:{handleClick(){this.isEdit=true },handleEnter(e){ //e是DOM对象//e.target.value可以实时获取输入框的值this.$emit('input',e.target.value)}} } </script>
-
-
内容修改,回车,修改标签信息
-
-
MyTable:
-
动态传递表格数据渲染
<MyTable :data="goods"></MyTable><tbody><tr v-for="(item,index) in data" :key="index"><td>{{index+1}}</td><td><img :src="item.picture" alt="" /></td><!-- <td><img src="../assets/img/1.jpg" alt="" /></td> --><td>{{item.name}}</td><td>标签组件<MyTag v-model="tempText"></MyTag></td></tr> </tbody><script>import MyTag from './MyTag.vue'export default{components:{MyTag},props:{data:{type:Array,required:true}}} </script>
-
表头支持用户自定义
-
主体支持用户自定义
-
子组件MyTag.vue
<template><div class="MyTag" ><input:value="value"v-focus@keyup.enter="handleEnter"@blur="isEdit=false"v-if="isEdit" type="text" placeholder="请输入标签" class="input"><div class="text" v-else @dblclick="handleClick" >{{value}}</div></div>
</template><script>export default{props:{value:String},data(){return{isEdit:false}},methods:{handleClick(){this.isEdit=true },handleEnter(e){ //e是DOM对象if(e.target.value.trim() === '') alert('标签内容不能为空')this.$emit('input',e.target.value)this.isEdit=false}}}</script><style scoped>.MyTag{cursor: pointer;.input{border: 1px solid #ccc;width: 100px;height: 40px;box-sizing: border-box;padding: 10px;color: #666;&::placeholder{color: #666;}}}
</style>
子组件MyTable.vue
<template><table><thead><tr><slot name="head"></slot></tr></thead><tbody><tr v-for="(item,index) in data" :key="index"><slot name="body" :item="item" :index="index"></slot></tr></tbody></table>
</template><script>export default{props:{data:{type:Array,required:true}}}
</script><style scoped>table{td,th{width: 300px;height: 100px;}thead{th{background-color: skyblue;height: 50px;}}tbody{td{text-align: center;/* background-color: gainsboro; */border-bottom: 1px solid gainsboro;}img{width: 100px;height: 100px;}}}
</style>
父组件App.vue
<template><div class="App" id="app"><MyTable :data="goods"><template #head><th>编号</th><th>图片</th><th>名称</th><th>标签</th></template><template #body="{item,index}"><td>{{index+1}}</td><td><img :src="item.picture" alt="" /></td><!-- <td><img src="../assets/img/1.jpg" alt="" /></td> --><td>{{item.name}}</td><td><!-- 标签组件 --><MyTag v-model="item.tag"></MyTag></td></template></MyTable></div>
</template><script>import MyTable from './components/MyTable.vue';import MyTag from './components/MyTag.vue';export default{components:{MyTable,MyTag},data(){return{tempText:'水杯',goods:[{id:101, //public目录不参与编译可直接访问picture:require('@/assets/img/1.jpg'),name:'梨皮朱泥三绝清代小品壶经典款紫砂壶',tag:'茶具'},{id:102, picture:require('@/assets/img/2.jpg'),name:'全防水HABU旋钮牛皮徒步鞋',tag:'男鞋'},{id:103, picture:require('@/assets/img/3.png'),name:'毛茸茸小熊出没睡衣',tag:'儿童服饰'},{id:104, // picture:'asset/4.jpg',picture:require('@/assets/img/4.jpg'),name:'儿童毛茸茸套头毛衣',tag:'儿童服饰'},]}},methods:{}}
</script><style></style>
main.js
import Vue from 'vue'
import App from './App.vue'Vue.config.productionTip = false//将获取焦点封装为全局指令
Vue.directive('focus',{inserted(el){el.focus()}
})new Vue({render:(createElement)=>{return createElement(App)
}
}).$mount('#app')