第四十七篇 vue - vue2 和 vue3 的对比

vue2 和 vue3 不同点汇总

1、生命周期
2、多根节点
3、Composition Api
4、异步组件
5、响应式原理
6、Teleport
7、虚拟 Dom
8、事件缓存
9Diff 算法优化
10、打包优化
11、TypeScript 支持
生命周期
1、Vue3 生命周期 整体上变化不大,Vue3 在大部分生命周期钩子名称上 + “on”,功能上是类似的
2、Vue3 在组合式 API( Composition API )中使用 生命周期钩子 时需要先引入,Vue2 在选项 API(Options API)中可以直接调用生命周期钩子

常用的生命周期对比

Vue2 生命周期 Vue3 生命周期
beforeCreate
created
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted
说明
Vue3 中 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地去定义
代码示例
1、 vue3
<script setup>
import { onMounted } from 'vue'; // 使用前需引入生命周期钩子
onMounted(() => {
// ...
});
// 可将不同的逻辑拆开成多个onMounted,依然按顺序执行,不会被覆盖
onMounted(() => {
// ...
});
</script>
2、vue2
<script>
export default {
mounted() { // 直接调用生命周期钩子
// ...
},
}
</script>
多根节点
1、Vue2 在模板中如果使用多个根节点时会报错
2、Vue3 支持多个根节点,也就是 fragment
原因
1、在 vue2 中
1、因为 vdom 是一个 单根树形结构 描述当前视图结构,patch 方法在遍历的时候从根节点开始遍历,它要求只有一个根节点
2、组件也是会转换成 vdom,所以也必须满足单根节点要求
2、在 vue3 中
1、因为 vue3 引入了 fragment 概念,这是一个抽象的节点,如果发现组件是多根的会自动创建一个 fragment 节点,把多根节点视为自己的 children
2、如果发现这是一个fragment节点,则直接遍历children创建或更新
Composition API
1Vue2 是 选项 APIOptions API),一个逻辑会散乱在文件不同位置( data、props、computed、watch、生命周期钩子等 ),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。
2Vue3 组合式 APIComposition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性,其还提供了较为完美的逻辑复用性方案
异步组件( Suspense )
1、Vue3 提供 Suspense 组件,允许程序在等待异步组件加载完成前渲染兜底的内容,如 loading,使用户的体验更平滑
2、使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback
Suspense 确保加载完异步内容时显示默认插槽,并将 fallback 插槽用作加载状态
代码示例
<tempalte>
<suspense>
<template #default>
<List />
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</suspense>
</template>
在 List 组件(有可能是异步组件,也有可能是组件内部处理逻辑或查找操作过多导致加载过慢等)未加载完成前,显示 Loading...(即 fallback 插槽内容),加载完成时显示自身(即 default 插槽内容)
Teleport
Vue3 提供 Teleport 组件可将部分 DOM 移动到 Vue app 之外的位置。比如项目中常见的 Dialog 弹窗
<button @click="dialogVisible = true">显示弹窗</button>
<teleport to="body">
<div class="dialog" v-if="dialogVisible">
我是弹窗,我直接移动到了body标签下
</div>
</teleport>
响应式原理
1Vue2 响应式原理基础是 Object.defineProperty
2Vue3 响应式原理基础是 Proxy
vue2 响应式基础
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;
}
});
提示 【 writable 和 value 】 与 【 getter 和 setter 】 不共存
Vue2 核心源码,略删减
function defineReactive(obj, key, val) {
// 一 key 一个 dep
const dep = new Dep()
// 获取 key 的属性描述符,发现它是不可配置对象的话直接 return
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) { return }
// 获取 getter 和 setter,并获取 val 值
const getter = property && property.get
const setter = property && property.set
if((!getter || setter) && arguments.length === 2) { val = obj[key] }
// 递归处理,保证对象中所有 key 被观察
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// get 劫持 obj[key] 的 进行依赖收集
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if(Dep.target) {
// 依赖收集
dep.depend()
if(childOb) {
// 针对嵌套对象,依赖收集
childOb.dep.depend()
// 触发数组响应式
if(Array.isArray(value)) {
dependArray(value)
}
}
}
}
return value
})
// set 派发更新 obj[key]
set: function reactiveSetter(newVal) {
...
if(setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 新值设置响应式
childOb = observe(val)
// 依赖通知更新
dep.notify()
}
}
vue3 响应式基础
1Vue3 为何会抛弃 vue2 的 Object.defineProperty
主要原因:无法监听 对象 或 数组 新增、删除 的元素
Vue2 相应解决方案
1、针对常用数组原型方法 push、pop、shift、unshift、splice、sort、reverse 进行了 hack 处理;提供 Vue.set 监听对象/数组新增属性
2、对象的新增/删除响应,还可以 new 个新对象,新增则合并新属性和入旧对象;删除则将删除属性后的对象深拷贝给新对象。
2Proxy
1ProxyES6 新特性,通过第2个参数 handler 拦截目标对象的行为
2、相较于 Object.defineProperty 提供语言全范围的响应能力,消除了局限性
1、支持 对象/数组的 新增、删除
2、监测 .length 修改
3MapSetWeakMapWeakSet 的支持
基本用法:创建对象的代理,从而实现基本操作的 拦截 和 自定义 操作
let handler = {
get(obj, prop) {
return prop in obj ? obj[prop] : '';
},
set() {
// ...
},
...
};
部分 vue3 的源码 reactive.ts
function createReactiveObject(target, isReadOnly, baseHandlers, collectionHandlers, proxyMap) {
...
// collectionHandlers: 处理Map、Set、WeakMap、WeakSet
// baseHandlers: 处理数组、对象
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
虚拟 Dom
Vue3 相比于 Vue2,虚拟 DOM 上增加 patchFlag 字段
我们借助 Vue3 Template Explorer 来看
<div id="app">
<h1>vue3虚拟DOM讲解</h1>
<p>今天天气真不错</p>
<div>{{name}}</div>
</div>
渲染函数如下所示
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue
const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n)
const _hoisted_1 = { id: app }
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, vue3虚拟DOM讲解, -1 /* HOISTED */))
const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天气真不错, -1 /* HOISTED */))
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(div, _hoisted_1, [
_hoisted_2,
_hoisted_3,
_createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */)
]))
}
3 个 _createElementVNode 的第4个参数即 patchFlag 字段类型
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 应该结束
}
事件缓存
1、Vue3 的cacheHandler可在第一次渲染后缓存我们的事件
2、相比于 Vue2 无需每次渲染都传递一个新函数,加一个 click 事件
<div id="app">
<h1>vue3事件缓存讲解</h1>
<p>今天天气真不错</p>
<div>{{name}}</div>
<span onCLick=() => {}><span>
</div>
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue
const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n)
const _hoisted_1 = { id: app }
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, vue3事件缓存讲解, -1 /* HOISTED */))
const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天气真不错, -1 /* HOISTED */))
const _hoisted_4 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(span, { onCLick: () => {} }, [
/*#__PURE__*/_createElementVNode(span)
], -1 /* HOISTED */))
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(div, _hoisted_1, [
_hoisted_2,
_hoisted_3,
_createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */),
_hoisted_4
]))
}
观察以上渲染函数,你会发现 click 事件节点为静态节点(HOISTED 为 -1),即不需要每次重新渲染
Diff 算法优化
1、vue2.x 的 虚拟 DOM 是进行全量比较
2、vue3 新增了 静态标记 PatchFlag
3、搬运 Vue3 patchChildren 源码
结合上文与源码 patchFlag 帮助 diff 时区分静态节点,以及不同类型的动态节点,一定程度地减少节点本身及其属性的比对
function patchChildren(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) {
// 获取新老孩子节点
const c1 = n1 && n1.children
const c2 = n2.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const { patchFlag, shapeFlag } = n2
// 处理 patchFlag 大于 0
if(patchFlag > 0) {
if(patchFlag && PatchFlags.KEYED_FRAGMENT) {
// 存在 key
patchKeyedChildren()
return
} els if(patchFlag && PatchFlags.UNKEYED_FRAGMENT) {
// 不存在 key
patchUnkeyedChildren()
return
}
}
// 匹配是文本节点(静态):移除老节点,设置文本节点
if(shapeFlag && ShapeFlags.TEXT_CHILDREN) {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
// 匹配新老 Vnode 是数组,则全量比较;否则移除当前所有的节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense,...)
} else {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
if(prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(c2 as VNodeArrayChildren, container,anchor,parentComponent,...)
}
}
}
}
patchUnkeyedChildren 源码如下所示
function patchUnkeyedChildren(c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength)
let i
for(i = 0; i < commonLength; i++) {
// 如果新 Vnode 已经挂载,则直接 clone 一份,否则新建一个节点
const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as Vnode)) : normalizeVnode(c2[i])
patch()
}
if(oldLength > newLength) {
// 移除多余的节点
unmountedChildren()
} else {
// 创建新的节点
mountChildren()
}
}
打包优化
1Tree-shaking
1、模块打包 webpack、rollup 等中的概念
2、移除 JavaScript 上下文中未引用的代码
3、主要依赖于 importexport 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用
2、以 nextTick 为例子,在 Vue2 中,全局 API 暴露在 Vue 实例上,即使未使用,也无法通过 tree-shaking 进行消除
import Vue from 'vue';
Vue.nextTick(() => {
// 一些和DOM有关的东西
});
3Vue3 中针对全局和内部的 API 进行了重构,并考虑到 tree-shaking 的支持。因此,全局 API现在只能作为 ES模块 构建的命名导出进行访问
import { nextTick } from 'vue'; // 显式导入
nextTick(() => {
// 一些和DOM有关的东西
});
1、通过这一更改,只要模块绑定器支持 tree-shaking,则 Vue 应用程序中未使用的 api 将从最终的捆绑包中消除,获得最佳文件大小
2、受此更改影响的全局API如下所示。
Vue.nextTick
Vue.observable (用 Vue.reactive 替换)
Vue.version
Vue.compile (仅全构建)
Vue.set (仅兼容构建)
Vue.delete (仅兼容构建)
3、内部 API 也有诸如 transition、v-model 等标签或者指令被命名导出
只有在程序真正使用才会被捆绑打包
4、Vue3 将所有运行功能打包也只有约 22.5kb,比 Vue2 轻量很多。
TypeScript 支持
Vue3 由 TypeScript 重写,相对于 Vue2 有更好的 TypeScript 支持
1、Vue2 Options API 中 option 是个简单对象,而 TypeScript 是一种类型系统,面向对象的语法,不是特别匹配。
2、Vue2 需要 vue-class-component 强化 vue 原生组件,也需要 vue-property-decorator 增加更多结合Vue特性的装饰器,写法比较繁琐
其他变化
1、移除了 Event Bus
1、Vue3 从实例中移除了 o n 、 ononoff 和 $once 方法
2、如果希望继续使用全局事件总线的话,就需要通过第三方库
Vue3 官方有推荐一些库,例如 mitt 或 tiny-emitter
2、vue3 取消了 vue2 中的过滤器
过滤器打破了大括号内的表达式 “只是 JavaScript” 的假设
建议使用
1、在双括号表达式中调用方法 实现
2、计算属性来替换 过滤器

Options API 与 Composition API

Vue 组件可以用两种不同的 API 风格编写
1、Options API 【 选项 Api 】
2、Composition API 【 组合式 Api 】
Options API 【 选项 Api 】
1、使用 Options API,我们使用选项对象定义组件的逻辑
1、例如 data、methods 和 mounted
2、由选项定义的属性在 this 内部函数中公开,指向组件实例
2、代码示例
<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>
Composition API 【 组合式 Api 】
使用 Composition API,我们使用导入的 API 函数定义组件的逻辑。
在 SFC 中 通常使用 Composition API
<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>
posted @   caix-1987  阅读(102)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示