首页 前端知识 【玩转全栈】----闹钟虐我千百遍?我待 Vue3 如初恋!

【玩转全栈】----闹钟虐我千百遍?我待 Vue3 如初恋!

2025-03-04 11:03:31 前端知识 前端哥 46 634 我要收藏

本文干货满满,没有一点废话,还请点赞+收藏+关注支持一下!!!

目录

简述Typescript

1、声明变量

2、数据类型

拥抱 vue3

ref与reactive

toRefs和toRef

computed 计算属性

Watch 监视

监视 ref定义的普通数据

监视 ref定义的对象数据

监视 reactive定义的数据

监视 ref 或 reactive 定义的对象中的属性、对象

监视上述的多个数据

watchEffect 

标签的 ref 属性

修改 App 组件

defineExpose

接口-泛型-自定义类型

接口

泛型

自定义类型

Props 传参

接收Studens +限制类型

接收Studens + 限制类型 + 限制必要性 + 指定默认值


简述Typescript

1、声明变量

关键字作用范围可重新赋值可重新声明变量提升推荐使用情况
let块级作用域可以不可以推荐用于大多数变量声明
const块级作用域不可以不可以推荐用于常量,尤其是引用不变的数据
var函数作用域或全局作用域可以可以是(提升)不推荐使用,除非在旧代码中兼容性需要

块级作用域 是指变量的作用域限制在代码块内,通常由大括号 {} 包围,例如循环、条件语句或任何包含代码的语句块。

函数作用域 则是指整个函数

2、数据类型

Javascript 的基本数据类型如下:

Typescript 中的数据类型:

拥抱 vue3

ref与reactive

对于一个简单的页面展示:

<template>
    <h2>我的第一个vue项目</h2>
     <div>
        <h2>姓名是{
  
  { name }},年龄为{
  
  { age }},电话是{
  
  { tel }}</h2>
        <h2>一款游戏({
  
  { game.name }}) 的星级为{
  
  { game.star }}</h2>
        <button @click="change_name">修改姓名</button>
        <button @click="add_age">修改年龄</button>
        <button @click="show_tel">显示电话</button>
        <button @click="add_star">增加星星</button>
        <h2></h2>
     </div>
</template>
<script setup>
    let name = "东华"
    let age = 20
    let tel = "122222"
    let game = {name:"王者",star:-1}

    function change_name(){
        name = "谭谭晨"
        console.log(name)
    }
    function add_age(){
        age += 1
    }
    function show_tel(){
        name = "谭谭晨"
    }
    function add_star(){
        game.star += 1
    }
</script>

<style >

</style>

直接按按钮,你会发现,页面上的值并没有改变,但使用console.log打印后,控制台显示的值却变了

这是因为 Vue 的响应式系统无法自动追踪这些变量的变化,从而导致视图(DOM)不会自动更新,但其本身确实是更新了。

解决办法就是使用ref、reactive进行包裹:

先引入:

import {ref, reactive} from 'vue'

将普通变量用ref包裹,数组、对象等用reactive包裹:

想让哪个数据是响应式的,就用ref包一下,包之后该数据就为对象,方法中需用.value调用

一个普通对象要变成响应式对象,需要reactive包裹,包裹后变为proxy对象

import {ref, reactive} from 'vue'
    let name = ref("东华")
    let age = ref(20)
    let tel = ref("122222")
    let game = reactive({name:"王者",star:-1})

    function change_name(){
        name.value = "谭谭晨"
        console.log(name)
    }
    function add_age(){
        age.value += 1
    }
    function show_tel(){
        alert("电话:"+tel.value)
    }
    function add_star(){
        game.star += 1
    }

上述代码则可以实现点击按钮,页面更新。

两者区别在何?

特性refreactive
定义用于创建对值的响应式引用。用于创建响应式对象,自动将对象的所有属性变为响应式。
使用场景常用于基本类型的响应式值。常用于创建包含多个属性的响应式对象或数组。
响应性对值本身的变化作出响应。对对象属性的变化作出响应。
类型用于单一基本类型,或存储在 ref 中的对象。用于对象或包含多个属性的数组。
复杂性比 reactive 简单,开销较小。比 ref 更复杂,涉及到代理对象的创建。
性能对于基本类型响应迅速,性能较好。由于代理的开销,对于大对象性能稍慢。

使用原则:

1、若需要一个基本类型的响应式数据,必须使用 ref。

2、若需要一个响应式对象,层次不深,ref、reactive 都可以。

3、若需要一个响应式对象,且层级较深,推荐使用 reactive。

        这里需要注意,当用 reactive 定义响应式数据时,直接修改里面的属性是行的,但是如果修改整个对象,就不行,例如:

game = {name:"部落冲突",star:2}

这样直接修改,页面是不会变化的,需要用到一个方法:

Object.assign(a1,a2,a3)

该方法的作用是将 a2 的属性赋给 a1,再将 a3 的属性赋给 a1,其中,重复的属性直接覆盖。

这里可以这么用:

Object.assign(game,{name:"部落冲突",star:2})

将{  }中的属性赋给 game,重复的属性就直接覆盖了。

但若使用 ref ,就更简单一点,直接 .value 即可,只要记住, .value 的一切都是响应式。

game.value = {name:"部落冲突",star:2}

toRefs和toRef

当我定义一个响应式对象,并结构赋值,将对象中的属性解封出来:

let car = reactive({
        name:"奔驰",
        price:2
    })
let {name,price} = car

        直接用 reactive 包裹的对象 car ,只要不直接改变对象,其中的属性一定是响应式的,但是结构赋值后就不是响应式了,大家可以试试看,将结构赋值出来的 name 和 price ,展示在页面上,并设置按钮改变,你会发现,页面并不会变换,但是控制台打印确实变了,这就说明响应式对象结构赋值出来的变量不是响应式的,要弄成响应式的,只需要使用 toRefs 和 toRef 即可:

import {toRefs,toRef} from 'vue'

将 car 对象进行包裹:

let {name,price} = toRefs(car)

这会使得新的 name 、price 分别与 car.name 、car.price 进行绑定,从而使其响应化,相当于将两者链接了。

如果想将对象中的单个属性拎出来,则使用 toRef

let name = toRef(car, 'name')

computed 计算属性

对于  <input>  标签的使用,绑定方式取决于是否需要与数据交互:

  1. 页面与数据无绑定:可以直接使用 value=" " 来设置输入框的初始值,但此时该值不会与数据动态绑定。

  2. 数据向输入框单向绑定:需要使用 :value=" " v-bind:value=" ",这样输入框的值会根据数据的变化而更新,但用户在输入框中进行的修改不会影响数据。

  3. 数据和输入框双向绑定:使用 v-model=" ",这样输入框的值和数据是双向绑定的,意味着数据的变化会更新到输入框,反之,用户在输入框中的修改也会更新到数据。

当用户输入性和名,需要计算姓名时:

<template>
    <div class="p_3">
        姓:<input type="text" v-model = "first_name"><br>
        名:<input type="text" v-model = "last_name"><br>
        <h2>姓名:</h2><span> {
  
  { first_name.slice(0,1).toUpperCase() + first_name.slice(1) + "-" + last_name}} </span><br>
    </div>
</template>

<script setup lang="ts">
    import {ref, computed} from 'vue'
    let first_name = ref("张")
    let last_name = ref("三")
</script>
<style>
    .p_3{
        width: 200px;
        height: 300px;
        background-color: rgb(26, 144, 18);
    }
    span{
        background-color: aliceblue;
    }
</style>

        这样的写法固然是可以的,但违背了 vue3 “尽量使 template 标签简洁”的原则,可以在 script 标签中进行编写,实现这种功能使用一般的函数是可以的,但使用函数有局限性:

  • 不缓存:每次调用方法时,它都会重新执行,不会缓存结果。
  • 使用场景:适用于不依赖于响应式数据,或每次需要重新执行的逻辑,通常是处理事件或响应用户操作时。
  • 访问方式:方法需要通过函数调用来使用(例如:this.methodName())。
  • 定义方式:通过 methods 选项定义。

两者区别如下:

特性计算属性(computed)方法(methods)
缓存计算属性是缓存的,只有依赖的响应式数据变化时才重新计算方法每次调用时都会重新执行,不缓存结果
用途适用于基于响应式数据计算出一个值的场景适用于不依赖于响应式数据的逻辑或执行某些操作的场景
调用方式通过属性访问,例如 this.fullName通过函数调用,例如 this.fullName()
性能提供优化,避免不必要的计算每次调用都会执行,不进行优化

这时就可以使用 computed 计算属性,首先引入:

import {computed} from 'vue'

编写方式如下:

let full_name = computed(()=>{
        return first_name.value.slice(0,1).toUpperCase() + first_name.value.slice(1) + "-" + last_name.value
    })

        这样就可以通过在 scrip 中计算出来,然后显示在页面上。但这样也有局限性,如果需要直接修改,是不行的,因为该变量是由 first_name 和 last_name 共同计算出来的,first_name 和 last_name并没有改变,所以 full_name 也就不会改变,解决方法是使用 get-set 写法:

let full_name = computed({
    get(){
        return first_name.value.slice(0,1).toUpperCase() + first_name.value.slice(1) + "-" + last_name.value
    },
    set(val){
        console.log(val)
        const [str_1,str_2] = val.split('-')
        // console.log(str_1,str_2)
        first_name.value = str_1
        last_name.value = str_2
    },    
})

function chang_name(){
    full_name.value = 'Wang-mazi'
}

get 里写的是读取的返回值,set 是写入。这里点击按钮,full_name 的值变成了 'wang-mazi '

页面并没有渲染,此时 set 中读取 val,打印 val 可知,其就是 'Wang-mazi',将名和姓分别赋给 first_name 和 last_name ,这样 full_name 就会改变。

Watch 监视

watch 的作用是监听数据的变化

  • 特点:Vue3中的watch只能监视以下四种数据

  1. ref定义的数据。

  2. reactive定义的数据。

  3. 函数返回一个值(getter函数)。

  4. 一个包含上述内容的数组。

场景一:

监视 ref定义的普通数据

let sum = ref(0)

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

watch(sum,(new_value,old_value)=>{
    console.log("sum变化了" ,new_value,old_value)
  })

        watch 传递两个参数,一个是要监听的数据,第二个是回调函数,可以写成箭头函数,可以接收两个参数,即变化前后的值。要停止监视也很简单, watch 返回的就是停止监视的回调函数代码,可用变量接收,在合适条件调用即可,例如:

const stop_watch = watch(sum,(new_value,old_value)=>{
    console.log("sum变化了" ,new_value,old_value)
    if(new_value > 5){
        stop_watch()
    }
  })

这里注意,watch 传递第一个参数不用 .value,因为监视的是整个对象,而不是对象中的 value

场景二:

监视 ref定义的对象数据

  let Person = ref({
    name:"李四",
    age:18
  })

  function change_name(){
    Person.value.name = "张三"
  }
  function change_age(){
    Person.value.age = 99
  }
  function change_Person(){
    Person.value = {name:"王麻子",age:199}
  }
 
  watch(Person,(new_value,old_value)=>{
    console.log("发生变化"+new_value,old_value)
  })

ref 定义的响应式对象,如果按照场景一的方式进行监视,你会发现,监视只对 change_Person 函数起效,即,只对修改整个对象起效,修改对象中的属性值并不会被监视到。如果想要监视对象中属性的变化,则可利用 watch 函数的第三个参数:

watch(Person,(new_value,old_value)=>{
    console.log("发生变化"+new_value,old_value)
  },{deep:true,immediate:true})

第三个参数为配置选项,deep:true 是使得 watch 函数进行深度监视,使其能检测到对象中属性的变化;immediate:true 是配置监视时机的,若没有次配置项,默认事件发生后再执行监视回调函数中的逻辑;若加上,则刷新页面就会先执行监视函数逻辑。

场景三:

监视 reactive定义的数据

  let Game = reactive({
    name:"王者荣耀",
    star:1
  })

  function change_gName(){
    Game.name = "部落冲突"
  }
  function change_star(){
    Game.star = -2
  }
  function change_Game(){
    Object.assign(Game,{name:"蛋仔派对",star:2})
  }
  
  watch(Game,(old_value,new_value)=>{
    console.log("整个游戏修改了"+old_value,new_value)
  })

监视 reactive 定义的数据其实是比较简单的,直接写即可,reactive 定义的响应式数据会默认开启深度检测,即既检测整个对象,也检测对象中的属性,也检测对象中的对象中的属性,层次无限深。

场景四:

监视 ref reactive 定义的对象中的属性、对象

对于 reactive 定义的对象,如果想具体监视对象中的某个属性(点击该修改属性按钮才会被监视,其他属性修改按钮或者整个对象修改按钮点击后都不会被监视),watch 中第一个参数直接写 【对象.属性】 肯定是不行的,因为前面也说了监视函数监视的数据只能是以下四种:

  1. ref定义的数据。

  2. reactive定义的数据。

  3. 函数返回一个值(getter函数)。

  4. 一个包含上述内容的数组。

所以此时需要将该属性弄成一个 getter 函数,所谓 getter 函数,接地气一点的说法就是能返回一个值的函数,此处用箭头函数,写返回值即可,例如:

watch(()=>{return Game.name},(old_value,new_value)=>{
    console.log("游戏名修改了"+old_value,new_value)
  })

        如果要监视对象中的对象,虽然直接写 【对象.对象】 是可以的,但是这样并不能检测到整个外部对象的变化,只能监视内部对象的变化,而写成函数式就能监视外部对象的变化,若还想监视内部对象的变化,加上一个深度监视即可,所以监视对象中的对象建议直接写函数式。

场景五:

监视上述的多个数据

监视多个数据,只需将数据全部塞入数组中,再传参即可,需要注意的是什么时候要写函数,什么时候要 .value ,都要根据数据源判断。

 watch([sum,()=>Person.value.name,()=>Game.name],(old_value,new_value)=>{
    console.log("整个游戏修改了"+old_value,new_value)
  },{deep:true})

watchEffect 

  • 官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。

  • watch对比 watchEffect

    1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同

    2. watch:要明确指出监视的数据

    3. watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。

示例:

<template>
    <div class="p_5">
        <h2>水位为{
  
  { height }}mm,水温为{
  
  { temp }}摄氏度</h2>
        <button @click="change_height">改变水位</button>
        <button @click="change_temp">改变水温</button>
    </div>
</template>
<script setup lang="ts">
    import {ref,watch} from 'vue'
    let height = ref(0)
    let temp = ref(0)
    
    function change_height(){
        height.value += 1
    }
    function change_temp(){
        temp.value += 10
    }
    const stop_watch = watch([height,temp],()=>{
        console.log("监视到了")
        if(height.value > 3||temp.value > 30){
            stop_watch()
        }
    }
</script>

此处代码仅仅监视了 height 和 temp 两个数据,使用 watch 监视当然可以,但如若需要监视几十上百个数据,传参就要写几十上百个,是非常麻烦的。此时使用 watchEffect 就能很好地节省代码量,watchEffect 不需要传递数组参数,直接进行逻辑比较即可:

const stop_watch_ = watchEffect(()=>{
        console.log("发生了变化")
        if(height.value > 3||temp.value > 30){
            stop_watch_()
        }
    })

这样会使得在监视复杂数据时更加便捷。

标签的 ref 属性

修改 App 组件

打开 App 组件,会发现还残留着许多 vue2 的写法:

<script lang="ts">
  import Person from './components/Person.vue'
  import Object_ from './components/object_.vue'
  import Ref_reactive from './components/ref_reactive.vue'
  import ToRef_toRefs from './components/toRef_toRefs.vue'
  import Computed from './components/computed.vue'
  import Whatch from './components/whatch.vue'
  import Watcheffect from './components/watcheffenct.vue'
  import Ref_in_tags from './components/ref_in_tags'

  export default{
    name: 'App',  //组件名
    components:{Person,Object_,Ref_reactive,ToRef_toRefs,Computed,Whatch,Watcheffect,Ref_in_tags},  //注册

  }

</script>

我们将其改成 vue3 写法:

<template>
  <!-- 使用标签 -->
  <Ref_reactive />
  <ToRef_toRefs />
  <Computed />
  <Watch />
  <WatchEffect />
  <Ref_in_tags />
</template>

<script setup lang="ts" name="App">
import Ref_reactive from './components/ref_reactive.vue'
import ToRef_toRefs from './components/toRef_toRefs.vue'
import Computed from './components/computed.vue'
import Watch from './components/watch.vue'
import WatchEffect from './components/watcheffect.vue'
import Ref_in_tags from './components/ref_in_tags.vue'
</script>

<style>

</style>

ref 在前面讲的是包裹得到响应式数据,在这里还有一个功能,类似于给相同标签做标记,或者说是局部变量

        原则上,在一个组件中,ID值应该是唯一的。然而,在父组件与子组件之间,可能会无意间定义相同的ID名。在这种情况下,如果尝试在子组件中通过ID来获取元素,而父组件中恰好存在相同ID的元素,并且该元素位于子组件元素之前,那么根据自上而下的搜索原则,首先被找到的将是父组件中的同名ID元素,而非预期中的子组件内的元素。这种情况显然不是我们所期望的结果。

为了避免这种ID冲突并确保每个组件仅能访问其自身的元素,可以采用  ref 属性。使用  ref不仅可以精确地引用组件内部的特定DOM元素或子组件实例,而且能够限定查找范围至本组件内部,从而避免了跨组件层级的ID混淆问题。换句话说,通过 ref,我们可以确保在某个组件内进行元素查找时,不会误检索到另一个组件内的同名元素,保证了组件间的独立性和封装性。这样修改后的表述更加清晰地传达了原意,并强调了使用  ref 解决ID冲突的优势。

使用方式也很简单,将  id = "person"  修改为   ref = "person" ,在 script 标签中定义该值, let person = ref( )  ,获取也更简洁,获取 id 值为  let d = document.getElementById('person')  ,而获取 ref 仅需 let d = person.value 即可。

<template>
    <div class="p_6">
        <h2 ref="person">打开</h2>
        <h2 id="person">打开</h2>
        <button @click="show_id">显示h2</button>
        <button @click="show_ref">显示h2</button>
    </div>
</template>
<script setup lang="ts">    
    import {ref} from 'vue'
    let person = ref()

    function show_id(){
        let d = document.getElementById('person')
        console.log(d)
    }
    function show_ref(){
        let d = person.value
        console.log(d)
    }
</script>

defineExpose

如果将 ref 写法写到子组件中,会触发子组件的保护机制,你所获取到的子组件对象不会有子组件定义的数据,被隐藏了。如果像让父组件能得到子组件定义的数据,需要用到 defineExpose

首先引入:

import {defineExpose} from 'vue'

定义数据:

let a = ref(0)
let b = ref(1)
let c = ref(2)

执行 defineExpose 函数,将想要解封的数据传入即可:

defineExpose({a,b,c})

此时就能得到子组件的值:

接口-泛型-自定义类型

        在 TypeScript 中,接口(Interfaces)用于定义对象的形状,规定对象应包含哪些属性和方法及其类型,从而提供了一种契约式的编程模型,增强了代码的可读性和一致性。泛型(Generics)允许编写能够处理多种类型的代码而无需在编译时指定具体类型,提供了更高的灵活性和复用性,同时保持了类型的安全性。自定义类型(Custom Types),通过 typeinterface 关键字创建,可以组合基本类型、联合类型、元组等,帮助开发者构建复杂的数据结构,并确保数据在整个应用程序中的正确使用。这三者共同作用,使得 TypeScript 在保证静态类型检查的同时,支持高度模块化和可维护的代码设计。通过它们,TypeScript 不仅扩展了 JavaScript 的功能,还提升了开发效率和代码质量。这些特性是现代 Web 开发中构建大型应用不可或缺的部分。

接口

新建一个 types 文件夹,新建一个 ts 文件,定义一个接口,并暴露:

StudentInter 是接口名

export interface StudentInter {
    name: string
    age: number
    from: string
}

引入该 ts 文件,并创建对应数据:

<script setup lang="ts">
    import {type StudentInter} from '@/types'

    // 定义一个数据
    let student:StudentInter = {
        name:"贱贱贱",
        age:18,
        from:"江西"
    }
</script>

泛型

当定义一个数组,并且数组中每个元素都符合某接口时,可使用泛型:

// 定义一个数组,是一个数组类型,数组中每一个元素都符合StudentInter接口规范
    let students:Array<StudentInter> = [
    {name:"贱贱贱",age:18,from:"江西"},
    {name:"贱贱贱",age:18,from:"江西"},
    {name:"贱贱贱",age:18,from:"江西"}
    ]

自定义类型

使用泛型定义数组稍显麻烦,可用自定义类型定义:

export type Student_List = Array<StudentInter>

//简写形式:
//export type Student_List = StudentInter[]

引用:

<script setup lang="ts">
    import {type StudentInter,type Student_List} from '@/types'

    let S_Li:Student_List = [
        {name:"贱贱贱",age:18,from:"江西"},
        {name:"贱贱贱",age:18,from:"江西"},
        {name:"贱贱贱",age:18,from:"江西"}
    ]
</script>

Props 传参

对于父组件的数据,可以通过 Props 传递给子组件

在父组件上直接通过参数传递:

<Props ref="Ref" a="我是a" b="我是b"/>

子组件中先引入 defineProps 

import { defineProps } from 'vue';

调用 defineProps  函数,传入需要的参数列表:

defineProps(['a','b'])

这样子组件中就有了 a 、b 数据,可以直接在页面上显示:

<h2>props传参</h2>
<h2>a:{
  
  { a }}</h2>
<h2>b:{
  
  { b }}</h2>

如果直接在控制台打印 a 、b 你会发现这样是不行的,而且还会报错,这是因为 defineProps(['a','b']) 是字符串,而不是变量,如果直接 console.log(a) ,a 接收不到变量,当然是会报错的。解决方法也很简单,defineProps()函数是有返回值的,可以使用变量接收:

const data =  defineProps(['a','b'])

这个是 data 是一个对象,打印出来发现,数据就藏在 Target 当中

接收Studens +限制类型

Props 结合接口-泛型-自定义类型运用也比较常见

就以前面定义的 Student_List 自定义类型来说,在父组件中定义数据,并传递:

<Props ref="Ref" a="我是a" b="我是b" :Studens="Studens"/>

const Studens = [
  {name:"小谭",age:19,from:"重庆",sex:"man"},
  {name:"小小",age:13,from:"江西",sex:"women"},
  {name:"大大",age:16,from:"南昌",sex:"man"},
]

注意:Props 传递字符串正常传,但传递变量需在前面加上 " : ",会自动解译成变量

在子组件中接收:

<template>
    <div class="p_7">
        <h2>props传参</h2>
        <ul>
            <li v-for="item in Studens">
                {
  
  { item.name}}是来自{
  
  { item.from }}的{
  
  { item.age }}岁的学生
            </li>
        </ul>
    </div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
import {type Student_List} from '@/types'
// 接收Studens +限制类型
const data = defineProps<{Studens:Student_List}>()
console.log(data)
</script>
<style scoped>
    .p_7{
        width: 200px;
        height: 300px;
        background-color: blueviolet;
    }
</style>

在接收 Students 时,使用泛型,让传入数据必须为 Student_List 自定义类型。如果不符合 Student_List 自定义类型,父组件中 Props 传递参数会 飘红报错

接收Studens + 限制类型 + 限制必要性 + 指定默认值

限制必要性很简单,根据 TS 的语法,加 " ? " 即可:

const data = defineProps<{Studens?:Student_List}>()

 限制必要性后,就算父组件不传值过来,子组件也不会报错。

也可以指定默认值,这样即使父组件未传值过来,子组件也要默认数据可显示

引入默认值:

import { withDefaults } from 'vue';

给 Students 一个默认值,写法如下:

withDefaults(defineProps<{Studens?:Student_List}>(),{
    Studens:()=>[{name:"小林",age:18,from:"九江"}]
})

感谢您的观看!!!

转载请注明出处或者链接地址:https://www.qianduange.cn//article/22558.html
标签
评论
发布的文章

图论-腐烂的橘子

2025-03-04 11:03:06

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!