对 vue3 进行全面讲解,文章较长,但保证都是干货,请耐心看完。
概述
本文会从以下
Vue3 新功能和原理两个方面进行讲解。
Vue3 新功能
- createApp
- emits属性
- 多事件处理
- Fragment
- 移除.sync 改为v-model
- 异步组件的引用方式
- 移除filter
- Teleport
- Suspense
- Composition API
- reactive
- ref toRef toRefs
- readonly
- computed
- watch watchEffect
- 钩子函数生命周期
原理
- Proxy 实现响应式
- 编译优化
- PathFlag 静态标记
- hoistStatic 静态提升
- cacheHandler 缓存事件
- SSR 优化
- Tree-shaking优化
- Vite - ES6 module
1. Vue3 比Vue2 有什么优势?
- 性能更好
- 体积更小
- 只是相对Vue2
- 更好的ts支持
- 更好的代码组织
- 更好的逻辑抽离
- 日益丰富的业务需求和框架代码编写之间的矛盾
- 更多的新功能
- vue3 的一些新功能,我们vue2可以通过一些方式实现,但是vue3 官方支持肯定更好
2 描述Vue3 生命周期
2.1 Options API 生命周期
- beforeDestroy 改为 beforeUnmount
- destroyed 改为 unmounted
- 其他沿用 Vue2 的生命周期
2.2 Composition API 生命周期
- 差不多,从 vue 中引入使用罢了
- 不过前面都加上on
- import { onBeforeMount } from 'vue'
2.3 LifeCycles.vue
<template>
<p>生命周期 {{msg}}</p>
</template>
<script>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
name: 'LifeCycles',
props: {
msg: String
},
// 等于 beforeCreate 和 created
setup() {
console.log('setup')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
},
// beforeCreate() {
// console.log('beforeCreate')
// },
// created() {
// console.log('created')
// },
// beforeMount() {
// console.log('beforeMount')
// },
// mounted() {
// console.log('mounted')
// },
// beforeUpdate() {
// console.log('beforeUpdate')
// },
// updated() {
// console.log('updated')
// },
// // beforeDestroy 改名
// beforeUnmount() {
// console.log('beforeUnmount')
// },
// // destroyed 改名
// unmounted() {
// console.log('unmounted')
// }
}
</script>
3 如何理解Composition API 和 Options API?
3.1 Composition API 带来了什么?
- 更好的代码组织
- 把散落的代码,抽离放置到了一起
- 更好的逻辑复用
- 更好的类型推导
{
data(){
return {
a: 10
}
}
method: {
fn(){
// 这里的this.a 为什不是this.data.a
const a = this.a;
}
},
mounted(){
// 这里的this.fn 为什不是this.method.fn
this.fn()
}
}
项目太大,逻辑过于复杂时,可以更好的发挥作用
Composition API 主要是针对大型项目应用要用的,如果是简单的1+1=2,反而体现不出来以三点
3.2 如何选择 Composition API 和 Options API?
- 不建议共用,会引起混乱(写法、代码组织和逻辑复用方式都不一样)
- 小型项目、业务逻辑简单,用
Options API - 中大型项目、逻辑复杂,用
Composition API
4. 如何理解ref、toRef 和 toRefs?
4.1 ref、toRef 和 toRefs是什么?
4.1.1 ref
- 生成值类型的响应式数据(注意不是引用类型)
- 可用于模板和reactive
- 通过.value修改值 Ref.vue 响应式
<template>
<p>ref demo {{ageRef}} {{state.name}}</p>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
name: 'Ref',
setup() {
const ageRef = ref(20) // 值类型 响应式
const nameRef = ref('服部')
const state = reactive({
name: nameRef
})
setTimeout(() => {
console.log('ageRef', ageRef.value)
ageRef.value = 25 // .value 修改值
nameRef.value = '服部平次&远山和叶'
}, 1500);
return {
ageRef,
state
}
}
}
</script>
RefTemplate.vue
<template>
<p ref="elemRef">我是一行文字</p>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
name: 'RefTemplate',
setup() {
const elemRef = ref(null)
onMounted(() => {
console.log('ref template', elemRef.value.innerHTML, elemRef.value)
})
return {
elemRef
}
}
}
</script>
4.1.2 toRef
- 针对一个响应式对象(
reactive封装)的prop(属性) - 创建一个
ref,具有响应式 - 两者保持引用关系
// toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
ToRef.vue
<template>
<p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>
<script>
import { ref, toRef, reactive } from 'vue'
export default {
name: 'ToRef',
setup() {
// 创建一个响应式对象
const state = reactive({
age: 20,
name: '服部'
})
const age1 = computed(() => {
return state.age + 1
})
// // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
// const state = {
// age: 26,
// name: '服部'
// }
const ageRef = toRef(state, 'age')
setTimeout(() => {
state.age = 25
}, 1500)
setTimeout(() => {
ageRef.value = 37 // .value 修改值
}, 3000)
return {
state,
ageRef
}
}
}
</script>
4.1.3 toRefs (注意这里是toRefs,不是toRef)
- 将响应式对象 (
reactive封装)转换为普通对象 - 对象的每个
prop都是对应的ref - 两者保持引用关系
ToRefs.vue
<template>
<p>toRefs demo {{age}} {{name}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive } from 'vue'
export default {
name: 'ToRefs',
setup() {
const state = reactive({
age: 26,
name: '服部'
})
const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象
// 每个属性,都是 ref 对象
// const { age: ageRef, name: nameRef } = stateAsRefs
// return {
// ageRef,
// nameRef
// }
setTimeout(() => {
state.age = 27
}, 1500)
return stateAsRefs
// 直接返回 state, 将会失去响应式
// return {
// ...state
// }
}
}
</script>
4.2 ref、toRef 和 toRefs最佳使用方式
4.2.1 最佳使用方式
- 用
reactive做 对象 的响应式,用ref做 值 类型响应式 setup中返回toRefs(state),或者toRef(state,‘xxx’)ref的变量命名都用xxxRef- 合成函数返回响应式对象,使用
toRefs
4.2.2 合成函数返回响应式对象
4.3 进阶,深入理解
4.3.1(一)为什么需要ref?
- 返回值类型,会丢失响应式;
- 定义时需要值类型为响应式可以用reactive,但是函数返回值等,也可能是值类型需要响应式
- 如setup、computed、合成函数都有可能返回值类型,需要响应式;
- Vue不定义ref,用户会自造ref,反而制造混乱
WhyRef.vue
<template>
<p>why ref demo {{state.age}} - {{age1}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive, computed } from 'vue'
function useFeatureX() {
const state = reactive({
x: 1,
y: 2
})
return toRefs(state)
}
export default {
name: 'WhyRef',
setup() {
const { x, y } = useFeatureX()
const state = reactive({
age: 26,
name: '服部'
})
// computed 返回的是一个类似于 ref 的对象,也有 .value
const age1 = computed(() => {
return state.age + 1
})
setTimeout(() => {
state.age = 27
}, 1500)
return {
state,
age1,
x,
y
}
}
}
</script>
4.3.2(二)为何ref需要value属性
- ref是一个对象(不丢失响应式),value存储值
- 通过.value属性的get和set实现响应式
- 用于模板、reactive时,不需要.value,其他情况都需要
值类型不是引用数据,所以值会丢失,响应式无法实现,所以需要用ref,ref的原理就是一个对象,value存储值
4.3.3(三)为什么需要toRef和toRefs
- 初衷:不丢失响应式的情况下,把对象数据 分解/扩散
- 前提:针对的是响应式对象(reactive封装的)非普通对象
- 注意:不创造响应式, 而是延续响应式
5. Vue3 升级了哪些重要的功能?
5.1 creatApp
5.2 emits属性
5.3 生命周期
5.4 多事件处理
5.5 Fragment
Vue3可以使用 多个节点的模版
5.6 移除.sync 改为v-model
5.7 异步组件的写法
5.7.1 Vue2 写法
5.7.2 Vue3 写法
defineAsyncCompontent
5.8 移除filter
使用computed也是可以处理的
移除原因可能是
- 类型推导、类型检测以及自动化的东西 有一些影响
用js的方式完成,让开发在某些方面会更简单些
5.9 Teleport
通过将 UI 和相关行为封装到组件中来构建 UI。我们可以将它们嵌套在另一个内部,以构建一个组成应用程序 UI 的树
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。
5.9 Suspense
Suspense 是一个试验性的新特性,其 API 可能随时会发生变动。特此声明,以便社区能够为当前的实现提供反馈。
该 <suspense> 组件提供了另一个方案,允许将等待过程提升到组件树中处理,而不是在单个组件中。
没什么创新的东西,只是封装了一个具名插槽
5.10 Composition API
- reactive
- ref toRef toRefs
- readonly
- setup
- watch watchEffect
- 生命周期钩子函数
6. Composition API 如何实现代码逻辑复用?
- 抽离逻辑代码到一个函数
- 函数命名约定为useXxxx格式(React Hooks也是)
- 在setup中引用useXxxx函数
6.1 以 MousePosition 为例
index.vue
<template>
<p>mouse position {{x}} {{y}}</p>
</template>
<script>
import { reactive } from 'vue'
import useMousePosition from './useMousePosition'
// import useMousePosition2 from './useMousePosition'
export default {
name: 'MousePosition',
setup() {
const { x, y } = useMousePosition()
return {
x,
y
}
// const state = useMousePosition2()
// return {
// state
// }
}
}
</script>
useMousePosition.js
import { reactive, ref, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
console.log('useMousePosition mounted')
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('useMousePosition unMounted')
window.removeEventListener('mousemove', update)
})
return {
x,
y
}
}
// 使用 reactive 实现,不用ref
// function useMousePosition2() {
// const state = reactive({
// x: 0,
// y: 0
// })
// function update(e) {
// state.x = e.pageX
// state.y = e.pageY
// }
// onMounted(() => {
// console.log('useMousePosition mounted')
// window.addEventListener('mousemove', update)
// })
// onUnmounted(() => {
// console.log('useMousePosition unMounted')
// window.removeEventListener('mousemove', update)
// })
// return state
// }
export default useMousePosition
// export default useMousePosition2
7. Vue3 如何实现响应式?
7.1 回顾Vue2.x 的object.defineProperty
observe.js
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
7.1.1 Object.defineProperty的缺点
- 深度监听需要一次性递归
- 深度比较深可能会页面卡着不动
- 无法监听新增属性/删除属性(Vue.set Vue.delete)
- 无法原生监听数组,需要特殊处理
7.2 Proxy 基本使用
- Proxy和Reflect很协调 参数也是对应好的
- target就是原生的data,deleteProperty删除哪个对象的哪个属性
- receiver就是proxyData,通过Reflect.get()的形式获取,通过Reflect.set()的形式设置
proxy-demo.js
// const data = {
// name: 'zhangsan',
// age: 20,
// }
const data = ['a', 'b', 'c']
const proxyData = new Proxy(data, {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
})
Reflect作用
- 和Proxy能力一一对应
- 规范化、标准化、函数式
- 替代掉Object上的工具函数
7.3 vue3用Proxy 实现响应式
7.3.1 Proxy 实现优点
- 深度监听,性能更好
- 可监听 新增/删除 属性
- 可监听数组变化
7.3.2 proxy-observe.js
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则返回
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
// 深度监听
// 性能如何提升的?
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
// 判断是否是新增的数据
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('已有的 key', key)
} else {
console.log('新增的 key', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
// 测试数据
const data = {
name: 'zhangsan',
age: 20,
info: {
city: 'beijing',
a: {
b: {
c: {
d: {
e: 100
}
}
}
}
}
}
const proxyData = reactive(data)
7.3.3 Proxy总结
- Proxy 能规避 Object.defineProperty 的问题
- Proxy 无法兼容所有浏览器,无法polyfill
请持续关注
# 8. v-model参数的用法
# watch 和 watchEffect 的区别是什么?
# setup 中 如何获取组件实例?
# Vue3 为何比Vue2 快?
# Vite 是什么?
# Composition API 和 React Hooks 的对比