在 vue3 中,新增了 setup 生命周期函数,它的执行时机是在 beforeCreate 生命函数之前执行,在这个函数中是不能通过 this 来获取实例的。
vue3 在组合式API中使用生命周期钩子时需要先引入,而 vue2 在选项API中可以直接调用生命周期钩子。
在 setup 中挂载生命周期钩子,当执行到对应的生命周期时,就调用对应的钩子函数。
// vue3
<script setup>
import { onMounted } from 'vue'; // 使用前需引入生命周期钩子
onMounted(() => {
// ...
});
// 可将不同的逻辑拆开成多个onMounted,依然按顺序执行,不会被覆盖
onMounted(() => {
// ...
});
</script>
// vue2
<script>
export default {
mounted() { // 直接调用生命周期钩子
// ...
},
}
</script>
在 vue2 模板中,使用多个根节点会报错,但在 vue3 中支持多个根节点。
// vue3
<template>
<header></header>
<main></main>
<footer></footer>
</template>
// vue2
// 只能存在一个根节点,需要用一个来包裹着
<template>
<div>
<header></header>
<main></main>
<footer></footer>
</div>
</template>
composition API
vue2 中选项API,逻辑会散乱在文件不同位置,比如实现某一个功能代码涉及到 data 、props 、watch 、生命周期等,这样对相同数据进行操作使用的代码会被分割到各个属性内,不利于阅读;选项API大量使用到 this,组件中的 props、data、methods都是绑定到 this 上下文,由 this 去访问,不利于代码逻辑的复用。
<template>
<button @click="increment">count is: {{ count }}</button>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++;
}
},
mounted() {
console.log(`The initial count is ${this.count}.`);
}
}
</script>
在 vue3 组合式API中对这一问题进行了优化,使用组合式API可以将同一逻辑内容写到一起,增强代码可读性和可维护性;组合式API在涉及组件之间提取、复用逻辑时会非常灵活,它打破了 this 的限制,一个合成函数只依赖于它的参数和全局引入的 vue apis,而不是 this 上下文,更利于复用。
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const count = ref(0);
function increment() {
count.value++;
}
onMounted(() => {
console.log(`The initial count is ${count.value}.`);
})
</script>
setup
setup 是一个新的组合选项,充当组件内部使用 composition API 的入口点。
setup 函数里 this 并不是期望的组件实例,为 undefined。
<template>
<div>{{ count }} {{ object.foo }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const object = reactive({ foo: 'bar' })
// 必须return,才可以在模版上使用
return {
count,
object
}
}
}
</script>
reacitive
用于实现对象数据类型的响应式侦测。
// 传入一个普通对象,返回值是一个经过 vue 代理过的响应式对象
const Counter = {
setup(){
//reactive实现响应式,适用于一组值
const state = reactive({
count:0
})
const add =()=>{
state.count ++
}
//返回一个代理后的对象,用来实现响应式
console.log(state)
return {
state,
add
}
},
template:`{{state.count}}
`,
};
ref
用于实现基础数据类型值的响应式侦测。
传入一个值,返回一个 vue 内部包装过的对象,并且改变 value 属性可以触发响应式更新,用于模板渲染时,不用.value去访问,内部会自动拆包。
const Counter = {
setup(){
//ref实现响应式,适合单个值场景
const count = ref(0)
const add =()=>{
count.value ++
}
console.log(count)
return {
count,
add
}
},
template:`{{count}}
`,
};
toRef toRefs
reactive 返回的代理对象在组合函数的传递过程中必须保持对返回对象的引用,保证它的响应式,不能被ES6解构。
// toRef
const pos = reactive({
x:0,
y:0
})
//将响应式对象某一个属性转化为ref
const xRef = toRef(pos,'x')
const yRef = toRef(pos,'y')
// toRefs
const pos = reactive({
x:0,
y:0
})
//将整个响应式对象的全部属性转化为ref,装在一个普通对象中
const posRefsObj = useRefs(pos)
//等价于
const posRefsObj = {
x:toRef(pos,'x')
y:toRef(pos,'y')
}
computed
传入一个计算函数,返回一个包装后的响应式引用对象。
const Counter = {
setup(){
const state = reactive({
count:0
})
//计算count是否是偶数,字体切换红绿颜色
let isOdd = computed(()=>state.count%2 === 0)
const add =()=>{
state.count ++
}
return {
state,
isOdd,
add
}
},
template:`{{state.count}}
`,
};
watch
主动监测响应式数据,数据改变后执行用户传入的回调。
const Counter = {
setup(){
//reactive实现响应式,适用于一组值
const state = reactive({
count:0
})
//计算count是否是奇数,字体切换红绿颜色
let isOdd = computed(()=>state.count%2 === 0)
watch(isOdd,(newValue)=>{
alert(newValue?'偶数':"奇数")
})
const add =()=>{
state.count ++
}
return {
state,
isOdd,
add
}
},
template:`{{state.count}}
`,
};
依赖注入
provide和inject提供依赖注入,功能类似2.x的provide/inject。两者都只能在当前组件的setup()中调用。
<template>
<div>
<Article></Article>
</div>
</template>
<script>
import {
ref,
provide
} from "vue";
import Article from "./components/Article";
export default {
setup() {
const articleList = ref([
{ id: 1, title: "1" },
{ id: 2, title: "2" },
{ id: 3, title: "3" }
]);
/*
provide 函数允许你通过两个参数定义 property:
property 的 name ( 类型)
property 的 value
*/
provide("list",articleList);
return {
articleList
};
},
components: {
Article
}
};
</script>
<template>
<div>
{{articleList[0].title}}
</div>
</template>
<script>
import { inject } from "vue";
export default {
setup() {
const articleList = inject('list',[]);
return {articleList};
},
};
</script>
异步组件
vue3 提供 suspense 组件,允许程序在等待异步组件加载完成前渲染兜底内容,使用户体验更完整。
使用它需要在模板中声明,包括两个命名插槽:default 和 fallback。在加载完异步内容时显示默认插槽,将 fallback 插槽用作加载状态。
<tempalte>
<suspense>
<template #default>
<List />
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</suspense>
</template>
teleport
vue3 提供 Teleport 组件可将部分 DOM 移动到 Vue app 之外的位置。
<button @click="dialogVisible = true">显示弹窗</button>
<teleport to="body">
<div class="dialog" v-if="dialogVisible">
我是弹窗,我直接移动到了body标签下
</div>
</teleport>
响应式原理
响应式是实现数据驱动视图的第一步,监听数据的变化,使用户在设置数据时,可以通知 vue 内部进行视图更新。
vue2 响应式原理基础是 Object.defineProperty,它的作用是直接在一个对象上定义一个新属性或者修改一个已经存在的属性。
// obj 需要定义属性的当前对象
// prop 当前需要定义的属性名
// desc 属性描述符
Object.defineProperty(obj, prop, desc)
// Object.defineProperty基本用法
let obj = {};
let name = 'leo';
Object.defineProperty(obj, 'name', {
enumerable: true, // 可枚举(是否可通过 for...in 或 Object.keys() 进行访问)
configurable: true, // 可配置(是否可使用 delete 删除,是否可再次设置属性)
// value: '', // 任意类型的值,默认undefined
// writable: true, // 可重写
get() {
return name;
},
set(value) {
name = value;
}
});
// 实现对原始类型和对象的响应式监听,数据变化时,会在数据更新后进行试图更新
function bindReactive (target, key, value) {
// 对象类型进行递归遍历
reactive(value)
Object.defineProperty(target, key, {
get () {
return value
},
set (newVal) {
if (newVal !== value) {
// 改变值的对象类型
reactive(newVal)
value = newVal
// 触发视图更新
renderView()
}
}
})
}
function reactive (target) {
// 首先,不是对象直接返回
if (typeof target !== 'object' || target === null) {
return target
}
// 遍历对象,对每个key进行响应式监听
for (let key in target) {
bindReactive(target, key, target[key])
}
}
const reactiveData = reactive(data)
虽然对原始类型和普通对象进行响应式监听,但它无法对数组的更改进行监听。在 vue2 中,通过 Object.create(prototype) 创建一个对象,使它的原型指向参数 prototype。
// 数组的原型
const prototype = Array.prototype
// 创建一个新的原型对象,他的原型是数组的原型(于是newPrototype上具有所有数组的api)
const newPrototype = Object.create(prototype)
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
methods.forEach(method => {
newPrototype[method] = () => {
prototype[method].call(this, ...args)
// 视图更新
renderView()
}
})
// 入口方法
function reactive (target) {
// 首先,不是对象直接返回
if (typeof target !== 'object' || target === null) {
return target
}
// 对于数组,原型修改
if (Array.isArray(target)) {
target.__proto__ = newPrototype
}
// 遍历对象,对每个key进行响应式监听
for (let key in target) {
bindReactive(target, key, target[key])
}
}
在 vue2 响应式实现中,Object.defineProperty 无法对数组进行响应式监听,实现中也有深度嵌套数据消耗性能。
而在 vue3 中,改用 Proxy 和 Reflect 代替 Object.defineProperty,对 vue2 响应式存在的问题有了更好的解决。
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义。也就是在目标对象的外层搭建一层拦截,外界通过目标对象的某些操作必须通过这层拦截
Reflect 是一个内置对象,提供拦截js操作的方法
// target 用proxy包装的目标对象
// handler 一个对象,属性是执行一个操作时定义代理的行为的函数
let proxy = new Proxy(target, handler)
// reflect对象常用方法
Reflect.get(): 获取对象身上某个属性的值
Reflect.set(): 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true
Reflect.has(): 判断一个对象是否存在某个属性
Reflect.deleteProperty(): 作为函数的delete操作符
function bindReactive (target) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则直接返回
return target
}
// 传给Proxy的handler
const handler = {
get(target, key) {
const reflect = Reflect.get(target, key)
// 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级
return bindReactive(reflect)
},
set(target, key, val) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
// 这里可以根具是否是已有的key,做不同的操作
if (Reflect.has(key)) {
} else {
}
const success = Reflect.set(target, key, val)
// 设置成功与否
return success
},
deleteProperty(target, key) {
const success = Reflect.deleteProperty(target, key)
// 删除成功与否
return success
}
}
// 生成proxy对象
const proxy = new Proxy(target, handler)
return proxy
}
// 实现数据响应式监听
const reactiveData = bindReactive(data)
虚拟DOM
相比于 vue2 ,虚拟DOM上增加了 patchFlag 字段。
1 代表节点为动态文本节点,在 diff 过程中,只需要比对文本内容,无需关注 class、style 等。-1为静态节点,都会保存为一个变量进行静态提升,可在重新渲染时直接引用,无需重新创建。
/ patchFlags 字段类型列举
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态类名
STYLE = 1 << 2, // 动态样式
PROPS = 1 << 3, // 动态属性,不包含类名和样式
FULL_PROPS = 1 << 4, // 具有动态 key 属性,当 key 改变,需要进行完整的 diff 比较
HYDRATE_EVENTS = 1 << 5, // 带有监听事件的节点
STABLE_FRAGMENT = 1 << 6, // 不会改变子节点顺序的 fragment
KEYED_FRAGMENT = 1 << 7, // 带有 key 属性的 fragment 或部分子节点
UNKEYED_FRAGMENT = 1 << 8, // 子节点没有 key 的fragment
NEED_PATCH = 1 << 9, // 只会进行非 props 的比较
DYNAMIC_SLOTS = 1 << 10, // 动态的插槽
HOISTED = -1, // 静态节点,diff阶段忽略其子节点
BAIL = -2 // 代表 diff 应该结束
}
diff算法
patchFlag 帮助 diff 区分静态节点以及不同类型的动态节点,一定程度上减少了节点本身及其属性的比对。
打包优化
vue3 中引入了 Tree-sharking ,它带来的打包体积更小,在 vue2 中很多函数都挂载在全局 Vue 对象上,虽然我们可能用不到,但打包时只要引入了 vue 这些全局函数仍然会打包进去。在 vue3 中,所有API通过ES6模块化的方式引入,这样可以在打包时对没有用到的API进行剔除,最小化打包体积。
创建 app 实例的方式从 new Vue() 变为 createApp 进行创建,这样以前在全局配置上的组件、插件等直接挂载在实例上的方法,通过创建实例来调用。
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router).mount("#app");