Vue3 源码解析(三):静态提升

news2024/11/28 4:32:27

什么是静态提升

Vue3 尚未发布正式版本前,尤大在一次关于 Vue3 的分享中提及了静态提升,当时笔者就对这个亮点产生了好奇,所以在源码阅读时,静态提升也是笔者的一个重点阅读点。

那么什么是静态提升呢?当 Vue 的编译器在编译过程中,发现了一些不会变的节点或者属性,就会给这些节点打上标记。然后编译器在生成代码字符串的过程中,会发现这些静态的节点,并提升它们,将他们序列化成字符串,以此减少编译及渲染成本。有时可以跳过一整棵树。

<div>
  <span class="foo">
    Static
  </span>
  <span>
    {{ dynamic }}
  </span>
</div>

例如这段模板代码,毫无疑问,我们能看出来 <span class="foo"> 这个节点,不论 dynamic 表达式如何变,它都不会再改变了。对于这样的节点,就可以打上标记进行静态提升。

而 Vue3 也可以对 props 属性进行静态提升。

<div id="foo" class="bar">
    {{ text }}
</div>

例如这段模板代码,Vue3 会跳过节点,仅仅将将不再会变动的 id="foo"class="bar" 进行提升。

编译后的代码字符串

上面的例子我们只是简单的分析了一些模板,现在我们通过一个例子,来了解静态提升前后的变化。

<div>
  <div>
    <span class="foo"></span>
    <span class="foo"></span>
    <span class="foo"></span>
    <span class="foo"></span>
    <span class="foo"></span>
  </div>
</div>

来看这样一个模板,符合静态提升的条件,但是如果没有静态提升的机制,它会被编译成如下代码:

const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue

return function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("div", null, [
      _createVNode("span", { class: "foo" }),
      _createVNode("span", { class: "foo" }),
      _createVNode("span", { class: "foo" }),
      _createVNode("span", { class: "foo" }),
      _createVNode("span", { class: "foo" })
    ])
  ]))
}

编译后生成的 render 函数很清晰,是一个柯里化的函数,返回一个函数,创建一个根节点的 div,children 里有再创建一个 div 元素,最后在最里面的 div 节点里创建五个 span 子元素。

如果进行静态提升,那么它会被编译成这样:

const { createVNode: _createVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue

const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<div><span class=\"foo\"></span><span class=\"foo\"></span><span class=\"foo\"></span><span class=\"foo\"></span><span class=\"foo\"></span></div>", 1)

return function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1
  ]))
}

静态提升以后生成的代码,我们可以看出有明显区别,它会生成一个变量: _hoisted_1,并打上 /*#__PURE__*/ 标记。 _hoisted_1 通过字符串的传参,调用 createStaticVNode 创建了静态节点。而 _createBlock 中由原来的多个创建节点的函数的传入,变为了仅仅传入一个函数。性能的提升自然不言而喻。

在知道了静态提升的现象后,我们就一起来看看源码中的实现。

transform 转换器

在上一篇文章中笔者提到编译时会调用 compiler-core 模块中 @vue/compiler-core/src/compile.ts 文件下的 baseCompile 函数。在这个函数的执行过程中会执行 transform 函数,传入解析出来的 AST 抽象语法树。那么我们首先一起看一下 transform 函数做了什么。

export function transform(root: RootNode, options: TransformOptions) {
  // 创建转换上下文
  const context = createTransformContext(root, options)
  // 遍历所有节点,执行转换
  traverseNode(root, context)
  // 如果编译选项中打开了 hoistStatic 开关,则进行静态提升
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }
  if (!options.ssr) {
    createRootCodegen(root, context)
  }
  // 确定最终的元信息 
  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
}

transform 函数很简短,并且从中文注释中,我们可以关注到在第 7 行代码的位置,转换器判断了编译时是否有开启静态提升的开关,若是打开的话则对节点进行静态提升。今天笔者的文章主要是介绍静态提升,那么就围绕静态提升的代码往下探索下去,而其余部分代码则不展开来细究了。

hoistStatic 静态提升转换

hoistStatic 的函数源码如下:

export function hoistStatic(root: RootNode, context: TransformContext) {
  walk(
    root,
    context,
    // 很不幸,根节点是不能被静态提升的
    isSingleElementRoot(root, root.children[0])
  )
}

从函数的声明中我们能够得知,静态提升转换器接收根节点以及转换器上下文作为参数。并且仅仅是调用了 walk 函数。

walk 函数很长,所以在我们讲解 walk 函数之前,我先将 walk 函数的函数签名写出来给大家讲一讲。

(node: ParentNode, context: TransformContext, doNotHoistNode: boolean) => void

从函数签名中可以看出,walk 函数的参数中需要一个 node 节点,context 转换器的上下文,以及 doNotHoistNode 这样一个布尔值来从外部告知该节点是否可以被提升。在 hoistStatic 函数中,传入了根节点,并且根节点是不可以被提升的。

walk 函数

接下来笔者会分段的给大家解析 walk 函数。

function walk(
  node: ParentNode,
  context: TransformContext,
  doNotHoistNode: boolean = false
) {
  let hasHoistedNode = false
  let canStringify = true

  const { children } = node
  for (let i = 0; i < children.length; i++) {
    const child = children[i]
    /* 省略逻辑 */
  }
   
  if (canStringify && hasHoistedNode && context.transformHoist) {
    context.transformHoist(children, context, node)
  }
}

walk 函数首先会声明两个标记,hasHoistedNode:记录该节点是否可以被提升; canStringify: 当前节点是否可以被字符序列化。

对于 canStringify 这个变量,源码是这样解释的:有一些转换,比如 @vue/compiler-sfc 中的 transformAssetUrls,用表达式代替静态的绑定。这些表达式是不可变的,所以它们依然是可以被合法的提升的,但是他们只有在运行时的时候才会被发现,因此不能提前评估。这只是字符串序列化之前的一个问题(通过 @vue/compiler-dom 的 transformHoist 功能),但是在这里允许我们执行一次完整的 AST 解析,并允许 stringifyStatic 在满足其字符串阈值后立即停止执行 walk 函数。

之后会遍历当前节点的 children 所有子节点,而 for 内处理的逻辑我们暂时忽略,后面再看。

执行完 for 循环之后,可以看到如果该节点能被提升且能被字符序列化,并且上下文中有 transformHoist 的转换器,则对当前节点通过提升转换器进行提升。由此可以推测出 for 循环主体内的工作就是遍历节点,并且判断是否可以被提升以及字符序列化,并将结果赋值给函数开头声明的这两个标记。这样的遍历行为跟函数名 walk 的意义也是一致的。

一起来看一下 for 循环体内的逻辑:

for (let i = 0; i < children.length; i++) {
  const child = children[i]
  // 只有简单的元素以及文本是可以被合法提升的
  if (
    child.type === NodeTypes.ELEMENT &&
    child.tagType === ElementTypes.ELEMENT
  ) {
    // 如果不允许被提升,则赋值 constantType NOT_CONSTANT 不可被提升的标记
    // 否则调用 getConstantType 获取子节点的静态类型
    const constantType = doNotHoistNode
      ? ConstantTypes.NOT_CONSTANT
      : getConstantType(child, context)
    // 如果获取到的 constantType 枚举值大于 NOT_CONSTANT
    if (constantType > ConstantTypes.NOT_CONSTANT) {
      // 根据 constantType 枚举值判断是否可以被字符序列化
      if (constantType < ConstantTypes.CAN_STRINGIFY) {
        canStringify = false
      }
      // 如果可以被提升
      if (constantType >= ConstantTypes.CAN_HOIST) {
        // 则将子节点的 codegenNode 属性的 patchFlag 标记为 HOISTED 可提升
        ;(child.codegenNode as VNodeCall).patchFlag =
          PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
        child.codegenNode = context.hoist(child.codegenNode!)
        // hasHoistedNode 记录为 true
        hasHoistedNode = true
        continue
      }
    } else {
      // 节点可能包含动态的子节点,但是它的 props 属性也可能能被合法提升
      const codegenNode = child.codegenNode!
      if (codegenNode.type === NodeTypes.VNODE_CALL) {
        // 获取 patchFlag
        const flag = getPatchFlag(codegenNode)
        // 如果不存在 flag,或者 flag 是文本类型
        // 并且该节点 props 的 constantType 值判断出可以被提升
        if (
          (!flag ||
            flag === PatchFlags.NEED_PATCH ||
            flag === PatchFlags.TEXT) &&
          getGeneratedPropsConstantType(child, context) >=
            ConstantTypes.CAN_HOIST
        ) {
          // 获取节点的 props,并在转换器上下文中执行提升操作
          const props = getNodeProps(child)
          if (props) {
            codegenNode.props = context.hoist(props)
          }
        }
      }
    }
  // 如果节点类型为 TEXT_CALL,则同样进行检查,逻辑与前面一致
  } else if (child.type === NodeTypes.TEXT_CALL) {
    const contentType = getConstantType(child.content, context)
    if (contentType > 0) {
      if (contentType < ConstantTypes.CAN_STRINGIFY) {
        canStringify = false
      }
      if (contentType >= ConstantTypes.CAN_HOIST) {
        child.codegenNode = context.hoist(child.codegenNode)
        hasHoistedNode = true
      }
    }
  }

  // walk further
  /* 暂时忽略 */
}

循环体内的函数较长,所以我们先不关注底部 walk further 的部分,为了便于理解,我逐行添加了注释。

通过最外层 if 分支顶部的注释,我们可以知道只有简单的元素和文本类型是可以被提升的,所以会先判断该节点是否是一个元素类型。如果该节点是一个元素,那么会检查 walk 函数的 doNotHoistNode 参数确认该节点是否能被提升,如果 doNotHoistNode 不为真,则调用 getConstantType 函数获取当前节点的 constantType。

export const enum ConstantTypes {
  NOT_CONSTANT = 0,
  CAN_SKIP_PATCH,
  CAN_HOIST,
  CAN_STRINGIFY
}

这是 ConstantType 枚举的声明,通过这个枚举可以将静态类型分为 4 个等级,而静态类型更高等级的节点涵盖了更小值的节点是所有能力。例如当一个节点被标记了 CAN_STRINGIFY,意味着它能够被字符序列化,所以它永远也是一个可以被静态提升(CAN_HOIST)以及跳过 PATCH 检查的节点。

在搞明白了 ConstantType 类型后,再接着看后续的判断,获取了元素类型节点的静态类型后,会判断静态类型的值是否大于 NOT_CONSTANT,如果条件为 true,则说明该节点可能能被提升或字符序列化。接着往下判断该静态类型能否被字符序列化,如果不能则修改 canStringify 的标记。之后判断静态类型能否被提升,如果可以被提升,则将子节点的 codegenNode 对象的 patchFlag 属性标记为 PatchFlags.HOISTED,执行转换器上下文中的 context.hoist 操作,并修改 hasHoistedNode 的标记。

至此元素类型节点的提升判断完毕,我们有发现有一个 PatchFlags 标记的存在,大家只要知道 Patch Flag 是在编译过程中生成的一些优化记号就行。

后续的代码是在判断当该节点不是简单元素时,尝试提升该节点的 props 中的静态属性,以及当节点为文本类型时,确认是否需要提升。限于篇幅原因,请大家自行查看上方代码。

在前面我隐藏了一段 walk further 的逻辑,从注释中来理解,这段代码的作用是继续查看一些分支情况,看看是否还有可能进行静态提升,代码如下:

  // walk further
  if (child.type === NodeTypes.ELEMENT) {
    // 如果子节点的 tagType 是组件,则继续遍历子节点
    // 以便判断插槽中的情况
    const isComponent = child.tagType === ElementTypes.COMPONENT
    if (isComponent) {
      context.scopes.vSlot++
    }
    walk(child, context)
    if (isComponent) {
      context.scopes.vSlot--
    }
  } else if (child.type === NodeTypes.FOR) {
    // 查看 v-for 类型的节点是否能够被提升
    // 但是如果 v-for 的节点中是只有一个子节点,则不能被提升
    walk(child, context, child.children.length === 1)
  } else if (child.type === NodeTypes.IF) {
    // 如果子节点是 v-if 类型,判断它所有的分支情况
    for (let i = 0; i < child.branches.length; i++) {
            // 如果只有一个分支条件,则不进行提升
      walk(
        child.branches[i],
        context,
        child.branches[i].children.length === 1
      )
    }
  }

walk futher 的部分会尝试判断元素为组件、v-for、v-if 的情况。再一次遍历组件的目的是为了检查其中的插槽是否能被静态提升。v-for 和 v-if 也是一样,检查 v-for 循环生成的节点以及 v-if 的分支条件能否被静态提升。但是这里需要注意,如果 v-for 是单一节点或者 v-if 的分支中只有一个分支判断那么均不会进行提升,因为它们会是一个 block 类型。

至此,walk 函数就给大家讲解完了。

总结

今天的这篇文章,带大家一起阅读了 Vue 源码中静态提升的部分,笔者通过编译后代码的区别给大家直观的举例了静态提升到底有什么作用,它让编译后的代码产生了怎样的区别。并且我们从 transform 函数一路向下深究,直至 walk 函数,我们在 walk 函数中看到了 Vue3 如何去遍历各个节点,并给他们打上静态类型的标记,以便于编译时进行针对性的优化。

由于篇幅限制,笔者并没有展开讲解 getConstantType 这个函数是如何区分各个节点类型来返回静态类型的,也没有讲解当一个节点可以被字符序列化时,context.transformHoist(children, context, node) 这行代码是如何将节点字符序列化的,这些都留给感兴趣的读者继续深入阅读。

如果这篇文章能够帮助到你再深一点的理解 Vue3 的特性,希望能给本文点一个喜欢❤️。如果想继续追踪后续文章,也可以关注我的账号或 follow 我的 github,再次谢谢各位可爱的看官老爷。



喜欢的朋友记得点赞、收藏、关注哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2248815.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

8款Pytest插件助力Python自动化测试

当测试用例变得复杂&#xff0c;或者需要处理大量测试数据时&#xff0c;插件通过使测试更加简洁和结构化而变得非常有用。Python凭借其简洁性和多功能性&#xff0c;成为自动化测试的热门选择&#xff0c;而pytest是最广泛使用的测试框架之一。虽然pytest本身功能强大&#xf…

【spark-spring boot】学习笔记

目录 说明RDD学习RDD介绍RDD案例基于集合创建RDDRDD存入外部文件中 转换算子 操作map 操作说明案例 flatMap操作说明案例 filter 操作说明案例 groupBy 操作说明案例 distinct 操作说明案例 sortBy 操作说明案例 mapToPair 操作说明案例 mapValues操作说明案例 groupByKey操作说…

Spring Boot 3 集成 Spring Security(2)授权

文章目录 授权配置 SecurityFilterChain基于注解的授权控制自定义权限决策 在《Spring Boot 3 集成 Spring Security&#xff08;1&#xff09;》中&#xff0c;我们简单实现了 Spring Security 的认证功能&#xff0c;通过实现用户身份验证来确保系统的安全性。Spring Securit…

Apache OFBiz xmlrpc XXE漏洞(CVE-2018-8033)

目录 1、漏洞描述 2、EXP下载地址 3、EXP利用 1、漏洞描述 Apache OFBiz是一套企业资源计划&#xff08;ERP&#xff09;系统。它提供了广泛的功能&#xff0c;包括销售、采购、库存、财务、CRM等。 Apache OFBiz还具有灵活的架构和可扩展性&#xff0c;允许用户根据业务需求…

【Android】ARouter的使用及源码解析

文章目录 简介介绍作用 原理关系 使用添加依赖和配置初始化SDK添加注解在目标界面跳转界面不带参跳转界面含参处理返回结果 源码基本流程getInstance()build()navigation()_navigation()Warehouse ARouter初始化init帮助类根帮助类组帮助类 completion 总结 简介 介绍 ARouter…

springboot整合hive

springboot整合hive pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.…

IntelliJ IDEA 中,自动导包功能

在 IntelliJ IDEA 中&#xff0c;自动导包功能可以极大地提高开发效率&#xff0c;减少手动导入包所带来的繁琐和错误。以下是如何在 IntelliJ IDEA 中设置和使用自动导包功能的详细步骤&#xff1a; 一、设置自动导包 打开 IntelliJ IDEA&#xff1a; 启动 IntelliJ IDEA 并打…

【MySQL课程学习】:MySQL安装,MySQL如何登录和退出?MySQL的简单配置

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;MySQL课程学习 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 MySQL在Centos 7环境下的安装&#xff1a; 卸载…

Easyexcel(7-自定义样式)

相关文章链接 Easyexcel&#xff08;1-注解使用&#xff09;Easyexcel&#xff08;2-文件读取&#xff09;Easyexcel&#xff08;3-文件导出&#xff09;Easyexcel&#xff08;4-模板文件&#xff09;Easyexcel&#xff08;5-自定义列宽&#xff09;Easyexcel&#xff08;6-单…

(计算机网络)期末

计算机网络概述 物理层 信源就是发送方 信宿就是接收方 串行通信--一次只发一个单位的数据&#xff08;串行输入&#xff09; 并行通信--一次可以传输多个单位的数据 光纤--利用光的反射进行传输 传输之前&#xff0c;要对信源进行一个编码&#xff0c;收到信息之后要进行一个…

uniapp跨域问题解决方案

uniapp跨域问题解决方案 引言 在使用 uni-app 本地开发 H5> 平台时&#xff0c;需要使用浏览器进行调试&#xff0c;而浏览器会有跨域的问题。比如直接通过本地IP地址去访问开发中的页面&#xff0c;同时这个页面会调一些现有的接口时&#xff0c;就面临着跨域的问题。 解决…

Android 基于Camera2 API进行摄像机图像预览

前言 近期博主准备编写一个基于Android Camera2的图像采集并编码为h.264的应用&#xff0c;准备分为三个阶段来完成&#xff0c;第一阶段实现Camera2的摄像机预览&#xff0c;第二阶段完成基于MediaCodec H.264编码&#xff0c;第三阶段完成基于MediaCodec H.264解码,针对不同…

设计模式:11、迭代器模式(游标)

目录 0、定义 1、迭代器模式的四种角色 2、迭代器模式的UML类图 3、示例代码 4、迭代器的next()方法与集合的get(int index)方法的效率对比&#xff08;LinkedList为例&#xff09; 0、定义 提供一种方法顺序访问一个聚合对象中的各个元素&#xff0c;而又不需要暴露该对象…

UE5连接VR(pico,quest)进行PC VR开发(没有废话全是干货)

一、PICO VR连接UE 首先picoVR&#xff0c;不管是pico neo3还是pico4&#xff0c;用到的软件就只有三个 分别是pico互联助手PICO 互联 | PICO (picoxr.com)、steam VR&#xff0c;虚幻引擎5 pico互联助手 在pico互联助手中你需要选择两种连接方式&#xff08;推荐USB连接&a…

《UnityShader 入门精要》更复杂的光照

代码&示例图见&#xff1a;zaizai77/Shader-Learn: 实现一些书里讲到的shader 到了这里就开启了书里的中级篇&#xff0c;之后会讲解 Unity 中的渲染路径&#xff0c;如何计算光照衰减和阴影&#xff0c;如何使用高级纹理和动画等一系列进阶内容 Unity 中的渲染路径 在U…

用nextjs开发时遇到的问题

这几天已经基本把node后端的接口全部写完了&#xff0c;在前端开发时考虑时博客视频类型&#xff0c;考虑了ssr&#xff0c;于是选用了nextJs&#xff0c;用的是nextUi,tailwincss,目前碰到两个比较难受的事情。 1.nextUI个别组件无法在服务器段渲染 目前简单的解决方法&…

Golang项目:实现一个内存缓存系统

要求 支持设定过期时间&#xff0c;精确到秒支持设定最大内存&#xff0c;当内存超过时做出合适的处理支持并发安全按照以下接口安全 type Cache interface{//size : 1KB 100KB 1MB 2MB 1GBSetMaxMemory(size string )bool//将value写入缓存Set(key string, val interface{},e…

Softing线上研讨会 | Ethernet-APL:推动数字时代的过程自动化

| &#xff08;免费&#xff09;线上研讨会时间&#xff1a;2024年11月19日 16:00~16:30 / 23:00~23:30 Ethernet-APL以10Mb/s的传输速率为过程工业中的现场设备带来了无缝以太网连接和本质安全电源&#xff0c;这不仅革新了新建工厂&#xff0c;也适用于改造现有工厂。 与现…

《Deep Multimodal Learning with Missing Modality: A Survey》中文校对版

文章汉化系列目录 文章目录 文章汉化系列目录摘要1 引言2 方法论分类&#xff1a;概述2.1 数据处理方面2.2 策略设计方面 3 数据处理方面的方法3.1 模态填充3.1.1 模态组合方法3.1.2 模态生成方法 3.2 面向表示的模型3.2.1 协调表示方法3.2.2 表示组合方法。3.2.3 表示生成方法…

python爬虫案例——猫眼电影数据抓取之字体解密,多套字体文件解密方法(20)

文章目录 1、任务目标2、网站分析3、代码编写1、任务目标 目标网站:猫眼电影(https://www.maoyan.com/films?showType=2) 要求:抓取该网站下,所有即将上映电影的预约人数,保证能够获取到实时更新的内容;如下: 2、网站分析 进入目标网站,打开开发者模式,经过分析,我…