前言今年又是一个非常寒冷的冬天,很多公司都开始裁员。市场从来不缺前端,但对高级前端的需求还是特别旺盛。为了区分应聘者在前端领域的能力深浅,一些大厂的面试官在面试过程中往往会考察一些前端框架的源码知识点。Vuejs作为世界上最顶级的框架之一,几乎在所有的面试场景中都或多或少地被提及。笔者之前在蚂蚁集团工作过,还是经常会问到Vue3的考点,下面根据多年的面试和面试经验,为小伙伴们整理一下最近大厂爱问的Vue3问题。那么我们针对问题举一反三,深入学习Vue3源码知识!场景一:Vue3.x相比Vue2.x做了哪些额外的性能优化?要理解Vue3的性能优化核心,需要理解Vuejs的核心设计理念。我们知道Vuejs官网上有一句话完美的概括了它:一个渐进的JavaScript框架,易学易用,性能卓越,适用于场景丰富的Web框架。其实我们的答案就在这句话中。首先,我们知道在浏览网页时,有两类场景会制约网页的性能:网络传输的瓶颈和CPU的瓶颈。那么要回答这个问题,我们可以直接从这两个方面入手。网络传输的瓶颈优化对于前端框架来说,最大的制约网络传输的因素就是代码量。码量越大,传输效率越慢。特别是针对SPA单页应用的CSR(clientsiderendering)。大帧资源意味着用户等待白屏的时间越长。Vue3在减少源代码大小方面做得最多的是通过细化的Tree-Shacking机制构建渐进式代码。1./*#__PURE__*/mark我们知道Tree-Shaking可以删除一些DC(deadcode)代码。但是,对于一些有副作用的功能代码,不能很好的识别和删除。例如:foo()functionfoo(obj){obj?.a}上面代码中,foo函数本身没有意义,只是读取对象obj的属性a,但是Tree-Shaking无法删除这个函数,因为上面的属性读取操作可能会产生副作用,因为obj可能是一个响应式对象,我们可能会在obj中定义一个getter,在getter中触发很多意想不到的操作。如果我们确认foo函数是一个没有副作用的纯函数,那么/*#__PURE__*/这个时候就派上用场了。它的作用是告诉打包者调用foo函数不会产生副作用。您可以放心地对其进行Tree-Shaking。另外,值得一提的是,Vue3源码中包含了大量的/*#__PURE__*/标识符,可见Vue3对源码量的控制是多么的用心!2、feature开关在Vue3源码中的rollup.config.mjs中有这么一段代码:{__FEATURE_OPTIONS_API__:isBundlerESMBuild?`__VUE_OPTIONS_API__`:true,}其中__FEATURE_OPTIONS_API__是构建时环境变量。我们知道Vue3在一些API上是兼容Vue3的,比如OptionsAPI。但是如果我们在项目中只使用CompositonAPI而不使用OptionsAPI,那么我们可以在项目构建的时候关闭这个选项,从而减少代码量。看看这个变量在Vue3源码中是如何使用的://兼容2.xoptionAPIif(__FEATURE_OPTIONS_API__){currentInstance=instancepauseTracking()applyOptions(instance,Component)resetTracking()currentInstance=null}用户可以通过Set__VUE_OPTIONS_API__预定义常量的值来控制是否应包含此代码。通常用户可以使用webpack.DefinePlugin插件来实现://webpack.DefinePlugin插件配置newwebpack.DefinePlugin({__VUE_OPTIONS_API__:JSON.stringify(true)//enablefeature})另外,类似的开发环境会通过__DEV__输出警告规则,在生产环境中消除这些警告以减小构建包的大小是类似的方法:if(__DEV__){console.warn(`valuecannotbemadereactive:${String(target)}`)}CPU瓶颈优化当项目变得庞大,组件数量众多时,很容易遇到CPU瓶颈。主流浏览器的刷新率为60Hz,即浏览器每(1000ms/60Hz)16.6ms刷新一次。我们知道JS可以操作DOM,而GUI渲染线程和JS线程是互斥的。所以JS脚本执行和浏览器布局绘制不能同时执行。每16.6ms需要完成以下任务:JS脚本执行-----样式布局-----样式绘制。而且样式画出来了,会出现丢帧,卡顿的情况。为了解决大元素组件渲染和更新滞后的问题,Vue的策略是一方面使用组件级的细粒度更新来控制更新的影响:在Vue3中,每个组件都会生成一个渲染函数,而这些渲染函数在执行过程中会进行数据访问。这时,这些渲染函数就被收集到sideeffect函数中,建立了data->sideeffect的映射关系。当数据发生变化时,会触发副作用函数的重新执行,即重新渲染。另一方面,在编译器中做了很多静态优化。得益于这些优化,我们可以编写出性能优异、易学易用的Vue项目。下面简单介绍几种编译时优化策略:1.定向更新假设模板如下:helloworld
{{msg}}
>其中带有p标签的节点是静态节点,第二个带有p标签的节点是动态节点。如果msg的值发生变化,那么理论上肉眼可见的最优更新方案应该是只做动态节点的第二次Diff,而不对第一个p-labeled节点进行diff。将上面的模板转换成vnode的结果大致是:constvnode={type:Symbol(Fragment),children:[{type:'p',children:'helloworld'},{type:'p',children:ctx.msg,patchFlag:1/*动态文本*/},],dynamicChildren:[{type:'p',children:ctx.msg,patchFlag:1/*动态文本*/},]}componentatthistime内存中有一个静态节点
helloworld
。在传统的diff算法中,仍然需要对静态节点进行不必要的diff。Vue3首先使用patchFlag标记动态节点
{{msg}}
,然后配合dynamicChildren收集动态节点,从而完成diff阶段只进行针对性更新的目的。2.静态提升接下来我们再说一遍,为什么要进行静态提升呢?如下模板所示:
渲染函数相当于:import{createElementVNodeas_createElementVNode,openBlockas_openBlock,createElementBlockas_createElementBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createElementBlock("div",null,[_createElementVNode("p",null,"text")]))}显然,p标签是静态的,它不会改变。但是上面渲染函数的问题也很明显。如果组件中有动态内容,当重新执行渲染函数时,即使p标签是静态的,也会重新创建其对应的VNode。所谓“静态提升”,就是提升渲染功能之外的一些静态节点或属性。如下代码所示:import{createElementVNodeas_createElementVNode,openBlockas_openBlock,createElementBlockas_createElementBlock}from"vue"const_hoisted_1=/*#__PURE__*/_createElementVNode("p",null,"text",-1/*HOISTED*/)const_hoisted_2=[_hoisted_1]导出函数render(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createElementBlock("div",null,_hoisted_2))}this这减少了创建VNode的性能消耗。这里静态推广步骤生成的吊机,会在codegenNode的代码生成阶段帮助我们生成静态推广相关的代码。预字符串化Vue3在编译时对静态提升的节点进行预字符串化。什么是预字符串化?我们来看一个例子:
...一共20+个节点对于这样一个有很多静态改进的模板场景,如果不考虑前置字符串那么生成的渲染函数会包含大量的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,]导出函数render(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createElementBlock("div",null,_hoisted_21))}createElementVNode很多连续创建vnode也会影响性能,所以可以通过预串化一次性创建这些静态节点。采用并串化后,生成的渲染函数如下:const_hoisted_1=/*#__PURE__*/_createStaticVNode("
...
",20)const_hoisted_21=[_hoisted_1]导出函数渲染(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createElementBlock("div",null,_hoisted_21))}这样一方面减少了不断创建createElementVNode带来的性能损失,另一方面减少了代码量。内容请参考作者编写的小册子:《Vue 3 技术揭秘》。下一篇文章将继续讲解Vue3响应式设计原理和异步调度更新策略。宣传自己的小册子如果你对Vue3感兴趣,想深入了解Vue3相关的设计理念,但是直接阅读Vue3的源码会很晦涩。比如一个baseCreateRenderer函数就有将近2000行的代码,可能半途而废。作者花了3个多月的时间潜心编写了一本小册子《Vue 3 技术揭秘》,从头到尾向你介绍Vue3的优秀设计!一方面,小册子会简化Vue3的核心源码,让你只关注核心逻辑实现;运行机制。本书主要分为5个模块,依次为你揭开Vue3的“神秘面纱”。模块1:渲染器实现原理。从根组件初始化开始,逐步介绍组件实例化、全量更新、diff过程等。Module2:响应式原理。核心介绍Vue3基于Proxy实现的响应式原理,深入解读依赖收集过程、响应式触摸过程及关联watch、computed、inject/provide函数实现和异步批量更新原理。在学习的过程中,你会逐渐体会到与Vue2响应式原理的区别,以及异步批量更新的区别。模块3:编译器实现原理。重点讲解模板是如何一步步编译成渲染函数的,以及Vue3在编译时做的大量编译时优化工作。模块四:内置组件的实现原理。主要介绍Vue3内置的几个常用组件:Transition、KeepAlive、Teleport、Suspense相关组件运行机制及实现原理。模块五:特殊元素和说明。重点分析v-model如何实现双向数据绑定,slot如何实现内容分发。为了方便大家,我整理了如下思维导图:相信在你掌握了这本小册子中这些模块的核心原理之后,你阅读Vue3的源码或者解决Vue3的疑难杂症都会更加得心应手。