浅谈vue模板编译原理

255 阅读5分钟

在日常的开发中,我们会把template之间的类似于HTML的内容成为模板。我们在模板中会写html代码、以及一些v-for、v-if等指令等。那么在vue中是怎么处理这些非html结构的代码的呢?最终渲染页面的这个过程又是怎样的呢?这里先用一段话总结一下。

Vue会识别template中的内容,划分为原生html部分进行编译以及非原生html部分,对于非原生html部分,则会通过一系列逻辑处理成render函数,render函数则会根据模板内容生成对应的Vnode。Vnode通过patch过程得到要渲染到页面的Vnode,最后根据Vnode创建真实的Dom节点并且插入到视图中。从而实现页面的渲染。

这个过程也就是我们所讲的Vue模板编译。如下图:

image.png

接下来,我们把问题拆分一下一步一步去理解模板编译的过程。

我们可以提出一个问题:

模板编译内部是如何处理模板形成render函数的呢?

要弄明白这个首先得明白一个东西 ——抽象语法树(AST),通过js对象来描述模板的属性、文本等。具体长什么样子可以通过这个去实现一下:在线解析AST

这里用一段话大致总结一下这个过程:

生成render函数一共有三个阶段,第一个阶段是模板解析阶段,将模板字符串用正则的方式解析成抽象语法树(AST)。第二个阶段是优化阶段,遍历AST,对于静态节点打上标记。第三个阶段是代码生成阶段,将AST转化为render函数。过程源码如下:

// 源码位置: /src/complier/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 模板解析阶段:用正则等方式解析 template 模板中的指令、class、style等数据,形成AST
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 优化阶段:遍历AST,找出其中的静态节点,并打上标记;
    optimize(ast, options)
  }
  // 代码生成阶段:将AST转换成渲染函数;
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})



图例:

image.png

现在已经知道了大概的过程,开始分步理解三个阶段分别做了哪些事情。

1、解析阶段

主要的源码:

// 代码位置:/src/complier/parser/index.js
/**
 * Convert HTML string to AST.
 * 将HTML模板字符串转化为AST
 */
export function parse(template, options) {
   // ...
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    // 当解析到开始标签时,调用该函数
    start (tag, attrs, unary) {
    },
    // 当解析到结束标签时,调用该函数
    end () {
    },
    // 当解析到文本时,调用该函数
    chars (text) {
    },
    // 当解析到注释时,调用该函数
    comment (text) {
    }
  })
  return root
}

可知,传入两个参数,template是要被解析的HTML模板字符串,option是转换时所需的选项。 除此之外,还定义了一些钩子函数。当解析到不同的节点时候,调用不同的钩子函数。

钩子函数具体的实现细节可以查阅源码。原理都是通过正则匹配创建不同的AST结构类型。

为了防止重复解析,解析器中提供一个游标控制器,伪代码如下:

function advance (n) {
  index += n   // index为解析游标
  html = html.substring(n)
}

但是这里有一个问题,我们都知道html中的标签往往是成对存在的,标签与标签之间存在层级的关系,那如果按照这种解析的方式,如何能保证解析出正确的ast树呢?其实,在vue中解析器的最开始会初始化一个stack(栈)用来维护这个节点层级。当遇到标签的开始

的时候,会调用start方法,这个时候会将当前标签推入到stack中。当遇到结束标签
的时候会调用end方法,如果有匹配到则会弹出当前节点进行解析成ast。这样就保证了AST的层级正确性。

2、优化阶段

上面初步了解了模板编译中的解析阶段所做的事情。进入到第二部分优化阶段。这一阶段主要是vue优化性能的。当模板中存在静态节点(首次渲染后不会再改变的节点)的时候,在这个阶段会给他们打上标记,在diff阶段、patch的时候则会跳过对这些节点的判断从达到性能优化的目的。

具体来说,在优化阶段中主要做了两件事。

第一件:遍历AST,对所有的静态节点打上标记。

第二件:遍历AST,对所有静态根节点打上标记。

代码如下:

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // 标记静态节点
  markStatic(root)
  // 标记静态根节点
  markStaticRoots(root, false)
}

具体过程这里不细讲,主要就是通过递归的方式去实现标记。

3、代码生成阶段

在Vue中,vue实例在挂载的时候会调用自身的render函数,来生成模板对应的Vnode。当我们在书写组件的时候,如果在代码中制指定了render选项,vue实例挂载的时候会调用自定义的render函数去进行生成Vnode。如果没有指定render选项,vue会自动根据template生成一个render函数供程序调用。

render函数的书写在后面会单独出一篇文章,这里不再扩展。

这里简单了解一下vue是如何自动根据template生成render函数的。

源码:

export function generate (ast,option) {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

我们可以知道Vnode有很多不同的类型,比如文本节点,注释节点,元素节点等,通过递归生成好的AST语法树,当遇到是文本类型就调用生成文本节点的方式创建文本类型的Vnode,当遇到元素类型则调用生成元素类型的Vnode的方式生成元素类型的Vnode。以此类推。最终生成一份完整的Vnode。这里只讲述了大概的过程,生成虚拟dom我后面会写一篇文章来讲述虚拟Dom以及diff算法。