网站首页 > 技术文章 正文
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
猜你喜欢
- 2025-05-02 优化 Node.js 性能:V8 内存管理和 GC 调优
- 2025-05-02 阿里P9师傅亲传98K+星的MySQL性能优化金字塔法则手册助我升职P7
- 2025-05-02 Web 性能优化、文档及代码编辑器相关的新提案
- 2025-05-02 Vite 性能篇:掌握这些优化策略,一起纵享丝滑!
- 2025-05-02 如何将20M+的字体压缩到几KB:前端性能优化的极致探索
- 2025-05-02 如何做 React 性能优化?(react项目性能优化)
- 2025-05-02 前端必看!7 个 Vue3 性能优化实战技巧,让页面飞起来
- 2025-05-02 前端性能优化新突破:图片加载提速 80% 的秘密武器
- 2025-05-02 前端掉坑血泪史!4 个 React 性能优化绝招让页面秒开
- 2025-05-02 开发瓶颈?6 个前端性能优化大招! 暗藏使用雷区!
你 发表评论:
欢迎- 最近发表
-
- 前端流行框架Vue3教程:13. 组件传递数据_Props
- 前端必看!10 个 Vue3 救命技巧,解决你 90% 的开发难题?
- JAVA和JavaScript到底是什么关系?是亲戚吗?
- Java和js有什么区别?(java和javascript的区别和联系)
- 东方标准|Web和Java的区别,如何选择这两个专业
- 前端面试题-JS 中如何实现大对象深度对比
- 360前端一面~面试题解析(360前端笔试)
- 加班秃头别慌!1 道 Vue 面试题,快速解锁大厂 offer 通关密码
- 焦虑深夜刷题!5 道高频 React 面试题,吃透 offer 稳了
- 2025Web前端面试题大全(整理版)面试题附答案详解,最全面详细
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端md5加密 (49)
- 前端路由 (55)
- 前端数组 (65)
- 前端定时器 (47)
- 前端懒加载 (45)
- 前端接口 (46)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle查询数据库 (45)
- oracle约束 (46)
- oracle 中文 (51)
- oracle链接 (47)
- oracle的函数 (57)
- mac oracle (47)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)