从 vue 源码看问题 —— vue 编译器解析的优化

简介: 从 vue 源码看问题 —— vue 编译器解析的优化

image.png


前言

编译器中的优化,其实就是做静态标记:

  • 通过遍历 AST 对象,为每个节点做 静态标记,通过标记其是否为静态节点,然后进一步标记出 静态根节点,方便在后续更新过程中跳过这些静态节点
  • 标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数

深入源码

createCompiler() 方法 —— 入口

文件位置:/src/compiler/index.js

其中主要是通过 optimize() 方法做静态标记,从而实现优化的.

/*
  在这之前做的所有的事情,只是为了构建平台特有的编译选项(options),比如 web 平台
  1、将 html 模版解析成 ast
  2、对 ast 树进行静态标记
  3、将 ast 生成渲染函数
     - 静态渲染函数放到 code.staticRenderFns 数组中
     - 动态渲染函数 code.render
     - 在将来渲染时执行渲染函数能够得到 vnode
 */
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  /* 
   将模版字符串解析为 AST 语法树
   每个节点的 ast 对象上都设置了元素的所有信息,如,标签信息、属性信息、插槽信息、父节点、子节点等
  */
  const ast = parse(template.trim(), options)
  /*
   优化,遍历 AST,为每个节点做静态标记
     - 标记每个节点是否为静态节点,,保证在后续更新中跳过这些静态节点
     - 标记出静态根节点,用于生成渲染函数阶段,生成静态根节点的渲染函数
 */
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  /*
    从 AST 语法树生成渲染函数
    如:code.render = "_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return _c('div',{key:item},[_v(_s(item))])}),0)"
  */
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
复制代码

optimize() 方法

文件位置:/src/compiler/optimizer.js

其中最主要的就是 markStatic(root)markStaticRoots(root, false) 方法.

/*
优化的目标:遍历生成的模板 AST 树并检测纯静态的子树,即永远不需要改变的 DOM
一旦检测到这些子树就可以:
   1. 将它们提升为常数,这样就不再需要在每次重新渲染时为其创建新节点
   2. 在修补过程中完全跳过它们
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  // 是否平台保留标签
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  // 遍历所有节点,给每个节点设置 static 属性,标识其是否为静态节点
  markStatic(root)
  // second pass: mark static roots.
  /* 
   进一步标记静态根,一个节点要成为静态根节点,需要具体以下条件:
    - 节点本身是静态节点,而且有子节点,而且子节点不只是一个文本节点,则标记为静态根
    - 静态根节点不能只有静态文本的子节点,因为这样收益太低,这种情况下始终更新它就好了
  */
  markStaticRoots(root, false)
}
复制代码

markStatic() 方法

文件位置:/src/compiler/optimizer.js

这里主要通过 isStatic(node) 方法来判断当前节点是否属于静态节点,针对子节点则通过递归调用 markStatic() 进行标记

/*
 在所有节点上设置 static 属性,用来标识是否为静态节点
 注意:如果有子节点为动态节点,则父节点也被认为是动态节点
*/
function markStatic (node: ASTNode) {
  // 通过 node.static 来标识节点是否为 静态节点
  node.static = isStatic(node)
  if (node.type === 1) {
    /*
    不将组件插槽内容设置为静态节点,这是为了避免:
      1. 组件无法改变插槽节点
      2. 静态插槽内容无法进行热重载
    */
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      // 递归终止条件:节点非平台保留标签 && 非 slot 标签 && 非内联模版,则直接结束
      return
    }
    // 遍历子节点,递归调用 markStatic 来标记这些子节点的 static 属性
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      // 如果子节点是非静态节点,则将父节点更新为非静态节点
      if (!child.static) {
        node.static = false
      }
    }
    // 如果节点存在 v-if、v-else-if、v-else 这些指令,则依次标记 block 中节点的 static
    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() 方法

文件位置:/src/compiler/optimizer.js

/*
 判断节点是否为静态节点
  - 动态节点:
    1. 包含表达式 {{ msg }},即  node.type === 2
    2. 包含 v-bind、v-if、v-for 等指令的都属于动态节点
    3. 组件、slot 插槽都为动态节点
    4. 父节点为含有 v-for 指令的 template 标签
  - 静态节点:除了动态节点的情况之外就属于静态节点,如文本节点,即 node.type === 3
*/
function isStatic (node: ASTNode): boolean {
  // node.type === 2 为表达式,如:{{ msg }} ,返回 false
  if (node.type === 2) {
    return false
  }
  // node.type === 3 为文本节点,返回 true 标记为静态节点
  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)
  ))
}
复制代码

markStaticRoots() 方法

文件位置:/src/compiler/optimizer.js

/*
 进一步标记静态根,一个节点要成为静态根节点,需要具体以下条件:
  - 节点本身是静态节点,且有子节点,而且子节点不只是一个文本节点,则标记为静态根
  - 静态根节点不能只有静态文本的子节点,因为这样收益太低,这种情况下始终更新它就好了
  @param { ASTElement } node 当前节点
  @param { boolean } isInFor 当前节点是否被包裹在 v-for 指令所在的节点内
*/
function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      // 节点是静态的 或 节点上有 v-once 指令,标记 node.staticInFor = true || false
      node.staticInFor = isInFor
    }
    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)
      }
    }
    // 如果节点存在 v-if、v-else-if、v-else 指令,则为 block 节点标记静态根
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}
复制代码

总结

静态标记的过程是什么?

  • 标记静态节点
  • 通过递归的方式标记所有的元素节点
  • 如果节点本身是 静态节点,但是存在 非静态的子节点,则将节点修改为 非静态节点
  • 标记静态根节点,基于静态节点进一步标记静态根节点
  • 如果节点本身是 静态节点&&有子节点&& 子节点 不全是文本节点,则标记为 静态根节点
  • 如果节点本身 不是静态根节点,则递归遍历 所有子节点,在子节点中 标记静态根

什么样的节点才可以被标记为静态节点?

  • 动态节点:
  • 包含表达式 {{ msg }},即 node.type === 2 则为动态节点
  • 包含 v-bind、v-if、v-for 等指令的都属于动态节点
  • 组件slot 插槽都为动态节点
  • 父节点为含有 v-for 指令的 template 标签
  • 静态节点:
  • 除了动态节点的情况之外就属于静态节点,如 文本节点 (node.type === 3)


目录
相关文章
|
5月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
778 0
|
6月前
|
JavaScript 数据可视化 前端开发
基于 Vue 与 D3 的可拖拽拓扑图技术方案及应用案例解析
本文介绍了基于Vue和D3实现可拖拽拓扑图的技术方案与应用实例。通过Vue构建用户界面和交互逻辑,结合D3强大的数据可视化能力,实现了力导向布局、节点拖拽、交互事件等功能。文章详细讲解了数据模型设计、拖拽功能实现、组件封装及高级扩展(如节点类型定制、连接样式优化等),并提供了性能优化方案以应对大数据量场景。最终,展示了基础网络拓扑、实时更新拓扑等应用实例,为开发者提供了一套完整的实现思路和实践经验。
835 77
|
5月前
|
JavaScript 前端开发 UED
Vue 手风琴实现的三种常用方式及长尾关键词解析
手风琴效果是Vue开发中常见的交互组件,可节省页面空间、提升用户体验。本文介绍三种实现方式:1) 原生Vue结合数据绑定与CSS动画;2) 使用Element UI等组件库快速构建;3) 自定义指令操作DOM实现独特效果。每种方式适用于不同场景,可根据项目需求选择。示例包括产品特性页、后台菜单及FAQ展示,灵活满足多样需求。附代码示例与资源链接,助你高效实现手风琴功能。
319 10
|
5月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
273 1
|
8月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
331 4
|
8月前
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
345 2
|
8月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
828 29
|
8月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

推荐镜像

更多
  • DNS