精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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模板優化

2017-07-26 14:50:37

前端模板

2016-09-29 09:57:08

JavascriptWeb前端模板

2022-12-30 20:41:15

編譯原理case

2020-09-07 11:14:02

Vue異步更新

2023-07-12 13:25:17

Vue 2模版編譯

2021-02-02 13:45:31

Vue代碼前端

2017-07-25 14:07:14

前端Vue模板渲染

2024-02-02 08:33:00

Vue模板性能

2019-11-15 15:20:27

Golang編譯器前端

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響應式代碼

2021-05-08 07:37:32

Vue 命名插槽

2020-06-09 11:35:30

Vue 3響應式前端

2019-07-01 13:34:22

vue系統數據

2013-10-09 14:14:58

C++編譯

2022-08-31 06:37:34

Vue 3模板

2021-04-27 07:39:40

Vue后臺管理
點贊
收藏

51CTO技術棧公眾號

视频一区国产视频| 精品国产影院| 国产精品久久久99| 91九色国产视频| 欧美极品aaaaabbbbb| 成人av地址| 在线看国产日韩| 干日本少妇视频| 神马午夜电影一区二区三区在线观看| 亚洲二区视频| 色婷婷综合久久久久中文字幕1| 俄罗斯女人裸体性做爰| 伊人久久国产| 亚洲精品成人精品456| 精品免费日产一区一区三区免费| 日本欧美www| 国产精品mm| 中文国产成人精品| 在线天堂www在线国语对白| 精品视频在线一区二区在线| 亚洲国产另类av| 日韩欧美在线一区二区| 欧美视频在线观看一区二区三区| 奇米777欧美一区二区| 九色精品美女在线| 国产欧美一区二区三区在线观看视频| swag国产精品一区二区| 欧美日韩高清一区二区不卡| 97国产在线播放| dy888亚洲精品一区二区三区| 久久一留热品黄| 91久久偷偷做嫩草影院| 中文字幕欧美人妻精品一区蜜臀| 激情欧美亚洲| 欧美成人精品在线| 少妇太紧太爽又黄又硬又爽小说| 日韩精选在线| 精品国产乱子伦一区| 拔插拔插华人永久免费| 成人在线视频播放| 天天av天天翘天天综合网色鬼国产| 正在播放一区| 97视频在线观看网站| 久久久久久久久久久电影| 好吊色欧美一区二区三区四区| 国产jzjzjz丝袜老师水多 | 不卡的av电影在线观看| 91精品在线看| 99久久久国产精品无码网爆| 精品在线亚洲视频| 国产精品入口福利| 五月婷婷激情五月| 老司机免费视频久久| 欧美一级大胆视频| 欧美三日本三级少妇99| 亚洲黄色av| 久久免费视频这里只有精品| 久久久美女视频| 国产精品videossex久久发布| 久久亚洲欧美日韩精品专区| 特一级黄色录像| 91精品国产成人观看| 久久久精品亚洲| 久草视频手机在线| 一区二区三区午夜探花| 久久色免费在线视频| 亚洲最大的黄色网址| 欧美欧美全黄| 久久久久日韩精品久久久男男| 欧美日韩中文视频| 亚洲视频1区| 日本免费一区二区三区视频观看| 国产一区免费看| 青草av.久久免费一区| 91精品久久久久久久| 99久久精品国产一区二区成人| 国产精品 日产精品 欧美精品| av噜噜色噜噜久久| 婷婷五月综合久久中文字幕| 久久免费视频色| 亚洲乱码一区二区三区| 成a人片在线观看www视频| 国产精品麻豆一区二区 | 99精品视频免费观看视频| 91av视频导航| 欧美高清69hd| 国产乱理伦片在线观看夜一区| 国产一区二区三区av在线| 午夜影院免费视频| 欧美激情一区二区| 视色,视色影院,视色影库,视色网| 26uuu亚洲电影在线观看| 亚洲一区二区视频在线观看| 欧美性久久久久| 福利一区和二区| 日韩精品一区二| 成年人免费观看视频网站| 日韩在线综合| 国精产品一区一区三区有限在线| 欧美日韩一级黄色片| 极品少妇xxxx精品少妇偷拍| 国产在线资源一区| 午夜视频在线免费观看| 亚洲在线成人精品| 无码内射中文字幕岛国片| 欧一区二区三区| 亚洲欧美精品suv| 亚洲色图综合区| 蜜桃伊人久久| 国产成人免费观看| 粉嫩av在线播放| 亚洲电影一区二区三区| 超碰超碰在线观看| 老司机在线精品视频| www.日韩视频| 国产熟妇一区二区三区四区| 国产精品一二三四| 亚洲国产精品视频一区| 不卡视频观看| 欧美剧情电影在线观看完整版免费励志电影| 在线中文字日产幕| 国产精品国产一区| 亲子乱一区二区三区电影| 精品国自产拍在线观看| 欧美国产亚洲另类动漫| 欧美 国产 综合| av不卡一区| 久热精品视频在线观看| 日本一本在线观看| 99这里只有久久精品视频| 日本一区二区免费高清视频| 欧美与亚洲与日本直播| 日韩经典中文字幕在线观看| 久久精品一级片| 激情综合亚洲精品| 亚洲欧美综合一区| 欧美韩国亚洲| 亚洲精选中文字幕| 97免费在线观看视频| 国产成人啪免费观看软件| 一区二区三区我不卡| 日韩视频网站在线观看| 亚洲人成77777在线观看网| 国产一区二区三区影院| 夫妻av一区二区| 老司机午夜免费福利视频| 在线成人免费| 久久伊人91精品综合网站| 在线观看免费高清视频| 国产农村妇女毛片精品久久麻豆| 97成人在线观看视频| 欧美人体视频| 欧美性视频网站| 你懂的在线看| 91黄色免费网站| 亚洲女优在线观看| 日本欧洲一区二区| 亚洲精品日韩精品| 国产成人免费精品| 日韩有码在线播放| 国产巨乳在线观看| 亚洲精品久久久蜜桃| 亚洲成人av免费观看| 欧美精品一卡| 国产视频精品网| 一个人www视频在线免费观看| 亚洲欧美国产精品| 无码人妻av一区二区三区波多野| 国产日韩综合av| 亚洲欧美视频二区| 亚洲色图网站| 国产精品视频500部| 成人ssswww在线播放| 精品视频久久久| 91丨九色丨海角社区| 1024成人网| 女同性αv亚洲女同志| 国产一区导航| 午夜精品福利一区二区| 成人豆花视频| 高清一区二区三区日本久| 欧美日韩影视| 91.com在线观看| 国产一级av毛片| 久久综合狠狠综合久久综合88| 中文av一区二区三区| 午夜视频一区| 免费久久99精品国产自| 日韩第二十一页| 国色天香2019中文字幕在线观看| 国产视频第一区| 欧美一卡2卡3卡4卡| 天天综合天天干| 国产精品国产自产拍在线| 中国男女全黄大片| 久久久久中文| 91精品国产毛片武则天| 午夜先锋成人动漫在线| 91影视免费在线观看| 三妻四妾完整版在线观看电视剧| 色偷偷偷综合中文字幕;dd| 性生交大片免费看女人按摩| 欧美中文字幕一二三区视频| 九九精品在线观看视频| 久久久国产综合精品女国产盗摄| 国产毛片久久久久久| 国产精品毛片在线| 国产树林野战在线播放| 久久av免费看| 国产91亚洲精品一区二区三区| 欧美色片在线观看| 午夜精品久久久久久久久久久久久 | 美女久久网站| 特级西西人体www高清大胆| 精品国精品国产自在久国产应用| 国产精品9999久久久久仙踪林| 欧美日韩视频免费看| 91高潮精品免费porn| 伊人影院在线视频| 怡红院精品视频| 四虎影视精品成人| 日韩精品在线一区| 亚洲天堂手机在线| 色婷婷av一区| 国产区在线观看视频| 亚洲一区二区三区四区中文字幕| 国产3级在线观看| 国产亚洲女人久久久久毛片| 国产老熟女伦老熟妇露脸| 紧缚奴在线一区二区三区| 欧美丰满熟妇xxxxx| 亚洲精选成人| 国产www免费| 欧美人与禽猛交乱配视频| 中文字幕欧美人与畜| 成人激情电影在线| 日本高清不卡三区| 蜜臀91精品国产高清在线观看| 国产精品伊人日日| 亚洲精品一区二区三区中文字幕| 国产女人精品视频| 精品久久毛片| 国产精品影院在线观看| 日本综合视频| 国产精品亚洲第一区| 国产精品高潮久久| 国产男女猛烈无遮挡91| 日韩毛片免费视频一级特黄| 国产美女被下药99| 国产xxxx视频| 日日夜夜精品视频免费| av动漫免费看| 久久av最新网址| 免费裸体美女网站| 久久亚洲美女| 国产一线二线三线在线观看| 男人的天堂久久精品| 午夜激情福利在线| 美国一区二区三区在线播放| 天天操天天爽天天射| 男女男精品视频| 免费一区二区三区在线观看| 精品一区二区精品| 特级西西444www| 国产精品一区二区在线观看不卡| 国产伦理在线观看| av不卡免费电影| 日本黄色特级片| 国产欧美精品在线观看| 肉色超薄丝袜脚交69xx图片| 亚洲人成精品久久久久| 久久精品人妻一区二区三区| 都市激情亚洲色图| 特级西西444www高清大视频| 欧美一区二区精品久久911| 高清一区二区三区四区| 亚洲精品视频二区| www.91在线| 九九久久久久久久久激情| 1区2区3区在线| 国产成人精品电影| 亚洲精品一区二区在线播放∴| 97自拍视频| 校花撩起jk露出白色内裤国产精品| 日韩久久在线| 在线国产一区二区| 18禁免费无码无遮挡不卡网站| 日韩黄色免费网站| 青青草精品在线| 99re视频精品| 精品视频第一页| 午夜电影一区二区| 在线观看毛片视频| 亚洲成年人在线播放| 精品视频一二区| 欧美巨大黑人极品精男| 日本综合字幕| 成人综合色站| 日韩电影二区| 欧美大片在线播放| 精品一区免费av| 久久午夜夜伦鲁鲁片| 中文字幕欧美一| 亚洲欧美综合自拍| 日韩一区二区不卡| 国自产拍在线网站网址视频| 欧美另类在线播放| 亚洲精品一区三区三区在线观看| 国产精品swag| 我不卡伦不卡影院| 欧美黄网站在线观看| 国产精品一区二区在线观看网站| 蜜乳av中文字幕| 亚洲视频一区在线| 免费视频网站在线观看入口| 精品久久久久久久久久久久久久久久久 | 精品国产黄色片| 亚洲一级片在线看| 国产传媒在线观看| 亚洲最大福利网| 欧美色图一区| 久章草在线视频| 不卡一区二区在线| 欧洲猛交xxxx乱大交3| 欧美视频一区二区三区| 青青色在线视频| 欧美极品美女视频网站在线观看免费 | 后进极品白嫩翘臀在线视频| 久久久www成人免费精品| 日韩高清不卡| 欧美一二三四五区| 亚洲人成人一区二区三区| 超级砰砰砰97免费观看最新一期| 国产精品美女久久久久久久网站| 激情五月婷婷网| 亚洲男人的天堂在线| 黄在线观看免费网站ktv| 国产精品二区在线| 综合国产在线| 男女视频在线观看网站| 国产精品久久久久久久久晋中| 97人妻一区二区精品视频| 国产视频在线一区二区| 伊人网在线播放| 久久五月天婷婷| 性色一区二区| 亚洲欧美视频在线播放| 欧美日韩亚洲一区二| 人成免费电影一二三区在线观看| 午夜精品久久久久久久99热| 欧美一区二区三区红桃小说| 久激情内射婷内射蜜桃| av中文一区二区三区| 久久午夜免费视频| 日韩精品在线电影| 自由日本语热亚洲人| 欧美在线激情| 青青青爽久久午夜综合久久午夜| 色婷婷国产精品免| 欧美撒尿777hd撒尿| 黄色在线免费网站| 亚洲在线观看视频| 欧美日韩1080p| 中文字幕乱视频| 欧美性色视频在线| 国产高清视频免费最新在线| 国产精品极品尤物在线观看 | 国产午夜精品全部视频播放 | 97av影视网在线观看| 欧美 日韩 国产一区二区在线视频| 国产伦理在线观看| 狠狠躁夜夜躁人人躁婷婷91| 国产视频第一区| 91系列在线观看| 亚洲国内欧美| 免费污网站在线观看| 欧美三级三级三级爽爽爽| av在线下载| 狠狠色综合网站久久久久久久| 丝袜脚交一区二区| www.毛片com| 日韩高清欧美高清| 我爱我色成人网| 只有这里有精品| 99视频国产精品| 一炮成瘾1v1高h| 久久久久久尹人网香蕉| 精品一区欧美| 四川一级毛毛片| 色哟哟精品一区| 视频在线观看入口黄最新永久免费国产 | 在线成人一区二区| 亚洲国产高清在线观看| 男人天堂999| 亚洲女爱视频在线| 欧美一区二区少妇| 亚洲一区二区三区777| 午夜亚洲伦理| 538精品在线视频| 精品亚洲国产视频|