bilibili:https://www.bilibili.com/video/BV1Ac411K7EQ/
简单谈谈Vue3的感悟,感觉没有说的那么方便和利于维护,写起来一堆变量 不像Vue2有区域划分,找起来快
认识Vue3
Vue3
Vue3组合式API体验
通过 Counter 案例 体验Vue3新引入的组合式API
<script>
export default {
data(){
return {
count:0
}
},
methods:{
addCount(){
this.count++
}
}
}
</script>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const addCount = ()=> count.value++
</script>
特点:
- 代码量变少
- 分散式维护变成集中式维护
Vue3更多的优势
使用create-vue搭建Vue3项目
认识create-vue
create-vue是Vue官方新的脚手架工具,底层切换到了 vite (下一代前端工具链),为开发提供极速响应
使用create-vue创建项目
前置条件 - 已安装16.0或更高版本的Node.js
执行如下命令,这一指令将会安装并执行 create-vue
npm init vue@latest
熟悉项目和关键文件
组合式API - setup选项
setup选项的写法和执行时机
写法
<script>
export default {
setup(){
},
beforeCreate(){
}
}
</script>
执行时机
在beforeCreate钩子之前执行
setup中写代码的特点
在setup函数中写的数据和方法需要在末尾以对象的方式return,才能给模版使用
<script>
export default {
setup(){
const message = 'this is message'
const logMessage = ()=>{
console.log(message)
}
// 必须return才可以
return {
message,
logMessage
}
}
}
</script>
<script setup>语法糖
script标签添加 setup标记,不需要再写导出语句,默认会添加导出语句
<script setup>
const message = 'this is message'
const logMessage = ()=>{
console.log(message)
}
</script>
组合式API - reactive和ref函数
reactive
接受对象类型数据的参数传入并返回一个响应式的对象
<script setup>
// 导入
import { reactive } from 'vue'
// 执行函数 传入参数 变量接收
const state = reactive({
msg:'this is msg'
})
const setSate = ()=>{
// 修改数据更新视图
state.msg = 'this is new msg'
}
</script>
<template>
{{ state.msg }}
<button @click="setState">change msg</button>
</template>
ref
接收简单类型或者对象类型的数据传入并返回一个响应式的对象
<script setup>
// 导入
import { ref } from 'vue'
// 执行函数 传入参数 变量接收
const count = ref(0)
const setCount = ()=>{
// 修改数据更新视图必须加上.value
count.value++
}
</script>
<template>
<button @click="setCount">{{count}}</button>
</template>
reactive 对比 ref
- 都是用来生成响应式数据
- 不同点
- reactive不能处理简单类型的数据
- ref参数类型支持更好,但是必须通过.value做访问修改
- ref函数内部的实现依赖于reactive函数
- 在实际工作中的推荐
- 推荐使用ref函数,减少记忆负担,小兔鲜项目都使用ref
组合式API - computed
计算属性基本思想和Vue2保持一致,组合式API下的计算属性只是修改了API写法
<script setup>
// 导入
import {ref, computed } from 'vue'
// 原始数据
const count = ref(0)
// 计算属性
const doubleCount = computed(()=>count.value * 2)
// 原始数据
const list = ref([1,2,3,4,5,6,7,8])
// 计算属性list
const filterList = computed(() => {
return list.value.filter(item => item > 2);
});
</script>
可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>
现在当你再运行 fullName.value = 'John Doe'
时,setter 会被调用而 firstName
和 lastName
会随之更新。
计算属性缓存 vs 方法
你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:
<p>{{ calculateBooksMessage() }}</p>
// 组件中
function calculateBooksMessage() {
return author.books.length > 0 ? 'Yes' : 'No'
}
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books
不改变,无论多少次访问 publishedBooksMessage
都会立即返回先前的计算结果,而不用重复执行 getter 函数。
这也解释了为什么下面的计算属性永远不会更新,因为 Date.now()
并不是一个响应式依赖:
const now = computed(() => Date.now())
相比之下,方法调用总是会在重渲染发生时再次执行函数。
为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list
,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list
。没有缓存的话,我们会重复执行非常多次 list
的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。
最佳实践
Getter 不应有副作用
计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。
避免直接修改计算属性值
从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。
组合式API - watch
侦听一个或者多个数据的变化,数据变化时执行回调函数,俩个额外参数 immediate控制立刻执行,deep开启深度侦听
1.侦听数据源类型
watch
的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX, oldX) => {
console.log(`x is ${newX},x was ${oldX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
</script>
注意,你不能直接侦听响应式对象的属性值,例如:
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})
这里需要用一个返回该属性的 getter 函数:
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
2. immediate
在侦听器创建时立即出发回调,响应式数据变化之后继续执行回调
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const count = ref(0)
// 2. 调用watch 侦听变化
watch(count, (newValue, oldValue)=>{
console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)
},{
immediate: true
})
</script>
3. deep
通过watch监听的ref对象默认是浅层侦听的,直接修改嵌套的对象属性不会触发回调执行,需要开启deep
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const state = ref({ count: 0 })
// 2. 监听对象state
watch(state, ()=>{
console.log('数据变化了')
})
const changeStateByCount = ()=>{
// 直接修改不会引发回调执行
state.value.count++
}
</script>
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const state = ref({ count: 0 })
// 2. 监听对象state 并开启deep
watch(state, ()=>{
console.log('数据变化了')
},{deep:true})
const changeStateByCount = ()=>{
// 此时修改可以触发回调
state.value.count++
}
</script>
4.一次性侦听器
每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true
选项。
watch(
source,
(newValue, oldValue) => {
// 当 `source` 变化时,仅触发一次
},
{ once: true }
)
5.watchEffect()
侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId
的引用发生变化时使用侦听器来加载一个远程资源:
const todoId = ref(1)
const data = ref(null)
watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
},
{ immediate: true }
)
特别是注意侦听器是如何两次使用 todoId
的,一次是作为源,另一次是在回调中。
我们可以用 watchEffect
函数来简化上面的代码。watchEffect()
允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
这个例子中,回调会立即执行,不需要指定 immediate: true
。在执行期间,它会自动追踪 todoId.value
作为依赖(和计算属性类似)。每当 todoId.value
变化时,回调会再次执行。有了 watchEffect()
,我们不再需要明确传递 todoId
作为源值。
你可以参考一下这个例子的 watchEffect
和响应式的数据请求的操作。
对于这种只有一个依赖项的例子来说,watchEffect()
的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect()
可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect()
可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
TIP
watchEffect
仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await
正常工作前访问到的属性才会被追踪。
6.watch
vs. watchEffect
watch
和 watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
组合式API - 生命周期函数
1. 选项式对比组合式
2. 生命周期函数基本使用
- 导入生命周期函数
- 执行生命周期函数,传入回调
<scirpt setup>
import { onMounted } from 'vue'
onMounted(()=>{
// 自定义逻辑
})
</script>
3. 执行多次
生命周期函数执行多次的时候,会按照顺序依次执行
<scirpt setup>
import { onMounted } from 'vue'
onMounted(()=>{
// 自定义逻辑
})
onMounted(()=>{
// 自定义逻辑
})
</script>
4.生命周期图示
下面是实例生命周期的图表。你现在并不需要完全理解图中的所有内容,但以后它将是一个有用的参考。
组合式API - 父子通信
1. 父传子
基本思想
- 父组件中给子组件绑定属性
- 子组件内部通过props选项接收数据
2. 子传父
基本思想
- 父组件中给子组件标签通过@绑定事件
- 子组件内部通过 emit 方法触发事件
组合式API - 模版引用
概念:通过 ref标识 获取真实的 dom对象或者组件实例对象
1. 基本使用
实现步骤:
- 调用ref函数生成一个ref对象
- 通过ref标识绑定ref对象到标签
![image.png](https://img-blog.csdnimg.cn/img_convert/85e8229bd1ec2a5bc793cc8b3f78d0e5.png)
2.组件上的 ref
这一小节假设你已了解组件的相关知识,或者你也可以先跳过这里,之后再回来看。
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
// child.value 是 <Child /> 组件的实例
})
</script>
<template>
<Child ref="child" />
</template>
如果一个子组件使用的是选项式 API 或没有使用 <script setup>
,被引用的组件实例和该子组件的 this
完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
有一个例外的情况,使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>
当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number }
(ref 都会自动解包,和一般的实例一样)。
组合式API - 依赖注入
1.provide()
提供一个值,可以被后代组件注入。
-
类型
function provide<T>(key: InjectionKey<T> | string, value: T): void
-
详细信息
provide()
接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。当使用 TypeScript 时,key 可以是一个被类型断言为
InjectionKey
的 symbol。InjectionKey
是一个 Vue 提供的工具类型,继承自Symbol
,可以用来同步provide()
和inject()
之间值的类型。与注册生命周期钩子的 API 类似,
provide()
必须在组件的setup()
阶段同步调用。 -
示例
<script setup> import { ref, provide } from 'vue' import { countSymbol } from './injectionSymbols' // 提供静态值 provide('path', '/project/') // 提供响应式的值 const count = ref(0) provide('count', count) // 提供时将 Symbol 作为 key provide(countSymbol, count) </script>
2.inject()
注入一个由祖先组件或整个应用 (通过 app.provide()
) 提供的值。
-
类型
// 没有默认值 function inject<T>(key: InjectionKey<T> | string): T | undefined // 带有默认值 function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T // 使用工厂函数 function inject<T>( key: InjectionKey<T> | string, defaultValue: () => T, treatDefaultAsFactory: true ): T
-
详细信息
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,
inject()
将返回undefined
,除非提供了一个默认值。第二个参数是可选的,即在没有匹配到 key 时使用的默认值。
第二个参数也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。在这种情况下,你必须将
true
作为第三个参数传入,表明这个函数将作为工厂函数使用,而非值本身。与注册生命周期钩子的 API 类似,
inject()
必须在组件的setup()
阶段同步调用。当使用 TypeScript 时,key 可以是一个类型为
InjectionKey
的 symbol。InjectionKey
是一个 Vue 提供的工具类型,继承自Symbol
,可以用来同步provide()
和inject()
之间值的类型。 -
示例
假设有一个父组件已经提供了一些值,如前面
provide()
的例子中所示:<script setup> import { inject } from 'vue' import { countSymbol } from './injectionSymbols' // 注入不含默认值的静态值 const path = inject('path') // 注入响应式的值 const count = inject('count') // 通过 Symbol 类型的 key 注入 const count2 = inject(countSymbol) // 注入一个值,若为空则使用提供的默认值 const bar = inject('path', '/default-path') // 注入一个值,若为空则使用提供的函数类型的默认值 const fn = inject('function', () => {}) // 注入一个值,若为空则使用提供的工厂函数 const baz = inject('factory', () => new ExpensiveObject(), true) </script>
3. 作用和场景
顶层组件向任意的底层组件传递数据和方法,实现跨层组件通信
4. 跨层传递普通数据
实现步骤
- 顶层组件通过
provide
函数提供数据- 底层组件通过
inject
函数获取数据
5. 跨层传递响应式数据
在调用provide函数时,第二个参数设置为ref对象
6. 跨层传递方法
顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件的数据
综合案例
1. 项目地址
git clone http://git.itcast.cn/heimaqianduan/vue3-basic-project.git
2. 项目说明
- 模版已经配置好了案例必须的安装包
- 案例用到的接口在 README.MD文件 中
- 案例项目有俩个分支,main主分支为开发分支,complete分支为完成版分支供开发完参考
3.代码实现
App.vue
<script setup>
import axios from "axios";
import Edit from "./components/Edit.vue";
import { onMounted, ref } from "vue";
// TODO: 列表渲染
const list = ref([]);
const getList = async () => {
const res = await axios.get("/list");
list.value = res.data;
};
onMounted(() => {
getList();
});
// TODO: 删除功能
const onDelete = async (id) => {
await axios.delete(`/del/${id}'`);
getList();
};
// TODO: 编辑功能
const editRef = ref(null);
const onEdit = (row) => {
// 因为Edit需要使用v-model绑定属性值,所以通过方法的参数实现父传子,不能通过传统的方法
editRef.value.open(row);
};
</script>
<template>
<div class="app">
<el-table :data="list">
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column label="姓名" prop="name" width="150"></el-table-column>
<el-table-column label="籍贯" prop="place"></el-table-column>
<el-table-column label="操作" width="150">
<template #default="{ row }">
<el-button type="primary" @click="onEdit(row)" link>编辑</el-button>
<el-button type="danger" @click="onDelete(row.id)" link
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
<Edit ref="editRef" @on-update="getList" />
</template>
<style scoped>
.app {
width: 980px;
margin: 100px auto 0;
}
</style>
Edit.vue
<script setup>
// TODO: 编辑
import axios from "axios";
import { ref } from "vue";
// 弹框开关
const dialogVisible = ref(false);
const form = ref({
name: "",
place: "",
id: "",
});
const open = (row) => {
console.log(row);
dialogVisible.value = true;
form.value = { ...row };
console.log(form);
};
defineExpose({
open,
});
const emit = defineEmits(["on-update"]);
const onComfirm = async () => {
await axios
.patch(`/edit/${form.value.id}`, {
name: form.value.name,
place: form.value.place,
})
.then(() => {
emit("on-update");
dialogVisible.value = false;
});
};
</script>
<template>
<el-dialog v-model="dialogVisible" title="编辑" width="400px">
<el-form label-width="50px">
<el-form-item label="姓名">
<el-input placeholder="请输入姓名" v-model="form.name" />
</el-form-item>
<el-form-item label="籍贯">
<el-input placeholder="请输入籍贯" v-model="form.place" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onComfirm">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<style scoped>
.el-input {
width: 290px;
}
</style>