Vue源码系列讲解——模板编译篇【五】(优化阶段)

这篇具有很好参考价值的文章主要介绍了Vue源码系列讲解——模板编译篇【五】(优化阶段)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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-for 的 template 标签;
    • 节点的所有属性的 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中找出并标记所有静态节点和所有静态根节点。

最后,分别通过逐行分析源码的方式分析了这两件事具体的内部工作原理。文章来源地址https://www.toymoban.com/news/detail-825972.html

到了这里,关于Vue源码系列讲解——模板编译篇【五】(优化阶段)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 记录-Vue.js模板编译过程揭秘:从模板字符串到渲染函数

    Vue.js是一个基于组件化和响应式数据流的前端框架。当我们在Vue中编写模板代码时,它会被Vue编译器处理并转换为可被浏览器解析的JavaScript代码。Vue中的模板实际上是HTML标记和Vue指令的组合,它们会被Vue编译器处理并转化为一个JavaScript渲染函数。 Vue中的模板编译分为两个阶

    2023年04月14日
    浏览(41)
  • vue系列(三)——手把手教你搭建一个vue3管理后台基础模板

    目录 一、前言: 二、网站页面分析 三、开发步骤 (一)、安装element (二)、安装使用svg插件 (三)、编写主界面框架代码  (四)、编写菜单栏  (五)、新建页面及路由 (六)、定制页面标签栏 第一步: 第二步: (七)、修改封装菜单栏 (八)、添加面包屑 四、结

    2023年04月24日
    浏览(61)
  • vue3源码学习api-vue-sfc文件编译

    vue 最有代表性质的就是.VUE 的文件,每一个vue文件都是一个组件,那么vue 组件的编译过程是什么样的呢 一个 Vue 单文件组件 (SFC),通常使用 *.vue 作为文件扩展名,它是一种使用了类似 HTML 语法的自定义文件格式,用于定义 Vue 组件。一个 Vue 单文件组件在语法上是兼容 HTML 的

    2024年02月05日
    浏览(45)
  • Vue源码学习(二):<templete>渲染第一步,模板解析

    好家伙,   在正式内容之前,我们来思考一个问题, 当我们使用vue开发页面时, tamplete中的内容是如何变成我们网页中的内容的 ?   它会经历四步: 解析模板:Vue会解析 template 中的内容,识别出其中的指令、插值表达式( {{}} ),以及其他元素和属性。 生成AST:解析模板后,

    2024年02月09日
    浏览(38)
  • Zabbix6.0全套落地方案-基于RHEL9系列源码编译安装-Linux+Nginx+Mysql+Redis生产级模板及Agent2客户端一键部署

    实践说明:基于RHEL9系列(CentOS9,AlmaLinux9,RockyLinux9等),但适用场景不限于此,客户端一键部署安装包基于RHEL8和RHEL9。 文档形成时期:2023年 因系统或软件版本不同,构建部署可能略有差异,但本文未做细分,对稍有经验者应不存在明显障碍。 限于篇幅,Zabbix配置基于Agent2一键

    2024年02月02日
    浏览(49)
  • 鲁棒优化入门(4)-两阶段鲁棒优化及行列生成算法(C&CG)超详细讲解(附matlab代码)

            本文的主要参考文献: Zeng B , Zhao L . Solving Two-stage Robust Optimization Problems by A Constraint-and-Column Generation Method[J]. Operations Research Letters, 2013, 41(5):457-461.         鲁棒优化是应对数据不确定性的一种优化方法,但单阶段鲁棒优化过于保守。为了解决这一问题,引入

    2024年02月12日
    浏览(46)
  • 基于Java+SpringBoot+Vue实验室安全考试系统(源码+文档+部署+讲解)

    毕设帮助、技术解答、源码交流 联系方式见文末。 本系统为用户而设计制作实验室安全考试系统,旨在实现实验室安全考试智能化、现代化管理。本实验室安全考试管理自动化系统的开发和研制的最终目的是将实验室安全考试的运作模式从手工记录数据转变为网络信息查询

    2024年02月22日
    浏览(54)
  • 探究Vue源码:mustache模板引擎(5) 对比rollup与webpack,在本地搭建webpack环境

    好 从本文开始 我们就来手写一下mustache这个库 他是模板引擎的一个祖先 将模板字符串编译成一个dom字符串 就是它的思想,这也是一个具有跨时代意义的思想 这里的话 我们还是搭一个 webpack 的项目环境 这里值得一提的是 mustache 他官方是通过rollup来进行打包的 很多第三方库

    2024年02月16日
    浏览(41)
  • 【vue3源码系列#01】vue3响应式原理(Proxy)

    专栏分享:vue2源码专栏,vue3源码专栏,vue router源码专栏,玩具项目专栏,硬核💪推荐🙌 欢迎各位ITer关注点赞收藏🌸🌸🌸 在学习 Vue3 是如何进行对象的响应式代理之前,我想我们应该先去了解下 ES6 新增的API Proxy 与 Reflect ,可参考【Vue3响应式入门#02】Proxy and Reflect 。之

    2024年02月05日
    浏览(51)
  • 基于SpringBoot+Vue“鼻护灵”微信小程序设计和实现(源码+LW+部署讲解)

    博主介绍 : ✌ 全网粉丝30W+,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流 ✌ 主要内容: SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、P

    2024年04月17日
    浏览(26)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包