都说Vue3跟Vue2比,性能优化很厉害!
ztj100 2025-06-10 04:17 55 浏览 0 评论
template模板不如jsx灵活,但是template相比jsx的固定性,可以在编译时获取许多信息,编译出可以在运行时执行尽可能少,性能尽可能好的代码。
Vue3性能优化的一个重要体现在编译优化,利用新的渲染器,编译出了相比vue2更小,更快的代码。
Tree Shaking - 优化体积
Vue3 源码中采用函数编写API,更加有利于Tree Shaking,而Tree Shaking的原理是利用ES6 Module的编译时加载,编译时就能确定模块的依赖关系,没有使用到的代码最终会被 webpack 或者 vite这样的构建工具删掉,js体积减小,网络传输就更快,js引擎解析也会更快,代码执行更快。
vue2项目打包体积对比
// App.vue 1
<template>
<div>test vue2 tree-shaking</div>
</template>
<script>
export default {
data() {
return {
name: "App",
};
},
};
</script>
// App.vue 2
<template>
<div>test vue2 tree-shaking</div>
</template>
<script>
export default {
data() {
return {
name: "App",
};
},
computed: {
fullName() {
return this.name + "vue2";
},
},
watch: {
name(newVal, oldVal) {
console.log(newVal, oldVal);
},
},
};
</script>
打包后vue文件大小没有变化
Vue3项目打包体积对比
// App.vue 1
<template>
<div>test vue3 tree-shaking</div>
{{ fullName }}
</template>
<script setup>
import { ref, computed, watch, nextTick, reactive } from "vue";
const name = ref("App");
const obj = reactive({
item: "tree-shaking",
});
const fullName = computed(() => name.value + "vue3");
watch(
() => name.value,
async (newVal, oldVal) => {
console.log(newVal, oldVal);
await nextTick();
obj.item = "vue3 tree-shaking";
}
);
</script>
打包vue文件大小有变化
Poxy - 优化数据劫持
vue2的数据劫持使用的是 Object.defineProperty,它的缺点也是众所周知,只能监听对象中已有的属性,不能监听对象的增加和删除,所以如果有一个嵌套层级很深的响应式对象数据,vue2无法知道代码运行时具体会访问哪个属性,所以在初始化这个对象的时候,vue2只能采取递归遍历的方式把对象的每一层每一个属性都变成响应式,这就会影响页面的初始化渲染速度;
而vue3就不一样了,它使用proxy进行数据劫持,对于多层嵌套的对象,由于proxy只能代理一层,所以vue3在真正访问到对象属性的时候,才去判断递归,而不是在初始化的时候就一股脑的递归。
下面看一下vue2和vue3在源码中的实现
vue2源码实现
function initData(vm: Component) {
let data: any = vm.$options.data
// 观测 data
observe(data)
}
export function observe(
value: any,
shallow?: boolean,
): Observer | void {
new Observer(value, shallow)
}
export class Observer {
constructor(
public value: any,
public shallow = false // 默认深层响应
) {
const keys = Object.keys(value);
// 遍历每一个属性变成响应式
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow);
}
}
}
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
) {
val = obj[key]
// 递归遍历,嵌套过深,性能损失
!shallow && observe(val, false, mock)
//...
}
vue3源码实现
// 简化版源码
// ref() ref也是包装过后的reactive
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown) {
return new RefImpl(rawValue)
}
class RefImpl<T> {
private _value: T
constructor(value: T) {
this._value = reactive(value)
}
get value() {
return this._value
}
set value(newVal) {
this._value = reactive(newVal)
}
}
// reactive()
export function reactive(target: object) {
return createReactiveObject(target)
}
function createReactiveObject(target: Target) {
const proxy = new Proxy(target, {
get(target: Target, key: string | symbol) {
const res = Reflect.get(target, key);
if (isObject(res)) {
// 对象属性被访问的时候才递归执行下一步 reactive,
// 优化数据初始化时性能
return reactive(res);
}
return res;
},
});
return proxy;
}
编译优化
静态提升
vue3将模版中的静态节点和属性提取到render函数外面,在组件更新的时候,减少vnode的创建带来的性能损耗
// App.vue
<script>
import { ref } from "vue";
export default {
setup() {
const msg = ref("vue hosited");
return { msg };
},
};
</script>
<template>
<div>
<h1>静态提升测试</h1>
<span>{{ msg }}</span>
</div>
</template>
预字符串化
当有大量连续的静态节点时,通过转化为字符串,既减少vnode创建过程,也可以减少代码体积
// App.vue
<script>
import { ref } from "vue";
export default {
setup() {
const msg = ref("vue hosited");
return { msg };
},
};
</script>
<template>
<div>
<h1>静态提升测试</h1>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>
<span>{{ msg }}</span>
</div>
</template>
缓存事件处理函数
每次render函数执行过后,生成新的vnode,对vnode的props中事件属性进行patch的时候,就直接取上一次缓存的函数,如果没有缓存,每次函数都是新的,引用不一致,会造成组件的更新
<template>
<div>
<h1 @click="msg = 'cache'">静态提升测试</h1>
<span @dblclick="msg = 'cache1'">{{ msg }}</span>
</div>
</template>
Block Tree
Block是vue3在编译模板过程中做的优化,收集动态子节点,能够在diff过程中根据动态子节点数量更新。
<script setup>
import { ref } from "vue";
const msg = ref("vue");
</script>
<template>
<div class="block">
<h1>Block</h1>
<span>{{ msg }}</span>
</div>
</template>
在浏览器控制台Network中可以看到模板被编译后
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "/node_modules/.vite/deps/vue.js?v=6f26e7ed";
const _hoisted_1 = { class: "block" };
const _hoisted_2 = /*#__PURE__*/ _createElementVNode(
"h1",
null,
"Block",
-1 /* HOISTED */
);
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", _hoisted_1, [
_hoisted_2,
_createElementVNode(
"span",
null,
_toDisplayString($setup.msg),
1 /* TEXT */
),
])
);
}
在render函数中调用了3个函数,openBlock,createElementBlock,createElementVNode,通过这个三个函数收集动态子节点
// /packages/runtime-core/src/vnode.ts
// 存储currentBlock数组
export const blockStack: (VNode[] | null)[] = []
// 当前block
export let currentBlock: VNode[] | null = null
// 向blockStack推入currentBlock
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
export function createElementBlock(
type: string | typeof Fragment,
props?: Record<string, any> | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[],
shapeFlag?: number
) {
return setupBlock(
createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
true /* isBlock */
)
)
}
function createBaseVNode(type, props = null, children = null,patchFlag = 0) {
const vnode = {
type,
props,
children,
patchFlag,
// ...
};
return vnode;
}
function setupBlock(vnode: VNode) {
// 在vnode上保留当前Block收集的动态子节点
vnode.dynamicChildren =
isBlockTreeEnabled > 0
? currentBlock || (EMPTY_ARR as any) : null
return vnode
}
例子中的render函数执行后返回一个vnode对象,如下,有type,children,dynamicChildren,props等属性
将图中的vnode对象简化一下,
{
type: "div",
props: {
class: "block",
},
children: [
{
type: "h1",
children: "Block",
},
{
type: "span",
children: "vue",
},
],
dynamicChildren: [
{
type: "span",
children: "vue",
},
],
};
更新的时候,就会根据vnode中的数据进行diff,在组件更新逻辑中,组件的更新最终还是会走到对普通 DOM 元素的更新,
// /packages/runtime-core/src/renderer.ts
const patch = (n1, n2, container, anchor = null, parentComponent = null) => {
const { type, ref, shapeFlag } = n2;
switch (type) {
if (shapeFlag & ShapeFlags.ELEMENT) {
// 更新普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 更新组件
processComponent(n1, n2, container, anchor, parentComponent);
}
}
};
const processElement = (n1, n2, container, anchor, parentComponent) => {
if (n1 == null) {
// 挂载
} else {
// 更新
patchElement(n1,n2,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized);
}
};
组件是抽象的普通Dom元素的集合,更新最终都会走到 patchElement 这个函数,
// /packages/runtime-core/src/renderer.ts
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const el = (n2.el = n1.el!);
let { patchFlag, dynamicChildren, dirs } = n2;
if (dynamicChildren) {
// 如果有dynamicChildren,只更新动态子节点
} else if (!optimized) {
// 全量更新所有子节点
}
PatchFlag
vue2 对比节点时,不知道这个节点哪些信息发生了变化,只能依次对比这些信息,vue3中,收集了dynamicChildren,已经减少对比静态子节点了,但是,动态子节点有许多属性,配合使用patchFlag,就可以知道哪些属性需要更新,就可以实现靶向更新。
vue3中patchFlag是包含一系列二进制操作值的枚举类型,
// /packages/shared/src/patchFlags.ts
export const enum PatchFlags {
// 动态文本的元素
TEXT = 1, //0b0000001 1
// 动态 class 的元素
CLASS = 1 << 1, //0b0000010 2
// 动态 style 的元素
STYLE = 1 << 2, //0b0000100 4
// 动态 props 的元素
PROPS = 1 << 3, //0b0001000 8
// 动态props和有key值绑定的元素
FULL_PROPS = 1 << 4, //0b0010000 16
// 静态节点
HOISTED = -1,
//...
}
认识一下跟二进制相关的几个操作符:
左移操作符 (<<),是将第一个操作数向左移动指定位数,左边超出的位数将会被清除,右边将会补零
按位与( &)运算符在两个操作数对应的二进位都为 1 时,该位的结果值才为 1;
按位或(| )运算符在其中一个或两个操作数对应的二进制位为 1 时,该位的结果值为 1。
patchFlag是在创建vnode的时候作为第四个参数传入,如下图
<template>
<div class="block">
<h1>Block</h1>
<span>{{ msg }}</span>
</div>
</template>
在 patchElement 对普通Dom元素进行更新的时候,就可以做到只对动态有变化的属性更新
// /packages/runtime-core/src/renderer.ts
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const el = (n2.el = n1.el!);
let { patchFlag, dynamicChildren, dirs } = n2;
patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS;
const oldProps = n1.props || EMPTY_OBJ;
const newProps = n2.props || EMPTY_OBJ;
if (dynamicChildren) {
// 如果有dynamicChildren,只更新动态子节点
} else if (!optimized) {
// 全量更新所有子节点
}
if (patchFlag > 0) {
if (patchFlag & PatchFlags.FULL_PROPS) {
// 如果元素的 props 中含有动态的 key,则需要全量比较 props
} else {
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
// 有动态的class, 更新class属性
}
}
if (patchFlag & PatchFlags.STYLE) {
// 有动态的style, 更新style属性
}
if (patchFlag & PatchFlags.PROPS) {
// 除了class和style外,其他动态的 prop 或者 attrs
const propsToUpdate = n2.dynamicProps!;
for (let i = 0; i < propsToUpdate.length; i++) {
// 遍历更新
}
}
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
// 更新动态的文本
}
}
} else if (!optimized && dynamicChildren == null) {
// 全量比较 props
}
};
原文链接:
https://juejin.cn/post/7294928126940512282
相关推荐
- Linux集群自动化监控系统Zabbix集群搭建到实战
-
自动化监控系统...
- systemd是什么如何使用_systemd/system
-
systemd是什么如何使用简介Systemd是一个在现代Linux发行版中广泛使用的系统和服务管理器。它负责启动系统并管理系统中运行的服务和进程。使用管理服务systemd可以用来启动、停止、...
- Linux服务器日常巡检脚本分享_linux服务器监控脚本
-
Linux系统日常巡检脚本,巡检内容包含了,磁盘,...
- 7,MySQL管理员用户管理_mysql 管理员用户
-
一、首次设置密码1.初始化时设置(推荐)mysqld--initialize--user=mysql--datadir=/data/3306/data--basedir=/usr/local...
- Python数据库编程教程:第 1 章 数据库基础与 Python 连接入门
-
1.1数据库的核心概念在开始Python数据库编程之前,我们需要先理解几个核心概念。数据库(Database)是按照数据结构来组织、存储和管理数据的仓库,它就像一个电子化的文件柜,能让我们高效...
- Linux自定义开机自启动服务脚本_linux添加开机自启动脚本
-
设置WGCloud开机自动启动服务init.d目录下新建脚本在/etc/rc.d/init.d新建启动脚本wgcloudstart.sh,内容如下...
- linux系统启动流程和服务管理,带你进去系统的世界
-
Linux启动流程Rhel6启动过程:开机自检bios-->MBR引导-->GRUB菜单-->加载内核-->init进程初始化Rhel7启动过程:开机自检BIOS-->M...
- CentOS7系统如何修改主机名_centos更改主机名称
-
请关注本头条号,每天坚持更新原创干货技术文章。如需学习视频,请在微信搜索公众号“智传网优”直接开始自助视频学习1.前言本文将讲解CentOS7系统如何修改主机名。...
- 前端工程师需要熟悉的Linux服务器(SSH 终端操作)指令
-
在Linux服务器管理中,SSH(SecureShell)是远程操作的核心工具。以下是SSH终端操作的常用命令和技巧,涵盖连接、文件操作、系统管理等场景:一、SSH连接服务器1.基本连接...
- Linux开机自启服务完全指南:3步搞定系统服务管理器配置
-
为什么需要配置开机自启?想象一下:电商服务器重启后,MySQL和Nginx没自动启动,整个网站瘫痪!这就是为什么开机自启是Linux运维的必备技能。自启服务能确保核心程序在系统启动时自动运行,避免人工...
- Kubernetes 高可用(HA)集群部署指南
-
Kubernetes高可用(HA)集群部署指南本指南涵盖从概念理解、架构选择,到kubeadm高可用部署、生产优化、监控备份和运维的全流程,适用于希望搭建稳定、生产级Kubernetes集群...
- Linux项目开发,你必须了解Systemd服务!
-
1.Systemd简介...
- Linux系统systemd服务管理工具使用技巧
-
简介:在Linux系统里,systemd就像是所有进程的“源头”,它可是系统中PID值为1的进程哟。systemd其实是一堆工具的组合,它的作用可不止是启动操作系统这么简单,像后台服务...
- Linux下NetworkManager和network的和平共处
-
简介我们在使用CentoOS系统时偶尔会遇到配置都正确但network启动不了的问题,这问题经常是由NetworkManager引起的,关闭NetworkManage并取消开机启动network就能正...
你 发表评论:
欢迎- 一周热门
-
-
MySQL中这14个小玩意,让人眼前一亮!
-
旗舰机新标杆 OPPO Find X2系列正式发布 售价5499元起
-
面试官:使用int类型做加减操作,是线程安全吗
-
C++编程知识:ToString()字符串转换你用正确了吗?
-
【Spring Boot】WebSocket 的 6 种集成方式
-
PyTorch 深度学习实战(26):多目标强化学习Multi-Objective RL
-
pytorch中的 scatter_()函数使用和详解
-
与 Java 17 相比,Java 21 究竟有多快?
-
基于TensorRT_LLM的大模型推理加速与OpenAI兼容服务优化
-
这一次,彻底搞懂Java并发包中的Atomic原子类
-
- 最近发表
-
- Linux集群自动化监控系统Zabbix集群搭建到实战
- systemd是什么如何使用_systemd/system
- Linux服务器日常巡检脚本分享_linux服务器监控脚本
- 7,MySQL管理员用户管理_mysql 管理员用户
- Python数据库编程教程:第 1 章 数据库基础与 Python 连接入门
- Linux自定义开机自启动服务脚本_linux添加开机自启动脚本
- linux系统启动流程和服务管理,带你进去系统的世界
- CentOS7系统如何修改主机名_centos更改主机名称
- 前端工程师需要熟悉的Linux服务器(SSH 终端操作)指令
- Linux开机自启服务完全指南:3步搞定系统服务管理器配置
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)