前端笔记-Vue3(下)

学习参考视频:尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程_哔哩哔哩_bilibili

vue3学习目标:

VUE 31、Vue3架构与设计理念
2、组合式API(Composition API)
3、常用API:ref、reactive、watch、computed
4、Vue3的生命周期
5、组件间通信(props、emit、defineModel)
6、了解插槽

Vue 生命周期钩子详解(Vue 2 & Vue 3 对比)

生命周期概念图解

https://cn.vuejs.org/assets/lifecycle_zh-CN.W0MNXI0C.png

生命周期阶段对比表

阶段Vue 2 钩子Vue 3 钩子触发时机典型用途
创建beforeCreatesetup()组件实例刚被创建初始化响应式数据
createdsetup()数据观测/事件配置完成异步请求数据
挂载beforeMountonBeforeMount模板编译/挂载前访问/操作DOM前的最后机会
mountedonMounted组件挂载到DOM后操作DOM、初始化第三方库
更新beforeUpdateonBeforeUpdate数据变化,DOM更新前获取更新前的DOM状态
updatedonUpdated虚拟DOM重新渲染后执行依赖DOM的操作
销毁beforeDestroyonBeforeUnmount实例销毁前清除定时器/取消订阅
destroyedonUnmounted实例销毁后清理内存/移除事件监听

Vue 3 生命周期示例

<template>
  <div class="demo">
    <h2>计数器: {{ count }}</h2>
    <button @click="increment">增加</button>
    <button @click="showComponent = false">卸载组件</button>
    
    <ChildComponent v-if="showComponent" />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 1. 创建阶段(setup)
const count = ref(0)
const showComponent = ref(true)

console.log('1. setup阶段 - 数据初始化完成')

// 3. 挂载阶段
onMounted(() => {
  console.log('3. mounted - 组件已挂载到DOM')
  // 这里可以安全地操作DOM
  document.title = `当前计数: ${count.value}`
})

// 4. 更新阶段
onUpdated(() => {
  console.log('4. updated - DOM已更新')
  // 避免在这里修改状态,可能导致无限循环!
})

// 方法
function increment() {
  count.value++
  console.log('2. 数据变化触发更新流程')
}

// 子组件示例
const ChildComponent = {
  setup() {
    onMounted(() => {
      console.log('子组件 mounted')
    })
    
    onUnmounted(() => {
      console.log('子组件 unmounted')
    })
    
    return {}
  },
  template: '<div>子组件内容</div>'
}
</script>

生命周期执行顺序演示

  1. 组件初始化时:

    1. setup阶段 - 数据初始化完成
    3. mounted - 组件已挂载到DOM
  2. 点击"增加"按钮时:

    2. 数据变化触发更新流程
    4. updated - DOM已更新
  3. 点击"卸载组件"时:

    子组件 unmounted

 挂载阶段完整顺序(父 → 子)

  1. 父组件​ setup() 执行
  2. 父组件​ onBeforeMount
  3. 子组件​ setup() 执行
  4. 子组件​ onBeforeMount
  5. 子组件​ onMounted(子组件先完成挂载)
  6. 父组件​ onMounted(父组件最后完成挂载)

常用钩子使用场景

1. onMounted - 最常用

onMounted(async () => {
  // 加载初始数据
  const response = await fetch('/api/data')
  data.value = await response.json()
  
  // 初始化图表
  chart = new Chart(document.getElementById('chart'), {
    // 配置项
  })
})

2. onUpdated - 谨慎使用

onUpdated(() => {
  // 典型用例:自动滚动到底部
  listContainer.value.scrollTop = listContainer.value.scrollHeight
})

3. onUnmounted - 资源清理

onUnmounted(() => {
  // 清除定时器
  clearInterval(timer)
  
  // 取消事件监听
  window.removeEventListener('resize', handleResize)
  
  // 销毁第三方库实例
  chart?.destroy()
})

Vue 2 与 Vue 3 生命周期对比

关键区别:

  1. ​Vue 3 使用 setup 替代了 beforeCreatecreated
  2. 所有钩子都需要显式导入
  3. 命名更语义化(unmount 替代 destroy)​
  4. 组合式API让相关代码更集中

补充 自定义Hooks

什么是自定义 Hook?

自定义 Hook 是 Vue 3 组合式 API 的核心实践方式,本质是一个封装了响应式逻辑的函数。它解决了两个核心问题:

  1. 逻辑复用​ - 避免重复代码
  2. 关注点分离​ - 让组件更专注于模板渲染

就比如,在写到一个组件里面存着在不同逻辑的数据和处理数据的方法时,全部都混用了

<template>
  <div class="hooks-test">
    <h2>当前求和为:{{sum}}</h2>
    <button @click="add">点我+1</button>
    <hr>
    <img v-for="(dog,index) in dogList" :src="dog" :key="index">
    <br>
    <button @click="getDog">再来一只狗</button>
  </div>
</template>

<script lang="ts" setup>
    import { ref,reactive } from 'vue'
    import axios from 'axios'    
   
    let sum = ref(0)
    let dogList = reactive([
      'https:\/\/images.dog.ceo\/breeds\/pembroke\/n02113023_2390.jpg'
    ])

    function add(){
        sum.value += 1
    }

    // function getDog(){
    //   axios.get('https://dog.ceo/api/breeds/image/random')
    //  .then(res => {
    //     dogList.push(res.data.message)
    //   })
    // }
    async function getDog(){
      try {
        let result = await axios.get('https://dog.ceo/api/breeds/image/random')
        dogList.push(result.data.message)
      } catch (error) {
        alert(error)
      }
      
    }


  </script>

  <style scoped>
.hooks-test{
  background-color: beige;
  padding: 20px;
  text-align: center;
  border: 1px solid black;
}
img{
  height: 200px;
  margin: 20px;
}
</style>

 图示代码又处理数字数据又处理狗狗数据,数据和处理逻辑没有分开,导致代码阅读时有一定的障碍,这个时候我们就可以使用hooks自定义,将实现同一个功能所需要的数据和方法单独封装在一起

Hook 基础结构

首先注意,hook命名规范是useXxxx.js/ts

1. 计数器 Hook 示例 (useCounter.ts)

import { ref, onMounted } from 'vue'

export default function(initialValue = 0) {
  // 响应式数据
  const count = ref(initialValue)
  
  // 操作方法
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue

  // 生命周期
  onMounted(() => {
    console.log('计数器Hook已挂载')
  })

  // 暴露给组件的API
  return {
    count,
    increment,
    decrement,
    reset
  }
}

2. 异步数据 Hook 示例 (useFetch.ts)

import { reactive, onMounted } from 'vue'
import axios from 'axios'

export default function(url: string) {
  const state = reactive({
    data: null as any,
    loading: false,
    error: null as Error | null
  })

  const fetchData = async () => {
    state.loading = true
    try {
      const response = await axios.get(url)
      state.data = response.data
    } catch (err) {
      state.error = err as Error
    } finally {
      state.loading = false
    }
  }

  onMounted(fetchData)

  return {
    ...state,
    refetch: fetchData
  }
}

组件中使用 Hook

1. 基础用法

<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <button @click="counter.increment">+</button>
    <button @click="counter.decrement">-</button>
    <button @click="counter.reset">Reset</button>
  </div>
</template>

<script setup lang="ts">
import useCounter from '@/hooks/useCounter'

const counter = useCounter(10) // 初始值10
</script>

2. 组合多个 Hook

<template>
  <div>
    <!-- 计数器 -->
    <div>{{ counter.count }}</div>
    
    <!-- 异步数据 -->
    <div v-if="posts.loading">Loading...</div>
    <div v-else>{{ posts.data }}</div>
  </div>
</template>

<script setup lang="ts">
import useCounter from '@/hooks/useCounter'
import useFetch from '@/hooks/useFetch'

const counter = useCounter()
const posts = useFetch('/api/posts')
</script>

最佳实践

1. 命名规范

  • 始终以 use 开头 (useXxx)
  • 一个文件只包含一个 Hook
  • 放在 src/hooks 目录下

2. 类型安全

// useUser.ts
import { ref } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

export default function() {
  const user = ref<User | null>(null)
  
  return {
    user
  }
}

3. 复杂 Hook 示例 (usePagination.ts)

import { reactive, computed } from 'vue'

export default function(totalItems: number, perPage = 10) {
  const state = reactive({
    currentPage: 1,
    perPage
  })

  const totalPages = computed(() => 
    Math.ceil(totalItems / state.perPage)
  )

  const nextPage = () => {
    if (state.currentPage < totalPages.value) {
      state.currentPage++
    }
  }

  return {
    ...state,
    totalPages,
    nextPage
  }
}

与 Vue 2 Mixins 对比

特性自定义 HookMixins
代码组织按功能组织按选项合并
命名冲突无(可重命名解构)容易冲突
类型支持完整 TypeScript 支持支持有限
逻辑复用显式导入全局混入
调试清晰的调用栈难以追踪逻辑来源

高级技巧

1. 本地存储 Hook (useLocalStorage.ts)

import { ref, watch } from 'vue'

export default function(key: string, defaultValue: any) {
  const data = ref(JSON.parse(localStorage.getItem(key) || 'null') || defaultValue)

  watch(data, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  }, { deep: true })

  return data
}

2. 鼠标位置 Hook (useMouse.ts)

import { ref, onMounted, onUnmounted } from 'vue'

export default function() {
  const x = ref(0)
  const y = ref(0)

  const update = (e: MouseEvent) => {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

常见问题解决

1. Hook 之间共享状态

// useSharedState.ts
import { reactive } from 'vue'

const state = reactive({
  count: 0
})

export function useSharedCounter() {
  const increment = () => state.count++
  
  return {
    count: state.count,
    increment
  }
}

2. 响应式解构问题

// ❌ 错误 - 失去响应性
const { count, increment } = useCounter()

// ✅ 正确 - 保持响应性
const counter = useCounter()
// 在模板中使用 counter.count

组件通信

为什么需要组件通信

在 Vue 中,组件是独立、可复用的模块,每个组件都有自己的数据和方法。但实际开发中,组件之间往往需要共享数据相互影响,比如:

  • 父组件​ 需要传递数据给 ​子组件​(比如列表数据传给子组件渲染)。
  • 子组件​ 需要通知 ​父组件​ 某些事件(比如按钮点击、表单提交)。
  • 兄弟组件​ 之间需要同步状态(比如购物车的商品数量变化)。

组件通信方式以及原理

(1)Props(父传子)—— 像“传纸条”​

原理​:父组件通过属性(props)把数据传给子组件,子组件声明接收。

  • 爸爸(父组件)给儿子(子组件)一张纸条(props),上面写着:“今天作业是数学”。
  • 儿子必须提前说好:“我能接收纸条(defineProps)”,才能看到内容。
<!-- 父组件(爸爸) -->
<Child :homework="mathHomework" />

<!-- 子组件(儿子) -->
<script setup>
const props = defineProps({
  homework: String // 声明接收纸条内容
})
console.log(props.homework) // "数学"
</script>

​(2)自定义事件(子传父)—— 像“按门铃”​

原理​:子组件触发事件(emit),父组件监听并处理。

  • 儿子(子组件)写完作业后,按一下门铃(emit)喊爸爸(父组件):“爸,我写完了!”
  • 爸爸提前在门口装了个监听器(@done),听到后就去检查作业。
<!-- 子组件(儿子) -->
<button @click="$emit('done', '数学作业写完了')">提交作业</button>

<!-- 父组件(爸爸) -->
<Child @done="handleDone" />

<script setup>
function handleDone(message) {
  console.log(message) // "数学作业写完了"
}
</script>

​(3)v-model(双向绑定)—— 像“父子共享笔记本”​

原理​:v-model = props(父传子) + emit(子传父)的语法糖。

  • 爸爸和儿子共用一个笔记本(v-model),爸爸写一句话,儿子能看到;儿子修改后,爸爸也能同步看到。
<!-- 父组件 -->
<Child v-model="message" /> <!-- 相当于下面两行 -->
<!-- <Child :modelValue="message" @update:modelValue="newVal => message = newVal" /> -->

<!-- 子组件 -->
<script setup>
const modelValue = defineModel() // 直接绑定父组件的 v-model
</script>
<input v-model="modelValue" /> <!-- 修改会自动同步到父组件 -->

​(4)事件总线(跨组件通信)—— 像“班级广播”​

原理​:用一个全局的“广播站”(mitt 或 $bus),任何组件都能发消息或收听。

  • 班长(组件A)广播:“今天大扫除!”
  • 所有同学(其他组件)只要提前订阅了广播,就能听到并行动。
// 事件中心(广播站)
import mitt from 'mitt'
const emitter = mitt()

// 组件A(班长):发广播
emitter.emit('clean', '擦黑板')

// 组件B(学生):提前订阅
emitter.on('clean', (task) => {
  console.log(`收到任务:${task}`) // "擦黑板"
})

​(5)$attrs(祖传孙)—— 像“快递代收点”​

原理​:父组件不处理的 props 会自动往下传递(像快递站不拆包,直接转给下一站)。

  • 爷爷(祖组件)给爸爸(父组件)一箱水果(props),但爸爸不吃,直接原样传给孙子(孙组件)。
<!-- 爷爷 -->
<Parent gift="苹果" />

<!-- 爸爸(不接收 gift,自动传给孙子) -->
<Child v-bind="$attrs" /> <!-- 相当于 <Child gift="苹果" /> -->

<!-- 孙子 -->
<script setup>
defineProps(['gift']) // 收到 "苹果"
</script>

​(6)provide/inject(跨层直传)—— 像“家族信托基金”​

原理​:祖先组件提供数据(provide),后代组件直接领取(inject),中间组件不用管。

  • 爷爷(祖组件)设立一笔“零花钱基金”(provide),孙子(孙组件)可以直接领,不需要经过爸爸(父组件)。
// 爷爷(提供数据)
provide('allowance', 100)

// 孙子(直接领取)
const money = inject('allowance') // 拿到 100

(7)defineModel 

有一篇很好的文章:一文搞懂 Vue3 defineModel 双向绑定:告别繁琐代码!随着vue3.4版本的发布,defineModel也 - 掘金

defineModel是否破坏了vue3的单向数据流呢?使用defineModel时,为什么子组件内没有任何关于prop - 掘金 

核心作用

  • 解决的问题​:传统 v-model 需要手动定义 props 和 emit,代码冗余。
  • 简化后的效果​:一行代码即可实现双向绑定。

传统 v-model vs defineModel**​

传统方式(Vue 3.3 之前)​

<!-- 子组件 -->
<script setup>
const props = defineProps(['modelValue']) // 接收父组件的 v-model
const emit = defineEmits(['update:modelValue']) // 定义更新事件

function handleChange(e) {
  emit('update:modelValue', e.target.value) // 通知父组件更新
}
</script>

<template>
  <input 
    :value="props.modelValue" 
    @input="handleChange" 
  />
</template>

使用 defineModel(Vue 3.3+)​

<!-- 子组件 -->
<script setup>
const modelValue = defineModel() // 自动绑定父组件的 v-model
</script>

<template>
  <input v-model="modelValue" /> <!-- 直接绑定,无需手动 emit -->
</template>

使用场景

场景 1:基础双向绑定
<!-- 父组件 -->
<ChildComponent v-model="message" />

<!-- 子组件 -->
<script setup>
const modelValue = defineModel() // 默认绑定父组件的 v-model
</script>

<template>
  <input v-model="modelValue" />
</template>
场景 2:自定义 v-model 参数

支持修改 v-model 的参数名(默认是 modelValue):

<!-- 父组件 -->
<ChildComponent v-model:title="pageTitle" />

<!-- 子组件 -->
<script setup>
const title = defineModel('title') // 绑定 v-model:title
</script>
场景 3:多 v-model 绑定
<!-- 父组件 -->
<UserForm 
  v-model:name="userName"
  v-model:age="userAge"
/>

<!-- 子组件 -->
<script setup>
const name = defineModel('name')
const age = defineModel('age')
</script>

实现原理

defineModel 在编译时会展开为以下代码:

// 编译后的等价代码
const props = defineProps({ modelValue: {} })
const emit = defineEmits(['update:modelValue'])

const modelValue = computed({
  get() { return props.modelValue },
  set(value) { emit('update:modelValue', value) }
})

所以它本质上仍是props + emit的封装。


高级用法

​(1)添加类型和默认值
<script setup>
const modelValue = defineModel({
  type: String,       // 类型校验
  default: '默认值'    // 默认值
})
</script>
​(2)修饰符处理

父组件使用修饰符(如 v-model.trim)时,可通过 defineModel 的返回值获取:

<script setup>
const [modelValue, modifiers] = defineModel()

// 如果父组件用 v-model.trim,modifiers 值为 { trim: true }
watchEffect(() => {
  if (modifiers.trim) {
    modelValue.value = modelValue.value.trim()
  }
})
</script>

注意事项

  1. Vue 版本要求​:必须 ≥ 3.3。
  2. ​仅适用于 <script setup>:不能在 Options API 中使用。
  3. 底层仍是单向数据流​:只是简化了代码,数据更新的原则不变。​
功能传统方式 (props + emit)defineModel
代码量需要手动定义 props 和 emit一行搞定
​多 v-model*需定义多个 props/emit直接指定参数名
修饰符支持需手动处理自动暴露 modifiers
类型检查需单独配置内置支持

插槽(Slots)

 什么是插槽

插槽(Slot)是 Vue 组件的一种机制,允许父组件向子组件传递内容​(HTML 结构、组件、文本等),子组件通过 <slot> 标签决定这些内容的显示位置
​让组件更灵活,可以动态填充内容,而不是写死在子组件内部。

什么时候需要用插槽

  • 组件内容需要动态定制​:一个弹窗组件,标题和按钮文字由父组件决定。
  • 布局组件需要插入可变内容​:一个卡片组件,卡片的头部、主体、底部由父组件定义。
  • 需要子组件向父组件传递数据​(作用域插槽):列表组件渲染数据,但父组件控制每一项的样式。

插槽的分类及示例

Vue 的插槽分为三类:​默认插槽具名插槽作用域插槽

​(1)默认插槽(无名插槽)​

  • 特点​:只有一个插槽,父组件的内容会全部填充到 <slot> 的位置。
  • 示例​:一个按钮组件,允许父组件自定义按钮文字。
<!-- 子组件 MyButton.vue -->
<template>
  <button class="my-btn">
    <slot>默认文字</slot> <!-- 父组件不传内容时显示“默认文字” -->
  </button>
</template>

<!-- 父组件 -->
<MyButton>提交</MyButton> <!-- 渲染:<button class="my-btn">提交</button> -->

​(2)具名插槽(Named Slots)​

  • 特点​:多个插槽通过 name 区分,父组件用 v-slot:name(或 #name)指定内容分发给哪个插槽。
  • 示例​:一个卡片组件,分头部、主体、底部三部分。
<!-- 子组件 MyCard.vue -->
<template>
  <div class="card">
    <header>
      <slot name="header"></slot> <!-- 具名插槽 -->
    </header>
    <main>
      <slot></slot> <!-- 默认插槽 -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<!-- 父组件 -->
<MyCard>
  <template #header>标题</template> <!-- v-slot:header 的简写 -->
  <p>这里是主体内容</p> <!-- 默认插槽内容 -->
  <template #footer>
    <button>确定</button>
  </template>
</MyCard>

​(3)作用域插槽(Scoped Slots)​

  • 特点​:子组件通过插槽向父组件传递数据,父组件可以自定义渲染逻辑。
  • 示例​:一个列表组件,子组件提供数据,父组件决定如何渲染每一项。
<!-- 子组件 MyList.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item"></slot> <!-- 将 item 数据传给父组件 -->
    </li>
  </ul>
</template>

<script setup>
const items = [
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' }
]
</script>

<!-- 父组件 -->
<MyList>
  <template v-slot="slotProps"> <!-- 接收子组件传递的数据 -->
    <span style="color: red">{{ slotProps.item.name }}</span>
  </template>
</MyList>

<!-- 渲染结果:
<ul>
  <li><span style="color: red">苹果</span></li>
  <li><span style="color: red">香蕉</span></li>
</ul>
-->

注意

Q1:v-slot 可以简写吗?​

  • 可以!v-slot:header 简写为 #header

Q2:默认插槽和作用域插槽能一起用吗?​

  • 可以!但需要分开写:
    <MyComponent>
      <template #default="slotProps">默认插槽内容</template>
      <template #footer>页脚</template>
    </MyComponent>

Q3:插槽内容能访问父组件的数据吗?​

  • 可以!插槽内容属于父组件的作用域,直接使用父组件的变量即可。

总结

插槽类型作用示例场景
默认插槽父组件传递单一内容按钮文字、弹窗内容
具名插槽父组件分发内容到多个指定位置卡片的头部、底部
作用域插槽子组件提供数据,父组件自定义渲染逻辑列表项渲染、表格自定义列

 

背景描述本数据集包含了多个与心力衰竭相关的特征,用于分析和预测患者心力衰竭发作的风险。数据集涵盖了从40岁到95岁不等年龄的患者群体,提供了广泛的生理和生活方式指标,以帮助研究人员和医疗专业人员更好地理解心衰的潜在风险因素。每条患者记录包含以下关键信息:年龄(Age):记录患者的年龄,心脏病的风险随年龄增长而增加。贫血(Anaemia):贫血可能影响心脏功能,记录患者是否患有贫血。高血压(High blood pressure):高血压是心脏病的主要风险因素之一。肌酸激酶(Creatinine phosphokinase, CPK):血液中的CPK水平可以反映心肌损伤。糖尿病(Diabetes):糖尿病与心脏病风险增加有关。射血分数(Ejection fraction):心脏每次收缩时泵出的血液百分比,是心脏功能的重要指标。性别(Sex):性别可能影响心脏病的风险和表现形式。血小板(Platelets):血小板水平可能与血液凝固和心脏病风险相关。血清肌酐(Serum creatinine):血液中的肌酐水平可以反映肾脏功能,与心脏病风险有关。血清钠(Serum sodium):钠水平的异常可能与心脏疾病相关。吸烟(Smoking):吸烟是心脏病的一个重要可预防风险因素。时间(Time):记录患者的随访期,用于观察长期健康变化。死亡事件(death event):记录患者在随访期间是否发生了死亡事件,作为研究的主要结果指标。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值