1. Vue3带来了什么
1.1 性能的提升
- 打包大小减少41%
- 初次渲染快55%, 更新渲染快133%
- 内存减少54%
- …
1.2 源码的升级
- 使用 Proxy 代替 defineProperty 实现响应式
- 重写虚拟 DOM、diff 算法的实现
- 引入 Tree-Shaking 的技术
- 如果没有引入 KeepAlive、Transition 等组件,打包时则不会去引入
- …
1.3 支持TypeScript
- Vue3 可以更好的支持 TypeScript
1.4 新的特性
-
Composition API(组合API)
- setup 配置
- ref 与 reactive
- watch 与 watchEffect
- provide 与 inject
- …
-
新的内置组件
- Fragment
- Teleport
- Suspense
-
其他改变
- 新的生命周期钩子
- data 选项应始终被声明为一个函数
- 移除keyCode支持作为 v-on 的修饰符
- …
2. 常用的 Composition(组合式) API
2.1 setup函数
-
在 Vue3 中组件用到的数据、方法等,都要配置在 setup 函数中
-
setup 的两种返回值
- 如果返回的是一个对象,则对象中的属性和方法在模板中才可以使用
- 如果返回的是一个渲染函数,则可以自定义渲染内容
-
setup 的参数
- props:父组件传过来的值
- context:上下文对象
-
setup 在 beforeCreate 前执行一次,this 是 undefined,所以 Vue2 中的配置项可以访问到 setup 中的数据,而 setup 不能通过 this 访问 Vue2 中的配置项,但是不建议混合使用
// 当返回值为一个渲染函数时,视图由渲染函数中的内容承包 import { h } from "vue"; export default { setup() { return () => h("h1", "哈哈"); } }
// 定义的数据和方法,需要以对象的形式去返回 export default { setup() { let num = 123; let str = "123"; function fn() { console.log("我是一个方法"); } return { num, str, fn, }; } }
-
当 setup 为 async 函数时,需要配合 Suspense 和异步组件使用
2.2 ref函数
- 作用:
- 定义一个响应式数据
- 创建响应式数据:
- 当定义一个基本数据类型的响应式时,使用的是 Object.defineProperty
- 当定义一个引用数据类型的响应式时,调用了 reactive 函数
- 获取
- 在 js 中需要以 .value 的形式去获取
- 在模板中直接获取
- 用法:
import { ref } from "vue"; export default { setup() { let num = ref(123); // 触发 fn 函数,改变 num 的值 function fn() { num.value = 234; } return { num, fn, }; } }
2.2.1 在模板上使用
<input ref="input">
<script setup>
import { ref } from 'vue'
// 必须和模板里的 ref 同名,就可以拿到 input DOM 对象
const input = ref()
</script>
2.2.2 在组件上使用
<myComponent ref="myComponent">
<script setup>
import { ref } from 'vue'
// 组件实例对象,不过在子组件中需要使用 expose 保留对应的属性,父组件才可以访问
const myComponent = ref()
</script>
2.2.3 函数模板引用
<input :ref="(el) => { el:当前元素 }">
2.3 reactive函数
-
作用:
- 定义一个响应式数据
-
创建响应式数据:
- 不能定义基本数据类型
- 当定义一个引用数据类型的响应式时,使用的是 Proxy 和 Reflect
-
获取:
- 在 js 中直接获取
- 在模板中直接获取
-
用法:
import { reactive } from "vue"; export default { setup() { // p 的所有属性都是响应式的 let p = reactive({ name: "小红", age: 18, }); function fn() { p.name = "小白"; p.age = 20; } return { p, fn, }; } }
-
ref 与 reactive 的区别:
- ref 定义数据后,如果在 js 中获取需要 .value,基本引用数据类型都可以定义
- reactive 定义数据后,可以直接获取,只能定义引用数据类型
- 最好使用 ref 定义基本数据类型,使用 reactive 定义引用数据类型
2.4 计算属性与侦听器
2.4.1 computed函数
与 Vue2 中的计算属性使用方法一致
import { computed } from 'vue'
export default {
setup(){
// 简写
let MyName = computed(() => {
return;
});
// 完整写法
let MyName1 = computed({
set(value) {},
get() {
return;
},
});
return {
MyName,
MyName1,
};
}
}
2.4.2 watch函数
与 Vue2 中的侦听器一样,但是采用的响应式不同,会分很多种情况
// 基本使用,watch 可以监视 getter/effect 函数、ref、reactive、或这些类型的数组。
watch(观察的数据,数据发生变化的回调,{配置项,可选});
-
情况一:监视 ref 定义的响应式数据
// 当观察 ref 响应式数据时,不需要 .value import { ref, watch } from "vue"; let num = ref(123); watch(num, (newValue, oldValue) => { console.log("值发生了改变", newValue, oldValue); });
-
情况二:监视 reactive 定义的响应式数据
// 引用数据类型无法获取正确的 oldValue,Vue2 也会有这个问题 // Proxy 默认开启了深度监视,关不掉 import { reactive, watch } from "vue"; let obj = reactive({ a: { b: { c: { d: 1, }, }, }, }); watch(obj, (newValue, oldValue) => { // 最里面的 d 不论怎么改变,watch 都可以检测到 console.log("值发生了改变", newValue, oldValue); } );
-
情况三:监视 getter 函数
// 需要记住一点,getter 函数是监视响应式对象中的某一个值,不是响应式的监视不到 // 如果返回值为引用数据类型,深度监视需要手动开启 import { ref, reactive, watch } from "vue"; // 情况一 let num = ref(1) // 当 num 的值变化时可以监视到到 watch(() => num.value, (v) => { console.log(v) }) // 情况二 let obj = reactive({ a: { b: 1, }, }); // 1. 修改 obj 中的某一个值时可以监视到 // 2. 需要手动开启 deep // 3. 不能修改 obj 的地址 watch(() => obj, (newValue, oldValue) => { console.log("值发生了改变", newValue, oldValue); }, { deep: true } ); // 情况三 let obj = {} // 这种情况就废了,监视不到 watch(() => obj, (newValue, oldValue) => { console.log("值发生了改变", newValue, oldValue); }, { deep: true } );
-
情况四:几种方式配合使用
import { ref, watch, reactive } from "vue"; let num = ref(123); let str = ref("123"); let obj = reactive({ a: { b: 1, }, }); watch([num, () => str.value, obj], (newValue, oldValue) => { // 其中一个值发生了改变,就会被检测到,会返回一个数组 console.log("值发生了改变", newValue, oldValue); }, { deep: true } );
总结:
- 一般使用 ref 定义基本数据类型的响应式,可以直接监视,并不会存在任何问题
- 使用 reactive 定义引用数据类型的响应式,监视默认会开启深度监视( 无法通过配置去关闭 )。此时 newValue 和 oldValue 的值一样,Vue2 也会有一个问题,可能是因为浏览器引用地址相同,展示数据也会相同
- 使用 getter 函数,具体看上面情况三的案例
- 数组可以监视多个数据,当其中一个数据发生改变时,都会被监视到并返回一个数组,并且数组中的每一项,必须为可监视数据
2.4.3 watchEffect函数
// watchEffect 中用到哪个响应式属性,那么就会监视那个响应式属性,并且也是深度监视,默认会执行一次
import { ref, watchEffect } from "vue"
let num = ref(10)
let str = ref("10")
watchEffect((beforeFn) => {
let newNum = num.value
let newStr = str.value
console.log("发生了改变")
beforeFn(() => {
console.log("我会先执行")
})
})
2.4.4 回调触发时机
// 当响应式状态值发生改变时,是先触发了侦听器,后触发的组件更新
// 下面这两种方式,可以获取到最新的 DOM,也就是在组件更新后触发
// flush:默认 pre 组件更新之前调用、sync 同步执行,post 组件更新之后执行
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
// 也可以使用这种方式
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
2.4.5 取消侦听器
// 和 Vue2 中全局使用侦听器是一样的
let unwatch = watch(source, callback)
2.5 自定义hook
-
hook 本质是一个函数,把 setup 函数中使用的 Composition API 进行了封装。
-
类似于 vue2 中的mixin
// 说白了就是函数的封装、复用 // hooks/useXxx.js 文件 export default function (){ // 一些公共的逻辑 return xxx } // 组件文件 import useXxx from "@/hooks/useXxx.js" setup(){ let xxx = useXxx() }
2.6 toRef与toRefs
-
toRef 为一个响应式对象的某个属性新建一个 ref,使这两个数据相连接(如果不是响应式数据,则失效)
// toRef 用法 const state = reactive({ foo: 1, bar: 2 }) // 此时 fooRef 和 state.foo 相连接,并且都是响应式的 const fooRef = toRef(state, 'foo') fooRef.value++ console.log(state.foo) // 2 state.foo++ console.log(fooRef.value) // 3
-
toRefs 为一个响应式对象的每一条属性新建一个 ref
// toRefs 用法 const state = reactive({ foo: 1, bar: 2 }) // 此时 stateAsRefs 与 state 相连接 const stateAsRefs = toRefs(state)
2.7 Provide与Inject
-
作用:实现祖孙组件间通信
-
用法:
// 祖先组件 import { provide } from "vue" setup(){ let data = "我是要传入的数据" provide('msg', data) }
// 后代组件 import { inject } from "vue" setup(){ inject('msg') }
3. 其他组合式API
3.1 shallowRef与shallowReactive
- shallowRef:只考虑基本数据类型的响应式,引用数据类型只有当地址发生改变时才会响应
- shallowReactive:对于引用数据类型,只对数据的第一层进行响应式
- 用法:和 ref/reactive 函数用法一样
注意:当数据不是响应式时,数据会发生改变,但是视图不会被更新,如果使用其他的方法去更新视图,那么视图也可以拿到 shallowReactive/shallowRef 中最新的数据
3.2 readonly与shallowReadonly
- readonly:让一个响应式数据变为只读的,所有的数据都为只读
- shallowReadonly:让一个响应式数据变为只读的,只是第一层的数据只读
- 用法:调用 readonly/shallowReadonly 函数,返回一个新的只读属性
- 应用场景:不希望数据被修改时
3.3 toRaw与markRaw
- toRaw:
- 作用:将一个由 reactive 定义的响应式对象,变为普通对象
- 用法:调用 toRaw 函数,返回一个新的普通对象
- markRaw:
- 作用:标记一个对象,使其永远不会再成为响应式对象
- 用法:调用 markRaw 函数,为原对象添加一个
__v_skip
为 true 的属性
3.4 customRef
-
作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
-
实现一个防抖效果:
<template> <input type="text" v-model="keyword" /> <h3>{{ keyword }}</h3> </template> <script> import { customRef } from "vue"; export default { name: "Demo", setup() { //自定义一个 myRef function myRef(value, delay) { let timer; return customRef((track, trigger) => { return { get() { track(); // 允许添加依赖 return value; }, set(newValue) { clearTimeout(timer); timer = setTimeout(() => { value = newValue; trigger(); // 触发依赖更新视图 }, delay); }, }; }); } let keyword = myRef("hello", 500); return { keyword, }; }, }; </script>
3.5 triggerRef
- 强制通过数据更新视图
3.6 响应式数据的判断
isRef
:检查一个值是否为一个 ref 对象isReactive
:检查一个对象是否是由 reactive 创建的响应式代理isReadonly
:检查一个对象是否是由 readonly 创建的只读代理isProxy
:检查一个对象是否是由 reactive 或者 readonly 方法创建的代理,readonly 也是经过 Proxy 进行的代理,这不过它是为只读的代理- 返回值:都为布尔值
4. 新的组件
4.1 Fragment
- 在 Vue2 中:组件必须有一个根标签
- 在 Vue3 中:如果组件没有根标签,那么默认会包裹在一个 Fragment 虚拟元素里
- 好处:减少标签层级,减小内存占用
4.2 teleport
-
作用:能够将组件的html结构移动到指定位置
-
用法:
<!-- to:把 teleport 标签中的内容,移动到 to 中指定的内容(追加) disabled:是否禁用移动 --> <teleport to="标签名/class类名/id名"> <div> <h1>我是要被移动的内容</h1> </div> </teleport>
4.3 Suspense(实验性)
-
当组件异步引入时,可以和 Suspense 配合 loading 展示
-
用法:生产环境请勿使用
-
异步引入组件
// 创建一个只有在需要时才会加载的异步组件(性能优化:在打包时不会打包到主文件,会单独打包到一个js文件) import { defineAsyncComponent } from 'vue' const Child = defineAsyncComponent(() => import('./components/Child.vue'))
-
使用 Suspense 包裹组件,并配置好 default 与 fallback
<template> <div class="app"> <h3>我是App组件</h3> <Suspense> <template #default> <!-- 当 Child 组件渲染好时展示 --> <Child/> </template> <template #fallback> <!-- 当 Child 组件未渲染好时展示 --> <h3>加载中.....</h3> </template> </Suspense> </div> </template>
-
5. 自定义指令的修改
// 全局自定义指令
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
// vue3 的自定义指令,在 setup 中默认使用 vXxx 命名的变量默认可以当做自定义指令使用
const vDirective = { // 指令生命周期的更改,全局的方式一样
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
6. 优化 Vdom
- 在 vue2 中,每次更新 diff 都是全量对比,vue3 则只对比有标记的,大大减少了非动态内容的对比消耗
patch flag
在生成虚拟 dom 时,对使用变量的节点打上标记,更新时只需要对比这些打标记的节点