乐趣区

关于前端:从面试题入手畅谈-Vue-3-性能优化

前言

往年又是一个十分凛冽的冬天,很多公司都开始人员精简。市场从来不缺前端,但对高级前端的需要还是特地强烈的。一些大厂的面试官为了辨别候选人对前端畛域能力的深度,常常会在面试过程中考查一些前端框架的源码性知识点。Vuejs 作为世界顶尖的框架之一,简直在所有的面试场景中或多或少都会被提及。

笔者之前在蚂蚁团体就任,对于 Vue 3 的考点还是会常常问的。接下来,我将依据多年的面试以及被面试教训,为小伙伴们梳理最近大厂爱问的 Vue 3 问题。而后咱们再依据问题触类旁通,深刻学习 Vue 3 源码常识!

场景一:Vue 3.x 绝对于 Vue 2.x 做了那些额定的性能优化?

要了解 Vue 3 的性能优化的外围,就须要理解 Vuejs 的外围设计理念。咱们晓得 Vuejs 官网上有一句话总结的特地到位:渐进式 JavaScript 框架,易学易用,性能杰出,实用于场景丰盛的 Web 框架。 其实咱们的答案就蕴藏在这句话里。

首先,咱们晓得当咱们浏览 Web 网页时,有两类场景会制约 Web 网页的性能

  1. 网络传输的瓶颈
  2. CPU 的瓶颈

所以要答复这个问题,就能够间接从这两方面动手。

网络传输的瓶颈优化

对于前端框架而言,制约网络传输的因素最大的就是代码体积,代码体积越大,传输效率越慢。尤其对于 SPA 单页利用的 CSR(客户端渲染)而言。一个大体积的框架资源,就意味着用户须要期待白屏的工夫越长。而 Vue 3 在缩小源码体积方面做的最多的就是通过精细化的 Tree-Shacking 机制来构建 渐进式 代码。

1. /*#__PURE__*/ 标记

咱们晓得 Tree-Shaking 能够删除一些 DC(dead code) 代码。然而对于一些有副作用的函数代码,却是无奈进行很好的辨认和删除,举个例子:

foo()

function foo(obj) {obj?.a}

上述代码中,foo 函数自身是没有任何意义的,仅仅是对对象 obj 进行了属性 a 的读取操作,然而 Tree-Shaking 是无奈删除该函数的,因为上述的属性读取操作可能会产生副作用,因为 obj 可能是一个响应式对象,咱们可能对 obj 定了一个 gettergetter 中触发了很多不可预期的操作。

如果咱们确认 foo 函数是一个不会有副作用的 污浊的函数,那么这个时候 /*#__PURE__*/ 就派上用场了,其作用就是通知打包器,对于 foo 函数的调用不会产生副作用,你能够释怀地对其进行 Tree-Shaking

另外,值得一提的是,在 Vue 3 源码中,蕴含了大量的 /*#__PURE__*/ 标识符,可见 Vue 3 对源码体积的管制是如许的用心!

2. 个性开关

Vue 3 源码中的 rollup.config.mjs 中有这样一段代码:

{__FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,}

其中 __FEATURE_OPTIONS_API__ 是一个构建时的环境变量,咱们晓得 Vue 3 在某些 API 方面是兼容 Vue 3 写法的,比方 Options API。然而如果咱们在我的项目中仅仅应用 Compositon API 而不想应用 Options API 那么咱们就能够在我的项目构建时敞开这个选项,从而缩小代码体积。咱们看看这个变量在 Vue 3 源码中是如何应用的:

// 兼容 2.x 选项式 API
if (__FEATURE_OPTIONS_API__) {
  currentInstance = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}

用户能够通过设置 __VUE_OPTIONS_API__ 预约义常量的值来管制是否要蕴含这段代码。通常用户能够应用 webpack.DefinePlugin 插件来实现:

// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({__VUE_OPTIONS_API__: JSON.stringify(true) // 开启个性
})

除此之外,相似的开发环境会通过 __DEV__ 来输入告警规定,而在生产环境剔除这些告警升高构建后的包体积都是相似的伎俩:

if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)
}

CPU 瓶颈优化

当我的项目变得宏大、组件数量繁多时,就容易遇到 CPU 的瓶颈。支流浏览器刷新频率为 60Hz,即每(1000ms / 60Hz)16.6ms 浏览器刷新一次。

咱们晓得,JS 能够操作 DOM,GUI 渲染线程 JS 线程 是互斥的。所以 JS 脚本执行浏览器布局、绘制 不能同时执行。

在每 16.6ms 工夫内,须要实现如下工作:

JS 脚本执行 -----  款式布局 ----- 款式绘制

当 JS 执行工夫过长,超出了 16.6ms,这次刷新就没有工夫执行 款式布局 款式绘制 了,也就呈现了丢帧的状况,会产生卡顿。

为了解决宏大元素组件渲染、更新卡顿的问题,Vue 的策略是一方面采纳了组件级的细粒度更新,管制更新的影响面:Vue 3 中,每个组件都会生成一个渲染函数,这些渲染函数执行时会进行数据拜访,此时这些渲染函数被收集进入副作用函数中,建设 数据 -> 副作用 的映射关系。当数据变更时,再触发副作用函数的从新执行,即从新渲染。

另一方面则在编译器中做了大量的动态优化,得益于这些优化,才让咱们能够 易学易用的写出性能杰出的 Vue 我的项目。 上面简略介绍几种编译时优化策略:

1. 靶向更新

假如有以下模板:

<template>
  <p>hello world</p>
  <p>{{msg}}</p>
</template>>

其中一个 p 标签的节点是一个动态的节点,第二个 p 标签的节点是一个动静的节点,如果当 msg 的值产生了变动,那么实践上肉眼可见最优的更新计划应该是只做第二个动静节点的 diff 而无需进行第一个 p 标签节点的 diff

上述模版转成 vnode 后的后果大抵为:

const vnode = {type: Symbol(Fragment),
  children: [{ type: 'p', children: 'hello world'},
    {type: 'p', children: ctx.msg, patchFlag: 1 /* 动静的 text */},
  ],
  dynamicChildren: [{ type: 'p', children: ctx.msg, patchFlag: 1 /* 动静的 text */},
  ]
}

此时组件内存在了一个动态的节点 <p>hello world</p>,在传统的 diff 算法里,还是须要对该动态节点进行不必要的 diff

Vue3 则是先通过 patchFlag 来标记动静节点 <p>{{msg}}</p> 而后配合 dynamicChildren 将动静节点进行收集,从而实现在 diff 阶段只做 靶向更新 的目标。

2. 动态晋升

接下来,咱们再来说一下,为什么要做动态晋升呢?如下模板所示:

<div>
  <p>text</p>
</div>

在没有被晋升的状况下其渲染函数相当于:

import {createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock} from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createElementBlock("div", null, [_createElementVNode("p", null, "text")
  ]))
}

很显著,p 标签是动态的,它不会扭转。然而如上渲染函数的问题也很显著,如果组件内存在动静的内容,当渲染函数从新执行时,即便 p 标签是动态的,那么它对应的 VNode 也会从新创立。

所谓的“动态晋升”,就是将一些动态的节点或属性晋升到渲染函数之外。如上面的代码所示:

import {createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock} from "vue"

const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "text", -1 /* HOISTED */)
const _hoisted_2 = [_hoisted_1]

export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}

这就实现了缩小 VNode 创立的性能耗费。

而这里的动态晋升步骤生成的 hoists,会在 codegenNode 会在生成代码阶段帮忙咱们生成动态晋升的相干代码。

预字符串化

Vue 3 在编译时会进行动态晋升节点的 预字符串化。什么是预字符串化呢?一起来看个示例:

<template>
  <p></p>
  ... 共 20+ 节点
  <p></p>
</template>

对于这样有大量动态晋升的模版场景,如果不思考 预字符串化 那么生成的渲染函数将会蕴含大量的 createElementVNode 函数:假如如上模板中有大量间断的动态的 p 标签,此时渲染函数生成的后果如下:

const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, null, -1 /* HOISTED */)
// ...
const _hoisted_20 = /*#__PURE__*/_createElementVNode("p", null, null, -1 /* HOISTED */)
const _hoisted_21 = [
  _hoisted_1,
  // ...
  _hoisted_20,
]

export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createElementBlock("div", null, _hoisted_21))
}

createElementVNode 大量连续性创立 vnode 也是挺影响性能的,所以能够通过 预字符串化 来一次性创立这些动态节点,采纳 与字符串化 后,生成的渲染函数如下:

const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<p></p>...<p></p>", 20)
const _hoisted_21 = [_hoisted_1]

export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createElementBlock("div", null, _hoisted_21))
}

这样一方面升高了 createElementVNode 间断创立带来的性能损耗,也侧面缩小了代码体积。

小结

本大节为大家解读了局部 Vue 3 性能优化的设计,更多的内容能够参考作者写的小册:《Vue 3 技术揭秘》。

接下来的文章将持续为大家解读 Vue 3 响应式设计原理和异步调度更新策略。

推广本人的小册

如果你对 Vue 3 感兴趣,想去深耕一下 Vue 3 相干的设计理念,然而间接去啃 Vue 3 源码会十分艰涩难懂,比方一个 baseCreateRenderer 函数就有靠近 2000 行代码,可能会让你大功告成。

作者花了 3 个多月的工夫醉生梦死的写了一个小册《Vue 3 技术揭秘》将会为您从头到尾的介绍 Vue 3 的优良设计!

小册一方面会 对 Vue 3 外围源码做适量的精简 ,让你能够只用关注外围逻辑实现;另一方面,也配了 大量的插图,一图胜千言,能够更加活泼地向你展现源码的运行机制。

本小册次要划分为了 5 大模块 来顺次为你揭开 Vue 3 的“神秘面纱”。

  • 模块一:渲染器实现原理。从根组件初始化开始,一步步介绍组件实例化、残缺更新、diff 过程等。
  • 模块二:响应式原理 。外围介绍 Vue 3 基于 Proxy 实现的响应式原理,深刻解读依赖收集过程、响应式触达过程和相关联的 watch、computed、inject/provide 函数实现以及异步批量更新原理。在学习的过程中,你会渐进式领会到 与 Vue 2 响应式原理的差别 以及异步批量更新的不同之处。
  • 模块三:编译器实现原理。重点解说模板是如何被一步步编译成渲染函数的,以及在编译时 Vue 3 所做的大量编译时优化的工作。
  • 模块四:内置组件实现原理。次要介绍 Vue 3 几个罕用的内置组件:Transition、KeepAlive、Teleport、Suspense 相干的组件运行机制和实现原理。
  • 模块五:非凡元素 & 指令。重点剖析 v-model 是如何实现双向数据绑定的,以及 slot 插槽是如何实现内容散发的。

为不便你了解,我整理出来了如下的思维导图:

置信把握了本小册这些模块的外围原理之后,你再去浏览 Vue 3 源码或者是解决 Vue 3 的疑难杂症时,会更加得心应手。

退出移动版