优秀的编程知识分享平台

网站首页 > 技术文章 正文

图文整理 Vue3.0性能提升 Vue3主要是通过哪几个点体现

nanyue 2024-09-10 16:06:38 技术文章 6 ℃

Vue3.0性能提升

  1. 响应式系统升级
  2. 收集依赖
  3. reactive
  4. ref
  5. toRefs
  6. computed
  7. 编译优化
  8. 源码体积优化

响应式系统升级

Vue3.0相比于2.x版本做了响应式系统的升级,在Vue2.x版本中是通过defineProperty去设置响应式,但在3.0中采取了Proxy的方式,这样做有三个优点

  • 可以监听动态添加对象的属性
  • 可以监听删除的属性
  • 可以监听数组的索引以及数组的length属性

收集依赖

收集依赖的过程主要是通过三个函数,这三个函数分别是

  1. effect 处理副作用的函数 它会接受一个函数作为参数 并且执行这个函数 并把这个函数存储起来 在收集依赖完毕后 重置
  2. track 建立关系 首先声明一个weakMap 用来建立对象和值得关系 对象 target 值 map 这个值是一个map对象 用来建立对象中的属性和属性值的关系 对象 key 值 set map中的值是一个set的数据结构 用来描述属性值 通过key 获取值
  3. trigger 通过target key 去获取weakMap中储存的响应式对象 找到后获取到存取的值并返回

编译时对VDom的性能优化

PatchFlag

首先看下面这个案例,模版中有三个P标签,其中只有最后一个P标签的TEXT部分是动态的



在之前的VDOM中,如果msg值发生改变,整个模版中的所有元素都需要重新渲染。但在Vue3.0中,在这个模版编译时,编译器会在动态标签末尾加上 /* Text*/ PatchFlag。只能带patchFlag 的 Node 才被认为是动态的元素,会被追踪属性的修改。并且 PatchFlag 会标识动态的属性类型有哪些,比如这里 的TEXT 表示只有节点中的文字是动态的。

每一个Block中的节点,就算很深,也是直接跟Block一层绑定的,可以直接跳转到动态节点而不需要逐个逐层遍历。

既有VDOM的灵活性,又有性能保证。

hoistStatic 静态节点提升

当使用hoistStatic时,所有 静态的节点都被提升到render方法之外。这意味着,他们只会在应用启动的时候被创建一次,而后随着每次的渲染被不停地复用。



在大型应用中对于内存有很大的优化。

cacheHandler 事件监听缓存

正常情况下,当绑定一个事件:

<div>
  <p @click="handleClick">静态代码</p>
</div>

模版会被编译为

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", { onClick: _ctx.handleClick }, "静态代码", 8 /* PROPS */, ["onClick"])
  ]))
}

其中事件会每次从全局上下文中获取。而当开启了cacheHandler之后

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", {
      onClick: _cache[1] || (_cache[1] = ($event, ...args) => (_ctx.handleClick($event, ...args)))
    }, "静态代码")
  ]))
}

编辑器会为你动态创建一个内联函数,内联函数里面再去引用当前组件上最新的handler。之后编辑器会将内联函数缓存。每次重新渲染时如果事件处理器没有变,就会使用缓存中的事件处理而不会重新获取事件处理器。这个节点就可以被看作是一个静态的节点。这种优化更大的作用在于当其作用于组件时,之前每次重新渲染都会导致组件的重新渲染,在通过handler缓存之后,不会导致组件的重新渲染了。

SSR 服务端渲染

当开启SSR了之后,如果我们模版中有一些静态标签,这些静态标签会被直接转化成文本。



其中的动态绑定依然是一个单独的字符串内嵌进去。这个性能肯定比React 转成VDOM在专为HTML快很多。

StaticNode 静态节点

刚才提到在SSR中静态的节点会被转化为纯字符串。如果在客户端,当静态节点嵌套足够多的时候,VUE编译器也会将VDOM转化为纯字符串的HTML。即 StaticNode。



通过这些操作,我们可以看下,跟vue2比可以快一倍以上,内存占用可以小一倍以上。



Tree Shaking

因为ES6模块是静态引用的,所以我们可以在编译时正确的判断到底加载了哪些代码。对代码全局做一个分析,找到那些没用被用到的模块、函数、变量,并把这些去掉。

当使用一个 bundle (webpack etc.)的时候,默认会加上 TreeShaking。Vue 3.0 中没有被用到的模块可以不被打包到编译后的文件中,被 TreeShake 掉。当只有一个HelloWorld的时候 Vue3打包后 13.5kb。所有的组件全部加载进来时是 22.5kb

Composition API

随着Vue组件的增大,组件内代码变得越来越难以理解和维护。其中的一些可以复用的代码很难被抽离出来。同时 Vue2.0还缺少 TS支持。在Vue2中,逻辑概念(功能)被管理在组件中,但是功能和组件并不是一对一关系。一个功能可以被多个组件使用同时一个组件可以有多个功能。在Vue中,一个功能可能需要依赖多个Options(components、props、data、computed、methods及生命周期方法)。

在 Composition API中提供可 setup 方法。以一个有搜索功能和 排序功能组件为例:

<script>
export default {
    setup() {
    
    }
}

function useSearch() {
    return { 
    ...useSearch(), 
    ...useSorting()
    }
}

function useSorting() {
    
}

</script>

Vue2 中的代码复用

Mixin

在Vue2中有几种方式可以复用代码,其中之一就是 Mixins。

  • Mixins可以实现组织功能
  • 容易发生冲突
  • 很难说明依赖关系
  • 代码不容易复用

Mixin 工厂

  • 可以方便复用
  • 明确的依赖关系
  • 弱命名空间
  • 隐性的属性添加

Scoped Slots

  • 解决了 Mixin 的问题
  • 增加了层级关系导致更难以理解
  • 很多配置信息
  • 灵活性更少
  • 性能较差

核心 API

  • reactive
  • ref
  • computed
  • readonly
  • watchEffect
  • watch
  • Lifecycle Hooks

Fragments

Vue3中不在要求模版的跟节点必须是只能有一个节点。跟节点和和render函数返回的可以是纯文字、数组、单个节点,如果是数组,会自动转化为 Fragments。

Teleport

对标 React Portal。可以做一些关于响应式的设计,如果屏幕宽度比较宽的时候,加入某些元素,屏幕变窄后移除。

Suspense

等待嵌套的异步依赖。再把一个嵌套的组件树渲染到页面上之前,先在内存中进行渲染,并记录所有的存在异步依赖的组件。只有所有的异步依赖全部被resolve之后,才会把整个书渲染到dom中。当你的组件中有一个 async的 setup函数,这个组件可以被看作是一个Async Component,只有当这个组件被Resolve之后,再把整个树渲染出来

  • async setup()
  • Async Component

Typescript

Vue3源码使用 TS重写,但不意味着vue3的项目也要使用TS。但Vue3会对 TS有更好的支持

  • 支持 TSX
  • 支持 Class component
  • 代码会变大一些


一、编译阶段

回顾Vue2,我们知道每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲染



试想一下,一个组件结构如下图

<template>
    <div id="content">
        <p class="text">静态文本</p>
        <p class="text">静态文本</p>
        <p class="text">{{ message }}</p>
        <p class="text">静态文本</p>
        ...
        <p class="text">静态文本</p>
    </div>
</template>


可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff 和遍历其实都是不需要的,造成性能浪费

因此,Vue3在编译阶段,做了进一步优化。主要有如下:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化

diff算法优化

vue3在diff算法中相比vue2增加了静态标记

关于这个静态标记,其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较

下图这里,已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高



关于静态类型枚举如下

export const enum PatchFlags {
  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}


静态提升

Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用

这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用

<span>你好</span>

<div>{{ message }}</div>


没有做静态提升之前

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("span", null, "你好"),
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))
}


做了静态提升之后

const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _hoisted_1,
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))
}

// Check the console for the AST


静态内容_hoisted_1被放置在render 函数外,每次渲染的时候只要取 _hoisted_1 即可

同时 _hoisted_1 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff

事件监听缓存

默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化

<div>
  <button @click = 'onClick'>点我</button>
</div>


没开启事件监听器缓存

export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
                                             // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
  ]))
})


开启事件侦听器缓存后

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
    }, "点我")
  ]))
}


上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使用

SSR优化

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染

div>
	<div>
		<span>你好</span>
	</div>
	...  // 很多个静态属性
	<div>
		<span>{{ message }}</span>
	</div>
</div>


编译后

import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<div${
    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
  }><div><span>你好</span>...<div><span>你好</span><div><span>${
    _ssrInterpolate(_ctx.message)
  }</span></div></div>`)
}


二、源码体积

相比Vue2,Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking

任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小

import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
    setup(props, context) {
        const age = ref(18)

        let state = reactive({
            name: 'test'
        })

        const readOnlyAge = computed(() => age.value++) // 19

        return {
            age,
            state,
            readOnlyAge
        }
    }
});


三、响应式系统

vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter和setter,实现响应式

vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历

  • 可以监听动态属性的添加
  • 可以监听到数组的索引和数组length属性
  • 可以监听删除属性

关于这两个 API 具体的不同,我们下篇文章会进行一个更加详细的介绍

给大家分享我收集整理的各种学习资料,前端小白交流、学习交流,也可以直接问我,我会组织大家一起做项目练习,帮助大家匹配一位学习伙伴互相监督学习-下面是学习资料参考。

前端学习交流、自学、学习资料等推荐 - 知乎

Tags:

最近发表
标签列表