本文干货满满,没有一点废话,还请点赞+收藏+关注支持一下!!!
目录
简述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
}
上述代码则可以实现点击按钮,页面更新。
两者区别在何?
特性 | ref | reactive |
---|
定义 | 用于创建对值的响应式引用。 | 用于创建响应式对象,自动将对象的所有属性变为响应式。 |
使用场景 | 常用于基本类型的响应式值。 | 常用于创建包含多个属性的响应式对象或数组。 |
响应性 | 对值本身的变化作出响应。 | 对对象属性的变化作出响应。 |
类型 | 用于单一基本类型,或存储在 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>
标签的使用,绑定方式取决于是否需要与数据交互:
页面与数据无绑定:可以直接使用
value=" "
来设置输入框的初始值,但此时该值不会与数据动态绑定。数据向输入框单向绑定:需要使用
:value=" "
或v-bind:value=" "
,这样输入框的值会根据数据的变化而更新,但用户在输入框中进行的修改不会影响数据。数据和输入框双向绑定:使用
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
只能监视以下四种数据:
ref
定义的数据。
reactive
定义的数据。函数返回一个值(
getter
函数)。一个包含上述内容的数组。
场景一:
监视 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 中第一个参数直接写 【对象.属性】 肯定是不行的,因为前面也说了监视函数监视的数据只能是以下四种:
-
ref
定义的数据。 -
reactive
定义的数据。 -
函数返回一个值(
getter
函数)。 -
一个包含上述内容的数组。
所以此时需要将该属性弄成一个 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
-
都能监听响应式数据的变化,不同的是监听数据变化的方式不同
-
watch
:要明确指出监视的数据 -
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),通过 type
或 interface
关键字创建,可以组合基本类型、联合类型、元组等,帮助开发者构建复杂的数据结构,并确保数据在整个应用程序中的正确使用。这三者共同作用,使得 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:"九江"}]
})
感谢您的观看!!!