组件的三大组成部分
注意点
- 结构:只能有一个根元素
- 样式:
- 全局样式(默认):影响所有组件
- 局部样式:scoped下样式,只作用于当前组件
- 逻辑: el 根实例独有,data是一个函数,其他配置项一致
scoped 样式冲突
默认情况 :写在组件中的样式会全局生效——因此很容易造成多个组件之间的样式冲突问题
-
全局样式 : 默认组件中的样式会作用到全局,任何一个组件中都会受到此样式的影响
-
局部样式 : 可以给组件加上 scoped 属性,可以 让样式只作用于当前组件
scoped 原理:
-
给当前组件模板的所有元素都会添加一个自定义属性 data-v-hash值
-
利用hash值不同可以区分不同组件
- hash值相同:在同一个组件范围之内
- hash值不同:在不同组件范围之内 BaseOne.vue
- css选择器后面,被自动处理添加上了属性选择器
div[data-v-5f6aa9d56]
BaseTwo.vue 代码
<template>
<div class="base-one">
BaseTwo
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
App.vue
<template>
<div id="app">
<BaseOne></BaseOne>
<BaseTwo></BaseTwo>
</div>
</template>
<script>
import BaseOne from './components/BaseOne'
import BaseTwo from './components/BaseTwo'
export default {
name: 'App',
components: {
BaseOne,
BaseTwo
}
}
</script>
data是一个函数
-
一个组件的 data 选项必须是一个函数。
-
目的:为了保证每个组件实例,维护 独立 的一份 数据 对象。
-
每次创建新的组件实例,都会新执行一次data 函数,得到一个新对象。
BaseCount.vue中代码:
<template>
<div class="base-count">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<script>
export default {
data: function () {
return {
count: 100,
}
},
}
</script>
<style>
.base-count {
margin: 20px;
}
</style>
App.vue 中代码:
<template>
<div class="app">
<BaseCount></BaseCount>
</div>
</template>
<script>
import BaseCount from './components/BaseCount'
export default {
components: {
BaseCount,
},
}
</script>
<style>
</style>
组件通信
什么是组件通信?
组件通信就是指组件与组件之间的数据传递
- 组件的数据是独立的,无法直接访问其他组件的数据。
- 想使用其他组件的数据,就需要组件通信
不同的组件关系 和 组件通信方案分类
组件关系分类
- 父子关系(直接包含) props& $emit
- 非父子关系(没有关联)provide & inject 或 eventbus
通信解决方案
父子通信流程图
父——>子
父组件通过 props 将数据传递给子组件
父组件通过 props 将数据传递给子组件
props:就是给组件标签身上新增的自定义属性
- 给组件添加标签,通过添加属性的方式传值
- 通过props进行接收
子——>父
- 子组件利用
$emit通知父组件,进行修改更新 - 父组件添加监听
- 提供处理函数,提供逻辑
Props 详解
Props 定义
Props 定义: 组件上注册的一些自定义属性
Props 作用:向子组件传递数据
特点:
-
可以 传递 任意数量 的prop
-
可以 传递 任意类型 的prop
父组件
App.vue中代码
<template>
<div class="app">
<UserInfo
:username="username"
:age="age"
:isSingle="isSingle"
:car="car"
:hobby="hobby"
></UserInfo>
</div>
</template>
<script>
import UserInfo from './components/UserInfo.vue'
export default {
data() {
return {
username: '小帅',
age: 28,
isSingle: true,
car: {
brand: '宝马',
},
hobby: ['篮球', '足球', '羽毛球'],
}
},
components: {
UserInfo,
},
}
</script>
<style>
</style>
子组件
UserInfo.vue中代码
<template>
<div class="userinfo">
<h3>我是个人信息组件</h3>
<div>姓名:{{username}}</div>
<div>年龄:{{age}}</div>
<div>是否单身:{{isSingle}}'是''否'</div>
<div>座驾:{{car,brand}}</div>
<div>兴趣爱好:{{hobby.join('、')}}</div>
</div>
</template>
<script>
export default {
}
</script>
<style>
.userinfo {
width: 300px;
border: 3px solid #000;
padding: 20px;
}
.userinfo > div {
margin: 20px 10px;
}
</style>
效果如下:
Props 校验
作用:组件的 prop 指定 验证要求,不符合要求,控制台就会有 错误提示 → 帮助开发者,快速发现错误
语法:
- 类型校验(最常用)
- 非空校验
- 默认值
- 自定义校验
1. 基础写法(类型校验)
- 语法:
props:{
校验的属性名:类型//Number String Boolean……
//属性名:键 类型:值
},
示例:
App.vue中代码
<template>
<div class="app">
<BaseProgress :w="width"></BaseProgress>
</div>
</template>
<script>
import BaseProgress from './components/BaseProgress.vue'
export default {
data() {
return {
width: 30,
//通过改变数值来改变进度条宽度
}
},
components: {
BaseProgress,
},
}
</script>
<style>
</style>
BaseProgress.vue中代码
<template>
<div class="base-progress">
<div class="inner" :style="{ width: w + '%' }">
<span>{{ w }}%</span>
</div>
</div>
</template>
<script>
export default {
// props: ['w']
props:{
w:Number
//若此时传入一个string (abc)则会报错
}
}
</script>
<style scoped>
.base-progress {
height: 26px;
width: 400px;
border-radius: 15px;
background-color: #272425;
border: 3px solid #272425;
box-sizing: border-box;
margin-bottom: 30px;
}
.inner {
position: relative;
background: #379bff;
border-radius: 15px;
height: 25px;
box-sizing: border-box;
left: -3px;
top: -2px;
}
.inner span {
position: absolute;
right: 0;
top: 26px;
}
</style>
效果图如下:
App.vue中传了width值,通过父传子的方式在BaseProgress.vue 中通过props接收,作用于div的盒子
数字和字符串可以直接default,但如果是数组和对象必须通过工厂函数返回默认值
2.完整写法(类型非空默认自定义)
props: {
校验的属性名:{
type:类型, //Number String Boolean…
required:true,//是否必填
default:默认值,//默认值
validator(value){
//自定义校验逻辑
return 是否通过校验
}
}
},
接上一个示例(其他部分都相同):
BaseProgress.vue中代码:
<script>
props: {
w:{
type:Number
//required:true
default:0
validator(value){
//cosole.log(value)
if (value >=0 &&value <=100){
return true
}else{
console.error('传入的prop w,必须是0-100的数字')
return false
}
}
},
</script>
注意:
1.default和required一般不同时写(因为当时必填项时,肯定是有值的)
2.default后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值
prop &data 单向数据流
1.共同点
都可以给组件提供数据
2.区别
- data 的数据是 自己 的 → 随便改
- prop 的数据是 外部 的 → 不能直接改,要遵循 单向数据流
- 谁的数据谁负责
3.单向数据流:
父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的
prop是只读的,不允许修改
小黑记事本-组件版
需求说明
- 拆分基础组件
- 渲染待办任务
- 添加任务
- 删除任务
- 底部合计 和 清空功能
- 持久化存储
拆分组件
小黑记事本原有的结构拆成三部分内容:头部(TodoHeader)、列表(TodoMain)、底部(TodoFooter)
渲染功能:
- 提供数据-提供在公共父组件 App.vue
- 通过父传子,将数据传递给TodoMain
- 利用v-for 渲染
添加功能
- 收集表单数据 v-model
- 监听时间 (回车+点击 都要进行添加)
- 子传父,将任务名称传递给父组件App.vue
- 父组件接收到数据后 进行添加 unshift(自己的数据自己负责)
<input
@keyup.enter="handleAdd"
//回车事件
v-model="todoName"placeholder="">
删除功能
- 监听事件(监听删除的点击) 携带id
- 子传父,将删除的id传递给父组件App.vue
- 进行删除 filter(自己的数据自己负责)
底部功能及持久化存储
- 底部合计:父组件传递list到底部组件 —>展示合计
- 清空功能:监听事件 —> 子传父 子组件通知父组件 —>父组件进行清空操作
- 持久化存储:atch深度监视list的变化 -> 往本地存储 ->进入页面优先读取本地数据
TodoHeader
<template>
<!-- 输入框 -->
<header class="header">
<h1>小黑记事本</h1>
<input placeholder="请输入任务" class="new-todo" v-model="todoName" @keyup.enter="handleAdd"/>
<button class="add" @click="handleAdd">添加任务</button>
</header>
</template>
<script>
export default {
data(){
return {
todoName:''
}
},
methods:{
handleAdd(){
// console.log(this.todoName)
this.$emit('add',this.todoName)
this.todoName = ''
}
}
}
</script>
<style>
</style>
TodoMain
<template>
<!-- 列表区域 -->
<section class="main">
<ul class="todo-list">
<li class="todo" v-for="(item, index) in list" :key="item.id">
<div class="view">
<span class="index">{{ index + 1 }}.</span>
<label>{{ item.name }}</label>
<button class="destroy" @click="handleDel(item.id)"></button>
</div>
</li>
</ul>
</section>
</template>
<script>
export default {
props: {
list: {
type: Array,
},
},
methods: {
handleDel(id) {
this.$emit('del', id)
},
},
}
</script>
<style>
</style>
TodoFooter
<template>
<!-- 统计和清空 -->
<footer class="footer">
<!-- 统计 -->
<span class="todo-count"
>合 计:<strong> {{ list.length }} </strong></span
>
<!-- 清空 -->
<button class="clear-completed" @click="clear">清空任务</button>
</footer>
</template>
<script>
export default {
props: {
list: {
type: Array,
},
},
methods:{
clear(){
this.$emit('clear')
}
}
}
</script>
<style>
</style>
App.vue
<template>
<!-- 主体区域 -->
<section id="app">
<TodoHeader @add="handleAdd"></TodoHeader>
<TodoMain :list="list" @del="handelDel"></TodoMain>
<TodoFooter :list="list" @clear="clear"></TodoFooter>
</section>
</template>
<script>
import TodoHeader from './components/TodoHeader.vue'
import TodoMain from './components/TodoMain.vue'
import TodoFooter from './components/TodoFooter.vue'
export default {
data() {
return {
list: JSON.parse(localStorage.getItem('list')) || [
{ id: 1, name: '打篮球' },
{ id: 2, name: '看电影' },
{ id: 3, name: '逛街' },
],
}
},
components: {
TodoHeader,
TodoMain,
TodoFooter,
},
watch: {
list: {
deep: true,
handler(newVal) {
localStorage.setItem('list', JSON.stringify(newVal))
},
},
},
methods: {
handleAdd(todoName) {
// console.log(todoName)
this.list.unshift({
id: +new Date(),
name: todoName,
})
},
handelDel(id) {
// console.log(id);
this.list = this.list.filter((item) => item.id !== id)
},
clear() {
this.list = []
},
},
}
</script>
<style>
</style>
index.css
html,
body {
margin: 0;
padding: 0;
}
body {
background: #fff;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
#app {
background: #fff;
margin: 180px 0 40px 0;
padding: 15px;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
#app .header input {
border: 2px solid rgba(175, 47, 47, 0.8);
border-radius: 10px;
}
#app .add {
position: absolute;
right: 15px;
top: 15px;
height: 68px;
width: 140px;
text-align: center;
background-color: rgba(175, 47, 47, 0.8);
color: #fff;
cursor: pointer;
font-size: 18px;
border-radius: 0 10px 10px 0;
}
#app input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#app input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#app input::input-placeholder {
font-style: italic;
font-weight: 300;
color: gray;
}
#app h1 {
position: absolute;
top: -120px;
width: 100%;
left: 50%;
transform: translateX(-50%);
font-size: 60px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.8);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.main {
position: relative;
z-index: 2;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
overflow: hidden;
}
.todo-list li {
position: relative;
font-size: 24px;
height: 60px;
box-sizing: border-box;
border-bottom: 1px solid #e6e6e6;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list .view .index {
position: absolute;
color: gray;
left: 10px;
top: 20px;
font-size: 22px;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 50px auto 0;
color: #bfbfbf;
font-size: 15px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
main.js
webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import './components/index.css'
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
效果图如下:
非父子通信(拓展)
event bus事件总线
作用:非父子组件之间,进行简易消息传递。(复杂场景→ Vuex)
步骤:
-
创建一个都能访问的事件总线 (空Vue实例)-utils/EventBus.js
import Vue from 'vue' const Bus = new Vue() export default Bus -
A组件(接受方),监听Bus实例的事件
created () { Bus.$on('sendMsg', (msg) => { this.msg = msg }) } -
B组件(发送方),触发Bus的$emit事件
Bus.$emit('sendMsg', '这是一个消息')
BaseA.vue(接受方)
<template>
<div class="base-a">
我是A组件(接受方)
<p>{{msg}}</p>
</div>
</template>
<script>
import Bus from '../utils/EventBus'
export default {
data() {
return {
msg: '',
}
},
created() {
Bus.$on('sendMsg', (msg) => {
// console.log(msg)
this.msg = msg
})
},
}
</script>
<style scoped>
.base-a {
width: 200px;
height: 200px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
</style>
BaseB.vue(发送方)
<template>
<div class="base-b">
<div>我是B组件(发布方)</div>
<button @click="sendMsgFn">发送消息</button>
</div>
</template>
<script>
import Bus from '../utils/EventBus'
export default {
methods: {
sendMsgFn() {
Bus.$emit('sendMsg', '今天天气不错,适合旅游')
},
},
}
</script>
<style scoped>
.base-b {
width: 200px;
height: 200px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
</style>
BaseC.vue中代码
<template>
<div class="base-c">
我是C组件(接受方)
<p>{{msg}}</p>
</div>
</template>
<script>
import Bus from '../utils/EventBus'
export default {
data() {
return {
msg: '',
}
},
created() {
Bus.$on('sendMsg', (msg) => {
// console.log(msg)
this.msg = msg
})
},
}
</script>
<style scoped>
.base-c {
width: 200px;
height: 200px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
</style>
App.vue
<template>
<div class="app">
<BaseA></BaseA>
<BaseB></BaseB>
<BaseC></BaseC>
</div>
</template>
<script>
import BaseA from './components/BaseA.vue'
import BaseB from './components/BaseB.vue'
import BaseC from './components/BaseC.vue'
export default {
components:{
BaseA,
BaseB,
BaseC
}
}
</script>
<style>
</style>
发布通知并不是一对一的关系,任何监听了这个消息的组件都能收到这个消息
provide-inject(拓展)
作用:跨层级共享数据
-
父子间 provide 提供数据
export default {
provide () {
return {
// 普通类型【非响应式】
color: this.color,
// 复杂类型【响应式】
userInfo: this.userInfo,
}
}
}
- 子/孙组件inject 取值使用
export default {
inject: ['color','userInfo'],
created () {
console.log(this.color, this.userInfo)
}
}
示例:
SonA.vue
<template>
<div class="SonA">我是SonA组件
<GrandSon></GrandSon>
</div>
</template>
<script>
import GrandSon from '../components/GrandSon.vue'
export default {
components:{
GrandSon
}
}
</script>
<style>
.SonA {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 200px;
}
</style>
SonB.vue
<template>
<div class="SonB">
我是SonB组件
</div>
</template>
<script>
export default {
}
</script>
<style>
.SonB {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 200px;
}
</style>
GrandSon.vue
<template>
<div class="grandSon">
我是GrandSon
{{ color }} -{{ userInfo.name }} -{{ userInfo.age }}
</div>
</template>
<script>
export default {
inject: ['color', 'userInfo'],
}
</script>
<style>
.grandSon {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 100px;
}
</style>
App.vue
<template>
<div class="app">
我是APP组件
<button @click="change">修改数据</button>
<SonA></SonA>
<SonB></SonB>
</div>
</template>
<script>
import SonA from './components/SonA.vue'
import SonB from './components/SonB.vue'
export default {
provide() {
return {
// 简单类型 是非响应式的
color: this.color,
// 复杂类型 是响应式的
userInfo: this.userInfo,
}
},
data() {
return {
color: 'pink',
userInfo: {
name: 'zs',
age: 18,
},
}
},
methods: {
change() {
this.color = 'red'
this.userInfo.name = 'ls'
},
},
components: {
SonA,
SonB,
},
}
</script>
<style>
.app {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
}
</style>
效果图如下:
v-model详解
v-model 原理
原理:v-model本质上是一个 语法糖 。例如应用在输入框上,就是value属性 和 input事件 的合写。
v-model = :value + input
作用:提供数据的双向绑定
(1)数据变,试图跟着变:value
(2)视图变,数据跟着变 @input
注意:$event 用于在模板中,获取事件的形参
<template>
<div id="app" >
<input v-model="msg" type="text">
<input :value="msg" @input="msg = $event.target.value" type="text">
</div>
</template>
event.target 触发事件源, .value拿到输入框的值,把拿到的值赋值给msg
示例:
App.vue
<template>
<div>
<input v-model="msg1" type="text"><br><br>
//模板中获取事件的形参-$event获取
<input :value="msg2" @input="msg2=$event.target.value" type="text"><br><br>
</div>
</template>
<script>
export default {
data(){
return{
msg1:'',
msg2:''
}
}
}
</script>
<style>
</style>
模板中获取事件的形参-$event获取
效果如图:
表单类组件封装 & v-model简化代码
表单类组件封装
- 父传子:数据应该是父组件 props传递过来的,v-model 拆解 绑定数据
- 子传父:监听输入,子传父传值给父组件修改
不可直接使用 v-model,v-model为双向绑定,会改变绑定的值,所以需要v-model拆解
父组件(使用)
<BaseSelect:cityId="selectId"@事件名="selectId = $event"/>
示例:
BaseSelect.vue
<template>
<div>
<select :value="selectId" @change="selectCity">
<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: {
selectCity(e) {
this.$emit('changeCity', e.target.value)
},
},
}
</script>
<style>
</style>
App.vue
<template>
<div class="app">
<BaseSelect
:selectId="selectId"
@changeCity="selectId = $event"
></BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '102',
}
},
components: {
BaseSelect,
},
}
</script>
<style>
</style>
下拉菜单: @change
效果图如下:
v-model 简化代码
父组件 v-model简化代码,实现 子组件和 父组件的 双向绑定
- 子组件中:props 通过value接收,事件触发通过 input
- 父组件中:v-model 给组件直接绑数据(:value+ @input)
子组件代码基本不变
- 只需将props 改为 value
- 触发事件通过input
BaseSelect.vue
<template>
<div>
<select :value="value" @change="handleeChange">
<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:{
value:String
},
methods:{
handleeChange(e){
//console.log(e.target.value)
this.$emit('input',e.target.value)
}
}
}
</script>
<style>
</style>
App.vue
<template>
<div class="app">
<BaseSelect v-model="selectId">
//更改为v-model
</BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '104'
}
},
components: {
BaseSelect,
},
}
</script>
<style>
</style>
透传Attributes
透传 attribute 指的是传递给一个组件,却没有被该组件声明为props 或emits 的attribute 或者v-on事件监听器。最常见例子 class style 和id
- 必须是唯一根元素
- 禁止继承
inheritAttrs:false
.sync 修饰符
-
作用:可以实现 子组件 与 父组件数据 的 双向绑定,简化代码
-
特点:props属性名可以自定义,非固定为value
简单理解:子组件可以修改父组件传过来的props值
- 场景
封装弹框类的基础组件, visible属性 true显示 false隐藏
- 本质
.sync修饰符 就是 :属性名 和 @update:属性名 合写
5.语法
父组件代码
//.sync写法
<BaseDialog :visible.sync="isShow" />
--------------------------------------
//完整写法
<BaseDialog
:visible="isShow"
@update:visible="isShow = $event"
/>
子组件代码
props: {
visible: Boolean
},
this.$emit('update:visible', false)
示例:
App.vue
<template>
<div class="app">
<button @click="openDialog">退出按钮</button>
<!-- isShow.sync => :isShow="isShow" @update:isShow="isShow=$event" -->
<BaseDialog :isShow.sync="isShow"></BaseDialog>
</div>
</template>
<script>
import BaseDialog from './components/BaseDialog.vue'
export default {
data() {
return {
isShow: false,
}
},
methods: {
openDialog() {
this.isShow = true
// console.log(document.querySelectorAll('.box'));
},
},
components: {
BaseDialog,
},
}
</script>
<style>
</style>
BaseDialog.vue
<template>
<div class="base-dialog-wrap" v-show="isShow">
<div class="base-dialog">
<div class="title">
<h3>温馨提示:</h3>
<button class="close" @click="closeDialog">x</button>
</div>
<div class="content">
<p>你确认要退出本系统么?</p>
</div>
<div class="footer">
<button>确认</button>
<button>取消</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
isShow: Boolean,
},
methods:{
closeDialog(){
this.$emit('update:isShow',false)
}
}
}
</script>
<style scoped>
.base-dialog-wrap {
width: 300px;
height: 200px;
box-shadow: 2px 2px 2px 2px #ccc;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 0 10px;
}
.base-dialog .title {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #000;
}
.base-dialog .content {
margin-top: 38px;
}
.base-dialog .title .close {
width: 20px;
height: 20px;
cursor: pointer;
line-height: 10px;
}
.footer {
display: flex;
justify-content: flex-end;
margin-top: 26px;
}
.footer button {
width: 80px;
height: 40px;
}
.footer button:nth-child(1) {
margin-right: 10px;
cursor: pointer;
}
</style>
:visible.sync => :visible + @update:visible
效果图如下:
ref 和 $ref获取dom和组件
ref 和 $ref
作用:利用ref 和 $ref可以获取dom元素或组件实例
特点:查找范围 —>当前组件内(更精确稳定)
querySelector 查找范围:整个页面
获取dom
- 目标标签-添加ref属性
<div ref="chartRef">我是渲染图表的容器</div>
- 恰当时机,通过this.$refs.xxx,获取目标标签(恰当时机:当前dom需要存在,mounted之后)
mounted(){
console.log(this.$refs.chartRef)
},
示例:
BaseChart.vue
<template>
<div class="base-chart-box" ref="baseChartBox">子组件</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
mounted() {
// 基于准备好的dom,初始化echarts实例
// document.querySelector 会查找项目中所有的元素
// $refs只会在当前组件查找盒子
// var myChart = echarts.init(document.querySelector('.base-chart-box'))
var myChart = echarts.init(this.$refs.baseChartBox)
// 绘制图表
myChart.setOption({
title: {
text: 'ECharts 入门示例',
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20],
},
],
})
},
}
</script>
<style scoped>
.base-chart-box {
width: 400px;
height: 300px;
border: 3px solid #000;
border-radius: 6px;
}
</style>
App.vue
<template>
<div class="app">
<div class="base-chart-box">
这是一个捣乱的盒子
</div>
<BaseChart></BaseChart>
</div>
</template>
<script>
import BaseChart from './components/BaseChart.vue'
export default {
components:{
BaseChart
}
}
</script>
<style>
.base-chart-box {
width: 300px;
height: 200px;
}
</style>
获取组件
- 目标组件-添加ref属性
<BaseForm ref="baseForm"></BaseForm>
- 恰当时机,通过
this.$refs.xxx,获取目标组件,就可以调用组件对象里面的方法this.$refs.baseForm.组件方法()
示例: BaseForm.vue
<template>
<div class="app">
<div>
账号: <input v-model="username" type="text">
</div>
<div>
密码: <input v-model="password" type="text">
</div>
<div>
<button @click="getFormData">获取数据</button>
<button @click="resetFormData">重置数据</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
username: 'admin',
password: '123456',
}
},
methods: {
//方法一 getValues 获取表单数据,返回一个对象
getFormData() {
console.log('获取表单数据', this.username, this.password);
},
//方法二 重置表格
resetFormData() {
this.username = ''
this.password = ''
console.log('重置表单数据成功');
},
}
}
</script>
<style scoped>
.app {
border: 2px solid #ccc;
padding: 10px;
}
.app div{
margin: 10px 0;
}
.app div button{
margin-right: 8px;
}
</style>
App.vue
<template>
<div class="app">
<h4>父组件 -- <button>获取组件实例</button></h4>
<BaseForm></BaseForm>
</div>
</template>
<script>
import BaseForm from './components/BaseForm.vue'
export default {
data(){
return{
}
},
methods: {
handleGet(){
console.log(this.$refs.baseForm.getFormData())
},
handleReset(){
this.$refs.baseForm. resetFormData()
}
},
components:{
BaseForm
}
}
</script>
<style>
</style>
方法一: getValues 获取表单数据,返回一个对象
方法二: resetValues 重置表单
Vue异步更新和$nextTick
异步更新
需求:编辑标题,编辑框自动聚焦
- 点击编辑,显示编辑框
- 让编辑框获取焦点
this.isShowEdit = true
//显示输入框
this.$refs.inp.focus()
//获取焦点
$nextTick
$nextTick:等DOM更新后,才会触发执行此方法里的函数体
语法:this.$nextTick(函数体)
this.$nextTick(()=>{
this.$refs.inp.focus()
})
示例:
App.vue
<template>
<div class="app">
<div v-if="isShowEdit">
<input v-model="editValue" type="text" ref="inp" />
<button>确认</button>
</div>
<div v-else>
<span>{{ title }}</span>
<button @click="handleEdit">编辑</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
title: '大标题',
isShowEdit: false,
editValue: '',
}
},
methods: {
handleEdit(){
//显示输入框(异步dom更新)
this.isShowEdit = true
//让输入框获取焦点
this.$nextTick(()=>{
console.log(this.$refs.inp)
this.$refs.inp.focus()
})
// setTimeout(() => {
// this.$refs.inp.focus()
// },1000)
}
}
}
</script>
<style>
</style>
效果图如下:
点击编辑时:
-
$nextTick等dom更新完,立刻去执行准备的函数体
-
setTimeout存在等待时间