当前位置: 首页 > news >正文

Vue组件开发进阶:从通信原理到DOM异步更新实战

一、组件的三大组成部分(结构/样式/逻辑)

1.组件的三大组成部分- 注意点说明

2.组件的样式冲突scoped

/*

  1.style中的样式 默认是作用到全局的

  2.加上scoped可以让样式变成局部样式

  组件都应该有独立的样式,推荐加scoped(原理)

  -----------------------------------------------------

  scoped原理:

  1.给当前组件模板的所有元素,都会添加上一个自定义属性

  data-v-hash值

  data-v-5f6a9d56  用于区分开不通的组件

  2.css选择器后面,被自动处理,添加上了属性选择器

  div[data-v-5f6a9d56]

*/

3.data 是一个函数

在 Vue 中,将 data 定义为 函数 而不是直接返回对象是 Vue 的核心设计原则之一。

BaseCount.vue

<template><div class="base-count"><button @click="count--">-</button><span>{{ count }}</span><button @click="count++">+</button></div>
</template><script>
export default {data(){return {count:999}}
}
</script><style scoped>.base-count{margin:20px;
}
</style>

App.vue

<template><div class="app"><BaseCount></BaseCount><BaseCount></BaseCount><BaseCount></BaseCount></div>
</template><script>
import BaseCount from './components/BaseCount.vue'
export default {components: {BaseCount,},
}
</script><style>
</style>

通过将 data 定义为函数,每个组件实例会通过调用 data() 获得独立的 data 对象

这样,每个组件实例的 count 都是独立的,互不干扰。


如果 data 是一个普通对象(非函数),所有组件实例会共享同一个数据对象。例如:

二、组件通信

1.什么是组件通信

2.不同的组件关系和组件通信方案分类

组件通信解决方案:

父子通信流程图:

Son.vue

<template><div style="border:3px solid #000;margin:10px"><!-- 3.直接使用props的值 -->我是Son组件 {{title}}</div>
</template><script>
export default {name: 'Son-Child',// 2.通过props来接受props:['title']
}
</script><style></style>

App.vue

<template><div class="app" style="border: 3px solid #000; margin: 10px">我是APP组件<!-- 1.给组件标签,添加属性方式 赋值 --><Son :title="myTitle"></Son></div>
</template><script>
import Son from './components/Son.vue'
export default {name: 'App',data() {return {myTitle: '世界,你好',}},components: {Son,},
}
</script><style>
</style>
  • name: 'App' 的作用是:
    1. 调试标识:在 Vue Devtools 中,组件会显示为 App,方便调试。
    2. 递归准备:如果未来需要让 App 组件递归调用自身,name 是必需的。
    3. 代码可读性:明确标识该组件为 App,便于团队协作和后续维护。

 

Son.vue

<template><div class="son" style="border: 3px solid #000; margin: 10px">我是Son组件 {{ title }}<button @click="changeFn">修改title</button></div>
</template><script>
export default {name: 'Son-Child',props: ['title'],methods: {changeFn() {// 通过this.$emit() 向父组件发送通知this.$emit('changTitle','快乐星球')},},
}
</script><style>
</style>

App.vue

<template><div class="app" style="border: 3px solid #000; margin: 10px">我是APP组件<!-- 2.父组件对子组件的消息进行监听 --><Son :title="myTitle" @changTitle="handleChange"></Son></div>
</template><script>
import Son from './components/Son.vue'
export default {name: 'App',data() {return {myTitle: '世界,你好',}},components: {Son,},methods: {// 3.提供处理函数,提供逻辑handleChange(newTitle) {this.myTitle = newTitle},},
}
</script><style>
</style>


3.什么是prop

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 {props:['username','age','isSingle','car','hobby']
}
</script><style>
.userinfo {width: 300px;border: 3px solid #000;padding: 20px;
}
.userinfo > div {margin: 20px 10px;
}
</style>

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>

4.props 校验

BaseProgress.vue

<template><div class="base-progress"><div class="inner" :style="{ width: w + '%' }"><span>{{ w }}%</span></div></div>
</template><script>
export default {// 1.基础写法(类型校验)// props: {//   w: Number,// },// 2.完整写法(类型、默认值、非空、自定义校验)props: {w: {type: Number,required: true,  //必须传个值给我default: 0,     //没传值的默认值validator(val) {// console.log(val)if (val >= 100 || val <= 0) {console.error('传入的范围必须是0-100之间')return false} else {return true   //返回true则通过了校验,反之。}},},},
}
</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

<template><div class="app"><BaseProgress :w="width"></BaseProgress></div>
</template><script>
import BaseProgress from './components/BaseProgress.vue'
export default {data() {return {width: 50,}},components: {BaseProgress,},
}
</script><style>
</style>

5.prop & data、单向数据流

6.非父子通信(拓展) - event bus 事件总线

创建事件总线(utils/EventBus.js

通过创建空 Vue 实例作为事件总线,供各组件访问:

import Vue from 'vue'
const Bus = new Vue() // 创建 Vue 实例作为事件总线
export default Bus    // 导出总线实例

接收方组件(如 A 组件)监听事件

在组件生命周期(如 created)中,通过 $on 监听总线事件:

import Bus from '../utils/EventBus.js' // 引入事件总线export default {created() {Bus.$on('事件名', (参数) => { // 监听事件,参数为发送方传递的数据// 处理接收的数据this.数据 = 参数})}
}

发送方组件(如 B 组件)触发事件

通过 $emit 触发总线事件并传参

import Bus from '../utils/EventBus.js' // 引入事件总线export default {methods: {触发事件() {Bus.$emit('事件名', '传递的数据') // 触发事件并传参}}
}

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>

EventBus.js

import Vue from 'vue'const Bus  =  new Vue()export default Bus

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>

7.非父子通信(拓展) - provide & inject

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>

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>

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>

三、综合案例:小黑记事本(组件版)

代码详见day04

四、进阶语法

1.v-model 原理

App.vue

<template><div class="app"><input type="text" v-model="msg1" /><br /><!-- v-model的底层其实就是:value和 @input的简写 --><input type="text" :value="msg2" @input="msg2 = $event.target.value" /></div>
</template><script>
export default {data() {return {msg1: '',msg2: '',}},
}
</script><style>
</style>

2.表单类组件封装& v-model 简化代码

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>

BaseSelect.vue

<template><div><select :value="value" @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: {value: String,},methods: {selectCity(e) {this.$emit('input', e.target.value)},},
}
</script><style>
</style>

App.vue

<template><div class="app"><BaseSelectv-model="selectId"></BaseSelect></div>
</template><script>
import BaseSelect from './components/BaseSelect.vue'
export default {data() {return {selectId: '102',}},components: {BaseSelect,},
}
</script><style>
</style>

3.        .sync 修饰符

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>

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>

4.ref 和$refs

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>

BaseForm.vue

<template><div><label>账号:<input type="text" v-model="account" /></label><br /><label>密码:<input type="password" v-model="password" /></label><br /><button @click="getFormData">获取数据</button><button @click="resetForm">重置数据</button></div>
</template><script>
export default {data() {return {account: '',password: ''};},methods: {getFormData() {console.log('组件内获取到的账号:', this.account);console.log('组件内获取到的密码:', this.password);},resetForm() {this.account = '';this.password = '';console.log('子组件:表单已重置');}}
};
</script>
  • 模板(<template>
    • 定义了一个包含两个输入框(账号和密码)及两个按钮(“获取数据” 和 “重置数据”)的表单。
    • v-model="account" 和 v-model="password" 实现输入框与组件数据 accountpassword 的双向绑定,输入内容实时同步到对应数据。
    • 按钮通过 @click 绑定方法 getFormData 和 resetForm,点击时触发对应操作。
  • 脚本(<script>
    • data 函数返回组件的响应式数据 account 和 password,用于存储输入框内容。
    • getFormData 方法:打印当前输入的账号和密码,模拟获取表单数据的业务逻辑。
    • resetForm 方法:将 account 和 password 重置为空字符串,清空输入框,并打印提示信息。

App.vue

<template><div><BaseForm ref="baseForm" /><button @click="callChildComponentMethod">父组件调用子组件方法</button></div>
</template><script>
import BaseForm from './components/BaseForm.vue'; // 确保路径正确export default {components: {BaseForm},methods: {callChildComponentMethod() {// 通过 $refs 获取子组件实例const formComponent = this.$refs.baseForm;if (formComponent) {// 调用子组件的方法formComponent.getFormData(); // 输出子组件内获取的数据formComponent.resetForm(); // 调用子组件的重置方法}}}
};
</script>
  • 模板(<template>
    • 使用 <BaseForm ref="baseForm" /> 引入子组件,并通过 ref="baseForm" 为子组件实例命名,便于父组件获取。
    • 定义一个按钮,点击时触发 callChildComponentMethod 方法。
  • 脚本(<script>
    • 先通过 import 引入子组件 BaseForm,再在 components 选项中注册,确保模板中可使用 <BaseForm> 标签。
    • callChildComponentMethod 方法:
      • 通过 this.$refs.baseForm 获取子组件实例(ref 注册的名称需与 $refs 访问的名称一致)。
      • 若实例存在,调用子组件的 getFormData 和 resetForm 方法,实现父组件控制子组件行为(如获取表单数据、重置表单)。

整体功能

  • 子组件 BaseForm.vue 实现了一个简单的表单交互,包含数据输入、获取和重置功能。
  • 父组件通过 ref 和 $refs 获取子组件实例,直接调用子组件方法,实现跨组件的逻辑控制,体现了 Vue 中父组件访问子组件实例及方法的能力。

5.Vue异步更新、$nextTick

需求

编辑标题, 编辑框自动聚焦

  • 点击编辑,显示编辑框

  • 让编辑框,立刻获取焦点

<template><div class="app"><div v-if="isShowEdit"><input type="text" v-model="editValue" ref="inp" /><button>确认</button></div><div v-else><span>{{ title }}</span><button @click="editFn">编辑</button></div></div>
</template><script>
export default {data() {return {title: '大标题',isShowEdit: false,editValue: '',}},methods: {editFn() {// 显示输入框this.isShowEdit = true  // 获取焦点this.$refs.inp.focus() }  },
}
</script> 

问题

"显示之后",立刻获取焦点是不能成功的!

原因:Vue 是异步更新DOM (提升性能)

解决方案

$nextTick:等 DOM更新后,才会触发执行此方法里的函数体

语法: this.$nextTick(函数体)

this.$nextTick(() => {this.$refs.inp.focus()
})

注意:$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向Vue实例

<template><div class="app"><div v-if="isShowEdit"><input type="text" v-model="editValue" ref="inp" /><button>确认</button></div><div v-else><span>{{ title }}</span><button @click="editFn">编辑</button></div></div>
</template><script>
export default {data() {return {title: '大标题',isShowEdit: false,editValue: '',};},methods: {editFn() {// 显示输入框this.isShowEdit = true;// 获取焦点this.$nextTick(() => {this.$refs.inp.focus();});},},
};
</script>    


http://www.mrgr.cn/news/100622.html

相关文章:

  • 【MySQL】Java代码操作MySQL数据库 —— JDBC编程
  • C++ 拷贝构造函数和重载赋值运算符
  • VLM-E2E:通过多模态驾驶员注意融合增强端到端自动驾驶——论文阅读
  • 一键叠图工具
  • ES练习册
  • LangChain入门(二)安装开发环境
  • 【Git】项目多个分支开发、维护与优化处理 ing
  • 3、CMake语法:制作和使用动态库和静态库
  • 细说fork-vfork-pthread_create-clone
  • 【dify+docker安装教程】
  • 八大排序——冒泡排序/归并排序
  • 【云计算】云计算中IaaS、PaaS、SaaS介绍
  • 安卓基础(点击项目)
  • 高能效计算:破解算力增长与能源约束的科技密码
  • 夜莺监控V8(Nightingale)二进制部署教程(保姆级)
  • 微分与积分(前言)
  • WPF之Label控件详解
  • VUE3:封装一个评论回复组件
  • 1.7无穷级数
  • Uniapp(vue):生命周期