为什么要学vue3
vue3的优势

vue3 组合式API vs vue2选项式 API

// vue2 options API
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
addCount() {
this.count++;
}
}
}
</script>
// vue3 Composition API
<script setup>
import { ref } from 'vue';
const count = ref(0)
const addCount = () => count.value++
</script>
从上述例子中可以看出:
组合式API相比 代码量 变少了- 分散式维护转为集中式维护,更易封装复用。
官方新的脚手架工具create-vue
create-vue是 vue官方新的脚手架工具,底层由原来Vue-CLI的webpack切换到了vite(新的构建工具),为开发提供极速响应。

使用create-vue创建项目
- 前提环境条件: 已安装
16.0或 更高版本的Node.js - 创建一个
Vue应用
// 创建命令
npm init vue@latest
// 该指令将会 安装并执行 create-vue
- 项目目录结构
——.vscode
——node_modules
——public
——src
——assets
——components
——App.vue // SFC单文件组件 script、template、style
// 变化一:script 和 template 顺序调整
// 变化二:template 不再要求唯一根元素
// 变化三:script 添加 setup 标识 支持 组合式API
——main.js // createApp函数创建应用实例
——.eslintrc.cjs
——env.d.ts // ts的声明文件。 识别.ts\.css\.scss\.js\.jsx等各个文件的声明
——.gitignore
——index.html // 单文件入口,提供id为app的挂载点
——package-lock.json
——package.json // 核心依赖项 变成了 vue3.x 和 vite
——README.md
——vite.config.js // 项目的配置文件 基于 vite的配置
组合式API —— setup
- 时机:在
beforeCreate钩子之前,自动执行
- 写法:定义
数据+函数然后以对象方式return
<script>
export default {
name: 'Person',
setup() {
// 数据
const msg = '这是一条数据';
// 函数
const logMsg = () => {
console.log(msg)
}
return {
msg,
logMsg
}
}
}
<script>
<template>
<!-- 使用数据和方法 -->
{{ msg }}
<button @click="logMsg">按钮</button>
</template>
- 语法糖(简单写法)
// 组件的配置
<script>
export default {
name: 'Person'
}
</script>
// setup处理
<script setup>
// 数据
const msg = '这是一条数据';
// 函数
const logMsg = () => { console.log(msg) }
</script>
安装插件 vite-plugin-vue-setup-extend
安装命令npm i vite-plugin-vue-setup-extend -D
然后再 vite.config.ts 中引入 插件
// vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueSetupExtend from 'vite-plugin-vue-setup-extend'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueSetupExtend()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
即可将上述代码 继续简化为
// setup处理
<script setup name='Person'>
// 数据
const msg = '这是一条数据';
// 函数
const logMsg = () => { console.log(msg) }
</script>
注意:
setup中的this指向的是undefined,不是组件实例了
组合式API —— reactive
- 作用: 接受
对象类型数据的参数传入并返回一个响应式的对象
<script setup>
// 导入
import { reactive } from 'vue'
// 执行函数、 传入参数、 变量接收
const state = reactive(对象类型数据)
</script>
// 访问
模版中:{{ state }}
script中: console.log(state)
- 坑点:
reactive定义数组 —— Vue 3 的reactive仅对初始对象进行响应式包装,直接赋值新对象会丢失响应性。
const tableData = reactive([]) // 定义
// 错误修改示例
const getData = async () => {
const { data } = await getDataApi(); // 模拟接口,获取数据
tableData = data; // 错误修改示例,这样会导致 tableData 的响应式丢失。
}
// 正确操作
// 方式一: 使用 ref 定义
// 方式二:先Object.assign
Object.assign(tableData, data)
// 方式三:定义时,包裹成对象,通过访问对象属性,重新赋值
const tableData = reactive({ list: [] })
tableData.list = data;
组合式API —— ref
- 作用:接收
简单类型 或 对象类型的数据的参数传入并返回一个响应式的对象
<script setup>
// 导入
import { ref } from 'vue'
// 执行函数、 传入参数、 变量接收
const state = ref(简单类型 或 复杂类型数据)
// 访问
模版中:{{ state }}
script中: console.log(state.value)
</script>
reactive vs ref对比
宏观角度看:
1、
ref用来定义:基本类型数据、对象类型数据2、
reactive用来定义: 对象类型数据
区别:
1、
reactive不能处理简单类型的数据。reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)。2、
ref参数类型支持更好,但是必须通过.value访问修改。(可以使用插件Vue - Official自动添加.value)
- vscode中扩展中,添加插件后,打开:
设置——扩展——Vue——勾选 Auto Insert: Dot Value3、
ref函数的内部实现依赖于reactive函数
使用原则:
1、若需要一个基本类型的响应式数据,必须使用
ref。2、若需要一个响应式对象,层级不深,
ref、reactive都可以。3、若需要一个响应式对象,且层级较深,推荐使用
reactive
toRefs与toRef: 用于处理响应式对象属性的工具函数,它们的主要作用是将响应式对象的属性转换为独立的ref对象,同时保持与源对象的响应式连接。
toRefs
功能:
- 将 整个响应式对象的所有属性 转换为多个独立的
ref,并保持与源对象的同步。- 适用于解构
reactive对象时保持响应性。特点:
- 批量转换:
- 返回一个对象,所有属性都被转换成
ref。
- 适用于解构
reactive对象。使用场景:
- 在
<script setup>或组合式函数中解构reactive对象时,避免丢失响应性。
toRef
功能:
- 将 响应式对象的某个属性 转换为一个单独的
ref,并保持与源对象的同步。- 如果源对象属性变化,
toRef生成的ref也会更新,反之亦然。特点:
- 适用于单个属性。
使用场景:
- 在组合式函数(Composables)中返回某个响应式对象的属性,并希望保持响应性。
核心区别
| 特性 | toRef | toRefs |
|---|---|---|
| 作用对象 | 单个属性 | 整个响应式对象的所有属性 |
| 返回值 | 单个 ref | 包含所有 ref 的对象 |
| 适用场景 | 需要单独处理某个属性 | 解构 reactive 对象时保持响应性 |
| 修改同步性 | 修改 ref 会同步更新源对象 | 修改任意 ref 会同步更新源对象 |
例1:
<script setup>
import { reactive } from 'vue'
// 此时 person 是响应式数据
let person = reactive({ name: '张三', age: 18 })
// 此时,解构的name, age是基本类型,只是读取了person对象中的 name 和age字段的值。不再是响应式数据。
let { name, age } = person;
</script>
那么如何将 name, age变成响应式的呢?
<script setup>
import { reactive, toRefs, toRef } from 'vue'
// 此时 person 是响应式数据
let person = reactive({ name: '张三', age: 18 })
// 此时,name, age 也是响应式的数据了。
let { name, age } = toRefs(person);
// 此时 n, a 也是响应式数据了。
let n = toRef(person, 'name')
let a = toRef(person, 'age')
</script>
标签的ref属性
- 用在普通
DOM标签上,获取的是DOM节点 - 用在组件标签上,获取的是组件实例对象。访问组件实例对象中的
属性和方法,需要在组件中通过defineExpose暴露给父组件。
// 1、用在普通 DOM 标签上
<script setup lang="ts" name="HelloWorld">
import { ref } from 'vue';
let sum = ref(0)
const divRef = ref(); // 创建一个devRef, 用于存储 ref 标记的内容
const showLog = () => {
console.log(divRef.value)
}
</script>
<template>
<div ref="divRef">{{ sum }}</div>
<button @click="showLog">输入h2</button>
</template>
// 2、用在组件标签上:示例请查看下方 defineExpose 讲解
组合式API —— computed
与 vue2 相比,仅修改了写法。
<script setup>
// 1、导入 computed 函数
import { computed, ref } form 'vue'
const state = ref({})
// 执行函数、变量接收、在回调参数中 return 计算值
const computedState = computed(() => {
return 基于响应式数据做计算之后的值
})
</script>
组合式API —— watch
-
作用:侦听
一个 或 多个数据的变化,数据变化时执行回调函数。- 侦听 单个数据变化
<script setup> // 导入 watch import { ref, watch } from 'vue' const count = ref(0) // 调用watch 侦听 单个数据变化 watch(count, (newValue, oldValue) => { console.log(`count 发生变化,变化前的值${oldValue},变化后的值${newValue}`) }) // 解除watch 监听 const stopWatch = watch(count, (newValue, oldValue) => { if (count > 10) { stopWatch() } }) </script>- 侦听 多个数据变化:不管哪个数据变化,都需要执行回调
<script setup> // 导入 watch import { ref, reactive, watch } from 'vue' const count = ref(0) const name = ref('张三') const person = reactive({ name: '张三', age: 18 }) // 调用watch 侦听 单个数据变化 ref watch([count, name], (newValue, oldValue) => { console.log(`count 发生变化,变化前的值${oldValue},变化后的值${newValue}`) }, { immediate: true, // 在侦听器创建时,立即触发回调 deep: true // 开启深度监听。 监听器默认是 浅层监听对象 的 }) // 调用watch 侦听 reactive 对象的数据变化 watch(person, (newValue, oldValue) => { console.log(person) // reactive 监听默认开启深度监听, 即 隐式创建了深度监听, // 并且该深度监视无法通过 {deep: false} 关闭 }) </script>- 侦听 对象的某个属性, 即一个函数,返回一个值
<script setup> // 导入 watch import { ref,reactive, watch } from 'vue' const info = ref({ name: '张三', age: 18 }) const person = reactive({ name: '张三', age: 18, car: { c1: '奥迪', c2: '宝马' } }) // 调用watch 侦听 单个数据变化 watch( () => info.value.age, (newValue, oldValue) => { console.log(`count 发生变化,变化前的值${oldValue},变化后的值${newValue}`) }) // 调用watch 监听 对象。此时监听的是 地址值,若想监视对象内部属性的变化,需要手动添加深度监听 watch(info, (newVal, oldVal) => { console.log('监听的是对象的 地址值') }) const changeC1 = () => { person.car.c1 = '大众' } const changeCar = () => { person.car = { c1: '雅迪', c2: '爱玛' } } // 监听响应式对象中的某个属性,且该属性是对象类型的: // 1、可以直接写 person.car。此时监听时,只有car内部变化(changeC1方法),才会触发监听。 // 当触发changeCar 时,无法监听到car的变化 // // 2、也能写函数 () => person.car。 此时可以监听到changeCar触发的car变化。即:对象的地址值, //若想监听对象内部的变化,此时需要手动开启深度监听 {deep: true}。 //(官方更推荐该方式) watch(() => person.car, (newVal, oldVal) => { console.log('person.car 变化了', '官方推荐的方式') }, {deep: true}) </script>
watchEffect:立即运行一个函数,同时响应式的追踪其依赖,并在依赖更改时重新执行该函数。
与watch的区别:
watch监听: 需要明确指出监听的数据
watchEffect监听: 不用明确指出监听的数据,函数中用到哪些属性,就会监听哪些属性
// 使用 watch 监听: 需要明确指出监听的数据
<script setup>
import { ref, watch } from 'vue';
const a = ref(0);
const b = ref(0);
const changeA = () => { a += 1; }
const changeB = () => { b += 1; }
watch([a, b], (newVal) => {
const [_a, _b] = newVal;
if (_a > 10 || _b > 10 ) {
console.log('监听达到条件后,触发执行')
}
})
</script>
// 使用 watchEffect 监听: 不用明确指出监听的数据,函数中用到哪些属性,就会监听哪些属性
<script setup>
import { ref, watchEffect } from 'vue';
const a = ref(0);
const b = ref(0);
const changeA = () => { a += 1; }
const changeB = () => { b += 1; }
watchEffect(() => {
if (a.value > 10 || b.value > 10 ) {
console.log('监听达到条件后,触发执行')
}
})
</script>
vue3生命周期API
| 选项式API | 组合式API |
|---|---|
beforeCreate/created | setup |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
- 生命周期函数基本使用
import { onMounted } from 'vue'
onMounted(() => {
// 自定义逻辑
})
- 执行多次
生命周期函数 是可以 执行多次的,多次执行时 传入的回调 会在 时机成熟时依次执行
import { onMounted } from 'vue'
onMounted(() => {
console.log('mount1')
})
onMounted(() => {
console.log('mount2')
})
vue3 组件通信
1、组合式API下的父传子:通过defineProps传递
- 父组件中给
子组件绑定属性
<script setup>
// 引入子组件
import SonVue from './son.vue';
import Person from './components/Person.vue';
import { type Persons } from '@/types';
let person = reactive<Persons>([
{ id: '1', name: '张三', age: 18 },
{ id: '2', name: '李四', age: 18 },
])
</script>
<template>
<Person :list="person" />
<SonVue msg='这是父组件传给子组件的一个值' />
</template>
- 子组件内部通过
props选项接收
// 通过 defineProps “编译器宏”接收子组件传递的数据
<script setup>
import { defineProps } from 'vue';// 由于 defineProps 是宏函数,在vue3中可以省略引入,直接使用
// 方式一:
const props = defineProps({
list: String
})
// 方式二:
import { type Persons } from '@/types'
const props = defineProps<{ list: Persons }>();
</script>
<template>
<div v-for="item in list">{{ item.name }}</div>
{{ msg }}
<template>
可以通过
withDefaults给defineProps设置默认值
// list 是否必传,并设置默认值
const props = withDefaults(defineProps<{ list?: Persons }>(), {
list: () => [
{id :'1', name: '王五', age: 18}
]
})
2、组合式API下的子传父: 通过defineEmits 事件传递
- 父组件中给
子组件标签通过@绑定事件
<script setup>
// 引入子组件
import SonVue from './son.vue'
const getMsg = (msg) => {
console.log(msg)
}
</script>
<template>
<SonVue @getMsgEvent="getMsg" />
</template>
- 子组件内部通过
emit方法触发事件
<script setup>
// 通过 defineEmits “编译器宏”生成emit方法
const emit = defineEmits(['getMsgEvent'])
const sendMsg = () => {
emit('getMsgEvent', '子组件回传给父组件的内容')
}
</script>
<template>
<button @click="sendMsg">发送数据</button>
<template>
3、组合式API-provide和inject
- 作用:顶层组件向任意的底层组件
传递数据和方法,实现跨层组件通信。1、顶层组件通过
provide函数提供数据2、底层组件通过
inject函数获取数据// 组件嵌套关系 // parentPage => childPage => grandSonPage 1、顶层组件 `parentPage` <script setup> import { provide } from 'vue' import childPage from './childPage.vue' // 1、顶层组件提供数据 provide('data-key', 'this is from parentPage data') </script> <template> <div class='page'> 顶层组件 <childPage /> </div> </template> 2、底层组件 `grandSonPage` <script setup> import { inject } from 'vue' // 2、接收数据 const parentData = inject('data-key') </script> <template> <div class='grandSonPage'> 底层组件 <div>来自顶层组件中的数据:{{ parentData }}</div> <div>来自顶层组件中的响应式数据:</div> </div> </template>
组合式API - 模版引用
1、概念
通过 ref标识获取真实的dom对象 或者 组件实例对象。因此,获取模版引用必须在组件挂载完毕。
2、使用
<script setup>
import { ref } from 'vue'
import TestCom from './test-com.vue';
// 1、调用 ref 函数得到 ref 对象
const h1Ref = ref(null);
const comRef = ref(null);
// 组件挂载完毕之后才能获取
onMounted(() => {
console.log(h1Ref.value)
console.log(comRef.value)
})
</script>
<template>
<!-- 2、通过 ref 标识绑定 ref对象 -->
<h1 ref="h1Ref">我是dom标签h1</h1>
<TestCom ref="comRef" />
</template>
3、defineExpose
使用<script setup>的组件是 默认关闭 的——即通过 模版引用 或者 $parent链获取到的组件的实例,不会 暴露 任何在 <script setup>中 声明的绑定。
可以通过 defineExpose编译器宏来显示指定在<script setup>组件中要暴露出去的属性 和 方法:
// 子组件 —— HelloWorld.vue
<script setup lang="ts" name="HelloWorld">
import { ref, defineExpose } from 'vue'; // 由于 defineExpose 是宏函数,在vue3中可以省略引入,直接使用
let sum = ref(0)
let count = ref(1)
let msg = ref('这是一条消息')
defineExpose({ sum, msg })
//
</script>
// 父组件 —— App.vue
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import { ref } from 'vue';
const hello_world = ref();
const show = () => { //这里可以拿到 sum, msg 但是拿不到 count
console.log(hello_world.value)
}
</script>
<template>
<HelloWorld ref="hello_world" />
<button @click="show">点击获取ref标记的内容</button>
</template>
当父组件通过 模版引用 的方式 获取到当前组件的实例,获取到的实例会像这样 { sum, msg }(ref会和在普通实例中一样被自动解包)
vue3新特性—— defineOptions
顾名思义,主要是用来定义Options API的选项。可以用defineOptions定义任意的选项, props、emits、expose、slots除外(因为这些可以使用defineXXX来做到)。
<script setup>
defineOptions({
name: 'Foo',
inheritAttrs: false,
// ... 更多自定义属性
})
</script>
vue3新特性——defineModel
在vue3中,自定义组件上使用 v-model,相当于传递一个modelValue属性,同时触发update:modelValue事件。
<Child v-model="isVisible" />
// 相当于
<Child :modelValue="isVisible" @update:modelValue="isVisible=$event" />
与 之前 先定义 props,再定义 emits 的方式相比,简化了很多代码。
<script setup>
const modeValue = defineModel()
modelValue.value++;
</script>
vue3自定义指令

// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router. from './router';
// 引入初始化样式文件
import '@/styles/common.scss';
// useIntersectionObserver 方法判断图片是否进入视口。通过第三方插件`vueuse`
import { useIntersectionObserver } from '@vueuse/core';
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
// 自定义全局指令
app.directive('img-lazy', {
mounted(el, binding) {
// el: 指令绑定的那个元素 例如:img
// binding:binding.value 指令等于号后面绑定的表达式的值。 例如:图片url
useIntersectionObserver(
el,
([{ isIntersecting }])=> {
if (isIntersecting) {
// 进入视口区域
el.src = binding.value;
}
}
)
}
})
// 指令使用,页面文件home.vue
<template>
<div v-for="item in list" :key="item.id">
<img v-img-lazy='item.picture' alt='' />
</div>
</template>
vue3-自定义插件
// directive/index.js
import { useIntersectionObserver } from '@vueuse/core';
// 自定义懒加载插件
export const lazyPlugin = {
install (app) {
app.directive('img-lazy', {
mounted(el, binding) {
// el: 指令绑定的那个元素 例如:img
// binding:binding.value 指令等于号后面绑定的表达式的值。 例如:图片url
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }])=> {
if (isIntersecting) {
// 进入视口区域
el.src = binding.value;
stop(); // 第一次完成赋值后,停止监听
}
}
)
}
})
}
}
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router. from './router';
// 引入初始化样式文件
import '@/styles/common.scss';
// 引入懒加载指令插件并注册
import { lazyPlugin } from '@/directive';
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(lazyPlugin)
app.mount('#app')
vue3-组件全局插件化注册
// components/index.js
// 将 components 中 通用型的所有组件都进行全局化注册
// 通过插件的方式,避免每次使用时都导入一次组件
import ImageView from './ImageView/index.vue';
import Sku from './sku/index.vue';
export const componentPlugin = {
install(app) {
// app.component('组件名', 组件配置对象)
app.component('ImageView', ImageView);
app.component('Sku', Sku);
}
}
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router. from './router';
// 引入初始化样式文件
import '@/styles/common.scss';
// 引入懒加载指令插件并注册
import { lazyPlugin } from '@/directive';
// 引入全局组件插件
import { componentPlugin } from '@/components';
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(lazyPlugin)
app.use(componentPlugin)
app.mount('#app')