日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
Vue模板編譯原理

Vue 3 發(fā)布在即,本來想著直接看看 Vue 3 的模板編譯,但是我打開 Vue 3 源碼的時候,發(fā)現(xiàn)我好像連 Vue 2 是怎么編譯模板的都不知道。從小魯迅就告訴我們,不能一口吃成一個胖子,那我只能回頭看看 Vue 2 的模板編譯源碼,至于 Vue 3 就留到正式發(fā)布的時候再看。

創(chuàng)新互聯(lián)公司 - 雅安移動機房,四川服務(wù)器租用,成都服務(wù)器租用,四川網(wǎng)通托管,綿陽服務(wù)器托管,德陽服務(wù)器托管,遂寧服務(wù)器托管,綿陽服務(wù)器托管,四川云主機,成都云主機,西南云主機,雅安移動機房,西南服務(wù)器托管,四川/成都大帶寬,成都機柜租用,四川老牌IDC服務(wù)商

Vue 的版本
很多人使用 Vue 的時候,都是直接通過 vue-cli 生成的模板代碼,并不知道 Vue 其實提供了兩個構(gòu)建版本。

 
 
 
 
  1. vue.js:完整版本,包含了模板編譯的能力;
 
 
 
 
  1. vue.runtime.js:運行時版本,不提供模板編譯能力,需要通過 vue-loader 進行提前編譯。

Vue不同構(gòu)建版本

完整版與運行時版區(qū)別
簡單來說,就是如果你用了 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. // 省略了部分代碼,只保留了關(guān)鍵部分
  2. import { compileToFunctions } from './compiler/index'
  3. const mount = Vue.prototype.$mount
  4. Vue.prototype.$mount = function (el) {
  5.   const options = this.$options
  6.   
  7.   // 如果沒有 render 方法,則進行 template 編譯
  8.   if (!options.render) {
  9.     let template = options.template
  10.     if (template) {
  11.       // 調(diào)用 compileToFunctions,編譯 template,得到 render 方法
  12.       const { render, staticRenderFns } = compileToFunctions(template, {
  13.         shouldDecodeNewlines,
  14.         shouldDecodeNewlinesForHref,
  15.         delimiters: options.delimiters,
  16.         comments: options.comments
  17.       }, this)
  18.       // 這里的 render 方法就是生成生成虛擬 DOM 的方法
  19.       options.render = render
  20.     }
  21.   }
  22.   return mount.call(this, el, hydrating)
  23. }
 
 
 
 
  1. 再看看 ./compiler/index 文件的 compileToFunctions 方法從何而來。
 
 
 
 
  1. import { baseOptions } from './options'
  2. import { createCompiler } from 'compiler/index'
  3. // 通過 createCompiler 方法生成編譯函數(shù)
  4. const { compile, compileToFunctions } = createCompiler(baseOptions)
  5. export { compile, compileToFunctions }

后續(xù)的主要邏輯都在 compiler 模塊中,這一塊有些繞,因為本文不是做源碼分析,就不貼整段源碼了。簡單看看這一段的邏輯是怎么樣的。

 
 
 
 
  1. export function createCompiler(baseOptions) {
  2.   const baseCompile = (template, options) => {
  3.     // 解析 html,轉(zhuǎn)化為 ast
  4.     const ast = parse(template.trim(), options)
  5.     // 優(yōu)化 ast,標記靜態(tài)節(jié)點
  6.     optimize(ast, options)
  7.     // 將 ast 轉(zhuǎn)化為可執(zhí)行代碼
  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.     return compiled
  27.   }
  28.   const createCompileToFunctionFn = () => {
  29.     // 編譯緩存
  30.     const cache = Object.create(null)
  31.     return (template, options, vm) => {
  32.       // 已編譯模板直接走緩存
  33.       if (cache[template]) {
  34.         return cache[template]
  35.       }
  36.       const compiled = compile(template, options)
  37.      return (cache[key] = compiled)
  38.     }
  39.   }
  40.   return {
  41.     compile,
  42.     compileToFunctions: createCompileToFunctionFn(compile)
  43.   }
  44. }

主流程
可以看到主要的編譯邏輯基本都在 baseCompile 方法內(nèi),主要分為三個步驟:

模板編譯,將模板代碼轉(zhuǎn)化為 AST;
優(yōu)化 AST,方便后續(xù)虛擬 DOM 更新;
生成代碼,將 AST 轉(zhuǎn)化為可執(zhí)行的代碼;

 
 
 
 
  1. const baseCompile = (template, options) => {
  2.   // 解析 html,轉(zhuǎn)化為 ast
  3.   const ast = parse(template.trim(), options)
  4.   // 優(yōu)化 ast,標記靜態(tài)節(jié)點
  5.   optimize(ast, options)
  6.   // 將 ast 轉(zhuǎn)化為可執(zhí)行代碼
  7.   const code = generate(ast, options)
  8.   return {
  9.     ast,
  10.     render: code.render,
  11.     staticRenderFns: code.staticRenderFns
  12.   }
  13. }

parse
AST
首先看到 parse 方法,該方法的主要作用就是解析 HTML,并轉(zhuǎn)化為 AST(抽象語法樹),接觸過 ESLint、Babel 的同學肯定對 AST 不陌生,我們可以先看看經(jīng)過 parse 之后的 AST 長什么樣。

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

 
 
 
 
  1. new Vue({
  2.   el: '#app',
  3.   template: `
  4.     
  5.       {{message}}
  6.       showName
  7.     
  •   `,
  •   data: {
  •     name: 'shenfq',
  •     message: 'Hello Vue!'
  •   },
  •   methods: {
  •     showName() {
  •       alert(this.name)
  •     }
  •   }
  • })
  • 經(jīng)過 parse 之后的 AST:

    Template AST
    AST 為一個樹形結(jié)構(gòu)的對象,每一層表示一個節(jié)點,第一層就是 div(tag: "div")。div 的子節(jié)點都在 children 屬性中,分別是 h2 標簽、空行、button 標簽。我們還可以注意到有一個用來標記節(jié)點類型的屬性:type,這里 div 的 type 為 1,表示是一個元素節(jié)點,type 一共有三種類型:

    元素節(jié)點;
    表達式;
    文本;
    在 h2 和 button 標簽之間的空行就是 type 為 3 的文本節(jié)點,而 h2 標簽下就是一個表達式節(jié)點。

    解析HTML
    parse 的整體邏輯較為復雜,我們可以先簡化一下代碼,看看 parse 的流程。

     
     
     
     
    1. import { parseHTML } from './html-parser'
    2. export function parse(template, options) {
    3.   let root
    4.   parseHTML(template, {
    5.     // some options...
    6.     start() {}, // 解析到標簽位置開始的回調(diào)
    7.     end() {}, // 解析到標簽位置結(jié)束的回調(diào)
    8.     chars() {}, // 解析到文本時的回調(diào)
    9.     comment() {} // 解析到注釋時的回調(diào)
    10.   })
    11.   return root
    12. }

    可以看到 parse 主要通過 parseHTML 進行工作,這個 parseHTML 本身來自于開源庫:simple html parser,只不過經(jīng)過了 Vue 團隊的一些修改,修復了相關(guān) 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.     // "<" 字符在當前 html 字符串開始位置
    9.     if (textEnd === 0) {
    10.       // 1、匹配到注釋: 
    11.       if (/^
    12.         const commentEnd = html.indexOf('-->')
    13.         if (commentEnd >= 0) {
    14.           // 調(diào)用 options.comment 回調(diào),傳入注釋內(nèi)容
    15.           options.comment(html.substring(4, commentEnd))
    16.           // 裁切掉注釋部分
    17.           advance(commentEnd + 3)
    18.           continue
    19.         }
    20.       }
    21.       // 2、匹配到條件注釋:   
    22.       if (/^
    23.         // ... 邏輯與匹配到注釋類似
    24.       }
    25.       // 3、匹配到 Doctype: 
    26.       const doctypeMatch = html.match(/^]+>/i)
    27.       if (doctypeMatch) {
    28.         // ... 邏輯與匹配到注釋類似
    29.       }
    30.       // 4、匹配到結(jié)束標簽: 
  •       const endTagMatch = html.match(endTag)
  •       if (endTagMatch) {}
  •       // 5、匹配到開始標簽: 
  •       const startTagMatch = parseStartTag()
  •       if (startTagMatch) {}
  •     }
  •     // "<" 字符在當前 html 字符串中間位置
  •     let text, rest, next
  •     if (textEnd > 0) {
  •       // 提取中間字符
  •       rest = html.slice(textEnd)
  •       // 這一部分當成文本處理
  •       text = html.substring(0, textEnd)
  •       advance(textEnd)
  •     }
  •     // "<" 字符在當前 html 字符串中不存在
  •     if (textEnd < 0) {
  •       text = html
  •       html = ''
  •     }
  •     
  •     // 如果存在 text 文本
  •     // 調(diào)用 options.chars 回調(diào),傳入 text 文本
  •     if (options.chars && text) {
  •       // 字符相關(guān)回調(diào)
  •       options.chars(text)
  •     }
  •   }
  •   // 向前推進,裁切 html
  •   function advance(n) {
  •     index += n
  •     html = html.substring(n)
  •   }
  • }
  • 上述代碼為簡化后的 parseHTML,while 循環(huán)中每次截取一段 html 文本,然后通過正則判斷文本的類型進行處理,這就類似于編譯原理中常用的有限狀態(tài)機。每次拿到 "<" 字符前后的文本,"<" 字符前的就當做文本處理,"<" 字符后的通過正則判斷,可推算出有限的幾種狀態(tài)。

    其他的邏輯處理都不復雜,主要是開始標簽與結(jié)束標簽,我們先看看關(guān)于開始標簽與結(jié)束標簽相關(guān)的正則。

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

    這段正則看起來很長,但是理清之后也不是很難。這里推薦一個正則可視化工具。我們到工具上看看startTagOpen:

    startTagOpen
    這里比較疑惑的點就是為什么 tagName 會存在 :,這個是 XML 的 命名空間,現(xiàn)在已經(jīng)很少使用了,我們可以直接忽略,所以我們簡化一下這個正則:

     
     
     
     
    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
    除了上面關(guān)于標簽開始和結(jié)束的正則,還有一段用來提取標簽屬性的正則,真的是又臭又長。

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

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

    attribute
    理清正則后可以更加方便我們看后面的代碼。

     
     
     
     
    1. while(html) {
    2.   last = html
    3.   let textEnd = html.indexOf('<')
    4.   // "<" 字符在當前 html 字符串開始位置
    5.   if (textEnd === 0) {
    6.     // some code ...
    7.     // 4、匹配到標簽結(jié)束位置: 
  •     const endTagMatch = html.match(endTag)
  •     if (endTagMatch) {
  •       const curIndex = index
  •       advance(endTagMatch[0].length)
  •       parseEndTag(endTagMatch[1], curIndex, index)
  •       continue
  •     }
  •     // 5、匹配到標簽開始位置: 
  •     const startTagMatch = parseStartTag()
  •     if (startTagMatch) {
  •       handleStartTag(startTagMatch)
  •       continue
  •     }
  •   }
  • }
  • // 向前推進,裁切 html
  • function advance(n) {
  •   index += n
  •   html = html.substring(n)
  • }
  • // 判斷是否標簽開始位置,如果是,則提取標簽名以及相關(guān)屬性
  • function parseStartTag () {
  •   // 提取 
  •   const start = html.match(startTagOpen)
  •   if (start) {
  •     const [fullStr, tag] = start
  •     const match = {
  •       attrs: [],
  •       start: index,
  •       tagName: tag,
  •     }
  •     advance(fullStr.length)
  •     let end, attr
  •     // 遞歸提取屬性,直到出現(xiàn) ">" 或 "/>" 字符
  •     while (
  •       !(end = html.match(startTagClose)) &&
  •       (attr = html.match(attribute))
  •     ) {
  •       advance(attr[0].length)
  •       match.attrs.push(attr)
  •     }
  •     if (end) {
  •       // 如果是 "/>" 表示單標簽
  •       match.unarySlash = end[1]
  •       advance(end[0].length)
  •       match.end = index
  •       return match
  •     }
  •   }
  • }
  • // 處理開始標簽
  • function handleStartTag (match) {
  •   const tagName = match.tagName
  •   const unary = match.unarySlash
  •   const len = match.attrs.length
  •   const attrs = new Array(len)
  •   for (let i = 0; i < l; i++) {
  •     const args = match.attrs[i]
  •     // 這里的 3、4、5 分別對應(yīng)三種不同復制屬性的方式
  •     // 3: attr="xxx" 雙引號
  •     // 4: attr='xxx' 單引號
  •     // 5: attr=xxx   省略引號
  •     const value = args[3] || args[4] || args[5] || ''
  •     attrs[i] = {
  •       name: args[1],
  •       value
  •     }
  •   }
  •   if (!unary) {
  •     // 非單標簽,入棧
  •     stack.push({
  •       tag: tagName,
  •       lowerCasedTag:
  •       tagName.toLowerCase(),
  •       attrs: attrs
  •     })
  •     lastTag = tagName
  •   }
  •   if (options.start) {
  •     // 開始標簽的回調(diào)
  •     options.start(tagName, attrs, unary, match.start, match.end)
  •   }
  • }
  • // 處理閉合標簽
  • function parseEndTag (tagName, start, end) {
  •   let pos, lowerCasedTagName
  •   if (start == null) start = index
  •   if (end == null) end = index
  •   if (tagName) {
  •     lowerCasedTagName = tagName.toLowerCase()
  •   }
  •   // 在棧內(nèi)查找相同類型的未閉合標簽
  •   if (tagName) {
  •     for (pos = stack.length - 1; pos >= 0; pos--) {
  •       if (stack[pos].lowerCasedTag === lowerCasedTagName) {
  •         break
  •       }
  •     }
  •   } else {
  •     pos = 0
  •   }
  •   if (pos >= 0) {
  •     // 關(guān)閉該標簽內(nèi)的未閉合標簽,更新堆棧
  •     for (let i = stack.length - 1; i >= pos; i--) {
  •       if (options.end) {
  •         // end 回調(diào)
  •         options.end(stack[i].tag, start, end)
  •       }
  •     }
  •     // 堆棧中刪除已關(guān)閉標簽
  •     stack.length = pos
  •     lastTag = pos && stack[pos - 1].tag
  •   }
  • }
  • 在解析開始標簽的時候,如果該標簽不是單標簽,會將該標簽放入到一個堆棧當中,每次閉合標簽的時候,會從棧頂向下查找同名標簽,直到找到同名標簽,這個操作會閉合同名標簽上面的所有標簽。接下來我們舉個例子:

     
     
     
     
    1.   

      test

    2.   

    3.   

    在解析了 div 和 h2 的開始標簽后,棧內(nèi)就存在了兩個元素。h2 閉合后,就會將 h2 出棧。然后會解析兩個未閉合的 p 標簽,此時,棧內(nèi)存在三個元素(div、p、p)。如果這個時候,解析了 div 的閉合標簽,除了將 div 閉合外,div 內(nèi)兩個未閉合的 p 標簽也會跟隨閉合,此時棧被清空。

    為了便于理解,特地錄制了一個動圖,如下:

    入棧與出棧
    理清了 parseHTML 的邏輯后,我們回到調(diào)用 parseHTML 的位置,調(diào)用該方法的時候,一共會傳入四個回調(diào),分別對應(yīng)標簽的開始和結(jié)束、文本、注釋。

     
     
     
     
    1. parseHTML(template, {
    2.   // some options...
    3.   // 解析到標簽位置開始的回調(diào)
    4.   start(tag, attrs, unary) {},
    5.   // 解析到標簽位置結(jié)束的回調(diào)
    6.   end(tag) {},
    7.   // 解析到文本時的回調(diào)
    8.   chars(text: string) {},
    9.   // 解析到注釋時的回調(diào)
    10.   comment(text: string) {}
    11. })

    處理開始標簽
    首先看解析到開始標簽時,會生成一個 AST 節(jié)點,然后處理標簽上的屬性,最后將 AST 節(jié)點放入樹形結(jié)構(gòu)中。

     
     
     
     
    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. }
    9. function createASTElement(tag, attrs, parent) {
    10.   const attrsList = attrs
    11.   const attrsMap = makeAttrsMap(attrsList)
    12.   return {
    13.     type: 1,       // 節(jié)點類型
    14.     tag,           // 節(jié)點名稱
    15.     attrsMap,      // 節(jié)點屬性映射
    16.     attrsList,     // 節(jié)點屬性數(shù)組
    17.     parent,        // 父節(jié)點
    18.     children: [],  // 子節(jié)點
    19.   }
    20. }
    21. const stack = []
    22. let root // 根節(jié)點
    23. let currentParent // 暫存當前的父節(jié)點
    24. parseHTML(template, {
    25.   // some options...
    26.   // 解析到標簽位置開始的回調(diào)
    27.   start(tag, attrs, unary) {
    28.     // 創(chuàng)建 AST 節(jié)點
    29.     let element = createASTElement(tag, attrs, currentParent)
    30.     // 處理指令: v-for v-if v-once
    31.     processFor(element)
    32.     processIf(element)
    33.     processOnce(element)
    34.     processElement(element, options)
    35.     // 處理 AST 樹
    36.     // 根節(jié)點不存在,則設(shè)置該元素為根節(jié)點
    37.     if (!root) {
    38.       root = element
    39.       checkRootConstraints(root)
    40.     }
    41.     // 存在父節(jié)點
    42.     if (currentParent) {
    43.       // 將該元素推入父節(jié)點的子節(jié)點中
    44.       currentParent.children.push(element)
    45.       element.parent = currentParent
    46.     }
    47.     if (!unary) {
    48.      // 非單標簽需要入棧,且切換當前父元素的位置
    49.       currentParent = element
    50.       stack.push(element)
    51.     }
    52.   }
    53. })

    處理結(jié)束標簽
    標簽結(jié)束的邏輯就比較簡單了,只需要去除棧內(nèi)最后一個未閉合標簽,進行閉合即可。

     
     
     
     
    1. parseHTML(template, {
    2.   // some options...
    3.   // 解析到標簽位置結(jié)束的回調(diào)
    4.   end() {
    5.     const element = stack[stack.length - 1]
    6.     const lastNode = element.children[element.children.length - 1]
    7.     // 處理尾部空格的情況
    8.     if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
    9.       element.children.pop()
    10.     }
    11.     // 出棧,重置當前的父節(jié)點
    12.     stack.length -= 1
    13.     currentParent = stack[stack.length - 1]
    14.   }
    15. })

    處理文本
    處理完標簽后,還需要對標簽內(nèi)的文本進行處理。文本的處理分兩種情況,一種是帶表達式的文本,還一種就是純靜態(tài)的文本。

     
     
     
     
    1. parseHTML(template, {
    2.   // some options...
    3.   // 解析到文本時的回調(diào)
    4.   chars(text) {
    5.     if (!currentParent) {
    6.       // 文本節(jié)點外如果沒有父節(jié)點則不處理
    7.       return
    8.     }
    9.     
    10.     const children = currentParent.children
    11.     text = text.trim()
    12.     if (text) {
    13.       // parseText 用來解析表達式
    14.       // delimiters 表示表達式標識符,默認為 ['{{', '}}']
    15.       const res = parseText(text, delimiters))
    16.       if (res) {
    17.         // 表達式
    18.         children.push({
    19.           type: 2,
    20.           expression: res.expression,
    21.           tokens: res.tokens,
    22.           text
    23.         })
    24.       } else {
    25.         // 靜態(tài)文本
    26.         children.push({
    27.           type: 3,
    28.           text
    29.         })
    30.       }
    31.     }
    32.   }
    33. })

    下面我們看看 parseText 如何解析表達式。

     
     
     
     
    1. // 構(gòu)造匹配表達式的正則
    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.     // 提取表達式開始位置前面的靜態(tài)字符,放入 token 中
    22.     if (index > lastIndex) {
    23.       rawTokens.push(tokenValue = text.slice(lastIndex, index))
    24.       tokens.push(JSON.stringify(tokenValue))
    25.     }
    26.     // 提取表達式內(nèi)部的內(nèi)容,使用 _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.   // 表達式后面還有其他靜態(tài)字符,放入 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.   }
    41. }

    首先通過一段正則來提取表達式:

    提取表達式
    看代碼可能有點難,我們直接看例子,這里有一個包含表達式的文本。

     
     
     
     
    1. 是否登錄:{{isLogin ? '是' : '否'}}

    運行結(jié)果

    解析文本
    optimize
    通過上述一些列處理,我們就得到了 Vue 模板的 AST。由于 Vue 是響應(yīng)式設(shè)計,所以拿到 AST 之后還需要進行一系列優(yōu)化,確保靜態(tài)的數(shù)據(jù)不會進入虛擬 DOM 的更新階段,以此來優(yōu)化性能。

     
     
     
     
    1. export function optimize (root, options) {
    2.   if (!root) return
    3.   // 標記靜態(tài)節(jié)點
    4.   markStatic(root)
    5. }

    簡單來說,就是把所以靜態(tài)節(jié)點的 static 屬性設(shè)置為 true。

     
     
     
     
    1. function isStatic (node) {
    2.   if (node.type === 2) { // 表達式,返回 false
    3.     return false
    4.   }
    5.   if (node.type === 3) { // 靜態(tài)文本,返回 true
    6.     return true
    7.   }
    8.   // 此處省略了部分條件
    9.   return !!(
    10.     !node.hasBindings && // 沒有動態(tài)綁定
    11.     !node.if && !node.for && // 沒有 v-if/v-for
    12.     !isBuiltInTag(node.tag) && // 不是內(nèi)置組件 slot/component
    13.     !isDirectChildOfTemplateFor(node) && // 不在 template for 循環(huán)內(nèi)
    14.     Object.keys(node).every(isStaticKey) // 非靜態(tài)節(jié)點
    15.   )
    16. }
    17. function markStatic (node) {
    18.   node.static = isStatic(node)
    19.   if (node.type === 1) {
    20.     // 如果是元素節(jié)點,需要遍歷所有子節(jié)點
    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.         // 如果有一個子節(jié)點不是靜態(tài)節(jié)點,則該節(jié)點也必須是動態(tài)的
    26.         node.static = false
    27.       }
    28.     }
    29.   }
    30. }

    generate
    得到優(yōu)化的 AST 之后,就需要將 AST 轉(zhuǎn)化為 render 方法。還是用之前的模板,先看看生成的代碼長什么樣:

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

    將生成的代碼展開:

     
     
     
     
    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.     ;
    10. }

    看到這里一堆的下劃線肯定很懵逼,這里的 _c 對應(yīng)的是虛擬 DOM 中的 createElement 方法。其他的下劃線方法在 core/instance/render-helpers 中都有定義,每個方法具體做了什么不做展開。

    render-helpers`
    具體轉(zhuǎn)化方法就是一些簡單的字符拼接,下面是簡化了邏輯的部分,不做過多講述。

     
     
     
     
    1. export function generate(ast, options) {
    2.   const state = new CodegenState(options)
    3.   
      分享文章:Vue模板編譯原理
      網(wǎng)頁網(wǎng)址:http://www.5511xx.com/article/dhhoedj.html