原稿:语雀 · 【原理三】模板编译 · 原目录:Vue › 【原理三】模板编译

Vue2.x 实例化到渲染

vue-template.svg

Vue2.x 模板编译

第一部分大图中 compileToFunction() 调用 compile(),定义位置即下方的 baseCompile()

image.png
var createCompiler = createCompilerCreator(function baseCompile (
    template,
    options
) {
  // 1.解析
  var ast = parse(template.trim(), options);
  if (options.optimize !== false) {
    // 2.优化
    optimize(ast, options);
  }
  // 3.代码生成
  var code = generate(ast, options);
  return {
    ast: ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
});
【原理三】模板编译 配图

说明 vue 模板编译过程分为3个阶段: 2. 解析:将模板字符串解析生成 AST,生成的 AST 元素节点总共有 3 种类型:普通元素、表达式、纯文本 4. 优化语法树:Vue 模板中并不是所有数据都是响应式的,有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 过程跳过比对,直接复用。优化阶段就是标记 AST Element 得静态根节点或静态节点 6. 生成代码:通过 generate 方法,将 ast 生成可执行的 render 函数


parse 过程

参考链接:

Vue 中对于模板的解析主要在 parse() 中进行,其中又会调用 parseHTML() ,并通过 options 传入四个钩子函数,在遇到不同标签时触发

parseHTML(template, {
    start (tag, attrs, unary) { // 每当解析到标签的开始位置时,触发该函数 },
    end () { // 每当解析到标签的结束位置时,触发该函数 },
    chars (text) { // 每当解析到文本时,触发该函数 },
    comment (text) { // 每当解析到注释时,触发该函数 }
})

循环解析

解析模板的过程就是循环的过程:用模板字符串来循环,每轮循环都从模板中截取一小段字符串,并触发相应的的钩子函数,重复直到模板被截成一个空字符串时结束循环,解析完毕

function parseHTML(html, options) {
  while (html) {
    // 截取模板字符串并触发钩子函数
  }
}

假设有以上的 HTML 模板,它在被 HTML 解析器解析的过程如下:

`<div>
  <p>{{name}}</p>
</div>`
1. 第一轮循环时,截取出一段字符串<div>,并且触发钩子函数start,截取后的结果为`  <p>{{name}}</p></div>`=======================================================================================2. 第二轮循环时,截取出一段字符串`  `并且触发钩子函数chars,截取后的结果为`<p>{{name}}</p></div>`=======================================================================================3. 第三轮循环时,截取出一段字符串<p>,并且触发钩子函数start,截取后的结果为`{{name}}</p></div>`=======================================================================================4. 第四轮循环时,截取出一段字符串{{name}},并且触发钩子函数chars,截取后的结果为:`</p></div>`=======================================================================================5. 第五轮循环时,截取出一段字符串</p>,并且触发钩子函数end,截取后的结果为:`</div>`=======================================================================================6. 第六轮循环时,截取出一段字符串:``并且触发钩子函数chars,截取后的结果为:`</div>`=======================================================================================

处理不同标签

在循环中需要判断具体的标签类型,做出不同的处理

export function parseHTML (html, options) {
    while (html) {
        if (!lastTag || !isPlainTextElement(lastTag)) {
            let textEnd = html.indexOf('<')
            if (textEnd === 0) {
                // 注释
                if (comment.test(html)) {}
                // 条件注释
                if (conditionalComment.test(html)) {}
                // DOCTYPE
                const doctypeMatch = html.match(doctype)
                if (doctypeMatch) {}
                // 结束标签
                const endTagMatch = html.match(endTag)
                if (endTagMatch) {}
                // 开始标签
                const startTagMatch = parseStartTag()
                if (startTagMatch) {}
            }

            let text, rest, next
            if (textEnd >= 0) {}
            if (textEnd < 0) {}
            if (options.chars && text) {}
        } else {
            // 父元素为script、style、textarea的处理逻辑
        }
    }
}
  • if (!lastTag || !isPlainTextElement(lastTag)):判断是否是纯文本(script、style、textarea)
  • if (comment.test(html)):判断是否是注释
  • if (conditionalComment.test(html)):判断是否是条件注释
  • if (doctypeMatch):判断是否是 DOCTYPE
  • if (endTagMatch):判断是否是结束标签,其中调用 parseEndTag(),触发 end 钩子
image.png
- if (startTagMatch):判断是否是开始标签,其中先调用 parseStartTag(),如果有返回值,那么会进行开始标签的处理,调用 handleEndTag(),触发 start 钩子
image.png
- 其余普通文本,解析后调用 char 钩子进行处理
image.png
- 不满足 if (!lastTag \|\| !isPlainTextElement(lastTag)):是纯文本,解析后也会进入 char 钩子

start 函数

start 钩子中 2. 初始化 AST 元素 4. 应用 preTransforms(在建立 ASTElement 基本信息后,进行节点的信息转换处理) 6. 处理动态指令

通过 start 钩子可以创建 ASTElement 结点

start: function start (tag, attrs, unary, start$1, end) {
  var element = createASTElement(tag, attrs, currentParent);

  // apply pre-transforms
  for (var i = 0; i < preTransforms.length; i++) {
    element = preTransforms[i](element, options) || element;
  }
  if (!inVPre) {
          processPre(element);
          if (element.pre) {
            inVPre = true;
          }
        }
  if (platformIsPreTag(element.tag)) {
    inPre = true;
  }
  if (inVPre) {
    processRawAttrs(element);
  } else if (!element.processed) {
    // structural directives
    processFor(element);
    processIf(element);
    processOnce(element);
  }
}

chars 函数

chars: function chars (text, start, end) {
  //...
  if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
    child = {
      type: 2,
      expression: res.expression,
      tokens: res.tokens,
      text: text
    };
  } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
    child = {
      type: 3,
      text: text
    };
  }
  //...
}

chars 钩子会调用 parseText(),后者会进行文本解析:使用正则判断文本是否带有变量,也就是检查文本中是否包含 {{xxx}} 语法

  • 如果是纯文本,则直接返回 undefined
  • 如果是带变量的文本,先把变量左边的文本添加到数组中,然后把变量改成 _s(x) 形式也添加到数组中。如果变量后面还有变量,则重复以上动作,直到所有变量都添加到数组中。当最后一个变量的后面还有文本,将它添加到数组中
function parseText (text) {
    const tagRE = /\{\{((?:.|\n)+?)\}\}/g
    if (!tagRE.test(text)) {
        return
    }

    const tokens = []
    let lastIndex = tagRE.lastIndex = 0
    let match, index
    while ((match = tagRE.exec(text))) {
        index = match.index
        // 先把 {{ 前边的文本添加到tokens中
        if (index > lastIndex) {
            tokens.push(JSON.stringify(text.slice(lastIndex, index)))
        }
        // 把变量改成`_s(x)`这样的形式也添加到数组中
        tokens.push(`_s(${match[1].trim()})`)
        // 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本
        lastIndex = index + match[0].length
    }
    // 当所有变量都处理完毕后,如果最后一个变量右边还有文本,就将文本添加到数组中
    if (lastIndex < text.length) {
        tokens.push(JSON.stringify(text.slice(lastIndex)))
    }
    return tokens.join('+')
}

parseText() 的效果如下

parseText('你好{{name}}, 你今年已经{{age}}岁啦')
// '"你好"+_s(name)+", 你今年已经"+_s(age)+"岁啦"'

回到 char 函数中:

  • 带变量的文本解析后会作为 AST 元素的 expression 属性,最后生成 ASTExpression 结点
  • 纯文本会对应一个纯文本的对象,最后生成 ASTText 结点

小结 Parse 过程

HTMLParser 在解析模版的过程中,将节点信息通过 start,end 和 chars 三个方法传递给 Vue.js 的 ast 构建程序,边解析边同时构建模版的 ast,最终生成一个完整的 AST 树

模版的 AST 节点有 3 种类型 ASTElement,ASTText 和 ASTExpression,对应 type 的1,2,3

  • ASTElement 基于 option.start 调用时的 tag 节点信息所创建。
  • ASTText 和 ASTExpression 两种节点是在 option.char 时创建,ASTText 即为纯文本节点,ASTExpression 包含了插值表达式