Vue 模板編譯原理

easyhappy發表於2018-03-15

原 文地址 ,如果感興趣或者對美股感興趣可以加我微信: xiaobei060537, 一起交流 ?。

Vue 模板編譯原理

關於vue的內部原理其實有很多個重要的部分,變化偵測,模板編譯,virtualDOM,整體執行流程等。

之前寫過一篇《深入淺出 - vue變化偵測原理》 講了關於變化偵測的實現原理。

那今天主要把 模板編譯 這部分的實現原理單獨拿出來講一講。

本文我可能不會在文章中說太多細節部分的處理,我會把 vue 對模板編譯這部分的整體原理講清楚,主要是讓讀者讀完文章後對模板編譯的整體實現原理有一個清晰的思路和理解。

關於 Vue 編譯原理這塊的整體邏輯主要分三個部分,也可以說是分三步,這三個部分是有前後關係的:

  • 第一步是將 模板字串 轉換成 element ASTs(解析器)
  • 第二步是對 AST 進行靜態節點標記,主要用來做虛擬DOM的渲染優化(優化器)
  • 第三步是 使用 element ASTs 生成 render 函式程式碼字串(程式碼生成器)

解析器

解析器主要乾的事是將 模板字串 轉換成 element ASTs,例如:

<div>
  <p>{{name}}</p>
</div>
複製程式碼

上面這樣一個簡單的 模板 轉換成 element AST 後是這樣的:

{
  tag: "div"
  type: 1,
  staticRoot: false,
  static: false,
  plain: true,
  parent: undefined,
  attrsList: [],
  attrsMap: {},
  children: [
      {
      tag: "p"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: {tag: "div", ...},
      attrsList: [],
      attrsMap: {},
      children: [{
          type: 2,
          text: "{{name}}",
          static: false,
          expression: "_s(name)"
      }]
    }
  ]
}
複製程式碼

我們先用這個簡單的例子來說明這個解析器的內部究竟發生了什麼。

這段模板字串會扔到 while 中去迴圈,然後 一段一段 的擷取,把擷取到的 每一小段字串 進行解析,直到最後截沒了,也就解析完了。

上面這個簡單的模板擷取的過程是這樣的:

<div>
  <p>{{name}}</p>
</div>
複製程式碼
<p>{{name}}</p>
</div>
複製程式碼
<p>{{name}}</p>
</div>
複製程式碼
{{name}}</p>
</div>
複製程式碼
</p>
</div>
複製程式碼
</div>
複製程式碼
</div>
複製程式碼

那是根據什麼截的呢?換句話說擷取字串有什麼規則麼?

當然有

只要判斷模板字串是不是以 < 開頭我們就可以知道我們接下來要擷取的這一小段字串是 標籤 還是 文字

舉個?:

<div></div> 這樣的一段字串是以 < 開頭的,那麼我們通過正則把 <div> 這一部分 match 出來,就可以拿到這樣的資料:

{
  tagName: 'div',
  attrs: [],
  unarySlash: '',
  start: 0,
  end: 5
}
複製程式碼

好奇如何用正則解析出 tagName 和 attrs 等資訊的同學可以看下面這個demo程式碼:

const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
let html = `<div></div>`
let index = 0
const start = html.match(startTagOpen)

const match = {
  tagName: start[1],
  attrs: [],
  start: 0
}
html = html.substring(start[0].length)
index += start[0].length
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
  html = html.substring(attr[0].length)
  index += attr[0].length
  match.attrs.push(attr)
}
if (end) {
  match.unarySlash = end[1]
  html = html.substring(end[0].length)
  index += end[0].length
  match.end = index
}
console.log(match)
複製程式碼

Stack

用正則把 開始標籤 中包含的資料(attrs, tagName 等)解析出來之後還要做一個很重要的事,就是要維護一個 stack

那這個 stack 是用來幹什麼的呢?

這個 stack 是用來記錄一個層級關係的,用來記錄DOM的深度。

更準確的說,當解析到一個 開始標籤 或者 文字,無論是什麼, stack 中的最後一項,永遠是當前正在被解析的節點的 parentNode 父節點。

通過 stack 解析器就可以把當前解析到的節點 push 到 父節點的 children 中。

也可以把當前正在解析的節點的 parent 屬性設定為 父節點。

事實上也確實是這麼做的。

但並不是只要解析到一個標籤的開始部分就把當前標籤 pushstack 中。

因為在 HTML 中有一種 自閉和標籤,比如 input

<input /> 這種 自閉和的標籤 是不需要 pushstack 中的,因為 input 並不存在子節點。

所以當解析到一個標籤的開始時,要判斷當前被解析的標籤是否是自閉和標籤,如果不是自閉和標籤才 pushstack 中。

if (!unary) {
  currentParent = element
  stack.push(element)
}
複製程式碼

現在有了 DOM 的層級關係,也可以解析出DOM的 開始標籤,這樣每解析一個 開始標籤 就生成一個 ASTElement (儲存當前標籤的attrs,tagName 等資訊的object)

並且把當前的 ASTElement push 到 parentNodechildren 中,同時給當前 ASTElementparent 屬性設定為 stack 中的最後一項

currentParent.children.push(element)
element.parent = currentParent
複製程式碼

< 開頭的幾種情況

但並不是所有以 < 開頭的字串都是 開始標籤,以 < 開頭的字串有以下幾種情況:

當然我們解析器在解析的過程中遇到的最多的是 開始標籤 結束標籤註釋

擷取文字

我們繼續上面的例子解析,div開始標籤 解析之後剩餘的模板字串是下面的樣子:

<p>{{name}}</p>
</div>
複製程式碼

這一次我們在解析發現 模板字串 不是以 < 開頭了。

那麼如果模板字串不是以 < 開頭的怎麼處理呢??

其實如果字串不是以 < 開頭可能會出現這麼幾種情況:

我是text <div></div>
複製程式碼

或者:

我是text </p>
複製程式碼

不論是哪種情況都會將標籤前面的文字部分解析出來,擷取這段文字其實並不難,看下面的例子:

// 可以直接將本 demo 放到瀏覽器 console 中去執行
const html = '我是text </p>'
let textEnd = html.indexOf('<')
const text = html.substring(0, textEnd)
console.log(text)
複製程式碼

當然 vue 對文字的擷取不只是這麼簡單,vue對文字的擷取做了很安全的處理,如果 < 是文字的一部分,那上面 DEMO 中擷取的內容就不是我們想要的,例如這樣的:

a < b </p>
複製程式碼

如果是這樣的文字,上面的 demo 肯定就掛了,擷取出的文字就會遺漏一部分,而 vue 對這部分是進行了處理的,看下面的程式碼:

let textEnd = html.indexOf('<')
let text, rest, next
if (textEnd >= 0) {
  rest = html.slice(textEnd)
  // 剩餘部分的 HTML 不符合標籤的格式那肯定就是文字
  // 並且還是以 < 開頭的文字
  while (
    !endTag.test(rest) &&
    !startTagOpen.test(rest) &&
    !comment.test(rest) &&
    !conditionalComment.test(rest)
  ) {
    // < in plain text, be forgiving and treat it as text
    next = rest.indexOf('<', 1)
    if (next < 0) break
    textEnd += next
    rest = html.slice(textEnd)
  }
  text = html.substring(0, textEnd)
  html = html.substring(0, textEnd)
}
複製程式碼

這段程式碼的邏輯是如果文字擷取完之後,剩餘的 模板字串 開頭不符合標籤的格式規則,那麼肯定就是有沒擷取完的文字

這個時候只需要迴圈把 textEnd 累加,直到剩餘的 模板字串 符合標籤的規則之後在一次性把 text模板字串 中擷取出來就好了。

繼續上面的例子,當前剩餘的 模板字串 是這個樣子的:

<p>{{name}}</p>
</div>
複製程式碼

擷取之後剩餘的 模板字串 是這個樣子的:

<p>{{name}}</p>
</div>
複製程式碼

被擷取出來的文字是這樣的:

"
 "
複製程式碼

擷取之後就需要對文字進行解析,不過在解析文字之前需要進行預處理,也就是先簡單加工一下文字,vue 是這樣做的:

const children = currentParent.children
text = inPre || text.trim()
  ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
  // only preserve whitespace if its not right after a starting tag
  : preserveWhitespace && children.length ? ' ' : ''
複製程式碼

這段程式碼的意思是:

  • 如果文字不為空,判斷父標籤是不是script或style,
    1. 如果是則什麼都不管,
    2. 如果不是需要 decode 一下編碼,使用github上的 he 這個類庫的 decodeHTML 方法
  • 如果文字為空,判斷有沒有兄弟節點,也就是 parent.children.length 是不是為 0
    1. 如果大於0 返回 ' '
    2. 如果為 0 返回 ''

結果發現這一次的 text 正好命中最後的那個 '',所以這一次就什麼都不用做繼續下一輪解析就好

繼續上面的例子,現在的 模板字串 變是這個樣子:

<p>{{name}}</p>
</div>
複製程式碼

接著解析 <p>,解析流程和上面的 <div> 一樣就不說了,直接繼續:

{{name}}</p>
</div>
複製程式碼

通過上面寫的文字的擷取方式這一次擷取出來的文字是這個樣子的 "{{name}}"

解析文字

其實解析文字節點並不難,只需要將文字節點 pushcurrentParent.children.push(ast) 就行了。

但是帶變數的文字和不帶變數的純文字是不同的處理方式。

帶變數的文字是指 Hello {{ name }} 這個 name 就是變數。

不帶變數的文字是這樣的 Hello Berwin 這種沒有訪問資料的純文字。

純文字比較簡單,直接將 文字節點的ast pushparent 節點的 children 中就行了,例如:

children.push({
  type: 3,
  text: '我是純文字'
})
複製程式碼

而帶變數的文字要多一個解析文字變數的操作:

const expression = parseText(text, delimiters) // 對變數解析 {{name}} => _s(name)
children.push({
  type: 2,
  expression,
  text
})
複製程式碼

上面例子中 "{{name}}" 是一個帶變數的文字,經過 parseText 解析後 expression_s(name),所以最後 pushcurrentParent.children 中的節點是這個樣子的:

{
  expression: "_s(name)",
  text: "{{name}}",
  type: 2
}
複製程式碼

結束標籤的處理

現在文字解析完之後,剩餘的 模板字串 變成了這個樣子:

</p>
</div>
複製程式碼

這一次還是用上面說的辦法,html.indexOf('<') === 0,發現是 < 開頭的,然後用正則去 match 發現符合 結束標籤的格式,把它擷取出來。

並且還要做一個處理是用當前標籤名在 stack 從後往前找,將找到的 stack 中的位置往後的所有標籤全部刪除(意思是,已經解析到當前的結束標籤,那麼它的子集肯定都是解析過的,試想一下當前標籤都關閉了,它的子集肯定也都關閉了,所以需要把當前標籤位置往後從 stack中都清掉)

結束標籤不需要解析,只需要將 stack 中的當前標籤刪掉就好。

雖然不用解析,但 vue 還是做了一個優化處理,children 中的最後一項如果是空格 " ",則刪除最後這一項:

if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
  element.children.pop()
}
複製程式碼

因為最後這一項空格是沒有用的,舉個例子:

<ul>
  <li></li>
</ul>
複製程式碼

上面例子中解析成 element ASTs之後 ul 的結束標籤 </ul>li 的結束標籤 </li> 之間有一個空格,這個空格也屬於文字節點在 ulchildren 中,這個空格是沒有用的,把這個空格刪掉每次渲染dom都會少渲染一個文字節點,可以節省一定的效能開銷。

現在剩餘的 模板字串 已經不多了,是下面的樣子:

</div>
複製程式碼

然後解析文字,就是一個其實就是一個空格的文字節點。

然後再一次解析結束標籤 </div>

</div>
複製程式碼

解析完畢退出 while 迴圈。

解析完之後拿到的 element ASTs 就是文章開頭寫的那樣。

總結一下

其實這樣一個模板解析器的原理不是特別難,主要就是兩部分內容,一部分是 擷取 字串,一部分是對擷取之後的字串做 解析

每擷取一段標籤的開頭就 pushstack中,解析到標籤的結束就 pop 出來,當所有的字串都截沒了也就解析完了。

上文中的例子是比較簡單的,不涉及一些迴圈啊,什麼的,註釋的處理這些也都沒有涉及到,但其實這篇文章中想表達的內容也不是來扣細節的,如果扣細節可能要寫一本小書才夠,一篇文章的字數可能只夠把一個大體的邏輯給大家講清楚,希望同學們見諒,如果對細節感興趣可以在下面評論,我們們一起討論共同學習進步~

優化器

優化器的目標是找出那些靜態節點並打上標記,而靜態節點指的是 DOM 不需要發生變化的節點,例如:

<p>我是靜態節點,我不需要發生變化</p>
複製程式碼

標記靜態節點有兩個好處:

  1. 每次重新渲染的時候不需要為靜態節點建立新節點
  2. 在 Virtual DOM 中 patching 的過程可以被跳過

優化器的實現原理主要分兩步:

  • 第一步:用遞迴的方式將所有節點新增 static 屬性,標識是不是靜態節點
  • 第二步:標記所有靜態根節點

什麼是靜態根節點? 答:子節點全是靜態節點的節點就是靜態根節點,例如:

<ul>
  <li>我是靜態節點,我不需要發生變化</li>
  <li>我是靜態節點2,我不需要發生變化</li>
  <li>我是靜態節點3,我不需要發生變化</li>
</ul>
複製程式碼

ul 就是靜態根節點。

如何將所有節點標記 static 屬性?

vue 判斷一個節點是不是靜態節點的做法其實並不難:

  1. 先根據自身是不是靜態節點做一個標記 node.static = isStatic(node)
  2. 然後在迴圈 children,如果 children 中出現了哪怕一個節點不是靜態節點,在將當前節點的標記修改成 falsenode.static = false

如何判斷一個節點是不是靜態節點?

也就是說 isStatic 這個函式是如何判斷靜態節點的?

function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}
複製程式碼

先解釋一下,在上文講的解析器中將 模板字串 解析成 AST 的時候,會根據不同的文字型別設定一個 type

type 說明
--- ---
1 元素節點
2 帶變數的動態文字節點
3 不帶變數的純文字節點

所以上面 isStatic 中的邏輯很明顯,如果 type === 2 那肯定不是 靜態節點 返回 false,如果 type === 3 那就是靜態節點,返回 true

那如果 type === 1,就有點複雜了,元素節點判斷是不是靜態節點的條件很多,我們們先一個個看。

首先如果 node.pretrue 直接認為當前節點是靜態節點,關於 node.pre 是什麼 請狠狠的點選我

其次 node.hasBindings 不能為 true

node.hasBindings 屬性是在解析器轉換 AST 時設定的,如果當前節點的 attrs 中,有 v-@:開頭的 attr,就會把 node.hasBindings 設定為 true

const dirRE = /^v-|^@|^:/
if (dirRE.test(attr)) {
  // mark element as dynamic
  el.hasBindings = true
}
複製程式碼

並且元素節點不能有 iffor屬性。

node.ifnode.for 也是在解析器轉換 AST 時設定的。

在解析的時候發現節點使用了 v-if,就會在解析的時候給當前節點設定一個 if 屬性。

就是說元素節點不能使用 v-if v-for v-else 等指令。

並且元素節點不能是 slotcomponent

並且元素節點不能是元件。

例如:

<List></List>
複製程式碼

不能是上面這樣的自定義元件

並且元素節點的父級節點不能是帶 v-fortemplate,檢視詳情 請狠狠的點選我

並且元素節點上不能出現額外的屬性。

額外的屬性指的是不能出現 type
tag attrsList attrsMap plain parent children attrs staticClass staticStyle 這幾個屬性之外的其他屬性,如果出現其他屬性則認為當前節點不是靜態節點。

只有符合上面所有條件的節點才會被認為是靜態節點。

如何標記所有節點?

上面講如何判斷單個節點是否是靜態節點,AST 是一棵樹,我們如何把所有的節點都打上標記(static)呢?

還有一個問題是,判斷 元素節點是不是靜態節點不能光看它自身是不是靜態節點,如果它的子節點不是靜態節點,那就算它自身符合上面講的靜態節點的條件,它也不是靜態節點

所以在 vue 中有這樣一行程式碼:

for (let i = 0, l = node.children.length; i < l; i++) {
  const child = node.children[i]
  markStatic(child)
  if (!child.static) {
    node.static = false
  }
}
複製程式碼

markStatic 可以給節點標記,規則上面剛講過,vue.js 通過迴圈 children 打標記,然後每個不同的子節點又會走相同的邏輯去迴圈它的 children 這樣遞迴下來所有的節點都會被打上標記。

然後在迴圈中判斷,如果某個子節點不是 靜態節點,那麼講當前節點的標記改為 false

這樣一圈下來之後 AST 上的所有節點都被準確的打上了標記。

如何標記靜態根節點?

標記靜態根節點其實也是遞迴的過程。

vue 中的實現大概是這樣的:

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    // For a node to qualify as a static root, it should have children that
    // are not just static text. Otherwise the cost of hoisting out will
    // outweigh the benefits and it's better off to just always render it fresh.
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
  }
}
複製程式碼

這段程式碼其實就一個意思:

當前節點是靜態節點,並且有子節點,並且子節點不是單個靜態文字節點這種情況會將當前節點標記為根靜態節點

額,,可能有點繞口,重新解釋下。

上面我們標記 靜態節點 的時候有一段邏輯是隻有所有 子節點 都是 靜態節點,當前節點才是真正的 靜態節點

所以這裡我們如果發現一個節點是 靜態節點,那就能證明它的所有 子節點 也都是靜態節點,而我們要標記的是 靜態根節點,所以如果一個靜態節點只包含了一個文字節點那就不會被標記為 靜態根節點

其實這麼做也是為了效能考慮,vue 在註釋中也說了,如果把一個只包含靜態文字的節點標記為根節點,那麼它的成本會超過收益~

總結一下

整體邏輯其實就是遞迴 AST 這顆樹,然後將 靜態節點靜態根節點 找到並打上標記。

程式碼生成器

程式碼生成器的作用是使用 element ASTs 生成 render 函式程式碼字串。

使用本文開頭舉的例子中的模板生成後的 AST 來生成 render 後是這樣的:

{
  render: `with(this){return _c('div',[_c('p',[_v(_s(name))])])}`
}
複製程式碼

格式化後是這樣的:

with(this){
  return _c(
    'div',
    [
      _c(
        'p',
        [
          _v(_s(name))
        ]
      )
    ]
  )
}
複製程式碼

生成後的程式碼字串中看到了有幾個函式呼叫 _c_v_s

_c 對應的是 createElement,它的作用是建立一個元素。

  1. 第一個引數是一個HTML標籤名
  2. 第二個引數是元素上使用的屬性所對應的資料物件,可選項
  3. 第三個引數是 children

例如:

一個簡單的模板:

<p title="Berwin" @click="c">1</p>
複製程式碼

生成後的程式碼字串是:

`with(this){return _c('p',{attrs:{"title":"Berwin"},on:{"click":c}},[_v("1")])}`
複製程式碼

格式化後:

with(this){
  return _c(
    'p',
    {
      attrs:{"title":"Berwin"},
      on:{"click":c}
    },
    [_v("1")]
  )
}
複製程式碼

關於 createElement 想了解更多請狠狠的點選我

_v 的意思是建立一個文字節點。

_s 是返回引數中的字串。

程式碼生成器的總體邏輯其實就是使用 element ASTs 去遞迴,然後拼出這樣的 _c('div',[_c('p',[_v(_s(name))])]) 字串。

那如何拼這個字串呢??

請看下面的程式碼:

function genElement (el: ASTElement, state: CodegenState) {
  const data = el.plain ? undefined : genData(el, state)
  const children = el.inlineTemplate ? null : genChildren(el, state, true)
	
  let code = `_c('${el.tag}'${
    data ? `,${data}` : '' // data
  }${
    children ? `,${children}` : '' // children
  })`
  
  return code
}
複製程式碼

因為 _c 的引數需要 tagNamedatachildren

所以上面這段程式碼的主要邏輯就是用 genDatagenChildren 獲取 datachildren,然後拼到 _c 中去,拼完後把拼好的 "_c(tagName, data, children)" 返回。

所以我們現在比較關心的兩個問題:

  1. data 如何生成的(genData 的實現邏輯)?
  2. children 如何生成的(genChildren 的實現邏輯)?

我們先看 genData 是怎樣的實現邏輯:

function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // ... 類似的還有很多種情況
  data = data.replace(/,$/, '') + '}'
  return data
}
複製程式碼

可以看到,就是根據 AST 上當前節點上都有什麼屬性,然後針對不同的屬性做一些不同的處理,最後拼出一個字串~

然後我們在看看 genChildren 是怎樣的實現的:

function genChildren (
  el: ASTElement,
  state: CodegenState
): string | void {
  const children = el.children
  if (children.length) {
    return `[${children.map(c => genNode(c, state)).join(',')}]`
  }
}

function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}
複製程式碼

從上面程式碼中可以看出,生成 children 的過程其實就是迴圈 AST 中當前節點的 children,然後把每一項在重新按不同的節點型別去執行 genElement genComment genText。如果 genElement 中又有 children 在迴圈生成,如此反覆遞迴,最後一圈跑完之後能拿到一個完整的 render 函式程式碼字串,就是類似下面這個樣子。

"_c('div',[_c('p',[_v(_s(name))])])"
複製程式碼

最後把生成的 code 裝到 with 裡。

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // 如果ast為空,則建立一個空div
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`
  }
}
複製程式碼

關於程式碼生成器的部分到這裡就說完了,其實原始碼中遠不止這麼簡單,很多細節我都沒有去說,我只說了一個大體的流程,對具體細節感興趣的同學可以自己去看原始碼瞭解詳情。

總結

本篇文章我們說了 vue 對模板編譯的整體流程分為三個部分:解析器(parser),優化器(optimizer)和程式碼生成器(code generator)。

解析器(parser)的作用是將 模板字串 轉換成 element ASTs

優化器(optimizer)的作用是找出那些靜態節點和靜態根節點並打上標記。

程式碼生成器(code generator)的作用是使用 element ASTs 生成 render函式程式碼(generate render function code from element ASTs)。

用一張圖來表示:

Vue 模板編譯原理

解析器(parser)的原理是一小段一小段的去擷取字串,然後維護一個 stack 用來儲存DOM深度,每擷取到一段標籤的開始就 pushstack 中,當所有字串都擷取完之後也就解析出了一個完整的 AST

優化器(optimizer)的原理是用遞迴的方式將所有節點打標記,表示是否是一個 靜態節點,然後再次遞迴一遍把 靜態根節點 也標記出來。

程式碼生成器(code generator)的原理也是通過遞迴去拼一個函式執行程式碼的字串,遞迴的過程根據不同的節點型別呼叫不同的生成方法,如果發現是一顆元素節點就拼一個 _c(tagName, data, children) 的函式呼叫字串,然後 datachildren 也是使用 AST 中的屬性去拼字串。

如果 children 中還有 children 則遞迴去拼。

最後拼出一個完整的 render 函式程式碼。

相關文章