Vue3
Vue3最大的变化可能就是JS的书写方式,推荐使用组合式API。为了和以前的选项式API进行区分,有了setup入口函数,只有在setup函数中才能使用组合式API。也就是说整个script标签中,export一个带有setup函数的对象就可以了。一个.vue文件如下所示
<script >
export default {
setup() {
const message = 'Welcome to Vue 3!'
return { message}
}
}
</script>
<template>
<div>{{message}}</div>
</template>
setup函数返回的对象的属性在模板中使用。但这样使用setup有点麻烦(先声明变量然后返回),于是有了setup语法糖<script setup> ,标签中顶层导入的对象,声明的变量和方法直接可以在template中使用,不用再return了。<script setup>还有一个好处是直接使用TS,在script标签中写lang='ts'
<script setup lang='ts'>
const message: string = 'Welcome to Vue 3!'
</script>
注意,<script setup>只是语法糖,Vue在编译期间自动将此语法中声明的所有变量和函数转换为适当的 setup() 返回对象。但此时message变化,模板并没有变化,要把message变成响应式数据。响应式数据就是谁使用了这个数据,这个数据发生变化后,谁就会自动变化。
const obj = { text: 'ok' } function update() { document.body.innerHTML = obj.text} update()
obj.text = 'hello' // 页面并没有发生变化
update使用了obj.text,但obj.text变化,update并没有重新执行,所以obj并不是响应式对象。
const obj = { text: 'ok' } let activeEffect const set = new Set() const ref = new Proxy(obj, { get(target, key) { set.add(activeEffect) return target[key] }, set(target, key, value) { target[key] = value; set.forEach(effect => effect()) } }) function effectFn(effect) { activeEffect = effect; effect() } function update() { document.body.innerHTML = ref.text } effectFn(update) setTimeout(() => { ref.text = 'hello' // 页面发生变化。}, 3000);
ref对象发生变化,update函数重新执行了,ref对象就是响应式数据。响应式数据的本质是它内部监听数据的使用,把使用者记录下来,当改变数据的时候,把使用者重新执行一遍,所以JS中不存在响应式数据,使用响应式数据,就要手动创建。在Vue中,ref()函数创建一个带value属性的响应式对象并返回,这个对象称为ref对象。访问ref对象的.value属性,就收集它的使用者,給.value设置值时,重新调用使用者。模板中使用ref对象,对象发生变化,模板重新渲染,所以只要数据变化,模版需要重新执行,就要创建和使用响应式对象。在模板中,写ref对象.value比较麻烦,vue做了特殊处理,可以直接使用ref。
<template>
<div>{{ message }}</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const message = ref("Hello World")
setTimeout(() => { message.value = "Vue3"}, 1000);
</script>
ref接受数组和对象时,返回的是深度响应对象, 对象及其嵌套属性发生变化,也会出触发更新
import { ref } from 'vue'
const user = ref({ name: "Maya", age: 20 })
user.value.name = "Rachel"
user.value = { name: "Samuel", age: 20}
即可以改变单个属性的值,比如name,又可以整体替换对象。有人觉得不好,建议使用shallowRef()和reactive()函数。如果创建响应式对象并在稍后将其替换为新值,使用shallowRef() ,比如异步数据的获取。如果创建响应式对象并仅更新其属性,使用reactive() 。
import { reactive } from 'vue'
const user = reactive({ name: "Maya", age: 20})
user.name = "Rachel"
user.age = 30
需要注意的是,reactive()返回的是传入对象的代理对象。对返回对象进行任何更改,都会反映在原始对象上,反之亦然。
import { reactive } from 'vue'
const defaultUser = {name: "Maya", age: 20}
const user = reactive(defaultUser)
user.name = "Rachel"
console.log(defaultUser) // { name: "Rachel", age: 20 }
defaultUser.name = "Samuel"
console.log(user) // { name: "Samuel", age: 20 }
defaultUser和user相互影响。因此,在使用reactive()时,使用展开语法 (...) 来创建一个新对象,然后再传递给reactive()。
import { reactive } from 'vue'
const defaultUser = { name: "Maya", age: 20}
const user = reactive({ ...defaultUser })
reactive()返回的对象也是深度响应,如果只想观察根对象的属性,而不观察其后代对象的属性,使用shallowReactive()
计算属性是根据响应式数据计算出或派生出的一个新响应式数据,因此,它能缓存,依赖的数据没变,就不用重新计算,也是因为这个,它仅做计算用,不该有任何副作用,把它当成只读的。computed() 方法期接收一个函数,函数的返回值就是一个计算属性。
<script setup lang="ts"> import { computed, reactive } from 'vue' const user = reactive({ name: "Maya", age: 20 }) const description = computed(() =>user.name + user.age) </script>
<template> <div>{{ description }}</div> </template>
现在computed接受的函数,可以接受一个参数,用于获取上一次计算属性的值
const alwaysSmall = computed((previous) => count.value <= 3 ? count.value : previous)
watch函数监听任何响应式数据的变化并触发回调函数,当监听的ref对象的value属性值是基本类型时,可以直接监听这个ref
<script setup lang="ts"> import { ref, watch } from 'vue' const count = ref(0) watch(count, (newCout, oldCount) => { console.log(`newCout is ${newCout}, oldCount is ${oldCount}`)}) </script> <template> <div>{{ count }}</div> <button @click="count++">点击</button> </template>
当ref对象的value值是一个对象时,要监听.value,
const count = ref({ number: 0 })
watch(count.value, (newCout, oldCount) => {
console.log(newCout === oldCount)
console.log(`newCout is ${newCout.number}, oldCount is ${oldCount.number}`)
})
</script>
<template>
<div>{{ count.number }}</div>
<button @click="count.number++">点击</button>
</template>
但看到控制台的输出,不太对,newCount 和oldCount相等,因为默认监听地址的变化,要想获取旧值,只能使用getter函数监听某个属性。
watch(() => count.value.number, (newCout, oldCount) => {
console.log(`newCout is ${newCout}, oldCount is ${oldCount}`)
})
监听reactive对象,也是一样,
const count = reactive({ number: 0 })
// 直接给 watch() 传入一个reactive对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发
watch(count, (newCount, oldCount) => { // newCount和oldCount都指向了新值,其实不用oldCount也无所谓
console.log(`newCout is ${newCount.number}, oldCount is ${oldCount.number}`)
})
watch(() => count.number, (newCount, oldCount) => { // 不能直接监听属性值,要使用getter函数。
console.log(`newCout is ${newCount}, oldCount is ${oldCount}`)
})
如果监听的响应式数据正好是回调函数中使用的数据,直接watchEffect,watchEffect() 自动跟踪回调的响应式依赖,依赖的数据发生变化,回调函数重新执行,但在回调函数重新执行之前,先执行清理函数。副作用函数准备下一次重新运行之前,都会先执行清理函数。onWatcherCleanup() 注册一个清理函数,当然组件卸载也会执行清理函数。watchEffect需要注意,它的回调会立即执行不需要指定 immediate: true。
<template>
<div>{{ id }}</div>
<button @click="id++">点击</button>
</template>
<script setup>
import { watchEffect, ref, toValue, onWatcherCleanup } from 'vue'
const id = ref(1)
watchEffect(() => { // id 发生变化,依赖的数据发生变化,回调函数重新执行,也称侦听器失效,
const controller = new AbortController()
// 监听id的变化,并使用id
fetch(`https://jsonplaceholder.typicode.com/todos/${toValue(id)}`, { signal: controller.signal })
.then(() => {}).catch(() => {})
onWatcherCleanup(() => { controller.abort() }) // 终止过期请求
})
Vue是单项数据流,父组件只能给子组件传递数据(通过props),子组件只能发射事件给父组件(自定义事件)。这时最好和TS相结合,表明组件接受什么属性,发射什么事件。新建TaskItem.vue
<template> <!-- template可以有多个根 -->
<div>{{ title }}</div>
<div>{{ description }}</div>
<input type="checkbox" :checked="completed" @change="onTaskCompleted"></input>
</template>
<script setup lang="ts">
import { watch } from 'vue';
type Task = { id: number, title: string; description?: string; completed: boolean } // prop名建议使用驼峰命名,greetingMessage:string
type EmitEvents = {
'task-completed-toggle': [task: Task] //key是事件名称,建议kebab-case命名,task是这次事件携带的数据
}
// const props = defineProps<Task>() // defineProps() 应该是编译指令,不需要import
const {id, title, description = "default", completed } = defineProps<Task>() //props可以解构赋值
const emit = defineEmits<EmitEvents>()
const onTaskCompleted = (event: Event) => {
emit("task-completed-toggle", {
id,
completed: (event.target as HTMLInputElement)?.checked,
});
}
// 当解构赋值传递给watch时,要注意使用getter
watch(() => title, () => { })
</script>
App.js中,
<template> <!-- 向子组件传递props可以使用camelCase形式,也可以使用kebab-case形式。建议使用kebab-case形式来监听自定义事件。组件可以使用驼峰命名了--> <!-- id=“1”表示有一个id属性,它的值是1,但:id=“id”,这是绑定,id属性绑定到哪里,此时字符串里面是任意的js表达式,数字,布尔值,undefined或null(id属性会被忽略),对象,数组, 变量,算术,布尔表达式等 --> <!-- v-bind=某个对象,相当于: :id="task.id" :title="task.title" 所有的属性都遍历一遍 --> <TaskItem v-bind="task" @task-completed-toggle="toggle" /> </template> <script setup> import { ref } from 'vue' import TaskItem from './TaskItem.vue'; const task = ref({"id": 1, "title": "学习Vue3", description: "看官方文档", "completed": false }) const toggle = (t) => { task.completed = t.completed } </script>
props 逐级透传的问题,有了provide和inject。在父组件provide()函数提供值,provide第一个参数是key,第二个参数是 value,所有后代组件通过inject(key)就能获取到value。v-model 也能用到组件上。
某些特殊的情况下,需要直接操作子组件,就使用ref。App.vue,
// 第一个参数必须与模板中子组件上的的 ref 属性值一模一样 const childRef = useTemplateRef('child') // childRef.value 将持有子组件的实例 <TaskItem v-bind="task" ref="child" />
子组件TaskItem,需要用defineExpose把属性和方法暴露出来,
defineExpose({ completed }) // 父组件直接操作completed属性
组件生命周期,需要注意,销毁变成了beforeUnmount and ummounted , 其他不常用,因为计算属性和监听器,很少直接使用update生命周期。setup虽然不是生命周期,但是当setup执行完以后,也相当于执行了beforeMounted created, 只要在这个两个生命周期中执行的代码,都可以在setup中直接写,也不用这两个生命周期。“Because setup is run around the beforeCreate and created lifecycle hooks, you do not need to explicitly define them. In other words, any code that would be written inside those hooks should be written directly in the setup function.” 在script 中可以直接 const fetchPosts = () => { ... } fetchPosts(); 来获取数据。
当slot 要暴露出数据时,必须使用v-bind 在slot标签上暴露出来,TaskItem.vue 改成
import { ref } from 'vue';
const items = ref([{
name: "Item 1",
description: "This is item 1",
thumbnail:
"https://res.cloudinary.com/mayashavin/image/upload/v1643005666/Demo/supreme_pizza",
}])
</script>
<template>
<ul><li v-for="item in items" :key="item.name">
<!-- v-slot 暴露数据 -->
<slot name="thumbnail" :item="item" />
<slot :item="item"></slot>
</li></ul>
</template>
App.vue
<template>
<!-- #slot名=“{暴露出来的数据}” 获取到命名slot暴露出来的数据, #thumbnail="{ item }"
v-slot= “{暴露出来的数据}” 获取到default slot暴露出来的数据。 -->
<TaskItem>
<template #thumbnail="{ item }">
<img v-if="item.thumbnail" :src="item.thumbnail" width="200" />
</template>
<template v-slot="{ item }">
<img v-if="item.thumbnail" :src="item.thumbnail" width="200" />
<div>{{ item.name }}</div>
</template>
</TaskItem>
</template>
动态组件,新建两个组件,Mike.vue(<template>Steve is on </template>)和Steve.vue(<template>Mike is on </template>)
然后在App.vue中,引入
<script setup lang="ts">
import { shallowRef } from 'vue';
import Mike from './Mike.vue';
import Steve from './Steve.vue';
const activeComponent = shallowRef(Mike);
</script>
<template>
<button @click="activeComponent = Mike">Mike组件</button>
<button @click="activeComponent = Steve">Steve组件</button>
<component :is="activeComponent"></component>
</template>
component的is属性绑定是组件,整个组件都可以作为一个变量,绑定哪个组件,哪个组件渲染。动态组件有一个问题,就是组件来回切换时,组件是卸载,再重新加载。如果不想组件卸载,可以使用keep-alive把compoent包起来。
<KeepAlive> <component :is="activeComponent"></component> </KeepAlive>
此时组件多了两个生命周期,
<template> Mike is on</template>
<script setup>
import { onActivated, onDeactivated } from 'vue';
onActivated(() => console.log('Mike is active'))
onDeactivated(() => console.log('Mike is deactive'))
</script>
如果只想缓存某个组件
<!-- include组件的名字,根据.vue文件名推断出来的 -->
<KeepAlive include="Mike">
<component :is="activeComponent"></component>
</KeepAlive>
The <Teleport> component accepts a prop to , which indicates the target container, 把 sky and coulds放到了id为sky 的元素下面。弹窗的实现方式。teleport还有一个属性diasable,只要true的时候才显示。在渲染teleport之前,一定确保id=sky 的元素存在。
<template>
<div> This is a house</div>
<Teleport to="#sky">
<div>Sky and clouds</div>
</Teleport>
</template>
自定义指令:指令都是直接操作dom元素的,自定义指令使开发者能够封装可重用的 DOM 操作逻辑。在Main.ts中,app.directive 创建全局指令
app.directive("awesome", {
// el 绑定到的dom元素,binding指令绑定的值
beforeMount(el, binding) {
if(binding.arg === 'red') {
el.style.color = 'red'
} else {
el.style.color = 'green'
}
el.innerHTML = binding.value;
}
})
使用的时候
<!-- 字符串的写法 --> <div v-awesome:red="'al'"></div>
transition 有一个mode 属性,当一个状态控制两个元素的显示和隐藏时,一个元素显示,另一个元素隐藏,给每一个元素设置唯一的key,然后mode 设为out-in
Vue路由最大的变化是useRouter和useRoute函数,主要是在script中获取route和router信息。route是一条路由的信息,包含locaiton,path等,Router是一个路由对象,有着push,go等跳转功能,用于编程式导航。useRoute返回的对象也是一个响应式对象,它的变化也会引起模板的渲染,如果现在route中解析出query,用let query = toRefs(route)
RouterView 相当于 <router-view v-slot="{Component, route}"> <component :is=Component :key="route.path"> </router-view> .这么写的好处是 keep-alive 或transition 把组件包起来。key 强制在复用的组件之间进行过渡。
全新的状态管理库pinia,核心还是store,用defineStore创建,两个参数,一个是store名,一个是对象,对象可以有state,getters和actions三个属性,state就是共享的状态,getters是状态派生出来的状态,通常是数据转换,不用每个组件都转换。actions则是更新状态,支持异步操作。和Vuex相比,Pinia去掉了mutation,action可以是异步,更为简洁。
export const useTodoListStore = defineStore('todoList', {
state: () => ({
todoList: []
}),
getters: {
count: (state) => state.todoList.length,
},
actions: {
fetchTodoList() {
fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json())
.then(json => this.todoList = json) // 通过this获取状态中的数据
}
}
})
在组件中使用,
<template>
<!-- <RouterView></RouterView> -->
<ul>
<h1>数量:{{ store.count }}</h1>
<li v-for="item in store.todoList" :key="item.id">
{{ item.title }}
</li>
</ul>
</template>
<script setup lang="ts">
import { useTodoListStore } from './store/noticeCount';
const store = useTodoListStore() // 所有组件都是拿到同一个store实例,数据在所有组件之间都是全局共享的。
store.fetchTodoList() // 调用action更新状态,全局中所有使用store的状态都会改变
</script>
当从store中获取多个属性时,可能想解构赋值,但此时需要对store进行包裹
const { count, todoList } = storeToRefs(store); // 必须使用storeToRefs,要不然失去响应式,只解构state和getters, 千万不要解构action,因为它里面有this,this绑定消失。
第二种定义store的方式是,第二个参数是一个函数,返回一个对象,对象中,状态是用 ref()或 reactive()方法创建的的响应式数据,getters是使用 computed()方法创建的计算属性,action是函数,完全和Vue的setup函数定义一样。
export const useTodoListStore = defineStore('todoList', () => {
const todoList = reactive([])
const count = computed(() => todoList.length)
const fetchTodoList = () => {
fetch('https://jsonplaceholder.typicode.com/todos')
.then(response => response.json())
.then(json => Object.assign(todoList, json))
}
return {todoList, count, fetchTodoList}
})
Pinia中的store都很小,因此它是一个多store。一个store 中,可以直接使用另外一个store,这也符合Vue中的组合式API

浙公网安备 33010602011771号