1. 程式人生 > 其它 >html呼叫rpst 原始碼_parseHTML 函式原始碼解析(四) AST 基本形成

html呼叫rpst 原始碼_parseHTML 函式原始碼解析(四) AST 基本形成

技術標籤:html呼叫rpst 原始碼

接上文:

李李:parseHTML 函式原始碼解析(三)​zhuanlan.zhihu.com

在上篇文章中我們已經把整個詞法分析的解析過程分析完畢了。

例如有html(template)字串:

<div id="app">
  <p>{{ message }}</p>
</div>

產出如下:

{
attrs: [" id="app"", "id", "=", "app", undefined, undefined]
end: 14
start: 0
tagName: "div"
unarySlash: ""
}

{
attrs: []
end: 21
start: 18
tagName: "p"
unarySlash: ""
}

看到這不禁就有疑問? 這難道就是AST(抽象語法樹)??

非常明確的告訴你答案:No 這不是我們想要的AST,parse 階段最終生成的這棵樹應該是與如上html(template)字串的結構一一對應的:

├── div
│   ├── p
│   │   ├── 文字

如果每一個節點我們都用一個 javascript 物件來表示的話,那麼 div 標籤可以表示為如下物件:

{
  type: 1,
  tag: "div"
}

由於每個節點都存在一個父節點和若干子節點,所以我們為如上物件新增兩個屬性:parent 和 children ,分別用來表示當前節點的父節點和它所包含的子節點:

{
  type: 1,
  tag:"div",
  parent: null,
  children: []
}

同時每個元素節點還可能包含很多屬性 (attributes),所以我們可以為每個節點新增attrsList屬性,用來儲存當前節點所擁有的屬性:

{
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
}

按照以上思路去描述之前定義的 html 字串,那麼這棵抽象語法樹應該長成如下這個樣子:

{
  type: 1,
  tag: "div",
  parent: null,
  attrsList: [],
  children: [{
      type: 1,
      tag: "p",
      parent: div,
      attrsList: [],
      children:[
         {
          type: 3,
          tag:"",
          parent: p,
          attrsList: [],
          text:"{{ message }}"
         }
       ]
  }],
}

實際上構建抽象語法樹的工作就是建立一個類似如上所示的一個能夠描述節點關係的物件樹,節點與節點之間通過 parent 和 children 建立聯絡,每個節點的 type 屬性用來標識該節點的類別,比如 type 為 1 代表該節點為元素節點,type 為 3 代表該節點為文字節點。

這裡可參考NodeType:https://www.w3school.com.cn/jsref/prop_node_nodetype.asp

回顧我們所學的 parseHTML 函式可以看出,他只是在生成 AST 中的一個重要環節並不是全部。 那在Vue中是如何把html(template)字串編譯解析成AST的呢?

在原始碼中:

function parse (html) {
  var root;
  
  parseHTML(html, {
   start: function (tag, attrs, unary) {
      // 省略...
    },
    end: function (){
      // 省略...
    }
  }) 
  return root
}

可以看到Vue在進行模板編譯詞法分析階段呼叫了parse函式,parse函式返回root,其中root 所代表的就是整個模板解析過後的 AST,這中間還有兩個非常重要的鉤子函式,之前我們沒有講到的,options.start 、options.end。

接下來重點就來看看他們做了什麼。

假設解析的html字串如下:

<div></div>

這是一個沒有任何子節點的div 標籤。如果要解析它,我們來簡單寫下程式碼。

function parse (html) {
  var root;
  
  parseHTML(html, {
   start: function (tag, attrs, unary) {
      var element = {
        type: 1,
        tag: tag,
        parent: null,
        attrsList: attrs,
        children: []
      }
      if (!root) root = element
    },
    end: function (){
      // 省略...
    }
  }) 
  return root
}

如上: 在start 鉤子函式中首先定義了 element 變數,它就是元素節點的描述物件,接著判斷root 是否存在,如果不存在則直接將 element 賦值給 root 。當解析這段 html 字串時首先會遇到 div 元素的開始標籤,此時 start 鉤子函式將被呼叫,最終 root 變數將被設定為:

{
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
}

html 字串複雜度升級: 比之前的 div 標籤多了一個子節點,span 標籤。

<div>
  <span></span>
</div>

此時需要把程式碼重新改造。

function parse (html) {
  var root;
  var currentParent;

  parseHTML(html, {
   start: function (tag, attrs, unary) {
      var element = {
        type: 1,
        tag: tag,
        parent: null,
        attrsList: attrs,
        children: []
      }
      if (!root){
        root = element;
       }else if(currentParent){
        currentParent.children.push(element)
      }
      if (!unary) currentParent = element
    },
    end: function (){
      // 省略...
    }
  }) 
  return root
}

我們知道當解析如上 html 字串時首先會遇到 div 元素的開始標籤,此時 start 鉤子函式被呼叫,root變數被設定為:

{
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
}

還沒完可以看到在 start 鉤子函式的末尾有一個 if 條件語句,當一個元素為非一元標籤時,會設定 currentParent 為該元素的描述物件,所以此時currentParent也是:

{
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
}

接著解析 html (template)字串,會遇到 span 元素的開始標籤,此時root已經存在,currentParent 也存在,所以會將 span 元素的描述物件新增到 currentParent 的 children 陣列中作為子節點,所以最終生成的 root 描述物件為:

{
  type: 1,
  tag:"div",
  parent: null,
  attrsList: []
  children: [{
     type: 1,
     tag:"span",
     parent: div,
     attrsList: [],
     children:[]
  }], 
}

到目前為止好像沒有問題,但是當html(template)字串複雜度在升級,問題就體現出來了。

<div>
 <span></span>
 <p></p>
</div>

在之前的基礎上 div 元素的子節點多了一個 p 標籤,到解析span標籤的邏輯都是一樣的,但是解析 p 標籤時候就有問題了。

注意這個程式碼:

if (!unary) currentParent = element

在解析 p 元素的開始標籤時,由於 currentParent 變數引用的是 span 元素的描述物件,所以p 元素的描述物件將被新增到 span 元素描述物件的 children 陣列中,被誤認為是 span 元素的子節點。而事實上 p 標籤是 div 元素的子節點,這就是問題所在。

為了解決這個問題,就需要我們額外設計一個回退的操作,這個回退的操作就在end鉤子函式裡面實現。

這是一個什麼思路呢?舉個例子在解析div 的開始標籤時:

stack = [{tag:"div"...}]

在解析span 的開始標籤時:

stack = [{tag:"div"...},{tag:"span"...}]

在解析span 的結束標籤時:

stack = [{tag:"div"...}]

在解析p 的開始標籤時:

stack = [{tag:"div"...},{tag:"p"...}]

在解析p 的標籤時:

這樣的一個回退操作看懂了嗎? 這就能保證在解析p開始標籤的時候,stack中儲存的是p標籤父級元素的描述物件。

接下來繼續改造我們的程式碼。

function parse (html) {
  var root;
  var currentParent;
  var stack = [];  

  parseHTML(html, {
   start: function (tag, attrs, unary) {
      var element = {
        type: 1,
        tag: tag,
        parent: null,
        attrsList: attrs,
        children: []
      }
      if (!root){
        root = element;
       }else if(currentParent){
        currentParent.children.push(element)
      }
      if (!unary){
          currentParent = element;
          stack.push(currentParent);
       } 
    },
    end: function (){
      stack.pop();
      currentParent = stack[stack.length - 1]
    }
  }) 
  return root
}

通過上述程式碼,每當遇到一個非一元標籤的結束標籤時,都會回退 currentParent 變數的值為之前的值,這樣我們就修正了當前正在解析的元素的父級元素。

以上就是根據 parseHTML 函式生成 AST 的基本方式,但實際上還不完美在Vue中還會去處理一元標籤,文字節點和註釋節點等等。

接下來你是否迫不及待要進入到原始碼部分去看看了? 但Vue這塊程式碼稍微複雜點,我們還需要有一些前期的預備知識。

接下文:

李李:parseHTML 函式原始碼解析(五) AST 預備知識​zhuanlan.zhihu.com