Vue - 核心语法

创建

创建vue3

npm create vue@latest
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Nightwatch / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
✔ Add Vue DevTools 7 extension for debugging? (experimental) … No / Yes

Scaffolding project in ./<your-project-name>...
Done.

创建vue2

安装vue-cli

npm install -g @vue/cli

创建

vue create vue2_lifecycle

image

插件

Vue (Official)

Vue VSCode Snippets

根组件

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

挂载应用

应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串:

createApp(App).mount('#app')

应用根组件的内容将会被渲染在容器元素里面。容器元素自己将不会被视为应用的一部分。

.mount() 方法应该始终在整个应用配置和资源注册完成后被调用。同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。

应用配置

应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误:

app.config.errorHandler = (err) => {
  /* 处理错误 */
}

应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件:

app.component('TodoDeleteButton', TodoDeleteButton)

这使得 TodoDeleteButton 在应用的任何地方都是可用的。我们会在指南的后续章节中讨论关于组件和其他资源的注册。你也可以在 API 参考中浏览应用实例 API 的完整列表。

确保在挂载应用实例之前完成所有应用配置!

先来一个简单的例子

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

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

// 响应式状态
let name = ref('张三')
let age = ref(18)
let tel = ref('13800000000')

function showTel() {
    console.log(tel.value)
}
function changeName() {
    name.value = '李四'
}

</script>

核心语法

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 响应式

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 对比

宏观角度看:

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>

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 - 响应式数据更新触发渲染时(开发) 调试渲染触发源(如 “哪个数据变了”)

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

最常用的场景是创建自定义 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

常用项目解构

  • 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  【唐】三三  阅读(24)  评论(0)    收藏  举报