一.组件及组件化

1.组件化的作用

由于之前的代码全写在一个App.vue这个文件里面,会到导致一个文件代码过于多而且不易复用,所以有组件化的思想。

2.组件的使用

①创建

创建一个.vue文件,使用setup的组合式api 方式会自动导出.vue文件

②导入

import 组件对象 from '相对路径'

// eg
import MyPanel from './components/MyPanel.vue'

③使用

把组件当做⾃定义标签使⽤(单双标签均可)

<组件名></组件名>
<组件名 />

// eg
<!-- ⼤驼峰标 双标签 -->
<MyPanel></MyPanel>
<!-- ⼤驼峰 ⾃闭合的单标签 -->
<MyPanel />
<!-- 烤串法 双标签 -->
<my-panel></my-panel>
<!-- 烤串法 ⾃闭合的单标签 -->
<my-panel />

3.组件的全局注册-小兔鲜中的图片放大和sku这样封装的

①特点

全局注册的组件,在项⽬的任何组件中都能使⽤

②步骤

1. 创建.vue组件(三个组成部分)

2. main.js 中进⾏全局注册

③使用方式

当成HTML标签直接使⽤:

• 双标签: <组件名></组件名>

• ⾃闭合的单标签: <组件名 />

④在main.js中注册的写法

import MyPanel from './components/MyPanel.vue'
const app = createApp(App)
// 注册全局组件
// app.component('组件名', 组件对象)

eg:
// ⼤驼峰组件名
app.component('MyPanel', MyPanel)
// 烤串法组件名
app.component('my-panel', MyPanel)

二.组件生命周期

1.生命周期的介绍

组件的生命周期的四个阶段:

1. 创建阶段:创建响应式数据

2. 挂载阶段:渲染模板
3. 更新阶段:修改数据,更新视图

4. 卸载阶段:卸载组件

2.组件生命周期的钩子

介绍:

       每个 Vue 组件实例在创建时都需要经历⼀系列的初始化步骤,⽐如设置好数据监听,编译模板,挂载实例到真实 DOM 树上,以及在数据改变时更新 DOM。在此过程中,会 ⾃动运⾏⼀些函数,这些函数被称为【Vue⽣命周期钩⼦】
       钩子的本质就是函数,只不过这些函数比较特殊,无需程序员调用,而是有vue3内部的执行机制自己调用。钩子函数存在的意义就是给了程序员在特定的时机添加自己的代码的机会,比如组件创建完毕了,就想发送ajax请求,那么可以在创建后的钩子函数中编写相关代码;还比如页面渲染完毕后,立刻让输入框自动聚焦,那么可以在挂载后的钩子函数中编写相关代码

生命周期的八个钩子:

分别是:
1. 创建阶段: 1. beforeCreate (): 创建前 2. created (): 创建后
2. 挂载阶段: 1. beforeMount (): 挂载前 2. mounted (): 挂载后
3. 更新阶段: 1. beforeUpdate (): 更新前 2. updated (): 更新后
4. 卸载阶段: 1. beforeUnmount (): 卸载前 2. unmounted (): 卸载后 

vue生命周期与钩子的关系图:

3.选项式API生命周期的钩子写法(vue2的写法)

代码示例:

App.vue 文件代码

<script setup>
import LifeCycle from './components/LifeCycle.vue'
import { ref } from 'vue'
const isLive = ref(true)
</script>
<template>
  <LifeCycle v-if="isLive" />
</template>
<style scoped></style>

LifeCycle.vue 文件代码

<script>
export default {
  // 提供响应式数据
  data() {
    return {
      count: 0,
    }
  },

  // 提供⽅法/函数
  methods: {
    fn() {
      console.log('fn 函数执⾏了')
    },
  },

  setup() {
    console.log('0-setup')
  },

  // 创建阶段(第⼀阶段):Vue组件创建/出⽣阶段:
  // 创建前:此时⽆法访问 data 数据,也⽆法调⽤ methods ⽅法
  beforeCreate() {
    console.log('1-beforeCreate')
    // console.log(this.count) // undefined
    // console.log(this.fn) // undefined
  },

  // 创建后:此时可以访问 data 数据,也可以调⽤ methods ⽅法
  created() {
    console.log('2-created')
    // console.log(this.count) // 0
    // console.log(this.fn)// 访问到函数
    // this.fn()

    // 开启定时器
    // 给当前组件实例新增了⼀个 timerId 属性,保存了当前定时器的 id 值
    // this.timerId = setInterval(() => {
    //     console.log(this.count)
    //     }, 1000)
    // },
  },

  // 挂载阶段(第⼆阶段):模版渲染阶段
  // 挂载前:此时写在 template 下的标签还没有变成真实DOM,故⽽⽆法获取DOM
  beforeMount() {
    console.log('3-beforeMount')
    // console.log(document.querySelector('p')) // null
  },

  // 挂载后:此时写在 template 下的标签已经变成了真实DOM,故⽽可以获取DOM(是最早可以操作DOM的时机)
  mounted() {
    console.log('4-mounted')
    // console.log(document.querySelector('p')) // <p>0</p>
    // document.querySelector('p').style.color = 'red'
  },

  // 更新阶段(第三阶段):数据变了,组件重新渲染的过程
  // 更新前
  beforeUpdate() {
    console.log('5-beforeUpdate')
    // console.log(this.count)
    // console.log(document.querySelector('p').innerText) // 旧内容(以前的内容)
  },

  // 更新后
  updated() {
    console.log('6-updated')
    // console.log(this.count)
    // console.log(document.querySelector('p').innerText) // 新内容
  },

  // 卸载阶段(第四阶段):组件移除阶段
  beforeUnmount() {
    console.log('7-beforeUnmount')
  },

  unmounted() {
    console.log('8-mounted')
    //关闭定时器
    // clearInterval(this.timerId)
  },
}
</script>
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="count++">+1</button>
  </div>
</template>

<style scoped></style>

比较实用的应该就只有:created,mounted,unmounted 这三个钩子。

4.组合式API生命周期钩子

创建阶段 挂载阶段 更新阶段 销毁阶段
Vue2 beforeCreate created beforeMount mounted beforeUpdate updated beforeUnmount unmounted
Vue3 setup (网络请求) onBeforeMount onMounted (操作 DOM) onBeforeUpdate onUpdated onBeforeUnmount onUnmounted (清理工作)

vue3中没有beforeCreate created

代码示例:

App.vue 文件代码

<script setup>
import LifeCycle from './components/LifeCycle.vue'
import { ref } from 'vue'
const isLive = ref(true)
</script>
<template>
  <LifeCycle v-if="isLive" />
</template>
<style scoped></style>

LifeCycle.vue 文件代码

<script setup>
import { onMounted, ref } from 'vue'
onMounted(() => {
  console.log('渲染之后')
  document.querySelector('p').style.color = 'green'
})
let count = ref(0)
</script>

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="count++">+1</button>
  </div>
</template>

<style scoped></style>

三.scoped

1.scoped作用

写在组件中的样式会 全局⽣效, 因此很容易造成多个组件之间的样式冲突问题。

1. 全局样式: 默认组件中的样式会作⽤到全局,任何⼀个组件中都会受到此样式的影响

2. 局部样式: 可以给组件加上scoped 属性,可以让样式只作⽤于当前组件的标签

2.scoped原理

1. 组件内所有标签都被添加data-v-hash值 的⾃定义属性

2. css选择器都被添加 [data-v-hash值] 的属性选择器

最终效果: 必须是当前组件的元素, 才会有这个⾃定义属性, 从⽽保证了样式只能作⽤到当前组件。

四.组件通信

1.介绍

作用:

之前把代码写在⼀起的,数据直接使⽤即可;现在是组件化开发,通过代码拆分和组合的⽅式进⾏开发,这种情况下,还要达到和不拆分之前⼀样的效果,这时组件之间难免需要数据传递,这就需要组件之间进⾏通信。

组件之间的关系:

1、⽗⼦关系:谁被使⽤, 谁就是⼦组件, 当前组件就是⽗组件

2、⾮⽗⼦关系

2.父传子(defineProps)(父给子传输数据)

传数据步骤:

1. ⼦组件通过 defineProps 接收数据(⼦接)

2. ⽗组件通过 ⾃定义属性 传递数据 (⽗传)

流程图:

代码案例:

App.vue 文件代码

<script setup>
import MyGoods from './components/MyGoods.vue'
// 商品列表
const goodsList = [
  {
    id: '4001172',
    name: '称⼼如意⼿摇咖啡磨⾖机咖啡⾖研磨机',
    price: 289,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001594',
    name: '⽇式⿊陶功夫茶组双侧把茶具礼盒装',
    price: 288,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001009',
    name: '⽵制⼲泡茶盘正⽅形沥⽔茶台品茶盘',
    price: 109,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001874',
    name: '古法温酒汝瓷酒具套装⽩酒杯莲花温酒器',
    price: 488,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001649',
    name: '⼤师监制⻰泉⻘瓷茶叶罐',
    price: 139,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '3997185',
    name: '与众不同的⼝感汝瓷⽩酒杯套组1壶4杯',
    price: 108,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '3997403',
    name: '⼿⼯吹制更厚实⽩酒杯壶套装6壶6杯',
    price: 100,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '3998274',
    name: '德国百年⼯艺⾼端⽔晶玻璃红酒杯2⽀装',
    price: 139,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
]
</script>
<template>
  <div class="list">
    <!-- 循环遍历、独⽴传值-->
    <MyGoods
      v-for="item in goodsList"
      :key="item.id"
      :imgUrl="item.picture"
      :title="item.name"
      :price="item.price"
    />
  </div>
</template>
<style lang="scss">
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.list {
  width: 1000px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
}
</style>

MyGoods.vue 文件代码

<script setup>
// 接受数据
defineProps(['imgUrl', 'title', 'price'])
</script>
<template>
  <div class="item">
    <img :src="imgUrl" :alt="title" />
    <p class="name">{{ title }}</p>
    <p class="price">{{ price }}.00</p>
  </div>
</template>
<style lang="scss" scoped>
.item {
  width: 240px;
  margin-left: 10px;
  padding: 20px 30px;
  transition: all 0.5s;
  margin-bottom: 20px;
  .item:nth-child(4n) {
    margin-left: 0;
  }
  &:hover {
    box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
    transform: translate3d(0, -4px, 0);
    cursor: pointer;
  }
  img {
    width: 100%;
  }
  .name {
    font-size: 18px;
    margin-bottom: 10px;
    color: #666;
  }
  .price {
    display: flex;
    align-items: center;
    font-size: 22px;
    color: firebrick;
    button {
      margin-left: 48px;
      font-size: 14px;
      outline: none;
    }
  }
  .price::before {
    content: '¥';
    font-size: 22px;
  }
}
</style>

3.子传父(defineEmits)(子组件给父组件通知事件)

需要的原因

子组件不能修改父组件传输的数据,因为 defineProps 是只读的。所以我们有了解决方法:⼦组件利⽤ emit 通知⽗组件,让父组件进⾏修改更新。

数据传输步骤:

1. ⽗组件内,⼦组件上,绑定⾃定义事件,@⾃定义事件="⽗修改数据的函数" (⽗绑定)

2. ⼦组件恰当时机, 触发⽗组件的⾃定义事件 , emit('⾃定义事件', 携带的参数...), 从⽽导致⽗组件修改函数的时候(⼦触发)
const emit = defineEmits()

代码案例:

App.vue

<script setup>
import { ref } from 'vue'
// 导⼊ MyGoods 商品组件
import MyGoods from './components/MyGoods2.vue'
// 商品列表
const goodsList = ref([
  {
    id: '4001172',
    name: '称⼼如意⼿摇咖啡磨⾖机咖啡⾖研磨机',
    price: 289,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001594',
    name: '⽇式⿊陶功夫茶组双侧把茶具礼盒装',
    price: 288,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001009',
    name: '⽵制⼲泡茶盘正⽅形沥⽔茶台品茶盘',
    price: 109,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001874',
    name: '古法温酒汝瓷酒具套装⽩酒杯莲花温酒器',
    price: 488,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001649',
    name: '⼤师监制⻰泉⻘瓷茶叶罐',
    price: 139,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '3997185',
    name: '与众不同的⼝感汝瓷⽩酒杯套组1壶4杯',
    price: 108,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '3997403',
    name: '⼿⼯吹制更厚实⽩酒杯壶套装6壶6杯',
    price: 100,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '3998274',
    name: '德国百年⼯艺⾼端⽔晶玻璃红酒杯2⽀装',
    price: 139,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
])
// i: 下标,让哪个对象的 price 减少
// price: 每次减少的价格
//提供修改的函数
const subPrice = (i, price) => {
  goodsList.value[i].price -= price
}
</script>
<template>
  <div class="list">
    <MyGoods
      v-for="(item, index) in goodsList"
      :key="item.id"
      :imgUrl="item.picture"
      :title="item.name"
      :price="item.price"
      :idx="index"
      @ccc="subPrice"
    />
  </div>
</template>
<style lang="scss">
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.list {
  width: 1000px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
}
</style>

MyGoods.vue

<script setup>
import { ref } from 'vue'
// 导⼊ MyGoods 商品组件
import MyGoods from './components/MyGoods2.vue'
// 商品列表
const goodsList = ref([
  {
    id: '4001172',
    name: '称⼼如意⼿摇咖啡磨⾖机咖啡⾖研磨机',
    price: 289,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001594',
    name: '⽇式⿊陶功夫茶组双侧把茶具礼盒装',
    price: 288,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001009',
    name: '⽵制⼲泡茶盘正⽅形沥⽔茶台品茶盘',
    price: 109,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001874',
    name: '古法温酒汝瓷酒具套装⽩酒杯莲花温酒器',
    price: 488,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '4001649',
    name: '⼤师监制⻰泉⻘瓷茶叶罐',
    price: 139,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '3997185',
    name: '与众不同的⼝感汝瓷⽩酒杯套组1壶4杯',
    price: 108,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '3997403',
    name: '⼿⼯吹制更厚实⽩酒杯壶套装6壶6杯',
    price: 100,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
  {
    id: '3998274',
    name: '德国百年⼯艺⾼端⽔晶玻璃红酒杯2⽀装',
    price: 139,
    picture: 'https://cxk-1305128831.cos.ap-beijing.myqcloud.com/11-00.gif',
  },
])
// i: 下标,让哪个对象的 price 减少
// price: 每次减少的价格
//提供修改的函数
const subPrice = (i, price) => {
  goodsList.value[i].price -= price
}
</script>
<template>
  <div class="list">
    <MyGoods
      v-for="(item, index) in goodsList"
      :key="item.id"
      :imgUrl="item.picture"
      :title="item.name"
      :price="item.price"
      :idx="index"
      @ccc="subPrice"
    />
  </div>
</template>
<style lang="scss">
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.list {
  width: 1000px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
}
</style>

4.祖先传后代

5.任意两个组件通信

五.props校验

作用:

我们前面使用 props 传输数据的时候,参数的格式传输错误,在控制台不会显示,我们添加了props校验之后,就会出现报错了,帮助开发者,快速发现错误。

完整语法格式:

defineProps({
  属性名: {
    type: 类型, // Number String Boolean ...
    required: true, // 是否必填
    default: 默认值, // 默认值
    // value: ⽗传⼦的值
    validator (value) {
      // ⾃定义校验逻辑
      return 布尔值
    }
  }
})

代码示例:里面有一种简单写法一种完整写法

App.vue 文件代码:

<script setup>
import { ref } from 'vue'
import MyProgress from './components/MyProgress.vue'
const width = ref(20)
</script>
<template>
  <div class="app">
    <MyProgress :width="width" />
    <MyProgress :width="80" />
  </div>
</template>
<style></style>

MyProgress.vue 文件代码:

<script setup>
// 不验证的写法
// defineProps(['width'])

// 验证的简易写法
// 属性:类型
// defineProps({
//   width: Number,
// })

// 验证的完整写法
defineProps({
  width: {
    type: Number,
    // required: true  // 写了这个default就不会生效了
    default: 50,
    validator: (value) => {
      if (value >= 0 && value <= 100) {
        return true
      } else {
        console.error('The width prop must be between 0 and 100')
        return false
      }
    },
  },
})
</script>
<template>
  <div class="my-progress">
    <div class="inner" :style="{ width: width + '%' }">
      <span>{{ width }}%</span>
    </div>
  </div>
</template>
<style scoped>
.my-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: -30px;
  top: 26px;
}
</style>

六.v-model进阶

1.原理

v-model本质上是⼀个语法糖。

例如应⽤在输⼊框上,就是 value 属性 和 input 事件的合写

<script setup>
    import { ref } from 'vue'
    const msg = ref('')
</script>
<template>
    <p>{{ msg }}</p>
    <input type="text" v-model="msg">
    <input type="text" :value="msg" @input="msg = $event.target.value" >
</template>

 $event.target.value 意思是获取当前输入框的内容。

2.v-model作用在组件上

作用:

实现父组件和子组件数据的双向绑定

代码实现:

App.vue 的代码实现

<script setup>
import { ref } from 'vue'
import BitSelect from './components/BitSelect.vue'
const activeId = ref('222')
</script>
<template>
  <!-- 组件标签上的 $event 指的是 emit 传递来的参数 -->
  <BitSelect :modelValue="activeId" @update:modelValue="activeId = $event" />
  <!--       自定义属性              自定义事件 -->

  <!-- 简化写法 -->
  <!-- <BitSelect v-model="activeId" /> -->
</template>

 BitSelect.vue

<script setup>
// 接收 modelValue 是父传子这里不能直接改,所以下面不能直接用 v-model
const props = defineProps({
  modelValue: {
    type: String,
    default: '',
  },
})
// 获取 emit
const emit = defineEmits()
</script>
<template>
  <!-- 原生标签上的 $event 就是事件对象 -->
  <!-- $event.target: select 原生DOM -->
  <!-- $event.target.value: 选中的 option 的 value -->
  <select :value="props.modelValue" @change="emit('update:modelValue', $event.target.value)">
    <option value="111">北京</option>
    <option value="222">上海</option>
    <option value="333">深圳</option>
    <option value="444">杭州</option>
    <option value="555">苏州</option>
  </select>
</template>

流程图

简化:

App.vue 文件的代码

<script setup>
import { ref } from 'vue'
import BitSelect from './components/BitSelect.vue'
const activeId = ref('222')
</script>
<template>
  <BitSelect v-model="activeId" />
</template>

  BitSelect.vue

<script setup>
// 子组件直接通过 defineModel() 来接收父组件传递的v-model值
// defineModel() 的返回值是一个 ref 数据,并且可以在子组件中直接操作这个 ref 数据,
// 并且可以在子组件中直接操作这个 ref 数据,会引起父组件的数据的同步更新
const model = defineModel()
</script>
<template>
  <select v-model="model">
    <option value="111">北京</option>
    <option value="222">上海</option>
    <option value="333">深圳</option>
    <option value="444">杭州</option>
    <option value="555">苏州</option>
  </select>
</template>

这里的defineModel是个语法糖。 

七.ref属性-模版引用

1.ref是什么

是作用在组件或者标签上的属性,是 vue 新增的,原生不具备这个属性的。

2.作用:

用来获取原生 DOM 或 组件实例(进而调用组件提供的方法)。

3.获取原生DOM

<!-- @format -->
<script setup>
import { onMounted, ref } from 'vue'

// 准备 ref 响应式数据
const divRef = ref(null)

// 组件挂载后
onMounted(() => {
  // 获取原生 div
  // console.log(divRef.value)/// <div></div>

  // 以设置 div 字体颜色为例(DOM操作的代码):
  divRef.value.style.color = 'red'
})
</script>

<template>
  <!-- 给目标元素添加ref属性并绑定数据 -->
  <div ref="divRef">Some Text...</div>
</template>

<style scoped></style>

这个获取是直接从自己这个组件上获取,比原本从整棵DOM树上获取会快一点 

4.使用组件实例调用组件暴露的方法和属性(defineExpose)(父组件调用子组件的属性和方法)

默认情况下在 <script setup> 语法糖下组件内部的属性和方法是不开放给父组件访问的,可以通过 defineExpose 编译宏指定哪些属性和方法允许访问

App.vue 文件代码

<script setup>
import { ref } from 'vue'
import MyForm from './components/MyForm.vue'
const formRef = ref(null)
// 登录
const onLogin = () => {
  console.log(formRef.value.ccc)
  console.log(formRef.value.validate())
  // 进⾏校验
  // if (formRef.value.validate()) {
  //   console.log('ok')
  // } else {
  //   console.log('error')
  // }
}
</script>
<template>
  <MyForm ref="formRef" />
  <button @click="onLogin">登 录</button>
</template>
<style>
#app {
  width: 300px;
  margin: 100px auto;
}
</style>

MyForm.vue 文件代码

<script setup>
// 表单校验
const validate = () => {
  return Math.random() > 0.5 ? true : false
}
// 暴漏给组件, ⽬的是⽗组件可以通过 ref 可以拿到⼦组件的⽅法
defineExpose({
  ccc: 100,
  validate,
})
</script>
<template>
  <div class="login-box">
    账⼾:<input type="text" /><br /><br />
    密码:<input type="password" /><br /><br />
  </div>
</template>

八.nextTick

1.作用

当数据变了,想获取更新后的DOM,需要把代码写在这个方法的回调中。直接获取是获取不到的,因为DOM操作是异步的,获取DOM会在更新DOM之前执行。

2.代码案例

<script setup>
import { nextTick, ref } from 'vue'

// 控制是否显示输入框
const isShowEdit = ref(false)

// 点击编辑按钮
const onEdit = () => {
  isShowEdit.value = true
  // 问题:当数据变了,发现获取DOM拿不到的
  // 原因:在 vue3 中,当数据变了,DOM的更新是异步的;
  // 也就是说,数据变了,想立即获取最新的DOM是拿不到的,此时DOM并没有更新
  // console.log(inputRef.value) // null

  // 解决:利用 nextTick() 这个方法,因为在这个方法的回调中,DOM更新完毕了
  nextTick(() => {
    inputRef.value.focus()
  })
}

const inputRef = ref(null)
</script>

<template>
  <div class="box">
    <h3>大标题</h3>
    <button @click="onEdit">编辑</button>
  </div>

  <div v-if="isShowEdit">
    <input type="text" ref="inputRef" />
    <button>确认</button>
  </div>
</template>
<style scoped>
.box {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 200px;
  height: 40px;
}
</style>

九.自定义指令-实践-小兔鲜的懒加载指令

1.作用

封装⼀段 公共的DOM操作 代码,便于复⽤。

2.基本使用 

①语法

注册

// main.js 中
app.directive('指令名', {
    // 元素挂载后(成为真实DOM) ⾃动触发⼀次⾃动执⾏
    mounted(el) {
        // el: 指令所在的DOM元素
    }
}

使用

<p v-指令名></p>
②代码示例

main.js 文件代码

// 按需导入 createApp 函数
import { createApp } from 'vue'
// 导入 App.vue
import App from './App.vue'
// 创建应用
const app = createApp(App)

// 定义指令
app.directive('jujiao', {
	// 指令所在的标签插入到真实DOM中自动执行一次
	mounted(el) {
		// el就是指令所在的DOM元素, 拿到 el 了,
		// 就可以对 el 做任何原生DOM操作
		// console.log('mounted', el)
		el.focus()
	}
})
// 指定渲染的位置
app.mount('#app')

App.vue 文件代码

<template>
	<input type="text" v-jujiao />
</template>

<script setup></script>

<style scoped></style>

3.绑定数据

①语法

1.在绑定指令时,可以通过“等号”的形式为指令 绑定 具体的参数值

<div v-color="colorStr">Some Text</div>

 2.通过 binding.value 可以拿到指令值,指令值修改会 触发 updated 钩⼦

app.directive('指令名', {
    // 挂载后⾃动触发⼀次
    mounted(el, binding) { },
    // 数据更新, 每次都会执⾏
    updated(el, binding) { }
})
②代码示例

main.js 文件代码

// 按需导入 createApp 函数
import { createApp } from 'vue'
// 导入 App.vue
import App from './App.vue'
// 创建应用
const app = createApp(App)

app.directive('color', {
	// 指令所在的DOM元素变成真实DOM了,会自动执行一次;
	// 换句话说,当指令所在表达式的值变了,该函数不会再次执行了
	mounted(el, binding) {
		// el: 指令所在元素
		// binding: 指令绑定的信息对象,binding.value 获取指令表达式的结果
		el.style.color = binding.value

		console.log('1-mounted')
	},
	// 每次指令绑定表达式的值变了,都会执行一次
	updated(el, binding) {
		el.style.color = binding.value
		console.log('2-updated')
	}
})

// 指定渲染的位置
app.mount('#app')

App.vue 文件代码

<script setup>
	import { ref } from 'vue'

	// 提供的颜色响应式数据
	const colorStr = ref('red')
</script>

<template>
	<!-- 使用自定义指令,并绑定表达式 -->
	<div v-color="colorStr">Some Text</div>
</template>

<style scoped></style>

main.js 的简化写法 

对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为。这种情况我们可以直接用一个函数的定义指令,如下所示:

app.directive('color', (el, binding) => {
    // 这会在 `mounted` 和 `updated` 时都调⽤
    el.style.color = binding.value
}

十.插槽

1.作用

让组件内部的⼀些 结构 ⽀持 ⾃定义

2.默认插槽

①基本语法

1. 组件内需要定制的结构部分,改⽤ <slot></slot> 占位

2. 使⽤组件时, <MyDialog></MyDialog>写成双标签 , 包裹结构, 传⼊替换slot

②代码示例

MyDialog.vue 文件代码

<script setup></script>
<template>
  <div class="dialog">
    <div class="dialog-header">
      <h3>友情提⽰</h3>
      <span class="close">✖</span>
    </div>
    <div class="dialog-content">
      <!-- 1. 在组件内标签不确定位置用 slot 组件占位 -->
      <slot> </slot>
    </div>
    <div class="dialog-footer">
      <button>取消</button>
      <button>确认</button>
    </div>
  </div>
</template>
<style scoped>
* {
  margin: 0;
  padding: 0;
}
.dialog {
  width: 470px;
  height: 230px;
  padding: 0 25px;
  background-color: #ffffff;
  margin: 40px auto;
  border-radius: 5px;
}
.dialog-header {
  height: 70px;
  line-height: 70px;
  font-size: 20px;
  border-bottom: 1px solid #ccc;
  position: relative;
}
.dialog-header .close {
  position: absolute;
  right: 0px;
  top: 0px;
  cursor: pointer;
}
.dialog-content {
  height: 80px;
  font-size: 18px;
  padding: 15px 0;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
.dialog-footer button {
  width: 65px;
  height: 35px;
  background-color: #ffffff;
  border: 1px solid #e1e3e9;
  cursor: pointer;
  outline: none;
  margin-left: 10px;
  border-radius: 3px;
}
.dialog-footer button:last-child {
  background-color: #007acc;
  color: #fff;
}
</style>

App.vue 文件代码

<script setup>
import MyDialog from './components/MyDialog.vue'
</script>
<template>
  <!-- 2. 在使用组件的时候,给组件传入想展示的结构,从而替换掉占位的 slot 组件 -->
  <MyDialog> 你取认要删除吗 </MyDialog>
  <MyDialog>
    <span>我已经删除了</span>
  </MyDialog>
</template>
<style scoped>
body {
  background-color: #b3b3b3;
}
</style>

3.插槽默认值

通过插槽完成了内容的定制,传什么显⽰什么, 但是如果不传,则是空⽩,如果我们需要有默认值在, <slot></slot> 标签里面写默认值就可以了,比较简单这里就不代码演示了。

4.具名插槽-这个自己写代码的时候错过几次了

①作用

一个组件有多个位置需要使用插槽是使用具名插槽。

②语法

 • 多个slot使⽤name属性区分名字

• template配合v-slot:名字 来分发对应标签

v-slot写起来太⻓,vue给我们提供⼀个简单写法 v-slot: 直接简写为 #

③代码示例 

MyDialog.vue 文件代码

<script setup></script>
<template>
  <div class="dialog">
    <div class="dialog-header">
      <slot name="header">
        <h3>温馨提⽰</h3>
      </slot>
      <span class="close">✖</span>
    </div>
    <div class="dialog-content">
      <slot>我是主体内容</slot>
    </div>
    <div class="dialog-footer">
      <slot name="footer">
        <button>取消</button>
        <button>确认</button>
      </slot>
    </div>
  </div>
</template>
<style>
* {
  margin: 0;
  padding: 0;
}
.dialog {
  width: 470px;
  height: 230px;
  padding: 0 25px;
  background-color: #ffffff;
  margin: 40px auto;
  border-radius: 5px;
}
.dialog-header {
  height: 70px;
  line-height: 70px;
  font-size: 20px;
  border-bottom: 1px solid #ccc;
  position: relative;
}
.dialog-header .close {
  position: absolute;
  right: 0px;
  top: 0px;
  cursor: pointer;
}
.dialog-content {
  height: 80px;
  font-size: 18px;
  padding: 15px 0;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
.dialog-footer button {
  width: 65px;
  height: 35px;
  background-color: #ffffff;
  border: 1px solid #e1e3e9;
  cursor: pointer;
  outline: none;
  margin-left: 10px;
  border-radius: 3px;
}
.dialog-footer button:last-child {
  background-color: #007acc;
  color: #fff;
}
</style>

App.vue 文件代码

<script setup>
import MyDialog from './components/MyDialog.vue'
</script>
<template>
  <!-- 1. 不传、都显⽰插槽默认内容 -->
  <MyDialog />
  <!-- 2. 传了 -->
  <MyDialog>
    <template v-slot:header>
      <h3>友情提⽰</h3>
    </template>
    <template v-slot:default> 请输⼊正确⼿机号 </template>
    <template v-slot:footer>
      <button>关闭</button>
    </template>
  </MyDialog>
  <!-- 3. 传了 -->
  <MyDialog>
    <template v-slot:header>
      <h3>警告</h3>
    </template>
    <template v-slot:default> 你确认要退出么? </template>
    <template v-slot:footer>
      <button>确认</button>
      <button>取消</button>
    </template>
  </MyDialog>
</template>
<style>
body {
  background-color: #b3b3b3;
}
</style>

5.作用域插槽

①作用

带数据的插槽, 可以让组件功能更强⼤、更灵活、复⽤性更⾼; ⽤ slot 占位的同时, 还可以给 slot 绑定数据, 将来使⽤组件时, 不仅可以传内容, 还能使⽤ slot 带来的数据。(父组件可以得到子组件的数据,供给父组件使用)

②代码示例

MyDialog.vue 文件代码

<script setup></script>

<template>
  <div class="dialog">
    <div class="dialog-header">
      <slot name="header"></slot>
      <span class="close">×</span>
    </div>
    <div class="dialog-content">
      <slot name="body" account="xiaobit" password="123456"></slot>
    </div>
    <div class="dialog-footer">
      <button>取消</button>
      <button>确认</button>
    </div>
  </div>
</template>

<style>
* {
  margin: 0;
  padding: 0;
}
.dialog {
  width: 470px;
  height: 230px;
  padding: 0 25px;
  background-color: #ffffff;
  margin: 40px auto;
  border-radius: 5px;
}
.dialog-header {
  height: 70px;
  line-height: 70px;
  font-size: 20px;
  border-bottom: 1px solid #ccc;
  position: relative;
}
.dialog-header .close {
  position: absolute;
  right: 0px;
  top: 0px;
  cursor: pointer;
}
.dialog-content {
  height: 80px;
  font-size: 18px;
  padding: 15px 0;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
.dialog-footer button {
  width: 65px;
  height: 35px;
  background-color: #ffffff;
  border: 1px solid #e1e3e9;
  cursor: pointer;
  outline: none;
  margin-left: 10px;
  border-radius: 3px;
}
.dialog-footer button:last-child {
  background-color: #007acc;
  color: #fff;
}
</style>

App.vue 文件代码

<script setup>
import MyDialog from './components/MyDialog.vue'
</script>

<template>
  <MyDialog>
    <template #header="obj">
      <h4>登录</h4>
    </template>
    <template #body="obj">
      <!-- 默认接收的数据 obj 是个空对象 -->
      <p>{{ obj }}</p>
      账号:
      <input type="text" v-model="obj.account" /><br /><br />
      密码:
      <input type="password" v-model="obj.password" /><br /><br />
    </template>
  </MyDialog>
</template>

<style scoped></style>
③代码工作流程图

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐