Vue3.0新特性技术分享
当下前端开发领域中,Vue.js已成为最受欢迎的JavaScript框架之一,而Vue 3.0则是Vue.js的最新版本,也是一次重大的更新。Vue 3.0的更新主要集中在性能优化和代码组织方面,下面我们将深入探讨Vue 3.0的新特性。
Composition API
-
Vue 3.0引入了Composition API,它提供了一种新的组织代码的方式。传统的Options API需要将相关代码分散到不同的选项中(如
data
,methods
,computed
,watch
等),而Composition API则允许我们将相关代码放在一起,提高了可读性和可维护性。 -
在Vue 2.0中,开发者使用Options API来定义组件的数据、计算属性、方法和生命周期钩子等。但是,随着应用程序的增长和复杂度的提高,Options API变得越来越难以维护。为了解决这个问题,Vue 3.0引入了Composition API。Composition API是基于函数的API,可以将功能逻辑划分为功能单元,从而更好地组织和复用代码。Composition API的核心是一个函数createXXX,其中XXX可以是任何自定义名称,这个函数可以返回一个对象,其中包含需要导出的组件选项,例如data、methods、computed、watcher等等。
-
Composition API的另一个好处是可以更好地处理逻辑复用。在Options API中,通常需要使用mixin或高阶组件来实现代码的复用,但这种方法可能会导致命名冲突或组件层次结构复杂。使用Composition API,可以更好地实现代码的复用,而不必担心这些问题。
-
此外,Composition API还可以更好地处理代码分离和测试。使用Composition API,可以更好地分离逻辑代码和渲染代码,并更容易进行单元测试和集成测试。
Composition API主要由两部分组成:setup
函数和一些新的响应式函数(reactive、 ref、 computed、 watch、 onMounted 、 onUpdated 、 onUnmounted)。
setup
setup
函数是Vue3中新引入的一个组件选项。它是一个在组件创建过程中会被调用的函数,用于设置组件的初始状态和逻辑。在这个函数中,我们可以使用Composition API中提供的各种函数来管理组件的状态和行为。同时,我们也可以在这个函数中返回一些变量、方法或者生命周期钩子函数,它们将被作为组件实例的属性和方法进行调用和使用。
setup
函数的主要目的是将组件逻辑从模板中分离出来,使得组件的代码更加清晰和可维护。由于setup
函数在模板编译之前被调用,因此我们可以在这里进行一些预处理,比如对一些响应式数据进行初始化,或者对一些数据进行计算、过滤、排序等操作。
下面是一个使用setup
函数的例子:
<template>
<div>{{ count }}</div>
</template>
<script>
import { reactive } from 'vue';
export default {
setup() {
const state = reactive({
count: 0,
});
setInterval(() => {
state.count++;
}, 1000);
return {
count: state.count,
};
},
};
</script>
在这个例子中,我们使用了reactive
函数创建了一个响应式对象state
,并在setup
函数中使用setInterval
函数来自动更新state.count
的值。然后,我们通过return
语句将count
属性返回,这样模板就可以通过这个属性来访问state.count
的值了。需要注意的是,返回的属性会被自动绑定到组件实例上,因此我们可以在组件内部通过this.count
来访问它。
reactive
reactive
函数是用来创建一个响应式对象的,它的使用方式和Vue 2.x中的Vue.observable
函数类似。我们可以将一个普通的JavaScript对象传给reactive
函数,然后就可以在这个对象上使用响应式数据了。例如:
import { reactive } from 'vue';
const state = reactive({
count: 0,
});
console.log(state.count); // 0
state.count++;
console.log(state.count); // 1
ref
ref
函数是用来创建一个响应式的基本数据类型值,例如数字、字符串、布尔值等。与reactive
不同,ref
函数返回的是一个包含值的对象,而不是一个代理对象。我们可以通过.value
来访问这个响应式值的实际值。例如:
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
computed
computed
函数是用来创建一个响应式的计算属性的,它接收一个函数作为参数,这个函数返回的值会被自动缓存并在需要时进行更新。例如:
import { reactive, computed } from 'vue';
const state = reactive({
count: 0,
});
const doubledCount = computed(() => {
return state.count * 2;
});
console.log(doubledCount.value); // 0
state.count++;
console.log(doubledCount.value); // 2
watch
watch
函数是用来监听一个响应式数据的变化的。与Vue 2.x中的$watch
方法不同,watch
函数返回的是一个取消监听的函数,我们可以在组件销毁时调用这个函数来取消监听。例如:
import { reactive, watch } from 'vue';
const state = reactive({
count: 0,
});
watch(
() => state.count,
(newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`);
}
);
state.count++; // count changed from 0 to 1
onMounted, onUpdated, onUnmounted
onMounted
、onUpdated
和onUnmounted
函数分别是在组件挂载、更新和卸载时被调用的钩子函数。它们可以用来执行一些副作用操作,例如发起网络请求、订阅事件等。例如
import { onMounted, onUpdated, onUnmounted } from 'vue';
export default {
setup() {
onMounted(() => {
console.log('Component mounted');
});
onUpdated(() => {
console.log('Component updated');
});
onUnmounted(() => {
console.log('Component unmounted');
});
},
};
watchEffect
当我们需要在函数体内对响应式数据进行监听,但是又不想声明一个完整的watcher时,watchEffect
函数就派上用场了。它接收一个函数作为参数,该函数中使用到的响应式数据变化时,函数会被自动执行。例如:
import { reactive, watchEffect } from 'vue';
const state = reactive({
count: 0,
});
watchEffect(() => {
console.log(`Count is now ${state.count}`);
});
state.count++; // Count is now 1
toRefs
toRefs
函数用于将一个响应式对象的属性转换为响应式引用。这个函数的使用场景通常是在传递响应式对象的属性作为props给子组件时。当我们将响应式对象的属性直接作为props传递给子组件时,子组件不再具备响应式,因此当属性值变化时,子组件不会重新渲染。但是,如果我们将响应式对象的属性转换为响应式引用,子组件就可以获取到完整的响应式能力了。例如:
import { reactive, toRefs } from 'vue';
const state = reactive({
count: 0,
});
export default {
props: toRefs(state),
template: `
<div>{{ count }}</div>
`,
};
组合逻辑式列
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const { createApp, reactive, ref, computed, toRefs, onMounted, onUnmounted } = Vue
function useMouse() {
// 响应式数据
const state = reactive({ x: 0, y: 0 })
const update = e => {
state.x = e.pageX
state.y = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.addEventListener('mousemove', update)
})
return toRefs(state)
}
function useTime() {
const state = reactive({ time: new Date() })
onMounted(() => {
setInterval(() => { state.time = new Date() }, 1000)
})
return toRefs(state)
}
const MyComp = {
template:`<div> x:{{x}} y{{y}}</div>
<p>time {{time}}</p>`,
setup(){
const {x,y}= useMouse()
const {time}= useTime()
return {x,y,time}
}
}
const app = createApp(MyComp)
app.mount('#app')
</script>
</body>
</html>
对比mixins,好处显而易见:
- x,y,time来源清晰
- 不会与data、props等命名冲突
Teleport组件
Teleport
组件是Vue3.0中新引入的一个组件,它可以帮助我们将组件的渲染结果移动到DOM树中的其他位置,从而实现更加灵活和精细的UI布局效果。例如,我们可以将一个弹窗组件的内容渲染到body节点下,从而实现一个可以浮在页面上方的弹窗效果。
使用Teleport
组件非常简单,只需要在组件中使用<Teleport to="selector">
标签来指定目标位置即可。下面是一个例子:
<template>
<div>
<button @click="showModal = true">显示弹窗</button>
<teleport to="body">
<div v-if="showModal" class="modal">
<h2>这是一个弹窗</h2>
<p>弹窗的内容可以渲染到其他位置</p>
<button @click="showModal = false">关闭弹窗</button>
</div>
</teleport>
</div>
</template>
<script>
export default {
data() {
return {
showModal: false,
};
},
};
</script>
在这个例子中,我们在弹窗组件的外部包裹了一个<Teleport to="body">
标签,将弹窗的内容渲染到了body节点下。需要注意的是,我们使用v-if
指令来控制弹窗的显示和隐藏,这样弹窗的内容只有在需要显示的时候才会被渲染到DOM中,从而避免了不必要的渲染和性能损耗。
需要注意的是,由于Teleport
组件会将组件的渲染结果移动到其他位置,因此可能会影响到组件内部的样式和事件绑定等。为了避免这种情况,我们可以使用<Teleport>
标签中的disabled
属性来禁用移动功能,或者使用<Teleport>
标签的另一个属性fallback
来指定一个备用的渲染结果。
Teleport
组件有两个主要的属性:to
和disabled
。
to
属性:指定目标位置。我们可以使用CSS选择器的语法来指定目标位置,例如to="#modal"
、to=".wrapper"
、to="body"
等。如果要将渲染结果移动到其他组件中,可以使用to="componentName"
的语法。需要注意的是,指定的目标位置必须在当前组件的祖先节点中存在,否则渲染结果将无法正确显示。disabled
属性:禁用移动功能。如果设置了disabled
属性,Teleport
组件将不会将组件的渲染结果移动到指定的位置,而是直接将渲染结果放在组件的当前位置。这种情况下,组件的样式和事件绑定等将不会受到影响。
除了上述两个属性外,Teleport
组件还有一个fallback
属性。当Teleport
组件无法将组件的渲染结果移动到指定的位置时,会使用fallback
属性中指定的备用渲染结果。这种情况通常发生在指定的目标位置不存在或者当前组件被销毁的情况下。例如,我们可以在Teleport
组件中使用一个默认的loading动画作为备用渲染结果:
<template>
<div>
<teleport to="#modal" :fallback="loading">
<modal v-if="showModal" />
</teleport>
</div>
</template>
<script>
export default {
data() {
return {
showModal: false,
loading: <div class="loading">Loading...</div>
};
}
};
</script>
在这个例子中,我们在Teleport
组件中使用了一个<div class="loading">Loading...</div>
作为备用渲染结果。如果Teleport
组件无法将<modal>
组件的渲染结果移动到#modal
指定的位置,就会使用loading
作为替代方案,从而避免了组件无法渲染的情况。
需要注意的是,fallback
属性中可以包含任意的Vue模板或渲染函数,因此可以根据具体的应用场景来选择合适的备用渲染结果。
Vue3.0性能优化
-
更快的渲染速度
Vue3.0通过使用
Proxy
对象代替Object.defineProperty
来实现响应式数据的监听,从而提高了渲染性能。同时,Vue3.0还优化了虚拟DOM的算法和实现,通过减少渲染节点的数量和避免不必要的重绘来提高渲染效率。 -
更小的包体积
Vue3.0在设计上更加模块化和精简,将一些不常用的功能进行拆分,使得打包后的代码体积更小。此外,Vue3.0还通过使用Tree shaking和ES Module的特性来进一步减小包体积。
-
更好的Tree shaking支持
Vue3.0支持使用ES Module语法进行按需加载,可以使得Tree shaking更加准确和有效。同时,Vue3.0还提供了全局的
createApp
函数,可以帮助我们将不同的组件进行分离和组合,使得Tree shaking的效果更好。 -
更好的TypeScript支持
Vue3.0在设计上更加注重类型推导和类型检查的支持,可以使得我们在使用TypeScript时更加方便和舒适。同时,Vue3.0还提供了一些新的TypeScript类型,如
DefineComponent
、ComponentPublicInstance
等,可以帮助我们更好地编写类型安全的Vue组件。
Vue3响应式原理
Vue2响应式
// 1.对象响应化:遍历每个key,定义getter、setter
// 2.数组响应化:覆盖数组原型方法,额外增加通知逻辑
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto) ;
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(
method => {
arrayProto[method] = function() {
originalProto[method].apply(this, arguments)
notifyUpdate()
}
})
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
// 增加数组类型判断,若是数组则覆盖其原型
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, arrayProto)
} else {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(obj, key, obj[key])
}
}
}
function defineReactive (obj, key, val) {
observe(val)
// 解决嵌套对象问题
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if (newVal !== val) {
observe(newVal)
// 新值是对象的情况
val = newVal
notifyUpdate()
}
}
})
}
function notifyUpdate() { console.log('页面更新!') }
- vue2响应式的弊端:
- 响应化过程需要递归遍历,消耗较大
- 新加或删除属性无法监听
- 数组响应化需要额外实现
- Map、Set、Class等无法响应式
- 修改语法有限制
Vue3响应式
- reactivity.js
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
// http://es6.ruanyifeng.com/#docs/proxy
const observed = new Proxy(obj, {
get(target, key, receiver) {
// Reflect用于执行对象默认操作,更规范、更友好
// Proxy和Object的方法Reflect都有对应
// http://es6.ruanyifeng.com/#docs/reflect
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
}
})
return observed
}
测试代码
const state = reactive({
foo: 'foo',
bar: { a: 1 }
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok
嵌套对象响应式
1.签到对象不能响应
// 4.设置嵌套对象属性
react.bar.a = 10 // no ok
- 添加对象类型递归
// 提取帮助方法
const isObject = val => val !== null && typeof val === 'object'
function reactive(obj) {
//判断是否对象
if (!isObject(obj)) {
return obj
}
const observed = new Proxy(obj, {
get(target, key, receiver) {
// ...
// 如果是对象需要递归
return isObject(res) ? reactive(res) : res
},
//...
}
避免重复代理
reactive(data) // 已代理过的纯对象
reactive(react) // 代理对象
解决方法:将之前代理结果缓存 get时直接使用
const toProxy = new WeakMap() // 形如obj:observed
const toRaw = new WeakMap() // 形如observed:obj
function reactive(obj) {
//...
// 查找缓存,避免重复代理
if (toProxy.has(obj)) {
return toProxy.get(obj)
}
if (toRaw.has(obj)) {
return obj
}
const observed = new Proxy(...)
// 缓存代理结果
toProxy.set(obj, observed)
toRaw.set(observed, obj)
return observed
}
// 测试效果
console.log(reactive(data) === state)
console.log(reactive(state) === state)
依赖收集
- 建立响应数据key和更新函数之间的对应关系。
- 用法
// 设置响应函数
effect(() => console.log(state.foo))
// 用户修改关联数据会触发响应函数
state.foo = 'xxx'
- 设计
实现三个函数:
effffect:将回调函数保存起来备用,立即执行一次回调函数触发它里面一些响应数据的getter
track:getter中调用track,把前面存储的回调函数和当前target,key之间建立映射关系
trigger:setter中调用trigger,把target,key对应的响应函数都执行一遍
4. 实现
设置响应函数,创建effffect函数
// 保存当前活动响应函数作为getter和effect之间桥梁
const effectStack = []
// effect任务:执行fn并将其入栈
function effect(fn) {
const rxEffect = function() {
// 1.捕获可能的异常
try {
// 2.入栈,用于后续依赖收集
effectStack.push(rxEffect)
// 3.运行fn,触发依赖收集
return fn()
} finally {
// 4.执行结束,出栈
effectStack.pop()
}
}
// 默认执行一次响应函数
rxEffect()
// 返回响应函数
return rxEffect
}
依赖收集和触发
function reactive(obj) {
// ...
const observed = new Proxy(obj, {
get(target, key, receiver) {
// ...
// 依赖收集
track(target, key)
return isObject(res) ? reactive(res) : res
},
set(target, key, value, receiver) {
// ...
// 触发响应函数
trigger(target, key)
return res
}
})
}
// 映射关系表,结构大致如下:
// {target: {key: [fn1,fn2]}}
let targetMap = new WeakMap()
function track(target, key) {
// 从栈中取出响应函数
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 获取target对应依赖表
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 获取key对应的响应函数集
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
if (!deps.has(effect)) {
deps.add(effect)
}
}
}
// 触发target.key对应响应函数
function trigger(target, key) {
// 获取依赖表
const depsMap = targetMap.get(target)
if (depsMap) {
// 获取响应函数集合
const deps = depsMap.get(key)
if (deps) {
// 执行所有响应函数
deps.forEach(effect => {
effect()
})
}
}
}
转载自:https://juejin.cn/post/7214015651826909239