Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

前言

在之前的 通过 debug 搞清楚. vue 文件怎么变成. js 文件 文章中我们讲过了 vue 文件是如何编译成 js 文件,通过那篇文章我们知道了,template 编译为 render 函数底层就是调用了@vue/compiler-sfc包暴露出来的compileTemplate函数。由于文章篇幅有限,我们没有去深入探索compileTemplate函数是如何将 template 模块编译为render函数,在这篇文章中我们来了解一下。

@vue下面的几个包

先来介绍一下本文中涉及到 vue 下的几个包,分别是:@vue/compiler-sfc@vue/compiler-dom@vue/compiler-core

  • @vue/compiler-sfc:用于编译 vue 的 SFC 文件,这个包依赖 vue 下的其他包,比如@vue/compiler-dom@vue/compiler-core。这个包一般是给 vue-loader 和 @vitejs/plugin-vue 使用的。

  • @vue/compiler-dom:这个包专注于浏览器端的编译,处理浏览器 dom 相关的逻辑都在这里面。

  • @vue/compiler-core:从名字你也能看出来这个包是 vue 编译部分的核心,提供了通用的编译逻辑,不管是浏览器端还是服务端编译最终都会走到这个包里面来。

先来看个流程图

先来看一下我画的 template 模块编译为render函数这一过程的流程图,让你对整个流程有个大概的印象,后面的内容看着就不费劲了。如下图:

从上面的流程图可以看到整个流程可以分为 7 步:

  • 执行@vue/compiler-sfc包的compileTemplate函数,里面会调用同一个包的doCompileTemplate函数。

  • 执行@vue/compiler-sfc包的doCompileTemplate函数,里面会调用@vue/compiler-dom包中的compile函数。

  • 执行@vue/compiler-dom包中的compile函数,里面会对options进行了扩展,塞了一些处理 dom 的转换函数进去。分别塞到了options.nodeTransforms数组和options.directiveTransforms对象中。然后以扩展后的options去调用@vue/compiler-core包的baseCompile函数。

  • 执行@vue/compiler-core包的baseCompile函数,在这个函数中主要分为 4 部分。第一部分为检查传入的 source 是不是 html 字符串,如果是就调用同一个包下的baseParse函数生成模版AST抽象语法树。否则就直接使用传入的模版AST抽象语法树。此时 node 节点中还有v-forv-model等指令。这里的模版AST抽象语法树结构和 template 模块中的代码结构是一模一样的,所以说模版AST抽象语法树就是对 template 模块中的结构进行描述。

  • 第二部分为执行getBaseTransformPreset函数拿到@vue/compiler-core包中内置的nodeTransformsdirectiveTransforms转换函数。

  • 第三部分为将传入的options.nodeTransformsoptions.directiveTransforms分别和本地的nodeTransformsdirectiveTransforms进行合并得到一堆新的转换函数,和模版AST抽象语法树一起传入到transform函数中执行,就会得到转换后的javascript AST抽象语法树。在这一过程中v-forv-model等指令已经被转换函数给处理了。得到的javascript AST抽象语法树的结构和将要生成的render函数的结构是一模一样的,所以说javascript AST抽象语法树就是对render函数的结构进行描述。

  • 第四部分为由于已经拿到了和 render 函数的结构一模一样的javascript AST抽象语法树,只需要在generate函数中遍历javascript AST抽象语法树进行字符串拼接就可以得到render函数了。

@vue/compiler-sfc包的compileTemplate函数

还是同样的套路,我们通过 debug 一个简单的 demo 来搞清楚compileTemplate函数是如何将 template 编译成 render 函数的。demo 代码如下:

<template>  <input v-for="item in msgList" :key="item.id" v-model="item.value" /></template><script setup lang="ts">import { ref } from "vue";const msgList = ref([  {    id: 1,    value: "",  },  {    id: 2,    value: "",  },  {    id: 3,    value: "",  },]);</script>

通过 debug 搞清楚. vue 文件怎么变成. js 文件 文章中我们已经知道了在使用 vite 的情况下 template 编译为 render 函数是在 node 端完成的。所以我们需要启动一个debug终端,才可以在 node 端打断点。这里以 vscode 举例,首先我们需要打开终端,然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。

compileTemplate函数在node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js文件中,找到compileTemplate函数打上断点,然后在debug终端中执行yarn dev(这里是以vite举例)。在浏览器中访问 http://localhost:5173/,此时断点就会走到`compileTemplate`函数中了。在我们这个场景中`compileTemplate`函数简化后的代码非常简单,代码如下:

function compileTemplate(options) {  return doCompileTemplate(options);}

@vue/compiler-sfc包的doCompileTemplate函数

我们接着将断点走进doCompileTemplate函数中,看看里面的代码是什么样的,简化后的代码如下:

import * as CompilerDOM from '@vue/compiler-dom'function doCompileTemplate({  source,  ast: inAST,  compiler}) {  const defaultCompiler = CompilerDOM;  compiler = compiler || defaultCompiler;  let { code, ast, preamble, map } = compiler.compile(inAST || source, {    // ...省略传入的options  });  return { code, ast, preamble, source, errors, tips, map };}

doCompileTemplate函数中代码同样也很简单,我们在 debug 终端中看看compilersourceinAST这三个变量的值是长什么样的。如下图:

从上图中我们可以看到此时的compiler变量的值为undefinedsource变量的值为 template 模块中的代码,inAST的值为由 template 模块编译而来的 AST 抽象语法树。不是说好的要经过parse函数处理后才会得到 AST 抽象语法树,为什么这里就已经有了 AST 抽象语法树?不要着急接着向下看,后面我会解释。

由于这里的compiler变量的值为undefined,所以compiler会被赋值为CompilerDOM。而CompilerDOM就是@vue/compiler-dom包中暴露的所有内容。执行compiler.compile函数,就是执行@vue/compiler-dom包中的compile函数。compile函数接收的第一个参数为inAST || source,从这里我们知道第一个参数既可能是 AST 抽象语法树,也有可能是 template 模块中的 html 代码字符串。compile函数的返回值对象中的code字段就是编译好的render函数,然后 return 出去。

@vue/compiler-dom包中的compile函数

我们接着将断点走进@vue/compiler-dom包中的compile函数,发现代码同样也很简单,简化后的代码如下:

import {  baseCompile,} from '@vue/compiler-core'function compile(src, options = {}) {  return baseCompile(    src,    Object.assign({}, parserOptions, options, {      nodeTransforms: [        ...DOMNodeTransforms,        ...options.nodeTransforms || []      ],      directiveTransforms: shared.extend(        {},        DOMDirectiveTransforms,        options.directiveTransforms || {}      )    })  );}

从上面的代码中可以看到这里的compile函数也不是具体实现的地方,在这里调用的是@vue/compiler-core包的baseCompile函数。看到这里你可能会有疑问,为什么不在上一步的doCompileTemplate函数中直接调用@vue/compiler-core包的baseCompile函数,而是要从@vue/compiler-dom包中绕一圈再来调用呢baseCompile函数呢?

答案是baseCompile函数是一个处于@vue/compiler-core包中的 API,而@vue/compiler-core可以运行在各种 JavaScript 环境下,比如浏览器端、服务端等各个平台。baseCompile函数接收这些平台专有的一些 options,而我们这里的 demo 是浏览器平台。所以才需要从@vue/compiler-dom包中绕一圈去调用@vue/compiler-core包中的baseCompile函数传入一些浏览器中特有的 options。在上面的代码中我们看到使用DOMNodeTransforms数组对options中的nodeTransforms属性进行了扩展,使用DOMDirectiveTransforms对象对options中的directiveTransforms属性进行了扩展。

我们先来看看DOMNodeTransforms数组:

const DOMNodeTransforms = [
  transformStyle
];

options对象中的nodeTransforms属性是一个数组,里面包含了许多transform转换函数用于处理 AST 抽象语法树。经过@vue/compiler-domcompile函数处理后nodeTransforms数组中多了一个处理 style 的transformStyle函数。这里的transformStyle是一个转换函数用于处理dom上面的 style,比如style="color: red"

我们再来看看DOMDirectiveTransforms对象:

const DOMDirectiveTransforms = {
  cloak: compilerCore.noopDirectiveTransform,
  html: transformVHtml,
  text: transformVText,
  model: transformModel,
  on: transformOn,
  show: transformShow
};

options对象中的directiveTransforms属性是一个对象,经过@vue/compiler-domcompile函数处理后directiveTransforms对象中增加了处理v-cloakv-htmlv-textv-modelv-onv-show等指令的transform转换函数。很明显我们这个 demo 中input标签上面的v-model指令就是由这里的transformModel转换函数处理。

你发现了没,不管是nodeTransforms数组还是directiveTransforms对象,增加的transform转换函数都是处理 dom 相关的。经过@vue/compiler-domcompile函数处理后,再调用baseCompile函数就有了处理 dom 相关的转换函数了。

@vue/compiler-core包的baseCompile函数

继续将断点走进vue/compiler-core包的baseCompile函数,简化后的baseCompile函数代码如下:

function baseCompile(  source: string | RootNode,  options: CompilerOptions = {},): CodegenResult {  const ast = isString(source) ? baseParse(source, options) : source  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()  transform(    ast,    Object.assign({}, options, {      nodeTransforms: [        ...nodeTransforms,        ...(options.nodeTransforms || []), // user transforms      ],      directiveTransforms: Object.assign(        {},        directiveTransforms,        options.directiveTransforms || {}, // user transforms      ),    }),  )  return generate(ast, options)}

我们先来看看baseCompile函数接收的参数,第一个参数为source,类型为string | RootNode。这句话的意思是接收的source变量可能是 html 字符串,也有可能是 html 字符串编译后的 AST 抽象语法树。再来看看第二个参数options,我们这里只关注options.nodeTransforms数组属性和options.directiveTransforms对象属性,这两个里面都是存了一堆转换函数,区别就是一个是数组,一个是对象。

我们再来看看返回值类型CodegenResult,定义如下:

interface CodegenResult {
  code: string
  preamble: string
  ast: RootNode
  map?: RawSourceMap
}

从类型中我们可以看到返回值对象中的code属性就是编译好的render函数,而这个返回值就是最后调用generate函数返回的。

明白了baseCompile函数接收的参数和返回值,我们再来看函数内的代码。主要分为四块内容:

  • 拿到由 html 字符串转换成的 AST 抽象语法树。

  • 拿到由一堆转换函数组成的nodeTransforms数组,和拿到由一堆转换函数组成的directiveTransforms对象。

  • 执行transform函数,使用合并后的nodeTransforms中的所有转换函数处理 AST 抽象语法树中的所有 node 节点,使用合并后的directiveTransforms中的转换函数对会生成 props 的指令进行处理,得到处理后的javascript AST抽象语法树

  • 调用generate函数根据上一步处理后的javascript AST抽象语法树进行字符串拼接,拼成render函数。

获取 AST 抽象语法树

我们先来看第一块的内容,代码如下:

const ast = isString(source) ? baseParse(source, options) : source

如果传入的source是 html 字符串,那就调用baseParse函数根据 html 字符串生成对应的 AST 抽象语法树,如果传入的就是 AST 抽象语法树那么就直接赋值给ast变量。为什么这里有这两种情况呢?

原因是baseCompile函数可以被直接调用,也可以像我们这样由 vite 的@vitejs/plugin-vue包发起,经过层层调用后最终执行baseCompile函数。在我们这个场景中,在前面我们就知道了走进compileTemplate函数之前就已经有了编译后的 AST 抽象语法树,所以这里不会再调用baseParse函数去生成 AST 抽象语法树了。那么又是什么时候生成的 AST 抽象语法树呢?

在之前的 通过 debug 搞清楚. vue 文件怎么变成. js 文件 文章中我们讲了调用createDescriptor函数会将vue代码字符串转换为descriptor对象,descriptor对象中拥有template属性、scriptSetup属性、styles属性,分别对应 vue 文件中的template模块、<script setup>模块、<style>模块。如下图:createDescriptor函数在生成template属性的时候底层同样也会调用@vue/compiler-core包的baseParse函数,将 template 模块中的 html 字符串编译为 AST 抽象语法树。

所以在我们这个场景中走到baseCompile函数时就已经有了 AST 抽象语法树了,其实底层都调用的是@vue/compiler-core包的baseParse函数。

获取转换函数

接着将断点走到第二块内容处,代码如下:

const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()

从上面的代码可以看到getBaseTransformPreset函数的返回值是一个数组,对返回的数组进行解构,数组的第一项赋值给nodeTransforms变量,数组的第二项赋值给directiveTransforms变量。

将断点走进getBaseTransformPreset函数,代码如下:

function getBaseTransformPreset() {  return [    [      transformOnce,      transformIf,      transformMemo,      transformFor,      transformFilter,      trackVForSlotScopes,      transformExpression      transformSlotOutlet,      transformElement,      trackSlotScopes,      transformText    ],    {      on: transformOn,      bind: transformBind,      model: transformModel    }  ];}

从上面的代码中不难看出由getBaseTransformPreset函数的返回值解构出来的nodeTransforms变量是一个数组,数组中包含一堆 transform 转换函数,比如处理v-oncev-ifv-memov-for等指令的转换函数。很明显我们这个 demo 中input标签上面的v-for指令就是由这里的transformFor转换函数处理。

同理由getBaseTransformPreset函数的返回值解构出来的directiveTransforms变量是一个对象,对象中包含处理v-onv-bindv-model指令的转换函数。

经过这一步的处理我们就拿到了由一系列转换函数组成的nodeTransforms数组,和由一系列转换函数组成的directiveTransforms对象。看到这里我想你可能有一些疑问,为什么nodeTransforms是数组,directiveTransforms却是对象呢?为什么有的指令转换转换函数是在nodeTransforms数组中,有的却是在directiveTransforms对象中呢?别着急,我们下面会讲。

transform函数

接着将断点走到第三块内容,transform函数处,代码如下:

transform(
  ast,
  Object.assign({}, options, {
    nodeTransforms: [
      ...nodeTransforms,
      ...(options.nodeTransforms || []), // user transforms
    ],
    directiveTransforms: Object.assign(
      {},
      directiveTransforms,
      options.directiveTransforms || {}, // user transforms
    ),
  }),
)

调用transform函数时传入了两个参数,第一个参数为当前的 AST 抽象语法树,第二个参数为传入的options,在options中我们主要看两个属性:nodeTransforms数组和directiveTransforms对象。

nodeTransforms数组由两部分组成,分别是上一步拿到的nodeTransforms数组,和之前在options.nodeTransforms数组中塞进去的转换函数。

directiveTransforms对象就不一样了,如果上一步拿到的directiveTransforms对象和options.directiveTransforms对象拥有相同的 key,那么后者就会覆盖前者。以我们这个例子举例:在上一步中拿到的directiveTransforms对象中有 key 为model的处理v-model指令的转换函数,但是我们在@vue/compiler-dom包中的compile函数同样也给options.directiveTransforms对象中塞了一个 key 为model的处理v-model指令的转换函数。那么@vue/compiler-dom包中的v-model转换函数就会覆盖上一步中定义的v-model转换函数,那么@vue/compiler-core包中v-model转换函数是不是就没用了呢?答案是当然有用,在@vue/compiler-dom包中的v-model转换函数会手动调用@vue/compiler-core包中v-model转换函数。这样设计的目的是对于一些指令的处理支持不同的平台传入不同的转换函数,并且在这些平台中也可以手动调用@vue/compiler-core包中提供的指令转换函数,根据手动调用的结果再针对各自平台进行一些特别的处理。

我们先来回忆一下前面 demo 中的代码:

<template>  <input v-for="item in msgList" :key="item.id" v-model="item.value" /></template><script setup lang="ts">import { ref } from "vue";const msgList = ref([  {    id: 1,    value: "",  },  {    id: 2,    value: "",  },  {    id: 3,    value: "",  },]);</script>

接着在 debug 终端中看看执行transform函数前的 AST 抽象语法树是什么样的,如下图:

从上图中可以看到 AST 抽象语法树根节点下面只有一个 children 节点,这个 children 节点对应的就是 input 标签。在 input 标签上面有三个 props,分别对应的是 input 标签上面的v-for指令、:key属性、v-model指令。说明在生成 AST 抽象语法树的阶段不会对指令进行处理,而是当做普通的属性一样使用正则匹配出来,然后塞到 props 数组中。

既然在生成 AST 抽象语法树的过程中没有对v-modelv-for等指令进行处理,那么又是在什么时候处理的呢?答案是在执行transform函数的时候处理的,在transform函数中会递归遍历整个 AST 抽象语法树,在遍历每个 node 节点时都会将nodeTransforms数组中的所有转换函数按照顺序取出来执行一遍,在执行时将当前的 node 节点和上下文作为参数传入。经过nodeTransforms数组中全部的转换函数处理后,vue 提供的许多内置指令、语法糖、内置组件等也就被处理了,接下来只需要执行generate函数生成render函数就行了。

nodeTransforms数组

nodeTransforms 主要是对 node 节点 进行操作,可能会替换或者移动节点。每个 node 节点都会将nodeTransforms数组中的转换函数按照顺序全部执行一遍,比如处理v-if指令的transformIf转换函数就要比处理v-for指令的transformFor函数先执行。所以nodeTransforms是一个数组,而且数组中的转换函数的顺序还是有讲究的。

在我们这个 demo 中 input 标签上面的v-for指令是由nodeTransforms数组中的transformFor转换函数处理的,很简单就可以找到transformFor转换函数。在函数开始的地方打一个断点,代码就会走到这个断点中,在 debug 终端上面看看此时的node节点是什么样的,如下图:

从上图中可以看到在执行transformFor转换函数之前的 node 节点和上一张图打印的 node 节点是一样的。

我们在执行完transformFor转换函数的地方打一个断点,看看执行完transformFor转换函数后 node 节点变成什么样了,如下图:

从上图我们可以看到经过transformFor转换函数处理后当前的 node 节点已经变成了一个新的 node 节点,而原来的 input 的 node 节点变成了这个节点的 children 子节点。新节点的source.content里存的是v-for="item in msgList"中的msgList变量。新节点的valueAlias.content里存的是v-for="item in msgList"中的item。我们发现 input 子节点的 props 数组现在只有两项了,原本的v-for指令的 props 经过transformFor转换函数的处理后已经被消费掉了,所以就只有两项了。

看到这里你可能会有疑问,为什么执行transform函数后会将 AST 抽象语法树的结构都改变了呢?

这样做的目的是在后续的generate函数中递归遍历 AST 抽象语法树时,只想进行字符串拼接就可以拼成 render 函数。这里涉及到模版AST抽象语法树Javascript AST抽象语法树的概念。

我们来回忆一下 template 模块中的代码:

<template><input v-for="item in msgList" :key="item.id" v-model="item.value" /></template>

template 模版经过parse函数拿到 AST 抽象语法树,此时的 AST 抽象语法树的结构和 template 模版的结构是一模一样的,所以我们称之为模版AST抽象语法树模版AST抽象语法树其实就是描述template模版的结构。如下图:

我们再来看看生成的render函数的代码:

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {  return _openBlock(true), _createElementBlock(    _Fragment,    null,    _renderList($setup.msgList, (item) => {      return _withDirectives((_openBlock(), _createElementBlock("input", {        key: item.id,        "onUpdate:modelValue": ($event) => item.value = $event      }, null, 8, _hoisted_1)), [        [_vModelText, item.value]      ]);    }),    128    /* KEYED_FRAGMENT */  );}

很明显模版AST抽象语法树无法通过简单的字符串拼接就可以拼成上面的render函数,所以我们需要一个结构和上面的 render 函数一模一样的Javascript AST抽象语法树Javascript AST抽象语法树的作用就是描述render函数的结构。如下图:

上面这个Javascript AST抽象语法树就是执行transform函数时根据模版AST抽象语法树生成的。有了Javascript AST抽象语法树后再来执行generate函数时就可以只进行简单的字符串拼接,就能得到render函数了。

directiveTransforms对象

directiveTransforms对象的作用是对指令进行转换,给node节点生成对应的props。比如给子组件上面使用了v-model指令,经过directiveTransforms对象中的transformModel转换函数处理后,v-mode节点上面就会多两个 props 属性:modelValueonUpdate:modelValue属性。directiveTransforms对象中的转换函数不会每次都全部执行,而是要 node 节点中有对应的指令,才会执行指令的转换函数。所以directiveTransforms是对象,而不是数组。

那为什么有的指令转换函数在directiveTransforms对象中,有的又在nodeTransforms数组中呢?

答案是在directiveTransforms对象中的指令全部都是会给 node 节点生成 props 属性的,那些不生成 props 属性的就在nodeTransforms数组中。

很容易就可以找到@vue/compiler-dom包的transformModel函数,然后打一个断点,让断点走进transformModel函数中,如下图:

从上面的图中我们可以看到在@vue/compiler-dom包的transformModel函数中会调用@vue/compiler-core包的transformModel函数,拿到返回的baseResult对象后再一些其他操作后直接return baseResult。从左边的 call stack 调用栈中我们可以看到transformModel函数是由一个buildProps函数调用的,看名字你应该猜到了buildProps函数的作用是生成 props 属性的。点击 Step Out 将断点跳出transformModel函数,走进buildProps函数中,可以看到buildProps函数中调用transformModel函数的代码如下图:

从上图中可以看到,name变量的值为modelcontext.directiveTransforms[name]的返回值就是transformModel函数,所以执行directiveTransform(prop, node, context)其实就是在执行transformModel函数。在 debug 终端中可以看到返回的props2是一个数组,里面存的是v-model指令被处理后生成的 props 属性。props 属性数组中只有一项是onUpdate:modelValue属性,看到这里有的小伙伴会疑惑了v-model指令不是会生成modelValueonUpdate:modelValue两个属性,为什么这里只有一个呢?答案是只有给自定义组件上面使用v-model指令才会生成modelValueonUpdate:modelValue两个属性,对于这种原生 input 标签是不需要生成modelValue属性的,因为 input 标签本身是不接收名为modelValue属性,接收的是 value 属性。

其实transform函数中的内容是非常复杂的,里面包含了 vue 提供的指令、filter、slot 等功能的处理逻辑。transform函数的设计高明之处就在于插件化,将处理这些功能的 transform 转换函数以插件的形式插入的,这样逻辑就会非常清晰了。比如我想看v-model指令是如何实现的,我只需要去看对应的transformModel转换函数就行了。又比如哪天 vue 需要实现一个v-xxx指令,要实现这个指令只需要增加一个transformXxx的转换函数就行了。

generate函数

经过上一步transform函数的处理后,已经将描述模版结构的模版AST抽象语法树转换为了描述render函数结构的Javascript AST抽象语法树。在前面我们已经讲过了Javascript AST抽象语法树就是描述了最终生成render函数的样子。所以在generate函数中只需要递归遍历Javascript AST抽象语法树,通过字符串拼接的方式就可以生成render函数了。

将断点走到执行generate函数前,看看这会儿的Javascript AST抽象语法树是什么样的,如下图:

从上面的图中可以看到Javascript AST模版AST的区别主要有两个:

  • node 节点中多了一个codegenNode属性,这个属性中存了许多 node 节点信息,比如codegenNode.props中就存了keyonUpdate:modelValue属性的信息。在generate函数中遍历每个 node 节点时就会读取这个codegenNode属性生成render函数

  • 模版AST中根节点下面的 children 节点就是 input 标签,但是在这里Javascript AST中却是根节点下面的 children 节点,再下面的 children 节点才是 input 标签。多了一层节点,在前面的transform函数中我们已经讲了多的这层节点是由v-for指令生成的,用于给v-for循环出来的多个节点当父节点。

将断点走到generate函数执行之后,可以看到已经生成render函数啦,如下图:

总结

现在我们再来看看最开始讲的流程图,我想你应该已经能将整个流程串起来了。如下图:

将 template 编译为 render 函数可以分为 7 步:

  • 执行@vue/compiler-sfc包的compileTemplate函数,里面会调用同一个包的doCompileTemplate函数。这一步存在的目的是作为一个入口函数给外部调用。

  • 执行@vue/compiler-sfc包的doCompileTemplate函数,里面会调用@vue/compiler-dom包中的compile函数。这一步存在的目的是入口函数的具体实现。

  • 执行@vue/compiler-dom包中的compile函数,里面会对options进行了扩展,塞了一些处理 dom 的转换函数进去。给options.nodeTransforms数组中塞了处理 style 的转换函数,和给options.directiveTransforms对象中塞了处理v-cloakv-htmlv-textv-modelv-onv-show等指令的转换函数。然后以扩展后的options去调用@vue/compiler-core包的baseCompile函数。

  • 执行@vue/compiler-core包的baseCompile函数,在这个函数中主要分为 4 部分。第一部分为检查传入的 source 是不是 html 字符串,如果是就调用同一个包下的baseParse函数生成模版AST抽象语法树。否则就直接使用传入的模版AST抽象语法树。此时 node 节点中还有v-forv-model等指令,并没有被处理掉。这里的模版AST抽象语法树的结构和 template 中的结构一模一样,模版AST抽象语法树是对 template 中的结构进行描述。

  • 第二部分为执行getBaseTransformPreset函数拿到@vue/compiler-core包中内置的nodeTransformsdirectiveTransforms转换函数。nodeTransforms数组中的为一堆处理 node 节点的转换函数,比如处理v-on指令的transformOnce转换函数、处理v-if指令的transformIf转换函数。directiveTransforms对象中存的是对一些 “会生成 props 的指令” 进行转换的函数,用于给node节点生成对应的props。比如处理v-model指令的transformModel转换函数。

  • 第三部分为将传入的options.nodeTransformsoptions.directiveTransforms分别和本地的nodeTransformsdirectiveTransforms进行合并得到一堆新的转换函数。其中由于nodeTransforms是数组,所以在合并的过程中会将options.nodeTransformsnodeTransforms中的转换函数全部合并进去。由于directiveTransforms是对象,如果directiveTransforms对象和options.directiveTransforms对象拥有相同的 key,那么后者就会覆盖前者。然后将合并的结果和模版AST抽象语法树一起传入到transform函数中执行,就可以得到转换后的javascript AST抽象语法树。在这一过程中v-forv-model等指令已经被转换函数给处理了。得到的javascript AST抽象语法树的结构和 render 函数的结构一模一样,javascript AST抽象语法树就是对render函数的结构进行描述。

  • 第四部分为由于已经拿到了和 render 函数的结构一模一样的javascript AST抽象语法树,只需要在generate函数中遍历javascript AST抽象语法树进行字符串拼接就可以得到render函数了。

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。