一、父子间通信
1.父传子:
2.子传父:
3.什么是prop
Prop定义:组件上注册的一些自定义属性
Prop作用:向子组件传递数据
特点:
- 可以传递任意数量的prop
- 可以传递任意类型的prop
3.1 props校验
思考:组件的prop可以乱传么?
不可以
作用:为组件的prop指定验证要求,不符合要求,控制台就会有错误提示——>帮助开发者,快速发现错误
语法:
①类型校验
②非空校验
③默认值
④自定义校验
props:{校验的属性名: 类型 //Number String Boolean Array Object…},
3.2更详细的props校验
props:{校验的属性名:{type:类型, // Number String Boolean …required: true, // 是否必填default: 默认值, // 默认值validator (value){// 自定义校验逻辑return 是否通过校验}}
},
4.prop & data、单向数据流
共同点:都可以给组件提供数据。
区别:
- data的数据是自己的——> 随便改
- prop的数据是外部的 ——> 不能直接改,要遵循 单向数据流
总结:
谁的数据谁负责
1.data:自己的数据,随便改(谁的数据谁负责)
2.prop传过来的数据(外部的数据)不能直接改
3.单向数据流:父组件的prop更新,会单向向下流动,影响到子组件
4.1综合案例
4.2核心代码
App.vue
<template><!-- 主体区域 --><section id="app"><TodoHeader @add="handleAdd"></TodoHeader><TodoMain @del="handleDel" :list="list"></TodoMain><TodoFooter @clear="handleClear" :list="list"></TodoFooter></section><router-view></router-view>
</template>
<script>
import TodoHeader from './components/TodoHeader.vue';
import TodoMain from './components/TodoMain.vue';
import TodoFooter from './components/TodoFooter.vue';
// 渲染功能:
// 1.提供数据 ——> 提供在公共的父组件 App.Vue
// 2.通过父传子,将数据传递给TodoMain
// 3.利用v-for渲染
// 添加功能
// 1.收集表单数据 v-model
// 2.监听事件(回车+点击 都要进行添加)
// 3.子传父,将任务名称传递给父组件App.vue
// 4.进行添加 unshift(自己的数据自己负责)
// 删除功能:
// 1.监听事件(监听删除的点击)携带 id
// 2.子传父,将删除的id传递给父组件App.vue
// 3.进行删除 filter(自己的数据自己负责)
// 底部合计:父传子传list ——> 渲染
// 清空功能:子传父 通知到父组件 ——> 父组件进行清空操作
// 持久化存储:watch深度监视list的变化 ——> 往本地存储 ——> 一进入页面优先读取本地
export default {data () {return {list: JSON.parse(localStorage.getItem('list')) || [{ id: 1, name: '打篮球' },{ id: 2, name: '看电影' },{ id: 3, name: '逛街' },]}},methods: {handleAdd (todoName) {this.list.unshift({id: +new Date(),name: todoName})},handleDel (id) {this.list = this.list.filter(item => item.id !== id)},handleClear () {this.list = []}},watch: {list: {deep: true,handler (newValue) {localStorage.setItem('list', JSON.stringify(newValue))}}},components: {TodoHeader,TodoMain,TodoFooter}
}
</script>
TodoMain.vue
<template><section class="main"><ul class="todo-list"><li v-for="(item, index) in list" class="todo"><div class="view"><span class="index">{{ index + 1 }}</span><label>{{ item.name }}</label><button @click="handleDel(item.id)" class="destroy"></button></div></li></ul></section>
</template>
<script>
export default {props: {list: Array},methods: {handleDel (id) {this.$emit('del', id)}}
}
</script>
Todoheader.vue
<template><header class="header"><h1>小黑笔记本</h1><input @keyup.enter="handleAdd" v-model="todoName" placeholder="请输入任务" class="new-todo" /><button @click="handleAdd" class="add">添加任务</button></header>
</template>
<script>
export default {data () {return {todoName: ''}},methods: {handleAdd () {// console.log(this.todoName)if (this.todoName.trim() === '') {alert('任务名称不能为空')return}this.$emit('add', this.todoName)this.todoName = ''}}
}
</script>
TodoFooter.vue
<template><!-- 统计和清空 --><footer class="footer"><!-- 统计 --><span class="todo-count">合 计:<strong>{{ list.length }}</strong></span><!-- 清空 --><button @click="clear" class="clear-completed">清空任务</button></footer>
</template>
<script>
export default {props: {list: Array},methods: {clear () {this.$emit('clear')}}
}
</script>
4.3总结
核心步骤:
①拆分基础组件
新建组件——> 拆分存放结构 ——> 导入注册使用
②渲染待办事项
提供数据(公共父组件)——> 父传子传递list ——> v-for渲染
③添加任务
收集数据 v-model ——>监听事件 ——> 子传父传递任务 ——> 父组件unshift
④删除任务
监听删除携带id ——> 子传父传递id ——> 父组件filter删除
⑤底部合计 和 清空功能
底部合计:父传子传递list ——> 合计展示
清空功能:监听点击 ——>子传父通知父组件 ——> 父组件清空
⑥持久化存储
watch监视数据变化,持久化到本地
二、非父子通信
1.非父子通信- event bus 事件总线
作用:非组件之间,进行简易消息传递。(复杂场景——>Vuex)
1.创建一个能够访问到的事件总栈(空Vue实例)——>utils/EventBus.js
improt Vue form 'vue'
const Bus = new Vue()
export default Bus
2.A组件(接收方),监听Bus实例的事件
created (){Bus.$on('sendMsg',(msg)=>{this.msg = msg})
}
3.B组件(发送方),触发Bus实例的事件
Bus.$emit('sendMsg','这是一个消息')
代码:
App.vue
<template><div><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>
BaseA.vue
<template><div class="baseA">我是A组件(接收方)<p>{{ msg }}</p></div>
</template>
<script>
import Bus from '../utils/EventBus'
export default {created () {// 2.在A组件(接收方),进行监听(订阅消息)Bus.$on('sendMsg', (msg) => {this.msg = msg})},data () {return {msg: ''}}
}
</script>
<style>
.baseA {width: 200px;height: 150px;padding: 10px;margin-top: 10px;border: 3px solid #000;border-radius: 5px;font-size: 20px;
}
</style>
BaseB.vue
<template><div class="baseB">我是B组件(发布方)<button @click="clickSend">发布通知</button></div>
</template>
<script>
import Bus from '../utils/EventBus'
export default {methods: {clickSend () {// 3.B组件(发件方)触发事件的方式传递参数(发布消息)Bus.$emit('sendMsg', '今日晴天,适合郊游')}}
}
</script>
BaseC.vue
<template><div class="baseC">我是C组件(接收方)<p>{{ msg }}</p></div>
</template>
<script>
import Bus from '../utils/EventBus'
export default {created () {// 2.在C组件(接收方),进行监听(订阅消息)Bus.$on('sendMsg', (msg) => {this.msg = msg})},data () {return {msg: ''}}
}
</script>
<style>
.baseA {width: 200px;height: 150px;padding: 10px;margin-top: 10px;border: 3px solid #000;border-radius: 5px;font-size: 20px;
}
</style>
EventBus.js
// 1.创建一个都能访问到的事件总线(空的Vue实例)
import { createApp } from 'vue';const Bus = createApp({}); // 创建一个空的Vue实例
export default Bus
2.非父子通信(拓展)-provide & inject
provide & inject 作用:跨层级共享数据。
1.父组件provide提供数据
export default {provide (){return {//普通类型【非响应式】color: this.color,// 复杂类型【响应式】userInfo: this.userInfo,}}
}
2.子/孙组件inject取值使用
export default {inject: ['color','userInfo'],created(){console.log(this.color,this.userInfo)}
}
代码:
App.vue
<template><div class="app"><span>我是App组件</span><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 = 'green'this.userInfo.name = 'ls'}},components: {SonA,SonB}
}
</script>
SonA.vue
<template><div class="SonA">我是SonA组件{{ color }} - {{ userInfo.name }} - {{ userInfo.age }}</div><GrandSon></GrandSon>
</template>
<script>
import GrandSon from './GrandSon.vue';
export default {inject: ['color', 'userInfo'],created () {console.log(this.color, this.userInfo)},components: {GrandSon}
}
</script>
SonB.vue
<template><div class="SonB">我是SonB组件{{ color }} - {{ userInfo.name }} - {{ userInfo.age }}</div>
</template>
<script>
export default {inject: ['color', 'userInfo'],created () {console.log(this.color, this.userInfo)}
}
</script>
GrandSon.vue
<template><div class="GrandSon">我是GrandSon组件{{ color }} - {{ userInfo.name }} - {{ userInfo.age }}</div>
</template>
<script>
export default {inject: ['color', 'userInfo'],created () {console.log(this.color, this.userInfo)}
}
</script>
三、v-model原理
1.原理:
v-model本质上是一个语法糖。例如应用在输入框上,就是value属性和input事件的合写。
作用:提供数据的双向绑定
①数据变,视图跟着变 :value
②页面输入改变,数据会自动改变 @input
注意:$event用于模板中,获取时间中的形参
<template><div class="app"><input v-model="msg" type="text">// 等价于<input :value="msg" @input="msg = $event.target.value" type="text"></div>
</template>
2.表单类组件封装&v-model简化代码
2.1表单类组件 封装
①父传子:数据 应该是父组件props传递过来的,v-model拆解 绑定数据
②子传父:监听输入,子传父传值给父组件修改
本质:实现了子组件 和 父组件数据 的双向绑定
2.2 父组件v-model 简化代码,实现 子组件 和父组件数据 双向绑定
①子组件中:props通过value接收,事件触发input
②父组件中:v-model给组件直接绑数据 (:value + @input)
代码:
App.vue
<template><div class="app"><!-- <BaseSelect :cityId="selectId" @changeId="selectId = $event"></BaseSelect> --><BaseSelect v-model="selectId"></BaseSelect></div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {data () {return {selectId: '104'}},components: {BaseSelect}
}
</script>
BaseSelect.vue
<template><div><!-- <select :value="cityId" @change="handleChange"> --><select :value="value" @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: {// cityId: Stringvalue: String},methods: {handleChange (e) {// console.log(e.target.value)// this.$emit('changeId', e.target.value)this.$emit('input', e.target.value)}}
}
</script>
四、sync修饰符
- 作用:可以实现 子组件 与 父组件数据 的双向绑定,简化代码
- 特点:prop属性名,可以自定义,非固定为value
- 场景:封装弹窗类的基础组件,visible属性 true显示 false隐藏
- 本质:就是 :属性名 和 @update:属性名 合写
代码:
App.vue
<template><div class="app"><button class="logout" @click="isShow = true">退出按钮</button><!-- :visible.sync => :visible + @update:visible --><BaseDialog :visible.sync="isShow"></BaseDialog></div>
</template>
<script>
import BaseDialog from './components/BaseDialog.vue';
export default {data () {return {isShow: false}},components: {BaseDialog}
}
</script>
BaseDialog.vue
<template><div v-show="visible"><div><div><h3>温馨提示:</h3><button class="close" @click="close"></button></div><div><p>你确定要退出本系统吗?</p></div><div><button @click="close">确认</button><button @click="close">取消</button></div></div></div>
</template>
<script>
export default {props: {visible: Boolean},methods: {close () {this.$emit('update:visible', false)}}
}
</script>
五、ref和$refs
作用:利用ref和$refs可以用于获取dom(文档对象模型中代表HTML元素的JavaScript对象)元素,或组件实例
特点:查找范围——>当前组件内(更精准稳定)
①获取dom:
1.目标标签-添加ref属性
<div ref="chartRef">我是渲染表的容器</div>
2.恰当时机,通过this.$refs.xxx,获取目标标签
mounted(){
console.log(this.$refs.chartRef)
},
代码:
App.vue
<template><div class="app"><BaseChart></BaseChart></div>
</template>
<script>
import BaseChart from './components/BaseChart.vue'
export default {data () {return {}},components: {BaseChart}
}
</script>
BaseChart.vue
<template><div ref="mychart" id="base-chart-box" style="width: 600px; height: 400px"></div>
</template>
<script>
// 装包:yarn add echarts
import * as echarts from 'echarts';
export default {mounted () {// const myChart = echarts.init(document.querySelector('.base-chart-box'))const myChart = echarts.init(this.$refs.mychart)// 指定图表的配置项和数据const option = {title: { text: 'My Chart' },xAxis: { data: ["衬衫", "羊毛衫", "裤子", "高跟鞋", "袜子"] },yAxis: {},series: [{ type: 'bar', data: [5, 20, 36, 10, 10] }],}// 使用刚指定的配置项和数据显示图表。myChart.setOption(option)}
}
</script>
效果图:
代码:
App.vue
<template><div class="app"><BaseForm ref="baseForm"></BaseForm><button @click="handleGet">获取数据</button><button @click="handleReset">重置数据</button></div>
</template>
<script>
import BaseForm from './components/BaseForm.vue'
export default {data () {return {}},methods: {handleGet () {console.log(this.$refs.baseForm.getValues())},handleReset () {this.$refs.baseForm.resetValues()}},components: {BaseForm}
}
</script>
BaseForm.vue
<template><form action="">账号:<input type="text" v-model="account" />密码:<input type="password" v-model="password" /></form>
</template>
<script>
export default {data () {return {account: '',password: ''}},methods: {// 方法1:收集表单数据,返回对象getValues () {return {account: this.account,password: this.password}},// 方法2:重置表单resetValues () {this.account = ''this.password = ''}}
}
</script>
六、Vue异步更新、$nextTick
需求:编辑标题,编辑框自动聚焦
1.点击编辑,显示编辑框
2.让编辑框立刻获取焦点
this.isShowEdit = true // 显示输入框
this.$refs.inp.focus() // 获取焦点
问题:“显示之后”,立刻获取焦点是不能成功的!
原因:vue是异步更新DOM(提升性能)
$nextTick:等DOM更细后,才会触发执行此方法里的函数体
语法:this.$nextTick(函数体)
this.$nextTick(()=>{
this.$refs.inp.focus()
})
代码:
App.vue
<template><div class="app"><div v-if="isShowEdit"><input ref="inp" v-model="editValue" type="text"><button>确认</button></div><!-- 默认状态 --><div v-else><span>{{ title }}</span><button @click="handleEdit">编辑</button></div></div>
</template>
<script>
export default {data () {return {title: '大标题',editValue: '',isShowEdit: false}},methods: {handleEdit () {// 1.显示输入框 (异步dom更新,获取不到数据)this.isShowEdit = true// 2.让 输入框 获取焦点($nextTick等dom更新完,立刻执行准备的函数体)this.$nextTick(() => {this.$refs.inp.focus()})}}
}
</script>
总结:
1.Vue是异步更新DOM的
2.想要在DOM更新完成之后做某件事,可以使用$nextTick