【Vue2.0源码学习】模板编译篇-模板解析阶段(优化阶段)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【Vue2.0源码学习】模板编译篇-模板解析阶段(优化阶段)

1. 前言

在前几篇文章中,我们介绍了模板编译流程三大阶段中的第一阶段模板解析阶段,在这一阶段主要做的工作是用解析器将用户所写的模板字符串解析成AST抽象语法树,理论上来讲,有了AST就可直接进入第三阶段生成render函数了。其实不然,Vue还是很看重性能的,只要有一点可以优化的地方就要将其进行优化。在之前介绍虚拟DOM的时候我们说过,有一种节点一旦首次渲染上了之后不管状态再怎么变化它都不会变了,这种节点叫做静态节点,如下:

<ul>
    <li>我是文本信息</li>
    <li>我是文本信息</li>
    <li>我是文本信息</li>
    <li>我是文本信息</li>
    <li>我是文本信息</li>
</ul>

在上面代码中,ul标签下面有5个li标签,每个li标签里的内容都是不含任何变量的纯文本,也就是说这种标签一旦第一次被渲染成DOM节点以后,之后不管状态再怎么变化它都不会变了,我们把像li的这种节点称之为静态节点。而这5个li节点的父节点是ul节点,也就是说ul节点的所有子节点都是静态节点,那么我们把像ul的这种节点称之为静态根节点。

OK,有了静态节点和静态根节点这两个概念之后,我们再仔细思考,模板编译的最终目的是用模板生成一个render函数,而用render函数就可以生成与模板对应的VNode,之后再进行patch算法,最后完成视图渲染。这中间的patch算法又是用来对比新旧VNode之间存在的差异。在上面我们还说了,静态节点不管状态怎么变化它是不会变的,基于此,那我们就可以在patch过程中不用去对比这些静态节点了,这样不就又可以提高一些性能了吗?

所以我们在模板编译的时候就先找出模板中所有的静态节点和静态根节点,然后给它们打上标记,用于告诉后面patch过程打了标记的这些节点是不需要对比的,你只要把它们克隆一份去用就好啦。这就是优化阶段存在的意义。

上面也说了,优化阶段其实就干了两件事:

  1. AST中找出所有静态节点并打上标记;
  2. AST中找出所有静态根节点并打上标记;

优化阶段的源码位于src/compiler/optimizer.js中,如下:

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // 标记静态节点
  markStatic(root)
  // 标记静态根节点
  markStaticRoots(root, false)
}

接下来,我们就对所干的这两件事逐个分析。

2. 标记静态节点

AST中找出所有静态节点并标记其实不难,我们只需从根节点开始,先标记根节点是否为静态节点,然后看根节点如果是元素节点,那么就去向下递归它的子节点,子节点如果还有子节点那就继续向下递归,直到标记完所有节点。代码如下:

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        node.static = false
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}

在上面代码中,首先调用isStatic函数标记节点是否为静态节点,该函数若返回true表示该节点是静态节点,若返回false表示该节点不是静态节点,函数实现如下:

function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // 包含变量的动态文本节点
    return false
  }
  if (node.type === 3) { // 不包含变量的纯文本节点
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}

该函数的实现过程其实也说明了如何判断一个节点是否为静态节点。还记得在HTML解析器在调用钩子函数创建AST节点时会根据节点类型的不同为节点加上不同的type属性,用type属性来标记AST节点的节点类型,其对应关系如下:

type取值 对应的AST节点类型
1 元素节点
2 包含变量的动态文本节点
3 不包含变量的纯文本节点

所以在判断一个节点是否为静态节点时首先会根据type值判断节点类型,如果type值为2,那么该节点是包含变量的动态文本节点,它就肯定不是静态节点,返回false

if (node.type === 2) { // 包含变量的动态文本节点
    return false
}

如果type值为2,那么该节点是不包含变量的纯文本节点,它就肯定是静态节点,返回true

if (node.type === 3) { // 不包含变量的纯文本节点
    return true
}

如果type值为1,说明该节点是元素节点,那就需要进一步判断。

node.pre ||
(
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
)

如果元素节点是静态节点,那就必须满足以下几点要求:

  • 如果节点使用了v-pre指令,那就断定它是静态节点;
  • 如果节点没有使用
v-pre
  • 指令,那它要成为静态节点必须满足:
  • 不能使用动态绑定语法,即标签上不能有v-@:开头的属性;
  • 不能使用v-ifv-elsev-for指令;
  • 不能是内置组件,即标签名不能是slotcomponent
  • 标签名必须是平台保留标签,即不能是组件;
  • 当前节点的父节点不能是带有 v-fortemplate 标签;
  • 节点的所有属性的 key 都必须是静态节点才有的 key,注:静态节点的key是有限的,它只能是type,tag,attrsList,attrsMap,plain,parent,children,attrs之一;

标记完当前节点是否为静态节点之后,如果该节点是元素节点,那么还要继续去递归判断它的子节点,如下:

for (let i = 0, l = node.children.length; i < l; i++) {
    const child = node.children[i]
    markStatic(child)
    if (!child.static) {
        node.static = false
    }
}

注意,在上面代码中,新增了一个判断:

if (!child.static) {
    node.static = false
}

这个判断的意思是如果当前节点的子节点有一个不是静态节点,那就把当前节点也标记为非静态节点。为什么要这么做呢?这是因为我们在判断的时候是从上往下判断的,也就是说先判断当前节点,再判断当前节点的子节点,如果当前节点在一开始被标记为了静态节点,但是通过判断子节点的时候发现有一个子节点却不是静态节点,这就有问题了,我们之前说过一旦标记为静态节点,就说明这个节点首次渲染之后不会再发生任何变化,但是它的一个子节点却又是可以变化的,就出现了自相矛盾,所以我们需要当发现它的子节点中有一个不是静态节点的时候,就得把当前节点重新设置为非静态节点。

循环node.children后还不算把所有子节点都遍历完,因为如果当前节点的子节点中有标签带有v-ifv-else-ifv-else等指令时,这些子节点在每次渲染时都只渲染一个,所以其余没有被渲染的肯定不在node.children中,而是存在于node.ifConditions,所以我们还要把node.ifConditions循环一遍,如下:

if (node.ifConditions) {
    for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if (!block.static) {
            node.static = false
        }
    }
}

同理,如果当前节点的node.ifConditions中有一个子节点不是静态节点也要将当前节点设置为非静态节点。

以上就是标记静态节点的全部逻辑。

3. 标记静态根节点

寻找静态根节点根寻找静态节点的逻辑类似,都是从AST根节点递归向下遍历寻找,其代码如下:

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    // For a node to qualify as a static root, it should have children that
    // are not just static text. Otherwise the cost of hoisting out will
    // outweigh the benefits and it's better off to just always render it fresh.
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

上面代码中,首先markStaticRoots 第二个参数是 isInFor,对于已经是 static 的节点或者是 v-once 指令的节点,node.staticInFor = isInFor,如下:

if (node.static || node.once) {
    node.staticInFor = isInFor
}

接着判断该节点是否为静态根节点,如下:

// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
// 为了使节点有资格作为静态根节点,它应具有不只是静态文本的子节点。 否则,优化的成本将超过收益,最好始终将其更新。
if (node.static && node.children.length && !(
    node.children.length === 1 &&
    node.children[0].type === 3
)) {
    node.staticRoot = true
    return
} else {
    node.staticRoot = false
}

从代码和注释中我们可以看到,一个节点要想成为静态根节点,它必须满足以下要求:

  • 节点本身必须是静态节点;
  • 必须拥有子节点 children
  • 子节点不能只是只有一个文本节点;

否则的话,对它的优化成本将大于优化后带来的收益。

如果当前节点不是静态根节点,那就继续递归遍历它的子节点node.childrennode.ifConditions,如下:

if (node.children) {
    for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
    }
}
if (node.ifConditions) {
    for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
    }
}

这里的原理跟寻找静态节点相同,此处就不再重复。

4. 总结

本篇文章介绍了模板编译过程三大阶段的第二阶段——优化阶段。

首先,介绍了为什么要有优化阶段,是为了提高虚拟DOMpatch过程的性能。在优化阶段将所有静态节点都打上标记,这样在patch过程中就可以跳过对比这些节点。

接着,介绍了优化阶段主要干了两件事情,分别是从构建出的AST中找出并标记所有静态节点和所有静态根节点。

最后,分别通过逐行分析源码的方式分析了这两件事具体的内部工作原理。

目录
相关文章
|
19天前
|
SQL 关系型数据库 MySQL
深入解析MySQL的EXPLAIN:指标详解与索引优化
MySQL 中的 `EXPLAIN` 语句用于分析和优化 SQL 查询,帮助你了解查询优化器的执行计划。本文详细介绍了 `EXPLAIN` 输出的各项指标,如 `id`、`select_type`、`table`、`type`、`key` 等,并提供了如何利用这些指标优化索引结构和 SQL 语句的具体方法。通过实战案例,展示了如何通过创建合适索引和调整查询语句来提升查询性能。
119 9
|
12天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
12天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
12天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
1月前
|
机器学习/深度学习 人工智能 PyTorch
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
本文探讨了Transformer模型中变长输入序列的优化策略,旨在解决深度学习中常见的计算效率问题。文章首先介绍了批处理变长输入的技术挑战,特别是填充方法导致的资源浪费。随后,提出了多种优化技术,包括动态填充、PyTorch NestedTensors、FlashAttention2和XFormers的memory_efficient_attention。这些技术通过减少冗余计算、优化内存管理和改进计算模式,显著提升了模型的性能。实验结果显示,使用FlashAttention2和无填充策略的组合可以将步骤时间减少至323毫秒,相比未优化版本提升了约2.5倍。
51 3
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
|
1月前
|
前端开发 UED
React 文本区域组件 Textarea:深入解析与优化
本文介绍了 React 中 Textarea 组件的基础用法、常见问题及优化方法,包括状态绑定、初始值设置、样式自定义、性能优化和跨浏览器兼容性处理,并提供了代码案例。
55 8
|
2月前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
57 12
|
1月前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
2月前
|
缓存 NoSQL Java
千万级电商线上无阻塞双buffer缓冲优化ID生成机制深度解析
【11月更文挑战第30天】在千万级电商系统中,ID生成机制是核心基础设施之一。一个高效、可靠的ID生成系统对于保障系统的稳定性和性能至关重要。本文将深入探讨一种在千万级电商线上广泛应用的ID生成机制——无阻塞双buffer缓冲优化方案。本文从概述、功能点、背景、业务点、底层原理等多个维度进行解析,并通过Java语言实现多个示例,指出各自实践的优缺点。希望给需要的同学提供一些参考。
52 7
|
1月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####