Vue3 编译原理直通车💥—— generate 篇

Vue3 编译原理之前的文章中,分别介绍了 Vue 模板编译成模板 AST 的过程 《Vue3 编译原理直通车💥------parser 篇》;以及模板 AST 是如何转换成 JavaScript AST《Vue3 编译原理直通车💥------transform 篇》

现在终于到了最后一个环节 ------ 通过 generate 函数将 JavaScript AST 生成目标 render 函数,用一张图来表示就是这样的:

下面我们一起看看具体的逻辑。

代码生成

相比较 parsertransformgenerate 的逻辑算是整个编译过程中最简单的了;它主要做的就是根据 JavaScript AST 的结构不断进行字符串拼接

假设我们目前有模板如下:

js 复制代码
<template>
  <div>
    <h1>{{ message }}</h1>
  </div>
</template>

这段模板对应的 JavaScript AST 如下:

js 复制代码
const ast = {
  type: 'VNODE_CALL', // 表示需要创建一个虚拟节点
  tag: 'div', // 虚拟节点对应的标签名称
  children: [
    {
      type: 'VNODE_CALL', // 表示需要创建一个虚拟节点
      tag: 'h1', // 虚拟节点对应的标签名称
      children: {
        type: 'INTERPOLATION', // h1 的 children 类型是一个插值表达式
        content: { // 对 INTERPOLATION 类型内容的补充
          type: 'SIMPLE_EXPRESSION', // 是个简单表达式 
          content: 'message' // 表达式内容为 message
        }
      },
      patchFlag: 'TEXT', // patchFlag 属性用于编译优化
      isBlock: false,
    }
  ],
  isBlock: true // 表示需要调用 openBlock 函数
}

那么,generate 具体做了哪些事呢?接着往下看。

创建上下文对象

首先,与 parsertransform 相同,在 generate 的过程中,同样使用了一个上下文对象 context

js 复制代码
function generate(ast) {
  // 创建一个上下文对象
  const context = createCodegenContext()
}

function createCodegenContext() {
  const context = {
    code: ``, // 生成的代码
    mode: 'function', // 生成代码的模式
    // 用于拼接代码
    push(code) {
      context.code += code
    },
    // 当前缩进的级别,初始为 0
    indentLevel: 0,
    // 用于换行
    newline(n) {
      context.push('\n' + `  `.repeat(n))
    },
    // 用于代码生成过程中的缩进,并换行
    indent() {
      newline(++context.indentLevel)
    },
    // 用于取消码生成过程中的缩进,并换行
    deindent() {
      newline(--context.indentLevel)
    }
  }
  return context
}

从上述代码中可以看到,context 对象中 维护了代码生成过程中的一些状态,以及用于拼接字符串的 push 函数、代码格式化的辅助函数等等

有了这些工具,接下来就可以愉快地生成渲染函数啦。

浏览器端 generate

我们知道 Vue 是一个跨端的 UI 框架,因此 generate 代码中还会 根据当前的运行环境等来用于确定代码生成的模式和是否需要特定的优化

例如:如果是服务端渲染,那么最终生成的渲染函数名称就是 ssrRender ,否则为 render;并且浏览器环境与非浏览器环境下最终生成的代码也是有所差异。

这部分逻辑都是为了磨平差异而做的特殊处理,其余部分就不展开讲了;相关源码在 packages/compiler-core/src/codegen.ts 下,有兴趣的小伙伴可以自行阅读。

我们这里以 生成的是浏览器端的渲染函数为例,伪代码如下:

js 复制代码
function generate(ast) {
  const context = createCodegenContext(ast)
  const {
    mode,
    push
  } = context

  // 生成 render 函数基本的函数体
  push(`function render(_ctx, _cache) {`)
  // 缩进并换行
  indent()
  // 拼接 with 语句
  push(`with (_ctx) {`)
  // 缩进并换行
  indent()
  // 将 helpers 数组中的函数名称用逗号做一个拼接
  // 比如:const { createElementVNode: _createElementVNode } = _Vue
  if (ast.helpers.length > 0) {
    push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = _Vue`)
    push(`\n`)
    // 换行
    newline()
  }
  // 调用 genNode,传入 ast 和 context
  genNode(ast, context)
  // 减少缩进
  deindent()
  // 拼接 with 语句的剩下半边括号
  push(`}`)
  // 减少缩进
  deindent()
  // 拼接 render 函数的剩下半边括号
  push(`}`)
}

值得一提的是,在源码中对 ast.helpers 做了一个判断;它的作用是什么呢?

实际上,在之前 transform 生成 JavaScript AST 的过程中会去 分析后续需要用到哪些函数,并存储在 JavaScript AST 中的 helpers 数组中,这样一来只有使用到的函数才会从 Vue 中正常导入,从而减小代码体积

genNode 函数

而对于 JavaScript AST 的主要处理逻辑,放在 genNode 函数中;针对我们开头的模板例子,genNode 的逻辑如下:

js 复制代码
function genNode(node, context) {
  switch (node.type) {
    case 'ELEMENT':
      genNode(node, context)
      break
    case 'SIMPLE_EXPRESSION':
      genExpression(node, context)
      break
    case 'INTERPOLATION':
      genInterpolation(node, context)
      break
    case 'VNODE_CALL':
      genVNodeCall(node, context)
      break
  }
}

可以看到,genNode 函数做的事情就是 根据 JavaScript ASTnode 的类型来调用不同的函数进行字符串拼接处理

下面我们再分别看看这些处理函数的具体逻辑:

genExpression 函数

genExpression 函数的逻辑也很简单,就是拿到 content 的内容,并判断是不是静态节点,是的话直接转化成 JSON 字符串:

js 复制代码
function genExpression(node, context) {
  // 从 node 上获取 content 和 isStatic
  const { content,  isStatic } = node
  // 调用 push 进行字符串拼接
  // 如果 isStatic 为 true,说明是静态节点,直接将其转化成 JSON 字符串
  context.push(isStatic ? JSON.stringify(content) : content)
}

genInterpolation 函数

genInterpolation 函数代码如下:

js 复制代码
function genInterpolation(node, context) {
  const { push } = context
  // 拼接一个 toDisplayString 函数
  push(`toDisplayString(`)
  // 开启递归调用 genNode
  genNode(node.content, context)
  // 递归完成后拼接剩余的 }
  push(`)`)
}

genVNodeCall 函数

genVNodeCall 函数代码如下:

js 复制代码
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
  const { push, indent, deindent } = context
  const {
    tag,
    children,
    patchFlag,
    isBlock
  } = node
  // 如果 isBlock 为 true,拼接 openBlock 函数
  if (isBlock) {
    push(`(openBlock(), `)
  }
  // 根据 isBlock 的值,判断拼接 createElementBlock 还是 createElementVNode
  const callHelper = isBlock ? 'createElementBlock' : 'createElementVNode'
  push(`${callHelper}(`)
  // 拼接 createElementBlock 函数的入参,这里以标签、children 和 patchFlag 为例
  const nodes = [tag, children, patchFlag]
  // 遍历这些入参
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (typeof node === 'string') {  // 如果节点是字符串,直接进行拼接
      push(node)
    } else if (Array.isArray(node)) { // 如果节点是个数组
      // 需要额外进行 [] 的拼接
      push(`[`)
      indent()
      genNode(node, context) // 递归调用 genNode 函数
      deindent()
      push(`]`)
    } else {
      // 递归调用 genNode 函数
      genNode(node, context)
    }
    // 每个参数直接的部分拼接逗号
    if (i < nodes.length - 1) {
      push(',')
    }
  }
  // 拼接 createElementBlock 函数的剩余半边括号
  push(`)`)
  // 拼接 openBlock 函数的剩余半边括号
  if (isBlock) { 
    push(`)`)
  }
}

看起来很长,但是一句话总结还是:字符串拼接拼接再拼接

这部分逻辑主要涉及到了创建虚拟节点时需要调用的一些方法,比如 createElementBlockcreateElementVNode 实际上和 h 函数差不多,不过比起普通的 h 函数,其中包含了一些编译优化的相关信息

关于编译优化又是个大工程哇,埋个坑有机会再开一篇来唠唠吧🧐。

render 函数

那么,以上就是 generate 函数的部分相关逻辑啦;而经过 generate 函数的一系列处理,开头的那段 Javascript AST 最终生成的渲染函数如下:

js 复制代码
const _Vue = Vue

return function render(_ctx, _cache) {
  with (_ctx) {
    const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    return (_openBlock(), _createElementBlock("div", null, [
      _createElementVNode("h1", null, _toDisplayString(message), 1 /* TEXT */)
    ]))
  }
}

总结

至此,Vue3 编译原理系列这班车就到终点站啦,我们来简单总结一下整个过程:

  1. 分析模板,将模板解析成对应的 模板 AST
  2. 深度优先遍历 模板 AST,访问其中节点并转换为 Javascript AST
  3. 根据 Javascript AST 上的节点类型,进行字符串拼接,最终生成模板渲染函数。

Over!👏🏻👏🏻👏🏻

相关推荐
学习ing小白1 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
一只小阿乐1 小时前
前端web端项目运行的时候没有ip访问地址
vue.js·vue·vue3·web端
计算机学姐1 小时前
基于python+django+vue的旅游网站系统
开发语言·vue.js·python·mysql·django·旅游·web3.py
真的很上进2 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er2 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063712 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl2 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码2 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347542 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
ch_s_t2 小时前
新峰商城之分类三级联动实现
前端·html