Vue - 核心语法

响应式: 核心

1 拉开序幕的setup

setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:

  1. 需要在非单文件组件中使用组合式 API 时。
  2. 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。

对于结合单文件组件使用的组合式 API,推荐通过 <script setup> 以获得更加简洁及符合人体工程学的语法。

<template>
    <div>
        <h2>Name: {{ name }}</h2>
        <p>Age: {{ age }}</p>
        <button @click="changeName">修改</button>
        <button @click="showTel">查看联系方式</button>
    </div>
</template>

<script>
// 响应式状态
let name = '张三'
let age = 18
let tel = '13800000000'

export default {
    setup() {
        // `setup()` 中访问 `this` 会是 `undefined`,vue3已经弱化this
        function showTel() {
            console.log(tel)
        }
        function changeName() {
            name = '李四'//这样修改页面是没有变化的
            console.log(name) //name 已经改了,但不是响应式
        }
        return { name, age, tel, showTel, changeName }
    }
}
</script>

setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。

setup 的语法糖

直接在script 添加setup

<script setup>
// 响应式状态
let name = '张三'
let age = 19
let tel = '13800000000'
function showTel() {
    console.log(tel)
}
function changeName() {
    name = '李四'//这样修改页面是没有变化的
    console.log(name) //name 已经改了,但不是响应式
}
</script>

这个时候如果组件命名的话有2种方法:

image

一、分开的script

<script>
export default {
    name: 'Person223',
}
</script>

<script setup>
// 响应式状态
let name = '张三'
let age = 19
let tel = '13800000000'
function showTel() {
    console.log(tel)
}
function changeName() {
    name = '李四'//这样修改页面是没有变化的
    console.log(name) //name 已经改了,但不是响应式
}
</script>

二、插件 vite-plugin-vue-setup-extend

安装

npm i vite-plugin-vue-setup-extend -d

vite.config.ts

import { fileURLToPath, URL } from "node:url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";
import vueSetupExtend from "vite-plugin-vue-setup-extend";

// https://vite.dev/config/
export default defineConfig({
  plugins: [
  vue(), 
  vueDevTools(), 
  vueSetupExtend()
  ],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});

src\components\Person.vue

<!--用了vite-plugin-vue-setup-extend,这里可以使用name配置名字-->
<script setup name="Person333">
// 响应式状态
let name = '张三'
let age = 19
let tel = '13800000000'
function showTel() {
    console.log(tel)
}
function changeName() {
    name = '李四'//这样修改页面是没有变化的
    console.log(name) //name 已经改了,但不是响应式
}
</script>

2 响应式

image

ref() 基本类型

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

作用:定义响应式变量。
语法:let xxx=ref(初始值)

返回值:一个RefImpl的实例对象,简称ref对象或ref,ref对象的value属性是响应式的。

注意点:

JS中操作数据需要:xxx.value,但模板中不需要要.value,直接使用即可。

对于let name=ref(‘张三')来说,name不是响应式的,name.value是响应式的。

<template>
    <div>
        <h2>Name: {{ name }}</h2>
        <p>Age: {{ age }}</p>
        <button @click="changeName">修改</button>
        <button @click="showTel">查看联系方式</button>
    </div>
</template>

<script setup name="Person">
import { ref } from "vue"

// 响应式状态
let name = ref('张三')  // 响应式数据
let age = ref(19)  // 响应式数据
let tel = '13800000000'
function showTel() {
    console.log(tel)
}
function changeName() {
    name.value = '李四'  // 响应式数据
    console.log(name.value)
}
</script>

reactive() 只能定义对象类型

响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。

值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。

若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代。

返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹,因此不等于源对象,建议只使用响应式代理,避免使用原始对象。

Car.vue

<template>
    <div>
        <h2>一辆车的名称: {{ car.name }}</h2>
        <p>Price: {{ car.price }}</p>
        <button @click="changePrice">修改价格</button>
    </div>
</template>

<script setup>
import { reactive } from "vue"

let carInfo = {
    name: '奔驰',
    price: 1000000
}

console.log(carInfo) // {name: '奔驰', price: 1000000}
let car = reactive(carInfo)
console.log(car) //Proxy(Object) {name: '奔驰', price: 1000000}
    
function changePrice() {
    car.price += 10000  //这里不用value,因为reactive已经将carInfo转换为响应式对象
}
</script>

image

ref 也可以定义对象,但是实际上对象里面还是调用的 reactive,所以对象最好直接使用 reactive

<script setup name="Person">
import { ref } from "vue"

let carInfo = {
 name: '奔驰',
 price: 1000000
}

console.log(carInfo)
let car = ref(carInfo)
console.log(car)
function changePrice() {
 car.value.price += 10000;
}
</script>

image

ref 与 reactive 对比

特性 ref(value) reactive(obj)
支持的数据类型 任意类型(基本类型:Number/String/Boolean;引用类型:Object/Array) 仅支持 引用类型(Object/Array/Map/Set 等,传基本类型无效)
访问 / 修改方式 必须通过 .value 访问(模板中自动解包,可省略 .value 直接访问 / 修改属性(无需 .value
响应式深度 深层响应(默认递归监听所有嵌套属性) 深层响应(默认递归监听所有嵌套属性,同 ref)
重新赋值(替换数据) 支持:直接 ref.value = 新值(哪怕是不同类型) 不支持:无法直接替换整个对象(会丢失响应式),只能修改属性
解构赋值兼容性 解构后仍保持响应式(需配合 toRefs() 或直接解构 .value 直接解构会丢失响应式(需用 toRefs() 转换)
适用场景 1. 基本类型数据;2. 单个引用类型数据;3. 需重新赋值的场景 1. 复杂嵌套对象 / 数组;2. 多个关联属性的集合(如用户信息、表单数据)

宏观角度看:

1.ref用来定义:基本类型数据、对象类型数据;

2.reactive用来定义:对象类型数据。

区别:

1.ref创建的变量必须使用.value(可以使用volar插件自动添加.value,当前插件名字是 Vue (Official))。

image

image

2.reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)。

<template>
    <div>
        <h2>一辆车的名称: {{ car.name }}</h2>
        <p>Price: {{ car.price }}</p>
        <button @click="changePrice">修改价格</button>
        <button @click="changeCar">修改整车</button>
    </div>
</template>

<script setup>
import { reactive } from "vue"

let carInfo = {
    name: '奔驰',
    price: 1000000
}
let car = reactive(carInfo)
function changePrice() {
    car.price += 10000;
}
function changeCar() {
    //失败1
    // car = {
    //     name: '保时捷',
    //     price: 2000000
    // };

    //失败2
    // car = reactive({
    //     name: '保时捷',
    //     price: 2000000
    // });

    //成功
    Object.assign(car, { name: '保时捷', price: 2000000 })
}
</script>

使用原则:

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

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

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

toRefs()

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

<template>
    <div>
        <h2>Name: {{ name }}</h2>
        <p>Age: {{ age }}</p>
        <button @click="changeName">修改</button>
    </div>
</template>

<script setup>
import { reactive } from "vue"

let person = reactive({
    name: '张三',
    age: 18
})

let { name, age } = person
console.log(name)
function changeName() {
    name += '~'
    console.log('name:' + name, "person.name:" + person.name)
}
</script>

这个时候,页面是没有变化的。这里的name打印出来也只是一个string的张三

image

如果使用toRefs ,消费者组件可以解构/展开返回的对象而不会失去响应性:

<template>
    <div>
        <h2>Name: {{ name }}</h2>
        <p>Age: {{ age }}</p>
        <button @click="changeName">修改</button>
    </div>
</template>

<script setup>
import { reactive, toRefs } from "vue"

let person = reactive({
    name: '张三',
    age: 18
})

let { name, age } = toRefs(person)
console.log(name)
function changeName() {
    name.value += '~'
    console.log('name:' + name.value, "person.name:" + person.name)
}
</script>

这里的name,打印出来是一个ObjectRefImpl

image

toRef()

可以将值、refs 或 getters 规范化为 refs (3.3+)。

也可以基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

const state = reactive({
  foo: 1,
  bar: 2
})

// 双向 ref,会与源属性同步
const fooRef = toRef(state, 'foo')

// 更改该 ref 会更新源属性
fooRef.value++
console.log(state.foo) // 2

// 更改源属性也会更新该 ref
state.foo++
console.log(fooRef.value) // 3

computed() 计算

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。

创建一个只读的计算属性 ref:

<template>
    <div class="Person">
        <span>First Name: <input type="text" v-model="firstname"></span> <br>
        <span>Last Name: <input type="text" v-model="lastname"></span> <br>
        <span>Full Name: <input type="text" v-model="fullname"></span>
    </div>
</template>

<script setup>
import { computed, ref } from "vue"

let firstname = ref('zhang')
let lastname = ref('san')
let fullname = computed(() => {
    return firstname.value.slice(0, 1).toUpperCase() + firstname.value.slice(1) + " " + lastname.value;
})
</script>

创建一个可写的计算属性 ref:

<template>
    <div class="Person">
        <span>First Name: <input type="text" v-model="firstname"></span> <br>
        <span>Last Name: <input type="text" v-model="lastname"></span> <br>
        <span>Full Name: <input type="text" v-model="fullname"></span>
        <span>修改名字: <button @click="changeName">修改</button></span>
    </div>
</template>

<script setup>
import { computed, ref } from "vue"

let firstname = ref('zhang')
let lastname = ref('san')

let fullname = computed({
    get() {
        return firstname.value.slice(0, 1).toUpperCase() + firstname.value.slice(1) + " " + lastname.value;
    },
    set(value) {
        firstname.value = value.split(" ")[0];
        lastname.value = value.split(" ")[1];
    }
})

function changeName() {
    fullname.value = "li si";
}
</script>

可写ref

调试:

const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

watch() 监听

// 侦听单个来源
function watch<T>(
  source: WatchSource<T>,
  callback: WatchCallback<T>,
  options?: WatchOptions
): WatchHandle

// 侦听多个来源
function watch<T>(
  sources: WatchSource<T>[],
  callback: WatchCallback<T[]>,
  options?: WatchOptions
): WatchHandle

type WatchCallback<T> = (
  value: T,
  oldValue: T,
  onCleanup: (cleanupFn: () => void) => void
) => void

type WatchSource<T> =
  | Ref<T> // ref
  | (() => T) // getter
  | (T extends object ? T : never) // 响应式对象

interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean // 默认:false
  deep?: boolean | number // 默认:false
  flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  once?: boolean // 默认:false (3.4+)
}

interface WatchHandle {
  (): void // 可调用,与 `stop` 相同
  pause: () => void
  resume: () => void
  stop: () => void
}

watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

第一个参数是侦听器的。这个来源可以是以下几种:

  • 一个函数,返回一个值
  • 一个 ref
  • 一个响应式对象
  • ...或是由以上类型的值组成的数组

第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。

第三个可选的参数是一个对象,支持以下这些选项:

  • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
  • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。在 3.5+ 中,此参数还可以是指示最大遍历深度的数字。参考深层侦听器
  • flush:调整回调函数的刷新时机。参考回调的刷新时机watchEffect()
  • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器
  • once:(3.4+) 回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。

watchEffect() 相比,watch() 使我们可以:

  • 懒执行副作用;
  • 更加明确是应该由哪个状态触发侦听器重新执行;
  • 可以访问所侦听状态的前一个值和当前值。

情况一:监视【ref】定义的基本数据

<template>
    <div class="Person">
        <h2>情况一:监视【ref】定义的基本数据</h2>
        <p>当前点击次数 {{ count }}</p>
        <span> <button @click="getSum">点击+1</button></span>
    </div>
</template>

<script setup>
import { watch, ref } from "vue"
let count = ref(0)

function getSum() {
    count.value++
}

const stop1 = watch(count, (newValue, oldValue) => {
    if (newValue > 5) {
        stop1()
    }
    console.log(newValue, oldValue)
})
</script>

停止侦听器:

const stop = watch(source, callback)

// 当已不再需要该侦听器时:
stop()

暂停/恢复侦听器:3.5+

const { stop, pause, resume } = watch(() => {})

// 暂停侦听器
pause()

// 稍后恢复
resume()

// 停止
stop()

情况二:监视【ref】定义的对象数据

监视 ref 定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。

<template>
    <div class="Person">
        <h2>情况二:监视【ref】定义的对象数据</h2>
        <p>姓名: {{ person.name }}</p>
        <p>年龄: {{ person.age }}</p>
        <span> <button @click="changeName">修改姓名</button></span>
        <span> <button @click="changePerson">修改人</button></span>
    </div>
</template>

<script setup>
import { watch, ref } from "vue"
//#region 情况二:监视【ref】定义的对象数据
let person = ref({
    name: "张三",
    age: 18
})
function changeName() {
    person.value.name += "~"
}

function changePerson() {
    person.value = {
        name: "王五",
        age: 20
    }
}

watch(person, (newValue, oldValue) => {
    console.log(newValue, oldValue)
})
//#endregion
</script>

上面代码,点击 changeName 是没有进入watch的,除非加入 deep

watch(person, (newValue, oldValue) => {
    console.log(newValue, oldValue)
}, {
    immediate: false, //初始化时执行
    deep: true //监听对象内部属性的修改
})

image

注意:
  • 若修改的是 ref 定义的对象中的属性,newValueoldValue 都是新值,因为它们是同一个对象。
  • 若修改整个 ref 定义的对象,newValue 是新值,oldValue 是旧值,因为不是同一个对象了。

情况三:监视【reactive】定义的对象数据,且默认开启深度监视

<template>
    <div class="Person">
        <h2>情况三:监视【reactive】定义的对象数据,且默认开启深度监视</h2>
        <p>姓名: {{ person.name }}</p>
        <p>年龄: {{ person.age }}</p>
        <span> <button @click="changeName">修改姓名</button></span>
        <span> <button @click="changePerson">修改人</button></span>
    </div>
</template>

<script setup>
import { reactive, watch } from "vue"

let person = reactive({
    name: "张三",
    age: 18
})
function changeName() {
    person.name += "~"
}

function changePerson() {
    Object.assign(person, {
        name: "王五",
        age: 20
    })
}

watch(person, (newValue, oldValue) => {
    console.log(newValue, oldValue)
})
</script>

情况四:监视 refreactive 定义的【对象类型】数据中的某个属性(侦听一个 getter 函数)

监视 refreactive 定义的【对象类型】数据中的某个属性,注意点如下:

  1. 若该属性值不是【对象类型】,需要写成函数形式。
  2. 若该属性值依然是【对象类型】,可直接编,也可写成函数,建议写成函数。
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

下面再来个例子:

<template>
    <div class="Person">
        <h2>情况三:监视【reactive】定义的对象数据,且默认开启深度监视</h2>
        <p>姓名: {{ person.name }}</p>
        <p>年龄: {{ person.age }}</p>
        <p>车1: {{ person.Car.Car1 }}</p>
        <p>车2: {{ person.Car.Car2 }}</p>
        <span> <button @click="changeCar">修改车1</button></span>
        <span> <button @click="changeCar2">修改车2</button></span>
        <span> <button @click="changeAllCar">修改整车</button></span>
    </div>
</template>

<script setup>
import { reactive, watch } from "vue"

let person = reactive({
    name: "张三",
    age: 18,
    Car: {
        Car1: "奔驰",
        Car2: "宝马"
    }
})
//对象属性换掉,调用 watch(person.Car,...
function changeCar() {
    person.Car.Car1 = "兰博基尼"
}
//对象属性换掉,调用 watch(person.Car,...
function changeCar2() {
    person.Car.Car2 = "路虎"
}
//整个对象换掉,调用 watch(() => person.Car,...
function changeAllCar() {
    person.Car = { Car1: "法拉利", Car2: "保时捷" }
}

watch(person.Car, (newValue, oldValue) => {
    console.log("直接写对象:", newValue, oldValue)
})

watch(() => person.Car, (newValue, oldValue) => {
    console.log("对象函数:", newValue, oldValue)
})
</script>

优化使用方法:

//既监视对象,也监视对象属性
watch(() => person.Car, (newValue, oldValue) => {
    console.log("对象函数:", newValue, oldValue)
}, { deep: true })

情况五:当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

watchEffect 自动化监视依赖

  • 官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
  • watch 对比 watchEffect
    1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
    2. watch:要明确指出监视的数据
    3. watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
const count = ref(0)

watchEffect(() => console.log("输出:", count.value))
// -> 输出 0

count.value++
// -> 输出 1

页面加载,这里会打印:

输出: 0
输出: 1

标签的 ref Attributes

用于注册模板引用

两种方式:

方式一:useTemplateRef

方式二:ref

<template>
    <div>
        <p ref="age2">Age: {{ age + 1 }}</p>
        <p ref="age3">Age: {{ age + 2 }}</p>
        <button @click="showRef2">查看ref2</button>
        <button @click="showRef3">查看ref3</button>
    </div>
</template>

<script setup name="Person">
import { ref, useTemplateRef } from "vue"

let age = ref(19)  // 响应式数据

let ref2 = useTemplateRef('age2') //方式一
let age3 = ref()  //方式二

function showRef2() {
    console.log(ref2.value) //<p>Age: 20</p>
}

function showRef3() {
    console.log(age3.value) //<p>Age: 21</p>
}
</script>

如果用于普通 DOM 元素,引用将是元素本身;如果用于子组件,引用将是子组件的实例。

或者 ref 可以接收一个函数值,用于对存储引用位置的完全控制:

<ChildComponent :ref="(el) => child = el" />

关于 ref 注册时机的重要说明:因为 ref 本身是作为渲染函数的结果来创建的,必须等待组件挂载后才能对它进行访问。

Props

一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute (关于透传 attribute,我们会在专门的章节中讨论)。

在使用 <script setup> 的单文件组件中,props 可以使用 defineProps() 宏来声明:

vue

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

在没有使用 <script setup> 的组件中,props 可以使用 props 选项来声明:

export default {
  props: ['foo'],
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.foo)
  }
}

注意传递给 defineProps() 的参数和提供给 props 选项的值是相同的,两种声明方式背后其实使用的都是 props 选项。

除了使用字符串数组来声明 props 外,还可以使用对象的形式:

// 使用 <script setup>
defineProps({
  title: String,
  likes: Number
})
// 非 <script setup>
export default {
  props: {
    title: String,
    likes: Number
  }
}

对于以对象形式声明的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。比如,如果要求一个 prop 的值是 number 类型,则可使用 Number 构造函数作为其声明的值。

对象形式的 props 声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在浏览器控制台中抛出警告。我们将在本章节稍后进一步讨论有关 prop 校验的更多细节。

如果你正在搭配 TypeScript 使用 <script setup>,也可以使用类型标注来声明 props:

<script setup lang="ts">
defineProps<{
  title?: string
  likes?: number
}>()
</script>

例子:

src\types\index.ts

export interface Person {
  id: string;
  name: string;
  age: number;
}

export type PersonList = Person[];

src\App.vue

<script setup lang="ts">
import { reactive, ref } from 'vue'
import Prop from './components/Prop.vue'
// 响应式状态
import { type PersonList } from '@/types';

let persons: PersonList = reactive([
  { name: '张三', age: 18, id: '1' },
  { name: '王五', age: 19, id: '2' }
])
</script>

<template>
  <Prop :thePersons="persons"></Prop>
  <!-- <Prop></Prop> -->
</template>

src\components\Prop.vue

<template>
    <div class="Prop">
        <h3>人员列表</h3>
        <ul>
            <li v-for="person in thePersons" :key="person.id">
                {{ person.name }} - {{ person.age }}岁
            </li>
        </ul>
    </div>
</template>

<script setup name="Prop" lang="ts">
import { type PersonList } from '@/types';
import type { PropType } from 'vue';

//1 只接收
// defineProps(["thePersons"]);

//2 接收 + 限制
//2.1 
// defineProps<{
//     thePersons: PersonList
// }>();
//2.2.1
// defineProps({ thePersons: Array as PropType<PersonList> });
//2.2.2
// defineProps({ thePersons: { type: Array as PropType<PersonList> } });


//3 接收 + 限制类型 + 限制必要性 + 指定默认值
// 3.1
withDefaults(defineProps<{ thePersons?: PersonList }>(),
    { thePersons: () => [{ id: "0", name: "默认姓名", age: 22 }] }
);
// 3.2
// defineProps({
//     thePersons: {
//         type: Array as PropType<PersonList>,
//         required: true,
//         default: () => [{ id: 1, name: "啊啊啊", age: 22 }]
//     }
// });

</script>

<style scoped>
.Prop {
    margin: 20px;
    padding: 10px;
    border: 1px solid #ccc;
}
</style>

响应式: 进阶

exported_image (4)

customRef()

创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。

  • 类型

    function customRef<T>(factory: CustomRefFactory<T>): Ref<T>
    
    type CustomRefFactory<T> = (
      track: () => void,
      trigger: () => void
    ) => {
      get: () => T
      set: (value: T) => void
    }
    
  • 详细信息

    customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 tracktrigger 两个函数作为参数,并返回一个带有 getset 方法的对象。

    一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。

  • 示例

    创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:

    import { customRef } from 'vue'
    
    export function useDebouncedRef(value, delay = 200) {
      let timeout
      return customRef((track, trigger) => {
        return {
          get() {
            track() //告诉Vue数据msg很重要你要对msg进行持续关注,一旦msg变化就去更新
            return value
          },
          set(newValue) {
            clearTimeout(timeout)
            timeout = setTimeout(() => {
              value = newValue
              trigger() //通知vue,数据变化了
            }, delay)
          }
        }
      })
    }
    

    在组件中使用:

    <script setup>
    import { useDebouncedRef } from './debouncedRef'
    const text = useDebouncedRef('hello')
    </script>
    
    <template>
      <input v-model="text" />
    </template>
    

CSS 功能

Vue 3 单文件组件(.vue)的 <style> 标签提供了一系列强大的 CSS 特性,核心目标是样式隔离灵活的样式定制,同时支持预处理器和动态样式绑定。

一、样式作用域隔离

通过 scoped 属性实现组件样式的局部隔离,避免样式污染全局。

核心原理

  • 给组件根元素及内部元素自动添加唯一 data-v-xxx 属性。
  • CSS 选择器会自动拼接该属性,仅匹配当前组件内的元素。

基础用法

<style scoped>
.title {
  color: red; /* 仅作用于当前组件的 .title 元素 */
}
</style>

全局样式混合

一个组件可同时包含 scoped 和非 scoped 样式:

    <style>
    /* 全局样式 */
    body { margin: 0; }
    </style>

    <style scoped>
    /* 局部样式 */
    .box { padding: 20px; }
    </style>

穿透 scoped 样式

当需要修改子组件(或第三方组件)内部样式时,使用 深度选择器

  • :deep():Vue 3 推荐写法

    <style scoped>
    /* 修改子组件 .child 类的样式 */
    :deep(.child) {
      font-size: 14px;
    }
    </style>
    
  • 兼容写法:>>>(适用于 CSS)、/deep/(适用于预处理器如 Sass)。

插槽选择器

默认情况下,作用域样式不会影响到 <slot/> 渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用 :slotted 伪类以明确地将插槽内容作为选择器的目标:

<style scoped>
:slotted(div) {
  color: red;
}
</style>

二、CSS 模块

通过 module 属性将样式转换为 CSS 模块,实现类名私有化动态绑定

  1. 基础用法
    • 样式类名会被编译为唯一哈希值,避免冲突。
    • 通过 $style 对象访问类名:
      <template>
        <div :class="$style.container">Hello</div>
      </template>
      
      <style module>
      .container {
        background: #f5f5f5;
        padding: 10px;
      }
      </style>
      
  2. 自定义模块名称
    module 指定名称,通过对应名称访问类名:
    <style module="myStyle">
    .text { color: blue; }
    </style>
    
    <template>
      <div :class="myStyle.text">CSS Module</div>
    </template>
    
  3. 组合使用
    支持结合 scoped、预处理器,也可在 <script setup> 中通过 useCssModule 访问:
    <script setup>
    import { useCssModule } from 'vue'
    const myStyle = useCssModule('myStyle') // 对应 style module="myStyle"
    </script>
    

三、预处理器支持

Vue SFC 原生支持 CSS 预处理器(如 Sass/Scss、Less、Stylus),只需安装对应依赖即可使用。

  1. 安装依赖
    # Sass/Scss
    npm install -D sass
    # Less
    npm install -D less
    
  2. 使用方法
    通过 lang 属性指定预处理器:
    <style lang="scss" scoped>
    $primary-color: #42b983;
    .btn {
      background: $primary-color;
    }
    </style>
    

四、CSS 变量注入

Vue 3 支持将组件的响应式数据注入到 CSS 中,实现动态样式绑定,核心是 v-bind() 函数。

  1. 基础用法
    • <style> 中通过 v-bind() 绑定 <script> 中的变量。
    • 变量更新时,CSS 样式会自动响应式变化:
      <script setup>
      import { ref } from 'vue'
      const color = ref('red') // 响应式变量
      </script>
      
      <template>
        <div class="text">Hello</div>
      </template>
      
      <style scoped>
      .text {
        /* 绑定响应式变量 */
        color: v-bind(color);
      }
      </style>
      
  2. 复杂类型支持
    支持绑定对象、计算属性,也可直接传入字符串值:
    <script setup>
    const theme = {
      bg: '#f5f5f5',
      padding: '20px'
    }
    </script>
    
    <style scoped>
    .box {
      background: v-bind('theme.bg');
      padding: v-bind('theme.padding');
    }
    </style>
    

五、其他特性

  1. src 导入外部样式
    可通过 src 属性引入外部 CSS 文件,支持结合 scoped/module
    <style scoped src="./styles/global.scss"></style>
    
  2. PostCSS 支持
    项目根目录创建 postcss.config.js 即可自动启用 PostCSS(如 autoprefixer 自动补全浏览器前缀)。

核心特性总结表

特性 核心作用 关键属性 典型场景
作用域样式 局部样式隔离 scoped 组件样式不污染全局
CSS 模块 类名私有化+动态绑定 module 大型项目避免类名冲突
预处理器 增强 CSS 语法 lang="scss/less" 样式嵌套、变量复用
CSS 变量注入 响应式样式绑定 v-bind() 主题切换、动态颜色

我可以帮你整理SFC CSS 特性的实战代码模板,包含作用域、CSS 模块、变量注入的完整示例,需要吗?

Vue 生命周期

Vue 2 生命周期(选项式 API)

  1. 初始化阶段
    • beforeCreate:实例创建前调用,数据观测和事件配置尚未完成
    • created:实例创建完成,已完成数据观测,但未挂载到 DOM
  2. 挂载阶段
    • beforeMount:模板编译完成,即将挂载到 DOM
    • mounted:组件已挂载到 DOM,可访问 DOM 元素(常用)
  3. 更新阶段
    • beforeUpdate:数据更新时触发,DOM 尚未重新渲染
    • updated:数据更新并完成 DOM 重新渲染后触发
  4. 销毁阶段
    • beforeDestroy:实例销毁前调用,仍可访问组件实例
    • destroyed:实例销毁后调用,所有事件监听和子组件已移除
生命周期钩子 执行时机 核心用途(常见场景)
beforeCreate 实例创建前,数据 / 事件未初始化 几乎不用(无访问实例的能力)
created 实例创建完成,数据已初始化 发起异步请求(如接口调用)、初始化数据
beforeMount 模板编译完成,DOM 未挂载 可修改数据(不会触发额外渲染)
mounted DOM 挂载完成 操作 DOM(如初始化第三方插件)、绑定事件
beforeUpdate 数据更新,DOM 未重新渲染 读取当前 DOM 状态(如获取输入框值)
updated 数据更新,DOM 已重新渲染 基于新 DOM 执行操作(如滚动定位)
beforeDestroy 组件销毁前,实例仍可用 清理资源(清除定时器、解绑事件)
destroyed 组件销毁完成,实例不可用 几乎不用(资源已清理)

Vue 3 生命周期流程图(组合式 API 为主)

Vue 3 钩子(组合式) 对应选项式钩子 执行时机 核心用途
- beforeCreate 实例创建前,数据 / 事件未初始化 几乎不用
- created 实例创建完成,数据已初始化 发起异步请求(组合式中可直接在 setup 写)
onBeforeMount beforeMount 模板编译完成,DOM 未挂载 修改数据(不触发额外渲染)
(常用)onMounted mounted DOM 挂载完成 操作 DOM、绑定事件(最常用)
onBeforeUpdate beforeUpdate 数据更新,DOM 未重新渲染 读取当前 DOM 状态
(常用)onUpdated updated 数据更新,DOM 已重新渲染 基于新 DOM 执行操作
(常用)onBeforeUnmount beforeUnmount 组件卸载前,实例仍可用 清理资源(清除定时器、解绑事件)
onUnmounted unmounted 组件卸载完成,实例不可用 几乎不用
onRenderTracked - 响应式数据被追踪时(开发环境) 调试响应式依赖(如 “谁触发了渲染”)
onRenderTriggered - 响应式数据更新触发渲染时(开发) 调试渲染触发源(如 “哪个数据变了”)
sequenceDiagram participant C as 组件实例 participant V as Vue引擎 participant D as DOM节点 participant K as KeepAlive容器 %% 初始化阶段 note over C,V: 初始化阶段 V->>C: 创建组件实例 C->>C: script setup顶层代码执行(等价于created+beforeMount) %% 挂载阶段 note over C,D: 挂载阶段 <br/> 核心渲染流程 V->>C: 触发onBeforeMount(DOM未生成,模板编译完成) V->>D: 渲染并挂载DOM到页面 V->>C: 触发onMounted(DOM渲染完成,可操作DOM/请求数据) %% 组件活跃状态 note over C: 活跃状态 <br/> 等待数据变化 C->>C: 处于活跃状态,等待响应式数据变化 %% 更新阶段(可循环) note over C,D: 更新阶段 <br/> 数据驱动DOM更新 V->>C: 响应式数据变化,触发onBeforeUpdate(数据已变,DOM未更) V->>D: 重新渲染并更新DOM节点 V->>C: 触发onUpdated(DOM已更新,处理后续逻辑) C->>C: 回到活跃状态 %% KeepAlive专属阶段 note over C,K: KeepAlive专属 <br/> 组件缓存激活/失活 K->>C: 触发onActivated(keep-alive组件激活时) K->>C: 触发onDeactivated(keep-alive组件失活时) %% 调试/错误处理钩子 note over C,V: 调试/错误处理 <br/> 开发调试专用 V->>C: 触发onErrorCaptured(捕获子组件错误) V->>C: 触发onRenderTracked(追踪渲染依赖,调试用) V->>C: 触发onRenderTriggered(查看渲染原因,调试用) %% 卸载阶段 note over C,D: 卸载阶段 <br/> 组件销毁清理 V->>C: 触发组件卸载(v-if=false/路由切换等) V->>C: 触发onBeforeUnmount(清理定时器/事件监听) V->>D: 移除DOM节点 V->>C: 触发onUnmounted(组件实例完全销毁)

onMounted()

注册一个回调函数,在组件挂载完成后执行。

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

const el = ref()

onMounted(() => {
  el.value // <div>
})
</script>

<template>
  <div ref="el"></div>
</template>

注册一个回调函数,在组件挂载完成后执行。

Vue 2 vs Vue 3 生命周期核心区别对比

对比维度 Vue 2 Vue 3
核心命名差异 销毁阶段:beforeDestroy/destroyed 卸载阶段:beforeUnmount/unmounted(更语义化)
API 风格支持 仅选项式 API(钩子写在组件选项中) 1. 选项式 API(兼容 Vue 2 写法) 2. 组合式 API(onXxx 函数,推荐)
新增钩子 onRenderTracked/onRenderTriggered(开发调试用)
入口差异 无特殊入口 组合式 API 需通过 setup() 函数定义钩子(Vue 3.2+ 也支持 <script setup> 语法糖)
执行时机兼容 - 除 “销毁→卸载” 命名外,其他钩子执行时机完全对齐 Vue 2,迁移成本低

关键总结(避坑指南)

  1. 最常用钩子:无论 Vue 2 还是 Vue 3,mounted(Vue 3 为 onMounted)和 “卸载 / 销毁” 钩子(onUnmounted/destroyed)是开发中最常用的 —— 前者做初始化,后者做资源清理。
  2. Vue 3 迁移注意:若从 Vue 2 迁移到 Vue 3,只需将 beforeDestroy 改为 beforeUnmountdestroyed 改为 unmounted(选项式 API);若用组合式 API,直接导入 onBeforeUnmount/onUnmounted 即可。
  3. 调试钩子onRenderTrackedonRenderTriggered 仅在开发环境生效,生产环境会被移除,可用于定位 “不必要的渲染” 问题。

Vue Hooks

在 Vue 中,Hooks(钩子函数)是 Composition API 中用于封装和复用逻辑的重要概念,类似于 React Hooks,但有其自身特点。Vue Hooks 允许你将组件逻辑抽离为可重用的函数,使代码结构更清晰、复用性更高。

核心特点:

  1. 逻辑复用:将组件中的复杂逻辑(如数据请求、表单处理、定时器等)抽离为独立的 Hook 函数,可在多个组件中复用。
  2. 组合式编程:通过组合多个 Hooks 实现复杂功能,替代了 Options API 中按选项(data、methods 等)划分的方式。
  3. 与组件生命周期关联:可以在 Hook 中使用 onMountedonUpdated 等生命周期钩子。
  4. 响应式集成:自然配合 refreactive 等响应式 API 使用,维护组件状态。

常见内置 Hooks(Vue 3):

Vue 提供了一系列内置 Hooks 用于处理组件生命周期、依赖注入等:

  • 生命周期类onMountedonUpdatedonUnmountedonBeforeMount
  • 依赖注入provideinject
  • 状态管理useAttrsuseSlotsuseCssModule
  • 其他watchwatchEffectcomputed(严格来说是响应式 API,但常与 Hooks 配合使用)

自定义 Hooks

https://cn.vuejs.org/guide/reusability/composables.html#conventions-and-best-practices

最常用的场景是创建自定义 Hooks,命名通常以 use 开头(如 useFetchuseForm)。

示例:创建一个处理数据请求的自定义 Hook

// hooks/useFetch.js
import { ref, onMounted } from "vue";

export default function (url: string) {
  const data = ref(null);
  const loading = ref(true);
  const error = ref(null);

  const fetchData = async () => {
    try {
      loading.value = true;
      const response = await fetch(url);
      data.value = await response.json();
    } catch (err: any) {
      error.value = err.message;
    } finally {
      loading.value = false;
    }
  };

  // 在组件挂载时执行请求
  onMounted(fetchData);

  // 返回响应式数据和方法
  return {
    data,
    loading,
    error,
    refetch: fetchData, // 提供重新请求的方法
  };
}

组件中使用 Hook

<template>
    <div>
        <div v-if="loading">加载中...</div>
        <div v-if="error">错误: {{ error }}</div>
        <ul v-if="data">
            <li v-for="item in data" :key="item.id">{{ item.name }}</li>
        </ul>
        <button @click="refetch">重新加载</button>
    </div>
</template>

<script setup>
import useFetch from '@/hooks/useFetch'
// // 直接复用请求逻辑
const { data, loading, error, refetch } = useFetch('https://api.example.com/data')

console.log(data.value);
</script>

image

VueUse 开源库

https://vueuse.org/

中文版 https://vueuse.nodejs.cn/functions.html#category=传感器&sort=category

场景分类 常用 Hooks 核心作用
基础工具 useCounteruseToggleuseClamp 计数器、布尔切换、文本截断
本地存储 useStorageuseLocalStorageuseSessionStorage 响应式本地存储
浏览器 API useWindowSizeuseScrolluseClipboard 窗口尺寸、滚动、剪贴板
网络请求 useFetchuseAxios 简化请求逻辑
状态管理 useState 轻量级跨组件状态共享
事件监听 useEventListeneruseClickOutside 事件绑定、外部点击监听
定时器 useTimeoutuseInterval 安全的定时器管理
DOM 操作 useElementSizeuseIntersectionObserver 元素尺寸、视口监听
主题 / 样式 useDarkuseTheme 暗黑模式、主题切换

常用项目解构

  • src
    • App.vue // 项目根组件,一级路由出口
    • api // 接口请求
    • assets// 静态资源
      • icons //svg icon 图标
      • images //image 图标。比如:xxx.png
      • logo.png // logo
    • components // 通用的业务组件。比如:一个组件在多个页面中使用到
    • constants // 常量
    • directives // 自定义指令
    • libs // 通用组件,可用于构建中后台物料库或通用组件库
    • main.js // 入口文件
    • permission.js // 页面权限控制中心
    • router// 路由
      • index.js // 路由处理中心
      • modules// 路由模块
        • mobile-routes.js // 移动端路由
        • pc-routes.js // PC 端路由
    • store// 全局状态
      • getters.js // 全局状态访问处理
      • index.js // 全局状态中心
      • modules // 状态子模块
    • styles// 全局样式
      • index.scss // 全局通用的样式处理
    • utils // 工具模块
    • vendor // 外部供应资源。比如:人类行为认证
    • views// 页面组件。与 components 的区别在于:此处组件对应路由表,以页面的形式展示
      • layout// 用于 PC 端,分割一级路由和二级路由
        • components // 该页面组件下的业务组件
        • index.vue //layout 组件
  • tailwind.config.js //tailwind css 配置文件,与 src 平级
  • vite.config.js //vite 配置文件,与 src 平级
posted @ 2025-09-04 15:45  【唐】三三  阅读(72)  评论(0)    收藏  举报