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>



v-model原理详解


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">
    <BaseSelect
      v-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>    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值