Vue3模版编译原理

更新日期: 2022-12-03阅读: 394标签: 原理

模版编译流程

vue3模版编译就是把template字符串编译成渲染函数

// template
<div><p>{{LH_R}}</p></div>

// render
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("p", null, _toDisplayString(_ctx.LH_R), 1 /* TEXT */)
  ]))
}

我会按照编译流程分3步分析

parse:将模版字符串转换成模版AST
transform:将模版AST转换为用于描述渲染函数的AST
generate:根据AST生成渲染函数
export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  // ...
  const ast = isString(template) ? baseParse(template, options) : template

  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
    prefixIdentifiers
  )
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

parse

parse对模版字符串进行遍历,然后循环判断开始标签和结束标签把字符串分割成一个个token,存在一个token列表,然后扫描token列表并维护一个开始标签栈,每当扫描一个开始标签节点,就将其压入栈顶,栈顶的节点始终作为下一个扫描的节点的父节点。这样,当所有Token扫描完成后,即可构建成一颗树形AST

以下是简化版parseChildren源码,是parse的主入口

function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[] // 节点栈结构,用于维护节点嵌套关系
): TemplateChildNode[] {
  // 获取父节点
  const parent = last(ancestors)
  const ns = parent ? parent.ns : Namespaces.html
  const nodes: TemplateChildNode[] = [] // 存储解析出来的AST子节点

  // 遇到闭合标签结束解析
  while (!isEnd(context, mode, ancestors)) {
    // 切割处理的模版字符串
    const s = context.source
    let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined

    if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
      if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
        // 解析插值表达式{{}}
        node = parseInterpolation(context, mode)
      } else if (mode === TextModes.DATA && s[0] === '<') {
        if (s[1] === '!') {
          // 解析注释节点和文档声明...
        } else if (s[1] === '/') {
          if (s[2] === '>') {
            // 针对自闭合标签,前进三个字符
            advanceBy(context, 3)
            continue
          } else if (/[a-z]/i.test(s[2])) {
            // 解析结束标签
            parseTag(context, TagType.End, parent)
            continue
          } else {
            // 如果不符合上述情况,就作为伪注释解析
            node = parseBogusComment(context)
          }
        } else if (/[a-z]/i.test(s[1])) {
          // 解析html开始标签,获得解析到的AST节点
          node = parseElement(context, ancestors)
        }
      }
    }
    if (!node) {
      // 普通文本节点
      node = parseText(context, mode)
    }

    // 如果节点是数组,就遍历添加到nodes中
    if (isArray(node)) {
      for (let i = 0; i < node.length; i++) {
        pushNode(nodes, node[i])
      }
    } else {
      pushNode(nodes, node)
    }
  }
  return nodes
}

就拿<div><p>LH_R</p></div>流程举例

  1. div开始标签入栈,context.source = <p>LH_R</p></div>,ancestors = [div]
  2. p开始标签入栈,context.source = LH_R</p></div>,ancestors = [div, p]
  3. 解析文本LH_R
  4. 解析p结束标签,p标签出栈
  5. 解析div结束标签,div标签出栈
  6. 栈空,模版解析完毕

transform

transform采用深度优先的方式对AST进行遍历,在遍历过程中,对节点的操作与转换采用插件化架构,都封装为独立的函数,然后转换函数通过context.nodeTransforms来注册

转换过程是优先转换子节点,因为有的父节点的转换依赖子节点

以下是AST遍历traverseNode核心源码

/* 
  遍历AST节点树,通过node转换器对当前节点进行node转换
  子节点全部遍历完成后执行对应指令的onExit回调退出转换
*/
export function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext
) {
  // 记录当前正在遍历的节点
  context.currentNode = node

  /* 
    nodeTransforms:transformElement、transformExpression、transformText...
    transformElement:负责整个节点层面的转换
    transformExpression:负责节点中表达式的转化
    transformText:负责节点中文本的转换
  */
  const { nodeTransforms } = context
  const exitFns = []
  // 依次调用转换工具
  for (let i = 0; i < nodeTransforms.length; i++) {
    /* 
      转换器只负责生成onExit回调,onExit函数才是执行转换主逻辑的地方,为什么要推到栈中先不执行呢?
      因为要等到子节点都转换完成挂载gencodeNode后,也就是深度遍历完成后
      再执行当前节点栈中的onExit,这样保证了子节点的表达式全部生成完毕
    */
    const onExit = nodeTransforms[i](node, context)
    if (onExit) {
      if (isArray(onExit)) {
        // v-if、v-for为结构化指令,其onExit是数组形式
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
    if (!context.currentNode) {
      // node was removed 节点被移除
      return
    } else {
      // node may have been replaced
      // 因为在转换的过程中节点可能被替换,恢复到之前的节点
      node = context.currentNode
    }
  }

  switch (node.type) {
    case NodeTypes.COMMENT:
      if (!context.ssr) {
        // inject import for the Comment symbol, which is needed for creating
        // comment nodes with `createVNode`
        // 需要导入createComment辅助函数
        context.helper(CREATE_COMMENT)
      }
      break
    case NodeTypes.INTERPOLATION:
      // no need to traverse, but we need to inject toString helper
      if (!context.ssr) {
        context.helper(TO_DISPLAY_STRING)
      }
      break

    // for container types, further traverse downwards
    case NodeTypes.IF:
      // 对v-if生成的节点束进行遍历
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      // 遍历子节点
      traverseChildren(node, context)
      break
  }
  // 当前节点树遍历完成,依次执行栈中的指令退出回调onExit
  context.currentNode = node
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}

generate

generate生成代码大致分为3步

1、创建代码生成上下文,因为该上下文对象是用于维护代码生成过程中程序的运行状态,如:

code:最终生成的渲染函数
push:拼接代码
indent:代码缩进
deindent:减少代码缩进

2、生成渲染函数的前置预设部分

module模式下:genModulePreamble()
function模式下:genFunctionPreamble
还有一些函数名,参数,作用域…

3、生成渲染函数

通过调用genNode,然后在genNode内部通过switch语句来匹配不同类型的节点,并调用对应的生成器函数

来自:https://songlh.top/2022/08/31/ Vue3模版编译原理/

链接: https://fly63.com/article/detial/12591

CSS定位之BFC背后的神奇原理

BFC已经是一个耳听熟闻的词语了,网上有许多关于 BFC 的文章,介绍了如何触发 BFC 以及 BFC 的一些用处(如清浮动,防止 margin 重叠等)。BFC直译为\"块级格式化上下文\"。它是一个独立的渲染区域,只有Block-level box参与

天天都在使用CSS,那么CSS的原理是什么呢?

作为前端,我们每天都在与CSS打交道,那么CSS的原理是什么呢?开篇,我们还是不厌其烦的回顾一下浏览器的渲染过程,学会使用永远都是最基本的标准,但是懂得原理,你才能触类旁通,超越自我。

JavaScript 中的函数式编程原理

做了一些研究,我发现了函数式编程概念,如不变性和纯函数。 这些概念使你能够构建无副作用的功能,而函数式编程的一些优点,也使得系统变得更加容易维护。我将通过 JavaScript 中的大量代码示例向您详细介绍函数式编程和一些重要概念。

Angular ZoneJS 原理

如果你阅读过关于Angular 2变化检测的资料,那么你很可能听说过zone。Zone是一个从Dart中引入的特性并被Angular 2内部用来判断是否应该触发变化检测

Vue.js响应式原理

updateComponent在更新渲染组件时,会访问1或多个数据模版插值,当访问数据时,将通过getter拦截器把componentUpdateWatcher作为订阅者添加到多个依赖中,每当其中一个数据有更新,将执行setter函数

new运算符的原理

一个继承自 Foo.prototype 的新对象被创建;使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数时,Foo 不带任何参数调用的情况

彻底弄懂HTTP缓存机制及原理

Http 缓存机制作为 web 性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系库中的一个基础环节,同时对于有志成为前端架构师的同学来说是必备的知识技能。

https的基本原理

HTTPS = HTTP + TLS/SSL,简单理解 HTTPS 其实就是在 HTTP 上面加多了一层安全层。HTTP 可以是 Http2.0 也可以是 Http1.1,不过现在 Http2.0 是强制要求使用 Https 的。使用非对称密钥(即公钥私钥))和对称密钥)(即共享密钥)相结合

Node中的Cookie和Session

HTTP是无状态协议。例:打开一个域名的首页,进而打开该域名的其他页面,服务器无法识别访问者。即同一浏览器访问同一网站,每次访问都没有任何关系。Cookie的原理是

理解Promise原理

Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态(即状态 immutable)。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!