Vue模板编译原理

开发
写过 Vue 的同学肯定体验过, .vue 这种单文件组件有多么方便。但是我们也知道,Vue 底层是通过虚拟 DOM 来进行渲染的,那么 .vue 文件的模板到底是怎么转换成虚拟 DOM 的呢?这一块对我来说一直是个黑盒,之前也没有深入研究过,今天打算一探究竟。

 

Vue 3 发布在即,本来想着直接看看 Vue 3 的模板编译,但是我打开 Vue 3 源码的时候,发现我好像连 Vue 2 是怎么编译模板的都不知道。从小鲁迅就告诉我们,不能一口吃成一个胖子,那我只能回头看看 Vue 2 的模板编译源码,至于 Vue 3 就留到正式发布的时候再看。

Vue 的版本
很多人使用 Vue 的时候,都是直接通过 vue-cli 生成的模板代码,并不知道 Vue 其实提供了两个构建版本。

 

  1. vue.js:完整版本,包含了模板编译的能力; 

 

  1. vue.runtime.js:运行时版本,不提供模板编译能力,需要通过 vue-loader 进行提前编译。 

 

Vue不同构建版本

 

完整版与运行时版区别
简单来说,就是如果你用了 vue-loader ,就可以使用 vue.runtime.min.js,将模板编译的过程交过 vue-loader,如果你是在浏览器中直接通过 script 标签引入 Vue,需要使用 vue.min.js,运行的时候编译模板。

编译入口

 

  1. 了解了 Vue 的版本,我们看看 Vue 完整版的入口文件(src/platforms/web/entry-runtime-with-compiler.js)。 

 

  1. // 省略了部分代码,只保留了关键部分 
  2. import { compileToFunctions } from './compiler/index' 
  3.  
  4. const mount = Vue.prototype.$mount 
  5. Vue.prototype.$mount = function (el) { 
  6.   const options = this.$options 
  7.    
  8.   // 如果没有 render 方法,则进行 template 编译 
  9.   if (!options.render) { 
  10.     let template = options.template 
  11.     if (template) { 
  12.       // 调用 compileToFunctions,编译 template,得到 render 方法 
  13.       const { render, staticRenderFns } = compileToFunctions(template, { 
  14.         shouldDecodeNewlines, 
  15.         shouldDecodeNewlinesForHref, 
  16.         delimiters: options.delimiters, 
  17.         comments: options.comments 
  18.       }, this) 
  19.       // 这里的 render 方法就是生成生成虚拟 DOM 的方法 
  20.       options.render = render 
  21.     } 
  22.   } 
  23.   return mount.call(this, el, hydrating) 

 

  1. 再看看 ./compiler/index 文件的 compileToFunctions 方法从何而来。 

 

  1. import { baseOptions } from './options' 
  2. import { createCompiler } from 'compiler/index' 
  3.  
  4. // 通过 createCompiler 方法生成编译函数 
  5. const { compile, compileToFunctions } = createCompiler(baseOptions) 
  6. export { compile, compileToFunctions } 

后续的主要逻辑都在 compiler 模块中,这一块有些绕,因为本文不是做源码分析,就不贴整段源码了。简单看看这一段的逻辑是怎么样的。

 

  1. export function createCompiler(baseOptions) { 
  2.   const baseCompile = (template, options) => { 
  3.     // 解析 html,转化为 ast 
  4.     const ast = parse(template.trim(), options) 
  5.     // 优化 ast,标记静态节点 
  6.     optimize(ast, options) 
  7.     // 将 ast 转化为可执行代码 
  8.     const code = generate(ast, options) 
  9.     return { 
  10.       ast, 
  11.       render: code.render, 
  12.       staticRenderFns: code.staticRenderFns 
  13.     } 
  14.   } 
  15.   const compile = (template, options) => { 
  16.     const tips = [] 
  17.     const errors = [] 
  18.     // 收集编译过程中的错误信息 
  19.     options.warn = (msg, tip) => { 
  20.       (tip ? tips : errors).push(msg) 
  21.     } 
  22.     // 编译 
  23.     const compiled = baseCompile(template, options) 
  24.     compiled.errors = errors 
  25.     compiled.tips = tips 
  26.  
  27.     return compiled 
  28.   } 
  29.   const createCompileToFunctionFn = () => { 
  30.     // 编译缓存 
  31.     const cache = Object.create(null
  32.     return (template, options, vm) => { 
  33.       // 已编译模板直接走缓存 
  34.       if (cache[template]) { 
  35.         return cache[template] 
  36.       } 
  37.       const compiled = compile(template, options) 
  38.      return (cache[key] = compiled) 
  39.     } 
  40.   } 
  41.   return { 
  42.     compile, 
  43.     compileToFunctions: createCompileToFunctionFn(compile) 
  44.   } 

主流程
可以看到主要的编译逻辑基本都在 baseCompile 方法内,主要分为三个步骤:

模板编译,将模板代码转化为 AST;
优化 AST,方便后续虚拟 DOM 更新;
生成代码,将 AST 转化为可执行的代码;

 

  1. const baseCompile = (template, options) => { 
  2.   // 解析 html,转化为 ast 
  3.   const ast = parse(template.trim(), options) 
  4.   // 优化 ast,标记静态节点 
  5.   optimize(ast, options) 
  6.   // 将 ast 转化为可执行代码 
  7.   const code = generate(ast, options) 
  8.   return { 
  9.     ast, 
  10.     render: code.render, 
  11.     staticRenderFns: code.staticRenderFns 
  12.   } 

parse
AST
首先看到 parse 方法,该方法的主要作用就是解析 HTML,并转化为 AST(抽象语法树),接触过 ESLint、Babel 的同学肯定对 AST 不陌生,我们可以先看看经过 parse 之后的 AST 长什么样。

下面是一段普普通通的 Vue 模板:

 

  1. new Vue({ 
  2.   el: '#app'
  3.   template: ` 
  4.     <div> 
  5.       <h2 v-if="message">{{message}}</h2> 
  6.       <button @click="showName">showName</button> 
  7.     </div> 
  8.   `, 
  9.   data: { 
  10.     name'shenfq'
  11.     message: 'Hello Vue!' 
  12.   }, 
  13.   methods: { 
  14.     showName() { 
  15.       alert(this.name
  16.     } 
  17.   } 
  18. }) 

经过 parse 之后的 AST:

 

Template AST
AST 为一个树形结构的对象,每一层表示一个节点,第一层就是 div(tag: "div")。div 的子节点都在 children 属性中,分别是 h2 标签、空行、button 标签。我们还可以注意到有一个用来标记节点类型的属性:type,这里 div 的 type 为 1,表示是一个元素节点,type 一共有三种类型:

元素节点;
表达式;
文本;
在 h2 和 button 标签之间的空行就是 type 为 3 的文本节点,而 h2 标签下就是一个表达式节点。

 

解析HTML
parse 的整体逻辑较为复杂,我们可以先简化一下代码,看看 parse 的流程。

 

  1. import { parseHTML } from './html-parser' 
  2.  
  3. export function parse(template, options) { 
  4.   let root 
  5.   parseHTML(template, { 
  6.     // some options... 
  7.     start() {}, // 解析到标签位置开始的回调 
  8.     end() {}, // 解析到标签位置结束的回调 
  9.     chars() {}, // 解析到文本时的回调 
  10.     comment() {} // 解析到注释时的回调 
  11.   }) 
  12.   return root 

可以看到 parse 主要通过 parseHTML 进行工作,这个 parseHTML 本身来自于开源库:simple html parser,只不过经过了 Vue 团队的一些修改,修复了相关 issue。

 

HTML parser
下面我们一起来理一理 parseHTML 的逻辑。

 

  1. export function parseHTML(html, options) { 
  2.   let index = 0 
  3.   let last,lastTag 
  4.   const stack = [] 
  5.   while(html) { 
  6.     last = html 
  7.     let textEnd = html.indexOf('<'
  8.  
  9.     // "<" 字符在当前 html 字符串开始位置 
  10.     if (textEnd === 0) { 
  11.       // 1、匹配到注释: <!-- --> 
  12.       if (/^<!\--/.test(html)) { 
  13.         const commentEnd = html.indexOf('-->'
  14.         if (commentEnd >= 0) { 
  15.           // 调用 options.comment 回调,传入注释内容 
  16.           options.comment(html.substring(4, commentEnd)) 
  17.           // 裁切掉注释部分 
  18.           advance(commentEnd + 3) 
  19.           continue 
  20.         } 
  21.       } 
  22.  
  23.       // 2、匹配到条件注释: <![if !IE]>  <![endif]> 
  24.       if (/^<!\[/.test(html)) { 
  25.         // ... 逻辑与匹配到注释类似 
  26.       } 
  27.  
  28.       // 3、匹配到 Doctype: <!DOCTYPE html> 
  29.       const doctypeMatch = html.match(/^<!DOCTYPE [^>]+>/i) 
  30.       if (doctypeMatch) { 
  31.         // ... 逻辑与匹配到注释类似 
  32.       } 
  33.  
  34.       // 4、匹配到结束标签: </div> 
  35.       const endTagMatch = html.match(endTag) 
  36.       if (endTagMatch) {} 
  37.  
  38.       // 5、匹配到开始标签: <div> 
  39.       const startTagMatch = parseStartTag() 
  40.       if (startTagMatch) {} 
  41.     } 
  42.     // "<" 字符在当前 html 字符串中间位置 
  43.     let text, rest, next 
  44.     if (textEnd > 0) { 
  45.       // 提取中间字符 
  46.       rest = html.slice(textEnd) 
  47.       // 这一部分当成文本处理 
  48.       text = html.substring(0, textEnd) 
  49.       advance(textEnd) 
  50.     } 
  51.     // "<" 字符在当前 html 字符串中不存在 
  52.     if (textEnd < 0) { 
  53.       text = html 
  54.       html = '' 
  55.     } 
  56.      
  57.     // 如果存在 text 文本 
  58.     // 调用 options.chars 回调,传入 text 文本 
  59.     if (options.chars && text) { 
  60.       // 字符相关回调 
  61.       options.chars(text) 
  62.     } 
  63.   } 
  64.   // 向前推进,裁切 html 
  65.   function advance(n) { 
  66.     index += n 
  67.     html = html.substring(n) 
  68.   } 

上述代码为简化后的 parseHTML,while 循环中每次截取一段 html 文本,然后通过正则判断文本的类型进行处理,这就类似于编译原理中常用的有限状态机。每次拿到 "<" 字符前后的文本,"<" 字符前的就当做文本处理,"<" 字符后的通过正则判断,可推算出有限的几种状态。

 

其他的逻辑处理都不复杂,主要是开始标签与结束标签,我们先看看关于开始标签与结束标签相关的正则。

 

  1. const ncname = '[a-zA-Z_][\\w\\-\\.]*' 
  2. const qnameCapture = `((?:${ncname}\\:)?${ncname})` 
  3. const startTagOpen = new RegExp(`^<${qnameCapture}`) 

这段正则看起来很长,但是理清之后也不是很难。这里推荐一个正则可视化工具。我们到工具上看看startTagOpen:

 

startTagOpen
这里比较疑惑的点就是为什么 tagName 会存在 :,这个是 XML 的 命名空间,现在已经很少使用了,我们可以直接忽略,所以我们简化一下这个正则:

 

  1. const ncname = '[a-zA-Z_][\\w\\-\\.]*' 
  2. const startTagOpen = new RegExp(`^<${ncname}`) 
  3. const startTagClose = /^\s*(\/?)>/ 
  4. const endTag = new RegExp(`^<\\/${ncname}[^>]*>`) 

 

startTagOpen

 

endTag
除了上面关于标签开始和结束的正则,还有一段用来提取标签属性的正则,真的是又臭又长。

 

  1. const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ 

把正则放到工具上就一目了然了,以 = 为分界,前面为属性的名字,后面为属性的值。

 

attribute
理清正则后可以更加方便我们看后面的代码。

 

  1. while(html) { 
  2.   last = html 
  3.   let textEnd = html.indexOf('<'
  4.  
  5.   // "<" 字符在当前 html 字符串开始位置 
  6.   if (textEnd === 0) { 
  7.     // some code ... 
  8.  
  9.     // 4、匹配到标签结束位置: </div> 
  10.     const endTagMatch = html.match(endTag) 
  11.     if (endTagMatch) { 
  12.       const curIndex = index 
  13.       advance(endTagMatch[0].length) 
  14.       parseEndTag(endTagMatch[1], curIndex, index
  15.       continue 
  16.     } 
  17.  
  18.     // 5、匹配到标签开始位置: <div> 
  19.     const startTagMatch = parseStartTag() 
  20.     if (startTagMatch) { 
  21.       handleStartTag(startTagMatch) 
  22.       continue 
  23.     } 
  24.   } 
  25. // 向前推进,裁切 html 
  26. function advance(n) { 
  27.   index += n 
  28.   html = html.substring(n) 
  29.  
  30. // 判断是否标签开始位置,如果是,则提取标签名以及相关属性 
  31. function parseStartTag () { 
  32.   // 提取 <xxx 
  33.   const start = html.match(startTagOpen) 
  34.   if (start) { 
  35.     const [fullStr, tag] = start 
  36.     const match = { 
  37.       attrs: [], 
  38.       start: index
  39.       tagName: tag, 
  40.     } 
  41.     advance(fullStr.length) 
  42.     let end, attr 
  43.     // 递归提取属性,直到出现 ">" 或 "/>" 字符 
  44.     while ( 
  45.       !(end = html.match(startTagClose)) && 
  46.       (attr = html.match(attribute)) 
  47.     ) { 
  48.       advance(attr[0].length) 
  49.       match.attrs.push(attr) 
  50.     } 
  51.     if (end) { 
  52.       // 如果是 "/>" 表示单标签 
  53.       match.unarySlash = end[1] 
  54.       advance(end[0].length) 
  55.       match.end = index 
  56.       return match 
  57.     } 
  58.   } 
  59.  
  60. // 处理开始标签 
  61. function handleStartTag (match) { 
  62.   const tagName = match.tagName 
  63.   const unary = match.unarySlash 
  64.   const len = match.attrs.length 
  65.   const attrs = new Array(len) 
  66.   for (let i = 0; i < l; i++) { 
  67.     const args = match.attrs[i] 
  68.     // 这里的 3、4、5 分别对应三种不同复制属性的方式 
  69.     // 3: attr="xxx" 双引号 
  70.     // 4: attr='xxx' 单引号 
  71.     // 5: attr=xxx   省略引号 
  72.     const value = args[3] || args[4] || args[5] || '' 
  73.     attrs[i] = { 
  74.       name: args[1], 
  75.       value 
  76.     } 
  77.   } 
  78.  
  79.   if (!unary) { 
  80.     // 非单标签,入栈 
  81.     stack.push({ 
  82.       tag: tagName, 
  83.       lowerCasedTag: 
  84.       tagName.toLowerCase(), 
  85.       attrs: attrs 
  86.     }) 
  87.     lastTag = tagName 
  88.   } 
  89.  
  90.   if (options.start) { 
  91.     // 开始标签的回调 
  92.     options.start(tagName, attrs, unary, match.start, match.end
  93.   } 
  94.  
  95. // 处理闭合标签 
  96. function parseEndTag (tagName, start, end) { 
  97.   let pos, lowerCasedTagName 
  98.   if (start == null) start = index 
  99.   if (end == nullend = index 
  100.  
  101.   if (tagName) { 
  102.     lowerCasedTagName = tagName.toLowerCase() 
  103.   } 
  104.  
  105.   // 在栈内查找相同类型的未闭合标签 
  106.   if (tagName) { 
  107.     for (pos = stack.length - 1; pos >= 0; pos--) { 
  108.       if (stack[pos].lowerCasedTag === lowerCasedTagName) { 
  109.         break 
  110.       } 
  111.     } 
  112.   } else { 
  113.     pos = 0 
  114.   } 
  115.  
  116.   if (pos >= 0) { 
  117.     // 关闭该标签内的未闭合标签,更新堆栈 
  118.     for (let i = stack.length - 1; i >= pos; i--) { 
  119.       if (options.end) { 
  120.         // end 回调 
  121.         options.end(stack[i].tag, start, end
  122.       } 
  123.     } 
  124.  
  125.     // 堆栈中删除已关闭标签 
  126.     stack.length = pos 
  127.     lastTag = pos && stack[pos - 1].tag 
  128.   } 

在解析开始标签的时候,如果该标签不是单标签,会将该标签放入到一个堆栈当中,每次闭合标签的时候,会从栈顶向下查找同名标签,直到找到同名标签,这个操作会闭合同名标签上面的所有标签。接下来我们举个例子:

 

  1. <div> 
  2.   <h2>test</h2> 
  3.   <p> 
  4.   <p> 
  5. </div> 

在解析了 div 和 h2 的开始标签后,栈内就存在了两个元素。h2 闭合后,就会将 h2 出栈。然后会解析两个未闭合的 p 标签,此时,栈内存在三个元素(div、p、p)。如果这个时候,解析了 div 的闭合标签,除了将 div 闭合外,div 内两个未闭合的 p 标签也会跟随闭合,此时栈被清空。

为了便于理解,特地录制了一个动图,如下:

 

入栈与出栈
理清了 parseHTML 的逻辑后,我们回到调用 parseHTML 的位置,调用该方法的时候,一共会传入四个回调,分别对应标签的开始和结束、文本、注释。

 

  1. parseHTML(template, { 
  2.   // some options... 
  3.  
  4.   // 解析到标签位置开始的回调 
  5.   start(tag, attrs, unary) {}, 
  6.   // 解析到标签位置结束的回调 
  7.   end(tag) {}, 
  8.   // 解析到文本时的回调 
  9.   chars(text: string) {}, 
  10.   // 解析到注释时的回调 
  11.   comment(text: string) {} 
  12. }) 

处理开始标签
首先看解析到开始标签时,会生成一个 AST 节点,然后处理标签上的属性,最后将 AST 节点放入树形结构中。

 

  1. function makeAttrsMap(attrs) { 
  2.   const map = {} 
  3.   for (let i = 0, l = attrs.length; i < l; i++) { 
  4.     const { name, value } = attrs[i] 
  5.     map[name] = value 
  6.   } 
  7.   return map 
  8. function createASTElement(tag, attrs, parent) { 
  9.   const attrsList = attrs 
  10.   const attrsMap = makeAttrsMap(attrsList) 
  11.   return { 
  12.     type: 1,       // 节点类型 
  13.     tag,           // 节点名称 
  14.     attrsMap,      // 节点属性映射 
  15.     attrsList,     // 节点属性数组 
  16.     parent,        // 父节点 
  17.     children: [],  // 子节点 
  18.   } 
  19.  
  20. const stack = [] 
  21. let root // 根节点 
  22. let currentParent // 暂存当前的父节点 
  23. parseHTML(template, { 
  24.   // some options... 
  25.  
  26.   // 解析到标签位置开始的回调 
  27.   start(tag, attrs, unary) { 
  28.     // 创建 AST 节点 
  29.     let element = createASTElement(tag, attrs, currentParent) 
  30.  
  31.     // 处理指令: v-for v-if v-once 
  32.     processFor(element) 
  33.     processIf(element) 
  34.     processOnce(element) 
  35.     processElement(element, options) 
  36.  
  37.     // 处理 AST 树 
  38.     // 根节点不存在,则设置该元素为根节点 
  39.     if (!root) { 
  40.       root = element 
  41.       checkRootConstraints(root) 
  42.     } 
  43.     // 存在父节点 
  44.     if (currentParent) { 
  45.       // 将该元素推入父节点的子节点中 
  46.       currentParent.children.push(element) 
  47.       element.parent = currentParent 
  48.     } 
  49.     if (!unary) { 
  50.      // 非单标签需要入栈,且切换当前父元素的位置 
  51.       currentParent = element 
  52.       stack.push(element) 
  53.     } 
  54.   } 
  55. }) 

处理结束标签
标签结束的逻辑就比较简单了,只需要去除栈内最后一个未闭合标签,进行闭合即可。

 

  1. parseHTML(template, { 
  2.   // some options... 
  3.  
  4.   // 解析到标签位置结束的回调 
  5.   end() { 
  6.     const element = stack[stack.length - 1] 
  7.     const lastNode = element.children[element.children.length - 1] 
  8.     // 处理尾部空格的情况 
  9.     if (lastNode && lastNode.type === 3 && lastNode.text === ' ') { 
  10.       element.children.pop() 
  11.     } 
  12.     // 出栈,重置当前的父节点 
  13.     stack.length -= 1 
  14.     currentParent = stack[stack.length - 1] 
  15.   } 
  16. }) 

处理文本
处理完标签后,还需要对标签内的文本进行处理。文本的处理分两种情况,一种是带表达式的文本,还一种就是纯静态的文本。

 

  1. parseHTML(template, { 
  2.   // some options... 
  3.  
  4.   // 解析到文本时的回调 
  5.   chars(text) { 
  6.     if (!currentParent) { 
  7.       // 文本节点外如果没有父节点则不处理 
  8.       return 
  9.     } 
  10.      
  11.     const children = currentParent.children 
  12.     text = text.trim() 
  13.     if (text) { 
  14.       // parseText 用来解析表达式 
  15.       // delimiters 表示表达式标识符,默认为 ['{{''}}'
  16.       const res = parseText(text, delimiters)) 
  17.       if (res) { 
  18.         // 表达式 
  19.         children.push({ 
  20.           type: 2, 
  21.           expression: res.expression, 
  22.           tokens: res.tokens, 
  23.           text 
  24.         }) 
  25.       } else { 
  26.         // 静态文本 
  27.         children.push({ 
  28.           type: 3, 
  29.           text 
  30.         }) 
  31.       } 
  32.     } 
  33.   } 
  34. }) 

下面我们看看 parseText 如何解析表达式。

 

  1. // 构造匹配表达式的正则 
  2. const buildRegex = delimiters => { 
  3.   const open = delimiters[0] 
  4.   const close = delimiters[1] 
  5.   return new RegExp(open + '((?:.|\\n)+?)' + close'g'
  6.  
  7. function parseText (text, delimiters){ 
  8.   // delimiters 默认为 {{ }} 
  9.   const tagRE = buildRegex(delimiters || ['{{''}}']) 
  10.   // 未匹配到表达式,直接返回 
  11.   if (!tagRE.test(text)) { 
  12.     return 
  13.   } 
  14.   const tokens = [] 
  15.   const rawTokens = [] 
  16.   let lastIndex = tagRE.lastIndex = 0 
  17.   let match, index, tokenValue 
  18.   while ((match = tagRE.exec(text))) { 
  19.     // 表达式开始的位置 
  20.     index = match.index 
  21.     // 提取表达式开始位置前面的静态字符,放入 token 中 
  22.     if (index > lastIndex) { 
  23.       rawTokens.push(tokenValue = text.slice(lastIndex, index)) 
  24.       tokens.push(JSON.stringify(tokenValue)) 
  25.     } 
  26.     // 提取表达式内部的内容,使用 _s() 方法包裹 
  27.     const exp = match[1].trim() 
  28.     tokens.push(`_s(${exp})`) 
  29.     rawTokens.push({ '@binding': exp }) 
  30.     lastIndex = index + match[0].length 
  31.   } 
  32.   // 表达式后面还有其他静态字符,放入 token 中 
  33.   if (lastIndex < text.length) { 
  34.     rawTokens.push(tokenValue = text.slice(lastIndex)) 
  35.     tokens.push(JSON.stringify(tokenValue)) 
  36.   } 
  37.   return { 
  38.     expression: tokens.join('+'), 
  39.     tokens: rawTokens 
  40.   } 

首先通过一段正则来提取表达式:

 

提取表达式
看代码可能有点难,我们直接看例子,这里有一个包含表达式的文本。

 

  1. <div>是否登录:{{isLogin ? '是' : '否'}}</div> 

 

运行结果

 

解析文本
optimize
通过上述一些列处理,我们就得到了 Vue 模板的 AST。由于 Vue 是响应式设计,所以拿到 AST 之后还需要进行一系列优化,确保静态的数据不会进入虚拟 DOM 的更新阶段,以此来优化性能。

 

  1. export function optimize (root, options) { 
  2.   if (!root) return 
  3.   // 标记静态节点 
  4.   markStatic(root) 

简单来说,就是把所以静态节点的 static 属性设置为 true。

 

  1. function isStatic (node) { 
  2.   if (node.type === 2) { // 表达式,返回 false 
  3.     return false 
  4.   } 
  5.   if (node.type === 3) { // 静态文本,返回 true 
  6.     return true 
  7.   } 
  8.   // 此处省略了部分条件 
  9.   return !!( 
  10.     !node.hasBindings && // 没有动态绑定 
  11.     !node.if && !node.for && // 没有 v-if/v-for 
  12.     !isBuiltInTag(node.tag) && // 不是内置组件 slot/component 
  13.     !isDirectChildOfTemplateFor(node) && // 不在 template for 循环内 
  14.     Object.keys(node).every(isStaticKey) // 非静态节点 
  15.   ) 
  16.  
  17. function markStatic (node) { 
  18.   node.static = isStatic(node) 
  19.   if (node.type === 1) { 
  20.     // 如果是元素节点,需要遍历所有子节点 
  21.     for (let i = 0, l = node.children.length; i < l; i++) { 
  22.       const child = node.children[i] 
  23.       markStatic(child) 
  24.       if (!child.static) { 
  25.         // 如果有一个子节点不是静态节点,则该节点也必须是动态的 
  26.         node.static = false 
  27.       } 
  28.     } 
  29.   } 

generate
得到优化的 AST 之后,就需要将 AST 转化为 render 方法。还是用之前的模板,先看看生成的代码长什么样:

 

  1. <div> 
  2.   <h2 v-if="message">{{message}}</h2> 
  3.   <button @click="showName">showName</button> 
  4. </div> 

 

  1.   render: "with(this){return _c('div',[(message)?_c('h2',[_v(_s(message))]):_e(),_v(" "),_c('button',{on:{"click":showName}},[_v("showName")])])}" 

将生成的代码展开:

 

  1. with (this) { 
  2.     return _c( 
  3.       'div'
  4.       [ 
  5.         (message) ? _c('h2', [_v(_s(message))]) : _e(), 
  6.         _v(' '), 
  7.         _c('button', { on: { click: showName } }, [_v('showName')]) 
  8.       ]) 
  9.     ; 

看到这里一堆的下划线肯定很懵逼,这里的 _c 对应的是虚拟 DOM 中的 createElement 方法。其他的下划线方法在 core/instance/render-helpers 中都有定义,每个方法具体做了什么不做展开。

 

render-helpers`
具体转化方法就是一些简单的字符拼接,下面是简化了逻辑的部分,不做过多讲述。

 

  1. export function generate(ast, options) { 
  2.   const state = new CodegenState(options) 
  3.   const code = ast ? genElement(ast, state) : '_c("div")' 
  4.   return { 
  5.     render: `with(this){return ${code}}`, 
  6.     staticRenderFns: state.staticRenderFns 
  7.   } 
  8.  
  9. export function genElement (el, state) { 
  10.   let code 
  11.   const data = genData(el, state) 
  12.   const children = genChildren(el, state, true
  13.   code = `_c('${el.tag}'${ 
  14.     data ? `,${data}` : '' // data 
  15.   }${ 
  16.     children ? `,${children}` : '' // children 
  17.   })` 
  18.   return code 

总结
理清了 Vue 模板编译的整个过程,重点都放在了解析 HTML 生成 AST 的部分。本文只是大致讲述了主要流程,其中省略了特别多的细节,比如:对 template/slot 的处理、指令的处理等等,希望大家在阅读这篇文章后有所收获。

 

责任编辑:姜华 来源: 更了不起的前端
相关推荐

2020-11-12 08:32:14

Vue3模板优化

2016-09-29 09:57:08

JavascriptWeb前端模板

2017-07-26 14:50:37

前端模板

2022-12-30 20:41:15

编译原理case

2023-07-12 13:25:17

Vue 2模版编译

2020-09-07 11:14:02

Vue异步更新

2017-07-25 14:07:14

前端Vue模板渲染

2021-02-02 13:45:31

Vue代码前端

2019-11-15 15:20:27

Golang编译器前端

2024-02-02 08:33:00

Vue模板性能

2021-05-08 07:37:32

Vue 命名插槽

2021-08-16 07:11:56

Go语言进程

2017-04-11 08:36:09

iOS编译应用

2023-05-08 08:05:42

内核模块Linux

2021-01-22 11:47:27

Vue.js响应式代码

2019-07-01 13:34:22

vue系统数据

2020-06-09 11:35:30

Vue 3响应式前端

2013-10-09 14:14:58

C++编译

2022-08-31 06:37:34

Vue 3模板

2021-04-27 07:39:40

Vue后台管理
点赞
收藏

51CTO技术栈公众号