42 KiB
Executable File
vue3
优点
- 打包体积更小
- 渲染速度更快
- 内存占用减少
- 更好的支持TS
- 使用Proxy代替defineProperty实现数据响应式
- 重写虚拟DOM的实现和Tree-Shaking
- 组合API (composition api) ,能够更好的组织逻辑,封装逻辑,复用逻辑
新增特性
- Composition (组合) API
- setup
- ref 和 reactive
- computed 和 watch
- 新的生命周期函数
- provide与inject
- ...
- 新组件
- Fragment - 文档碎片
- Teleport - 瞬移组件的位置
- Suspense - 异步加载组件的loading界面
- 其它API更新
- 全局API的修改
- 将原来的全局API转移到应用对象
- 模板语法变化
使用vite快速创建项目
开发环境下基于浏览器 原生 ES 模块, 有兼容性问题: chrome > 61, firefox >60, safair > 10.1 。在生产环境下基于 Rollup 打包。
使用 npm:
# npm 6.x
$ npm init vite@latest <project-name> --template vue
# npm 7+,需要加上额外的双短横线
$ npm init vite@latest <project-name> -- --template vue
$ cd <project-name>
$ npm install
$ npm run dev
或者 yarn:
$ yarn create vite <project-name> --template vue
$ cd <project-name>
$ yarn
$ yarn dev
或者 pnpm:
$ pnpm create vite <project-name> -- --template vue
$ cd <project-name>
$ pnpm install
$ pnpm dev
Composition API(组合式api)
2.x的叫option API (配置 api)
生命周期
创建 挂载 更新 销毁, 每个阶段都有两个钩子,一前一后
beforeCreate
-> 使用setup()
created
创建实例前 -> 使用setup()
beforeMount
挂载DOM前 ->onBeforeMount
mounted
挂载DOM后 ->onMounted
beforeUpdate
更新组件前 ->onBeforeUpdate
updated
更新组件后 ->onUpdated
beforeDestroy
卸载销毁前 ->onBeforeUnmount
destroyed
卸载销毁后 s->onUnmounted
errorCaptured
->onErrorCaptured
可以多次使用同一个钩子,执行顺序和书写顺序相同。
setup 函数
- setup执行的时机
- 在beforeCreate之前执行(一次), 此时组件对象还没有创建
- ==this是undefined==, 不能通过this来访问data/computed/methods / props
- 其实所有的composition API相关回调函数中也都不可以
- setup的返回值
- 一般都返回一个对象: 为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性/方法
- 若返回一个渲染函数,则可以自定义渲染内容
- 返回对象中的属性会与data函数返回对象的属性合并成为组件对象的属性, 返回对象中的方法会与methods中的方法合并成功组件对象的方法, 如果有重名, setup优先
- 一般不要混用: methods中可以访问setup提供的属性和方法, 但在setup方法中不能访问data和methods
- setup不能是一个async函数,因为使用async后返回值不再是return的对象,而是promise,模板看不到return对象中的属性(后期也可以使用suspense和异步组件的配合得到promise)
- setup的参数
- setup(props, context) / setup(props, {attrs, slots, emit})
- props: 包含props配置声明且传入了的所有属性的对象
- attrs: 包含没有在props配置中声明的属性的对象, 相当于 this.$attrs
- slots: 包含所有传入的插槽内容的对象, 相当于 this.$slots
- emit: 用来分发自定义事件的函数, 相当于 this.$emit
ps: 官方文档里带$的api, 可以在模板里使用, 但不能在setup里使用
setup 语法糖
它向模板公开了所有的顶层绑定
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 Typescript 声明 props 和抛出事件。
- 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
- 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
以 define
开头的 api 都为编译器宏(compiler macros )api,只能在 <script setup>
中使用。它们不需要被导入,并且在处理 <script setup>
时被编译掉。
注意:define
类 api 必须直接在 setup 中外层进行使用,你无法将其放在方法中。
defineProps
// js 基础用法
intetface UserInfo {
name: string,
age: number
}
defineProps({
name: {
type: String,
required: false,
default: 'Petter',
},
userInfo: Object as PropsType<UserInfo>, // 注解 Props https://v3.cn.vuejs.org/guide/typescript-support.html#%E6%B3%A8%E8%A7%A3-props
tags: Array,
})
// 使用编译器宏
const props = defineProps<{
foo: string
bar?: number
}>()
// 为了解决在TS类型声明下无法进行设置默认值, 使用了新的api withDefaults, 这个方法并非属于编译器宏api,但是这个 api 由defineProps衍生而出
withDefaults(defineProps<{
size?: number
labels?: string[]
}>(), {
size: 3,
labels: () => ['default label']
})
两种方法不能同时使用
defindEmits
// 基础用法
// 声明
const emits = defineEmits(['change', 'delete'])
// 使用
emits('change')
// ts声明
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// 使用
emits('change',1)
两种方法不能同时使用
defineExpose
在vue2中, 所有暴露在模板上的东西都隐含地暴露在组件实例上,也就是父组件可以通过ref 或者子链可以全量获取到子组件所有的属性、方法。大多数时候,这种全量暴露是过度的,而 vue3 setup 中必须进行手动暴露。
const a = 1
const b = ref(2)
defineExpose({ a, b, })
useSlots, useAttrs
useSlots: 获取插槽数据
useAttrs: 用来获取 attrs 数据,但和 vue2 不同,里面包含了
class
、属性
、方法
。
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
限制
-
修改选项配置需要单开一个 script
// Vue 3 SFC 一般会自动从组件的文件名推断出组件的 name。在大多数情况下,不需要明确的 name 声明。 // 唯一需要的情况是当你需要 <keep-alive> 包含或排除或直接检查组件的选项时,你需要这个名字。 <script> export default { name: 'YourName', inheritAttrs: false, customOptions: {}, } </script> <script setup> // your code </script>
-
ts兼容
// 通过解构的方式去导入类型,setup sugar 会进行自动导出, 此时ts会报错 //解决方法: import type { test } from "./test"
ref 函数
常用于简单数据类型定义为响应式数据
- 在 script 中修改值,获取值的时候,需要
.value
- 在模板 template 中使用ref声明的响应式数据,不需要.value
- 其实也可以定义复杂数据类型的响应式数据, 如果用ref对象/数组, 内部会自动将对象/数组转换为reactive的代理对象
- 基本数据类型:响应式是靠
Object.defineProperty()
的get
与set
进行设置和取值操作
<template>
<h2>{{count}}</h2>
<hr>
<button @click="update">更新</button>
</template>
<script>
import { ref } from 'vue'
export default {
/* 使用vue3的composition API */
setup () {
// 定义响应式数据 ref对象
const count = ref(1)
console.log(count)
// 更新响应式数据的函数
function update () {
// alert('update')
count.value = count.value + 1
}
return {
count,
update
}
}
}
</script>
- 当你明确知道需要的是一个响应式数据 对象 那么就使用 reactive 即可
- 其他情况使用ref
toRef 函数
转换响应式对象中某个属性为单独响应式数据,并且值是关联的。
- 语法:
const name = toRef(person,"name")
注意: reactive 对象取出的所有属性值都是非响应式的。
使用场景: 有一个响应式对象数据,但是模版中只需要使用其中一项数据。
<template>
<div class="container">
{{name}} <button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { reactive, toRef } from 'vue'
export default {
name: 'App',
setup () {
// 1. 响应式数据对象
const obj = reactive({
name: 'ls',
age: 10
})
console.log(obj)
// 2. 模板中只需要使用name数据
// 注意:从响应式数据对象中解构出的属性数据,不再是响应式数据
// let { name } = obj 不能直接解构,出来的是一个普通数据
const name = toRef(obj, 'name')
// console.log(name)
const updateName = () => {
console.log('updateName')
// toRef转换响应式数据包装成对象,value存放值的位置
name.value += '+'
}
return {name, updateName}
}
}
</script>
<style scoped lang="less"></style>
toRefs 函数
转换响应式对象中所有属性为单独响应式数据,对象成为普通对象,并且值是关联的
- 语法:
const person = toRefs(person)
使用场景:剥离响应式对象(解构|展开),想使用响应式对象中的多个或者所有属性做为响应式数据。
<template>
<div class="container">
<div>{{name}}</div>
<div>{{age}}</div>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { reactive, toRef, toRefs } from 'vue'
export default {
name: 'App',
setup () {
// 1. 响应式数据对象
const obj = reactive({
name: 'ls',
age: 10
})
console.log(obj)
// 2. 解构或者展开响应式数据对象
// const {name,age} = obj
// console.log(name,age)
// const obj2 = {...obj}
// console.log(obj2)
// 以上方式导致数据就不是响应式数据了
const obj3 = toRefs(obj)
console.log(obj3)
const updateName = () => {
// obj3.name.value = 'zs'
obj.name = 'zs'
}
return {...obj3, updateName} //这里用了解构
}
}
</script>
<style scoped lang="less"></style>
isRef
检查值是否为一个 ref 对象。
unref
如果参数是一个 ref
,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val
的语法糖函数
shallowRef函数
shallow: 浅层的
创建一个跟踪自身
.value
变化的 ref,但不会使其值也变成响应式的简单理解: 只处理了value的响应式, 不进行对象的reactive处理
使用场景: 有一个对象数据, 后面会产生新的对象来替换(比如: 动态组件)
let sum = shallowRef(0)
const update1 = ()=>{
sum.value++ // 基础数据类型是响应式的
}
let sum2 = shallowRef({count: 0})
const update2 = ()=> {
sum2.value.count++ // 对象不是响应式的, 视图不更新
}
const update3 = ()=>{
sum2.value = {cont: 3} // value是响应式的, 视图会更新
}
triggerRef函数
手动使 shallowRef 的对象响应一次
let sum2 = shallowRef({count: 0})
const update1 = ()=>{
sum2.value.count++ // 对象不是响应式的, 视图不更新
}
const trigger => {
triggerRef(sum2) // 视图触发更新一次
}
customRef
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收
track
和trigger
函数作为参数,并且应该返回一个带有get
和set
的对象。
使用 customRef 实现 防抖 的示例
import {customRef} from 'vue'
/*
实现函数防抖的自定义ref
*/
function useDebouncedRef<T>(value: T, delay = 200) {
let timeout: number
return customRef((track, trigger) => {
return {
get() {
// 告诉Vue追踪数据
track()
return value
},
set(newValue: T) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
// 告诉Vue去触发界面更新
trigger()
}, delay)
}
}
})
}
const keyword = useDebouncedRef('', 500)
reactive 函数
一般用来定义复杂数据类型成为响应数据
- reactive定义的响应式是『深层次的』,对象中的所有对象都是响应式的
- 注意: 用解构取出的单独属性, 不再是响应式的(用toRef)
- 给reactive赋值 {} , 不再是响应式(用ref). 举例: 从接口获取数据, 直接把data赋值给reactive, 会失去相应式. 改为用ref定义, 给ref.value赋值
- 内部基于es6的proxy实现,通过代理对象内部数据进行操作
<template>
<h2>name: {{state.name}}</h2>
<h2>age: {{state.age}}</h2>
<h2>wife: {{state.wife}}</h2>
<hr>
<button @click="update">更新</button>
</template>
<script>
/*
reactive:
作用: 定义多个数据的响应式
const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
响应式转换是“深层的”:会影响对象内部所有嵌套的属性
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
*/
import {
reactive,
} from 'vue'
export default {
setup () {
/*
定义响应式数据对象
*/
const state = reactive({
name: 'tom',
age: 25,
wife: {
name: 'marry',
age: 22
},
})
console.log(state, state.wife)
const update = () => {
state.name += '--'
state.age += 1
state.wife.name += '++'
state.wife.age += 2
}
return {
state,
update,
}
}
}
</script>
-
用proxy实现数据相应式后, 就不再需要v2的this.$set了(proxy响应是深层次的)
<template> ReactiveTest: <br> <div v-for="(user, index) in userInfos" :key="index"> 名字: {{user.name}} 年龄: {{user.age}} <button @click="ageAddOne(index)">年龄+1</button> </div> <button style="width:100px; height:50px;" @click="addUser">增加成员</button> </template> <script setup lang="ts"> import { reactive } from 'vue' let userInfos = reactive( [ { name: '张三', age: 12 }, { name: '李四', age: 13 } ] ) const addUser = ()=>{ userInfos.push({ name: 'test'+ ( userInfos.length + 1 ), age: 22 // proxy响应是深层次的 }) } const ageAddOne = (index)=>{ userInfos[index].age += 1 } </script>
-
当将
ref
分配给reactive
property 时,ref 将被自动解包。const count = ref(1) const obj = reactive({}) obj.count = count console.log(obj.count) // 1 console.log(obj.count === count.value) // true
shallowReactive
proxy代理有性能问题, https://www.zhihu.com/question/460330154, https://www.cnblogs.com/zmj97/p/10954968.html
创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值)
简单理解: 只有第一层是响应式的
应用: 有一个对象数据, 结构比较深, 但变化时只是外层属性变化
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
readonly函数
接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。
const original = reactive({ count: 0 })
const copy = readonly(original)
original.count++
copy.count++ // 警告!
shallowReadonly
创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)
const data = shallowReadonly({
a: 1,
b: {
c: 2
}
})
const update = ()=>{
data.a ++ // error
data.b.c++ // 成功
}
isReactive
检查对象是否是由 reactive
创建的响应式代理。
如果该代理是 readonly
创建的,但包裹了由 reactive
创建的另一个代理,它也会返回 true
。
const state = reactive({
name: 'John'
})
// 从普通对象创建的只读 proxy
const plain = readonly({
name: 'Mary'
})
console.log(isReactive(plain)) // -> false
// 从响应式 proxy 创建的只读 proxy
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true
isProxy
检查对象是否是由 reactive
或 readonly
创建的 proxy。
toRaw
- 返回由
reactive
或readonly
方法转换成响应式代理的普通对象。 - 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。
const state = reactive<any>({
name: 'tom',
age: 25,
})
const testToRaw = () => {
const user = toRaw(state)
user.age++ // 界面不会更新
}
markRaw
- 标记一个对象,使其永远不会转换为代理。返回对象本身
- 应用场景:
- 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
- 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
const state = reactive<any>({
name: 'tom',
age: 25,
})
const testMarkRaw = ()=>{
const likes = ['a', 'b']
state.likes = markRaw(likes) // likes数组就不再是响应式的了
}
同一性风险: 因为原始选择退出仅在根级别,因此,如果将嵌套在内的、未标记的原始对象添加进响应式对象,然后再次访问该响应式对象,就会得到原始对象被代理后的版本
const foo = markRaw({
nested: {}
})
const bar = reactive({
// 虽然 `foo` 被标记为原始,但 foo.nested 不是。
nested: foo.nested
})
console.log(foo.nested === bar.nested) // false
==vue2和vue3的响应式区别==
vue2
- 核心:
- 对象: 通过
Object.defineProperty()
对象的已有属性值的读取和修改进行劫持(监视/拦截) - 数组: 通过
重写数组
更新数组一系列更新元素的方法
来实现元素修改的劫持
- 对象: 通过
Object.defineProperty(data, 'count', {
get () {},
set () {}
})
- 问题
- 对象直接新添加的属性或删除已有属性, 界面不会自动更新
- 直接通过下标替换元素或更新length, 界面不会自动更新 arr[1] = {}
vue3
核心:
- ref:通过
Object.defineProperty()
的set和get属性实现响应式(数据劫持) - reactive:通过
Proxy
来实现响应式(数据劫持),并通过Reflect操作原对象内部的数据 - 文档:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proxy 与 Reflect</title>
</head>
<body>
<script>
const user = {
name: "John",
age: 12
};
/*
proxyUser是代理对象, user是被代理对象
后面所有的操作都是通过代理对象来操作被代理对象内部属性
*/
const proxyUser = new Proxy(user, {
get(target, prop) {
console.log('劫持get()', prop)
return Reflect.get(target, prop)
},
set(target, prop, val) {
console.log('劫持set()', prop, val)
return Reflect.set(target, prop, val); // (2)
},
deleteProperty (target, prop) {
console.log('劫持delete属性', prop)
return Reflect.deleteProperty(target, prop)
}
});
// 读取属性值
console.log(proxyUser===user)
console.log(proxyUser.name, proxyUser.age)
// 设置属性值
proxyUser.name = 'bob'
proxyUser.age = 13
console.log(user)
// 添加属性
proxyUser.sex = '男'
console.log(user)
// 删除属性
delete proxyUser.sex
console.log(user)
</script>
</body>
</html>
computed 函数
计算属性 computed函数:
- 与computed配置功能一致
- 普通用法: 只有getter
- 高级用法: 有getter和setter
- 在script中获取computed的值需要
.value
, 在模板中不用
<template>
<h2>cpmputed</h2>
fistName: <input v-model="user.firstName"/><br>
lastName: <input v-model="user.lastName"/><br>
fullName1(只有getter的计算属性): <input v-model="fullName1"/><br>
fullName2(有getter与setter的计算属性): <input v-model="fullName2"><br>
</template>
<script lang="ts">
import {
reactive,
ref,
computed
} from 'vue'
export default {
setup () {
const user = reactive({
firstName: 'A',
lastName: 'B'
})
// 只有getter的计算属性
const fullName1 = computed(() => {
console.log('fullName1')
return user.firstName + '-' + user.lastName
})
// 有getter与setter的计算属性
const fullName2 = computed({
get () {
console.log('fullName2 get')
return user.firstName + '-' + user.lastName
},
set (value: string) {
console.log('fullName2 set')
const names = value.split('-')
user.firstName = names[0]
user.lastName = names[1]
}
})
// 在script中获取computed的值, 要用.value
console.log(fullName2.value)
return {
user,
fullName1,
fullName2
}
}
}
</script>
watch 函数
监听 watch函数:
- 与watch配置功能一致
- 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
- 默认初始时不执行回调, 但可以通过配置immediate为true, 来指定初始时立即执行第一次
- 通过配置deep为true, 来指定深度监视
- 监听reactive对象中的属性, 必须通过函数返回该属性来指定
- 回调函数可以接收到两个参数, newValue和oldValue
<template>
<h2>watch</h2>
fistName: <input v-model="user.firstName"/><br>
lastName: <input v-model="user.lastName"/><br>
fullName1: <input v-model="fullName1"/><br>
</template>
<script lang="ts">
import {
reactive,
ref,
watch,
} from 'vue'
export default {
setup () {
const user = reactive({
firstName: 'A',
lastName: 'B'
})
const fullName1 = ref('')
/*
使用watch的2个特性:
深度监视
初始化立即执行
*/
watch(user, (newVal,oldVal) => {
console.log('深度监听, 立即执行')
fullName1.value = user.firstName + '-' + user.lastName
}, {
immediate: true, // 是否初始化立即执行一次, 默认是false
deep: true, // 是否是深度监视, 默认是false
})
/*
watch一个数据
默认在数据发生改变时执行回调
*/
watch(fullName1, (newVal,oldVal) => {
console.log('监听一个数据')
const names = newVal.split('-')
user.firstName = names[0]
user.lastName = names[1]
})
/*
watch多个数据:
使用数组来指定
如果是ref对象, 直接指定
如果是reactive对象中的属性, 必须通过函数返回该属性来指定
*/
watch([() => user.firstName, () => user.lastName, fullName1], (values) => {
console.log('监视多个数据', values)
})
return {
user,
fullName1
}
}
}
</script>
watchEffect 函数
- vue3特性
- 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
- 默认初始时就会执行第一次, 从而可以收集需要监视的数据
- 监视数据发生变化时回调
<template>
<h2>watchEffect</h2>
fistName: <input v-model="user.firstName"/><br>
lastName: <input v-model="user.lastName"/><br>
fullName1: <input v-model="fullName1"/><br>
</template>
<script lang="ts">
import {
reactive,
ref,
watchEffect
} from 'vue'
export default {
setup () {
const user = reactive({
firstName: 'A',
lastName: 'B'
})
const fullName1 = ref('')
/*
watchEffect: 监视所有回调中使用的数据
*/
watchEffect(() => {
console.log('watchEffect')
fullName1.value = user.firstName + '-' + user.lastName
})
return {
user,
fullName1
}
}
}
</script>
ref标签属性
绑定DOM或者组件
- 单个元素:先申明ref响应式数据,返回给模版使用,通过ref绑定数据
注意: 写法和vue2不同, 把一个使用ref
声明的响应式数据绑定到标签的ref属性上, 使用时要用.value
<template>
<h2>App</h2>
<input type="text">---
<input type="text" ref="inputRef">
</template>
<script lang="ts">
import { onMounted, ref } from 'vue'
/*
ref获取元素: 利用ref函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
*/
export default {
setup() {
const inputRef = ref<HTMLElement|null>(null)
onMounted(() => {
inputRef.value && inputRef.value.focus()
})
return {
inputRef
}
},
}
</script>
v-for中的ref
- 遍历的元素:先定义一个空数组,定一个函数获取元素,返回给模版使用,通过 ref 绑定这个函数
- 视图更新时, 要清空数组(在onBeforeUpdate )
https://v3.cn.vuejs.org/guide/migration/array-refs.html#frontmatter-title
<template>
<h1>{{ number }}</h1>
<button @click="number++">加油~</button>
<!-- 获取多个DOM节点ref属性需要绑定一个函数 -->
<div v-for="i in 4" :key="i" :ref="setItemRef">{{ i }}</div>
</template>
<script>
import { onBeforeUpdate, onUpdated, ref } from 'vue'
export default {
setup() {
const number = ref(0)
// 用于接收 dom 节点的数组
let itemRefs = []
// 在函数形参中获取到真实 DOM 节点
const setItemRef = el => {
if (el) {
itemRefs.push(el)
}
}
// 💥注意:视图更新的时候,ref 属性绑定的函数会被多次触发,所以需要在更新时重置数组
onBeforeUpdate(() => {
console.log('视图更新了');
itemRefs = []
})
onUpdated(() => {
console.log(itemRefs);
})
return { setItemRef, number }
},
}
</script>
自定义hook函数
自定义hook的作用类似于vue2中的mixin技术
使用Vue3的组合API封装的可复用的功能函数
文件命名时最好在前面加上
use
在setup中能使用的函数,在hook函数中也都可以使用
和mixin的区别
- mixin中的变量和方法是隐式引入(在mixinsp配置中引入), 而hook是要主动调用
- 在一个组件中使用多个mixin可能会出现,函数和变量重名现象,就会导致冲突或覆盖现象。而使用Hook函数时,因为变量和函数是显示引用,我们就可以通过解构赋值,来避免函数和变量重名现象。
- mixins不能传入参数改变它的逻辑
例子1: 收集用户鼠标点击的页面坐标
hooks/useMousePosition.ts
import { ref, onMounted, onUnmounted } from 'vue'
/*
收集用户鼠标点击的页面坐标
*/
export default function useMousePosition () {
// 初始化坐标数据
const x = ref(-1)
const y = ref(-1)
// 用于收集点击事件坐标的函数
const updatePosition = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}
// 挂载后绑定点击监听
onMounted(() => {
document.addEventListener('click', updatePosition)
})
// 卸载前解绑点击监听
onUnmounted(() => {
document.removeEventListener('click', updatePosition)
})
return {x, y}
}
使用:
<template>
<div>
<h2>x: {{x}}, y: {{y}}</h2>
</div>
</template>
<script setup lang="ts">
/*
在组件中引入并使用自定义hook
自定义hook的作用类似于vue2中的mixin技术
自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂
*/
import useMousePosition from './hooks/useMousePosition'
const {x, y} = useMousePosition()
</script>
v-model
变化的总体概述:
-
非兼容
:用于自定义组件时,
v-model
prop 和事件默认名称已更改:- prop:
value
->modelValue
; - 事件:
input
->update:modelValue
;
- prop:
-
非兼容:
v-bind
的.sync
修饰符和组件的model
选项已移除,可在v-model
上加一个参数代替; -
新增:现在可以在同一个组件上使用多个
v-model
绑定; -
新增:现在可以自定义
v-model
修饰符。
ps: vue2.2引入model
组件选项, 但只允许在组件上使用一个v-model
3.x新语法
父组件
<ChildComponent v-model="pageTitle" />
<!-- 是以下的简写: -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
子组件
<script>
export default {
name: 'ChildComponent',
props: {
// 1. 子组件通过 modelValue 接收 v-model 传递过来的数据
modelValue: {
type: Boolean,
},
},
setup(props, { emit }) {
// ...
// 2. 子组件通过 update:modelValue 通知父组件做 v-model 值的更新
emit('update:modelValue', val)
// ...
}
},
}
</script>
自定义model名(可以用来替代原来的.sync
语法糖)
父组件
<InputComponent v-model:input="text" />
子组件
<template>
输入: <input type="text" :value="input" @change='textChange'/>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const props = defineProps<{
input: string, // 1.定义props
}>()
const emit = defineEmits<{
(e:'update:input', values: string): void // 2. 定义emit
}>()
const textChange = (e)=>{
let value = e.target.value
emit('update:input', value) // 3.发送emit
}
</script>
自定义修饰符
ps: 内置的有.trim
, number
, .lazy
添加到组件 v-model
的修饰符将通过 modelModifiers
prop 提供给组件 ( 如果v-model
自定义model名, prop 名为自定义名
+ Modifiers
)
-
例子: 把v-model的输入首字母变大写
父组件
<ChildComponent v-model.capitalize="myText"></ChildComponent>
子组件
<script> export default { name: 'ChildComponent', props: { modelValue: { type: Boolean, }, modelModifiers: { // 接收修饰符号 default: ()=>{} } }, setup(props, { emit }) { // ... // 2. 用法就是先判断修饰符的值是不是为true,是的话就处理数据 if(modelModifiers.capitalize){ val = val.charAt(0).toUpperCase() + val.slice(1) } emit('update:modelValue', val) // ... } }, created() { // 1. 当组件的 created 生命周期钩子触发时,modelModifiers prop 会包含 capitalize,且其值为 true console.log(this.modelModifiers) // { capitalize: true } } } </script>
-
例子: 自定义model名 + 自定义修饰符号
父组件
<InputComponent v-model:input.addPlus="text" />
子组件
<template> 输入: <input type="text" :value="input" @change='textChange'/> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' const props = defineProps<{ input: string, inputModifiers: { // '自定义model名' + 'Modifiers' default: ()=>{} } }>() const emit = defineEmits<{ (e:'update:input', value: string): void }>() console.log() const textChange = (e)=>{ let value = e.target.value if(props.inputModifiers.addPlus){ // 判断是否存在自定义修饰符 if(value.charAt(0) !== '+'){ value = '+' + value } } emit('update:input', value) } onMounted(()=>{ console.log('modelModifiers',props.modelModifiers) }) </script> <style scoped> </style>
依赖注入
无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。
这个特性有两个部分:父组件有一个
provide
选项来提供数据,子组件有一个inject
选项来开始使用这些数据。==只能在setup生命周期里使用==
可以将依赖注入看作是"长距离的 prop",除了:
- 父组件不需要知道哪些子组件使用了它 provide 的 property
- 子组件不需要知道 inject 的 property 来自哪里
语法
// 祖父组件
import { provide } from 'vue'
// provide 有两个参数: 第一个name (<String> 类型), 第二个value
// 如果要传递多个, 包装成对象放到第二个参数里, 不建议多次provide
provide('data', {
name: '张三',
age: 12
})
// 孙组件
import { inject } from 'vue'
// inject有两个参数: 第一个name, 第2个为默认值(可选)
const data = inject('data')
添加响应性
在 provide 值时使用 ref 或 reactive。
修改响应式 property
-
尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部
-
如果需要在孙组件更新数据, 建议在祖父组件provide一个更新的方法(==该方法的作用域在祖父组件==)
-
如果要保证provide的数据不被修改, 使用
readonly
祖父组件
import { provide, reactive, readonly } from 'vue' const userInfo = reactive({ name: '张三', age: 12 }) const env = '祖父组件' const updateUserInfo = ({name, age})=>{ console.log('provide方法的作用域', env) // provide的方法的作用域在组父组件, 这里打印的env是'组父组件' if(name){ userInfo.name = name } if(age !== undefined){ userInfo.age = age } } provide('userInfo',readonly(userInfo)) provide('updateUserInfo', updateUserInfo)
孙组件
<template> <div>孙组件</div>{{userInfo2.name}}{{userInfo2.age}}岁 <br/> <button style="width:100px;height:50px;" @click="ageAddOne">年龄+1</button> </template> <script setup lang="ts"> import { inject } from 'vue' const userInfo2 = inject('userInfo') const updateUserInfo = inject('updateUserInfo') const env = '孙组件' const ageAddOne = ()=>{ // reactive设置了readonly, 直接修改不成功报错 // userInfo2.age += 1 // 需要通过父组件provide传来的方法修改 updateUserInfo({ age:userInfo2.age + 1 }) } </script>
新组件
Fragment(片段)
- 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
- 好处: 减少标签层级, 减小内存占用
<template>
<h2>aaaa</h2>
<h2>aaaa</h2>
</template>
Teleport 传送门
一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。
最常见的例子就是全屏的模态框
<Teleport>
接收一个 to prop
来指定传送的目标。to
的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。这段代码的作用就是告诉 Vue"把以下模板片段传送到 body 标签下"。
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
Suspense(不确定的)
试验性特性
新指令
v-memo
记录指令下的模板树。该指令期望一个数组,如果数组内的值,都没有发生更新,那么指令下的模板树也不会发生更新。
官网提到
v-memo
仅用于性能关键场景中的微优化,一般使用到的地方应该是渲染大型v-for
列表(其中length > 1000
)。
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
<p>...more child nodes</p>
</div>
v-if和v-for优先级
- v-if优先v-for解析(和vue2不同)
手写api
shallowReactive 与 reactive
const reactiveHandler = {
get (target, key) {
if (key==='_is_reactive') return true
return Reflect.get(target, key)
},
set (target, key, value) {
const result = Reflect.set(target, key, value)
console.log('数据已更新, 去更新界面')
return result
},
deleteProperty (target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('数据已删除, 去更新界面')
return result
},
}
/*
自定义shallowReactive
*/
function shallowReactive(obj) {
return new Proxy(obj, reactiveHandler)
}
/*
自定义reactive
*/
function reactive (target) {
if (target && typeof target==='object') {
if (target instanceof Array) { // 数组
target.forEach((item, index) => {
target[index] = reactive(item)
})
} else { // 对象
Object.keys(target).forEach(key => {
target[key] = reactive(target[key])
})
}
const proxy = new Proxy(target, reactiveHandler)
return proxy
}
return target
}
/* 测试自定义shallowReactive */
const proxy = shallowReactive({
a: {
b: 3
}
})
proxy.a = {b: 4} // 劫持到了
proxy.a.b = 5 // 没有劫持到
/* 测试自定义reactive */
const obj = {
a: 'abc',
b: [{x: 1}],
c: {x: [11]},
}
const proxy = reactive(obj)
console.log(proxy)
proxy.b[0].x += 1
proxy.c.x[0] += 1
shallowRef 与 ref
/*
自定义shallowRef
*/
function shallowRef(target) {
const result = {
_value: target, // 用来保存数据的内部属性
_is_ref: true, // 用来标识是ref对象
get value () {
return this._value
},
set value (val) {
this._value = val
console.log('set value 数据已更新, 去更新界面')
}
}
return result
}
/*
自定义ref
*/
function ref(target) {
if (target && typeof target==='object') {
target = reactive(target)
}
const result = {
_value: target, // 用来保存数据的内部属性
_is_ref: true, // 用来标识是ref对象
get value () {
return this._value
},
set value (val) {
this._value = val
console.log('set value 数据已更新, 去更新界面')
}
}
return result
}
/* 测试自定义shallowRef */
const ref3 = shallowRef({
a: 'abc',
})
ref3.value = 'xxx'
ref3.value.a = 'yyy'
/* 测试自定义ref */
const ref1 = ref(0)
const ref2 = ref({
a: 'abc',
b: [{x: 1}],
c: {x: [11]},
})
ref1.value++
ref2.value.b[0].x++
console.log(ref1, ref2)
shallowReadonly 与 readonly
const readonlyHandler = {
get (target, key) {
if (key==='_is_readonly') return true
return Reflect.get(target, key)
},
set () {
console.warn('只读的, 不能修改')
return true
},
deleteProperty () {
console.warn('只读的, 不能删除')
return true
},
}
/*
自定义shallowReadonly
*/
function shallowReadonly(obj) {
return new Proxy(obj, readonlyHandler)
}
/*
自定义readonly
*/
function readonly(target) {
if (target && typeof target==='object') {
if (target instanceof Array) { // 数组
target.forEach((item, index) => {
target[index] = readonly(item)
})
} else { // 对象
Object.keys(target).forEach(key => {
target[key] = readonly(target[key])
})
}
const proxy = new Proxy(target, readonlyHandler)
return proxy
}
return target
}
/* 测试自定义readonly */
/* 测试自定义shallowReadonly */
const objReadOnly = readonly({
a: {
b: 1
}
})
const objReadOnly2 = shallowReadonly({
a: {
b: 1
}
})
objReadOnly.a = 1
objReadOnly.a.b = 2
objReadOnly2.a = 1
objReadOnly2.a.b = 2
isRef, isReactive 与 isReadonly
/*
判断是否是ref对象
*/
function isRef(obj) {
return obj && obj._is_ref
}
/*
判断是否是reactive对象
*/
function isReactive(obj) {
return obj && obj._is_reactive
}
/*
判断是否是readonly对象
*/
function isReadonly(obj) {
return obj && obj._is_readonly
}
/*
是否是reactive或readonly产生的代理对象
*/
function isProxy (obj) {
return isReactive(obj) || isReadonly(obj)
}
/* 测试判断函数 */
console.log(isReactive(reactive({})))
console.log(isRef(ref({})))
console.log(isReadonly(readonly({})))
console.log(isProxy(reactive({})))
console.log(isProxy(readonly({})))
常见错误
-
引入组件时,
.vue
后缀不能省略 -
@
路径不生效, 在tsconfig.json
里添加配置"compilerOptions": { //... "paths": { "@/*": [ "./src/*" ], }, //... }
在
vite.config.ts
export default defineConfig({ base: "./", resolve: { alias: [ { find: "@", replacement: resolve(__dirname, "./src") // __dirname报错, 则需要安装 `@types/node` npm包 } ] }, // ... })
-
less支持
// 1. 先安装less 和 less-loader // 在vite.config.ts export default defineConfig({ css: { preprocessorOptions: { less: { javascriptEnabled: true } } }, // ... })
h 函数直接生成虚拟dom
待补充...
render函数渲染
待补充...
动态加载组件
import()
函数和 defineAsyncComponent