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

什么是静态提升

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,再次谢谢各位可爱的看官老爷。



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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++优选算法十四 优先级队列(堆)

C 中的优先级队列&#xff08;Priority Queue&#xff09;是一种容器适配器&#xff0c;它提供队列的功能&#xff0c;但元素不是按照插入的顺序被访问&#xff0c;而是根据它们的优先级被访问。默认情况下&#xff0c;优先级队列是一个最大堆&#xff08;Max-Heap&#xff09;…

综合练习--轮播图

本篇博客将教大家实现一个基础的轮播图。 源代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0&qu…

“AI玩手机”原理揭秘:大模型驱动的移动端GUI智能体

作者&#xff5c;郭源 前言 在后LLM时代&#xff0c;随着大语言模型和多模态大模型技术的日益成熟&#xff0c;AI技术的实际应用及其社会价值愈发受到重视。AI智能体&#xff08;AI Agent&#xff09;技术通过集成行为规划、记忆存储、工具调用等机制&#xff0c;为大模型装上…

光伏电站的智慧施工详解

光伏电站的智慧施工是利用先进的技术和管理方法&#xff0c;提高施工效率、质量和安全性&#xff0c;降低成本&#xff0c;实现光伏电站建设的智能化、数字化和绿色化。 下面从鹧鸪云智慧施工软件详细施工管理的步骤说起。 项目总览 包含我负责的项目、我参与的项目、我创建…

django——创建 Django 项目和 APP

2.创建 Django 项目和 APP 命令&#xff1a; 创建Django项目 django-admin startproject name 创建子应用 python manager.py startapp name 2.1 创建工程 在使用Flask框架时&#xff0c;项目工程目录的组织与创建是需要我们自己手动创建完成的。 在django中&#xff0c;…

李春葆《数据结构》-课后习题代码题

一&#xff1a;假设不带权有向图采用邻接矩阵 g 存储&#xff0c;设计实现以下功能的算法&#xff1a; &#xff08;1&#xff09;求出图中每个顶点的入度。 代码&#xff1a; void indegree(MatGraph g){int i,j,n;printf("各个顶点的入度&#xff1a;\n");for(i…

wsl安装

一. wsl简介 1. wsl和wsl2的区别 wsl需要把linux命令翻译为windows命令&#xff0c;性能差一些。 wsl2直接使用linux内核&#xff0c;不需要翻译&#xff0c;性能好&#xff0c;但开销相对大一点&#xff0c;因为需要多运行一个hyper-v虚拟机 (并非完整的虚拟机&#xff0c;是…

Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 大数据篇正在更新&#xff01;https://blog.csdn.net/w776341482/category_12713819.html 目前已经更新到了&#xff1a; MyBatis&#xff…

GPT中转站技术架构

本文介绍阿波罗AI中转站&#xff08;https://api.ablai.top/&#xff09;的技术架构&#xff0c;该中转API的技术架构采用了分布式架构、智能调度和API中转等技术&#xff0c;确保了全球范围内的高效访问和稳定运行。以下是对该技术架构的详细分析&#xff1a; 分布式架构 分…

远程服务器Docker使用本地代理加速访问外部资源

Docker在pull镜像的时候非常缓慢&#xff0c;但是远程主机没有安装代理&#xff0c;就很为难&#xff0c;现在分享一个可以让远程服务器使用本地代理加速的方法 配置Docker代理 新建文件夹 mkdir -p /etc/systemd/system/docker.service.d 切换到这个文件夹里 cd /etc/system…

【详解】树链剖分之重链剖分

终于搞懂了树链剖分的一些皮毛了…… 树链剖分 “树链剖分”&#xff0c;顾名思义&#xff0c;就是把一棵树剖分成一条条的链…… 重链剖分 重链剖分的基本概念 重链剖分是树链剖分的一种&#xff0c;它会把树剖分成一条条重链…… 什么是重链呢&#xff1f; 重链就是连接…

RocketMQ: 部署结构与存储特点

RocketMQ 是什么 它是一个队列模型的消息中间件&#xff0c;具有高性能、高可靠、高实时、分布式特点 Producer、Consumer、队列都可以分布式Producer 向一些队列轮流发送消息 队列集合称为 TopicConsumer 如果做广播消费则一个 consumer 实例消费这个 Topic 对应的所有队列如果…

帮助中心FAQ系统:打造卓越客户服务体验的关键驱动力

在当今这个信息爆炸的时代&#xff0c;企业为了保持市场竞争力&#xff0c;必须不断提升客户服务体验。FAQ&#xff08;常见问题解答&#xff09;系统&#xff0c;作为一种高效且便捷的用户服务工具&#xff0c;正日益受到企业的青睐。本文将阐述FAQ系统的核心价值、功能特性以…

如何使用 Python 开发一个简单的文本数据转换为 Excel 工具

目录 一、准备工作 二、理解文本数据格式 三、开发文本数据转换为Excel工具 读取CSV文件 将DataFrame写入Excel文件 处理其他格式的文本数据 读取纯文本文件&#xff1a; 读取TSV文件&#xff1a; 四、完整代码与工具封装 五、使用工具 六、总结 在数据分析和处理的…

Elasticsearch向量搜索:从语义搜索到图搜图只有一步之遥

续 上集说到语义搜索&#xff0c;这集接着玩一下图搜图&#xff0c;这种场景在电商中很常见——拍照搜商品。图搜图实现非常类似语义搜索&#xff0c;代码逻辑结构都很类似… 开搞 还是老地方modelscope找个Vision Transformer模型&#xff0c;这里选用vit-base-patch16-224…

Flink【基于时间的双流联结 Demo】

前言 1、基于时间的双流联结&#xff08;Join&#xff09; 对于两条流的合并&#xff0c;很多情况我们并不是简单地将所有数据放在一起&#xff0c;而是希望根据某个字段的值将它们联结起来&#xff0c;“配对”去做处理。例如用传感器监控火情时&#xff0c;我们需要将大量温度…

大数据入门-什么是Flink

这里简单介绍Flink的概念、架构、特性等。至于比较详细的介绍&#xff0c;会单独针对这个组件进行详细介绍&#xff0c;可以关注博客后续阅读。 一、概念 Apache Flink 是一个框架和分布式处理引擎&#xff0c;用于在无边界和有边界数据流上进行有状态的计算。 Flink的四大基…

KubeVirt下gpu operator实践(GPU直通)

KubeVirt下gpu operator实践(GPU直通) 参考《在 KubeVirt 中使用 GPU Operator》&#xff0c;记录gpu operator在KubeVirt下实践的过程&#xff0c;包括虚拟机配置GPU直通&#xff0c;容器挂载GPU设备等。 KubeVirt 提供了一种将主机设备分配给虚拟机的机制。该机制具有通用性…

How to update the content of one column in Mysql

How to update the content of one column in Mysql by another column name? UPDATE egg.eggs_record SET sold 2024-11-21 WHERE id 3 OR id 4;UPDATE egg.eggs_record SET egg_name duck egg WHERE id 2;

【K8S系列】imagePullSecrets配置正确,但docker pull仍然失败,进一步排查详细步骤

如果 imagePullSecrets 配置正确,但在执行 docker pull 命令时仍然失败,可能存在以下几种原因。以下是详细的排查步骤和解决方案。 1. 检查 Docker 登录凭证 确保你使用的是与 imagePullSecrets 中相同的凭证进行 Docker 登录: 1.1 直接登录 在命令行中,执行以下命令: …