Vue源碼解析之Template轉化爲AST的實現方法

這篇文章主要介紹了Vue源碼解析之Template轉化爲AST的實現方法,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

什麼是AST

在Vue的mount過程中,template會被編譯成AST語法樹,AST是指抽象語法樹(abstract syntax tree或者縮寫爲AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式。

Virtual Dom

Vue的一個厲害之處就是利用Virtual DOM模擬DOM對象樹來優化DOM操作的一種技術或思路。

Vue源碼中虛擬DOM構建經歷 template編譯成AST語法樹 -> 再轉換爲render函數 最終返回一個VNode(VNode就是Vue的虛擬DOM節點)

本文通過對源碼中AST轉化部分進行簡單提取,因爲源碼中轉化過程還需要進行各種兼容判斷,非常複雜,所以筆者對主要功能代碼進行提取,用了300-400行代碼完成對template轉化爲AST這個功能。下面用具體代碼進行分析。

 function parse(template) {
    var currentParent;  //當前父節點
    var root;      //最終返回出去的AST樹根節點
    var stack = [];
    parseHTML(template, {
      start: function start(tag, attrs, unary) {
        ......
      },
      end: function end() {
       ......
      },
      chars: function chars(text) {
        ......
      }
    })
    return root
  }

第一步就是調用parse這個方法,把template傳進來,這裏假設template爲 <div id="app"><span>{{message}}</span></div>

然後聲明3個變量

currentParent -> 存放當前父元素,root -> 最終返回出去的AST樹根節點,stack -> 一個棧用來輔助樹的建立

接着調用parseHTML函數進行轉化,傳入template和options(包含3個方法 start,end,chars 等下用到這3個函數再進行解釋)接下來先看parseHTML這個方法

function parseHTML(html, options) {
    var stack = [];  //這裏和上面的parse函數一樣用到stack這個數組 不過這裏的stack只是爲了簡單存放標籤名 爲了和結束標籤進行匹配的作用
    var isUnaryTag$$1 = isUnaryTag;  //判斷是否爲自閉合標籤
    var index = 0;
    var last;
    while (html) {
      //  第一次進入while循環時,由於字符串以<開頭,所以進入startTag條件,並進行AST轉換,最後將對象彈入stack數組中
      last = html;
      var textEnd = html.indexOf('<');
      if (textEnd === 0) {   // 此時字符串是不是以<開頭
        // End tag:
        var endTagMatch = html.match(endTag);
        if (endTagMatch) {
          var curIndex = index;
          advance(endTagMatch[0].length);
          parseEndTag(endTagMatch[1], curIndex, index);
          continue
        }

        // Start tag:  // 匹配起始標籤
        var startTagMatch = parseStartTag();  //處理後得到match
        if (startTagMatch) {
          handleStartTag(startTagMatch);
          continue
        }
      }

      // 初始化爲undefined 這樣安全且字符數少一點
      var text = (void 0), rest = (void 0), next = (void 0);
      if (textEnd >= 0) {   // 截取<字符索引 => </div> 這裏截取到閉合的<
        rest = html.slice(textEnd); //截取閉合標籤
        // 處理文本中的<字符
        // 獲取中間的字符串 => {{message}}
        text = html.substring(0, textEnd); //截取到閉合標籤前面部分
        advance(textEnd);        //切除閉合標籤前面部分

      }
      // 當字符串沒有<時
      if (textEnd < 0) {
        text = html;
        html = '';
      }
      // // 處理文本
      if (options.chars && text) {
        options.chars(text);
      }
    }
  }

函數進入while循環對html進行獲取<標籤索引 var textEnd = html.indexOf('<');如果textEnd === 0 說明當前是標籤<xxx>或者</xxx> 再用正則匹配是否當前是結束標籤</xxx>。var endTagMatch = html.match(endTag); 匹配不到那麼就是開始標籤,調用parseStartTag()函數解析。

function parseStartTag() {   //返回匹配對象
  var start = html.match(startTagOpen);     // 正則匹配
  if (start) {
    var match = {
      tagName: start[1],    // 標籤名(div)
      attrs: [],        // 屬性
      start: index       // 遊標索引(初始爲0)
    };
    advance(start[0].length);
    var end, attr;
    while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { 
      advance(attr[0].length); 
      match.attrs.push(attr);
    }
    if (end) {
      advance(end[0].length);   // 標記結束位置
      match.end = index;   //這裏的index 是在 parseHTML就定義 在advance裏面相加
      return match     // 返回匹配對象 起始位置 結束位置 tagName attrs
    }
  }
}

該函數主要是爲了構建一個match對象,對象裏面包含tagName(標籤名),attrs(標籤的屬性),start(<左開始標籤在template中的位置),end(>右開始標籤在template中的位置) 如template = <div id="app"><div><span>{{message}}</span></div></div> 程序第一次進入該函數 匹配的是div標籤 所以tagName就是div
start:0 end:14 如圖:

接着把match返回出去 作爲調用handleStartTag的參數

var startTagMatch = parseStartTag();  //處理後得到match
if (startTagMatch) {
  handleStartTag(startTagMatch);
  continue
}

接下來看handleStartTag這個函數:

 function handleStartTag(match) {
  var tagName = match.tagName;
  var unary = isUnaryTag$$1(tagName) //判斷是否爲閉合標籤 
  var l = match.attrs.length;
  var attrs = new Array(l);
  for (var i = 0; i < l; i++) {
    var args = match.attrs[i];
    var value = args[3] || args[4] || args[5] || '';
    attrs[i] = {
      name: args[1],
      value: value
    };
  }
  if (!unary) {
    stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs});
    lastTag = tagName;
  }
  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end);
  }
  }

函數中分爲3部分 第一部分是for循環是對attrs進行轉化,我們從上一步的parseStartTag()得到的match對象中的attrs屬性如圖

當時attrs是上面圖這樣子滴 我們通過這個循環把它轉化爲只帶name 和 value這2個屬性的對象 如圖:

接着判斷如果不是自閉合標籤,把標籤名和屬性推入棧中(注意 這裏的stack這個變量在parseHTML中定義,作用是爲了存放標籤名 爲了和結束標籤進行匹配的作用。)接着調用最後一步 options.start 這裏的options就是我們在parse函數中 調用parseHTML是傳進來第二個參數的那個對象(包含start end chars 3個方法函數) 這裏開始看options.start這個函數的作用:

start: function start(tag, attrs, unary) {
  var element = {
    type: 1,
    tag: tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    parent: currentParent,
    children: []
  };
  processAttrs(element);
  if (!root) {
    root = element;
  } 
  if(currentParent){
    currentParent.children.push(element);
    element.parent = currentParent;
  }
  if (!unary) {
    currentParent = element;
    stack.push(element);
  }
}

這個函數中 生成element對象 再連接元素的parent 和 children節點 最終push到棧中

此時棧中第一個元素生成 如圖:

完成了while循環的第一次執行,進入第二次循環執行,這個時候html變成<span>{{message}}</span></div> 接着截取到<span> 處理過程和第一次一致 經過這次循環stack中元素如圖:

接着繼續執行第三個循環 這個時候是處理文本節點了 {{message}}

// 初始化爲undefined 這樣安全且字符數少一點
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) {   // 截取<字符索引 => </div> 這裏截取到閉合的<
  rest = html.slice(textEnd); //截取閉合標籤
  // 處理文本中的<字符
  // 獲取中間的字符串 => {{message}}
  text = html.substring(0, textEnd); //截取到閉合標籤前面部分
  advance(textEnd);        //切除閉合標籤前面部分
}
// 當字符串沒有<時
if (textEnd < 0) {
  text = html;
  html = '';
}
// 另外一個函數
if (options.chars && text) {
  options.chars(text);
}

這裏的作用就是把文本提取出來 調用options.chars這個函數 接下來看options.chars

chars: function chars(text) {
  if (!currentParent) {  //如果沒有父元素 只是文本
    return
  }

  var children = currentParent.children; //取出children
  // text => {{message}}
  if (text) {
    var expression;
    if (text !== ' ' && (expression = parseText(text))) {
      // 將解析後的text存進children數組
      children.push({
        type: 2,
        expression: expression,
        text: text
      });
    } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
      children.push({
        type: 3,
        text: text
      });
    }
  }
}
})

這裏的主要功能是判斷文本是{{xxx}}還是簡單的文本xxx,如果是簡單的文本 push進父元素的children裏面,type設置爲3,如果是字符模板{{xxx}},調用parseText轉化。如這裏的{{message}}轉化爲 _s(message)(加上_s是爲了AST的下一步轉爲render函數,本文中暫時不會用到。) 再把轉化後的內容push進children。

又走完一個循環了,這個時候html = </span></div> 剩下2個結束標籤進行匹配了

 var endTagMatch = html.match(endTag);
  if (endTagMatch) {
    var curIndex = index;
    advance(endTagMatch[0].length);
    parseEndTag(endTagMatch[1], curIndex, index);
    continue
  }

接下來看parseEndTag這個函數 傳進來了標籤名 開始索引和結束索引

 function parseEndTag(tagName, start, end) {
  var pos, lowerCasedTagName;
  if (tagName) {
    lowerCasedTagName = tagName.toLowerCase();
  }
  // Find the closest opened tag of the same type
  if (tagName) { // 獲取最近的匹配標籤
    for (pos = stack.length - 1; pos >= 0; pos--) {
      // 提示沒有匹配的標籤
      if (stack[pos].lowerCasedTag === lowerCasedTagName) {
        break
      }
    }
  } else {
    // If no tag name is provided, clean shop
    pos = 0;
  }
  
  if (pos >= 0) {
    // Close all the open elements, up the stack
    for (var i = stack.length - 1; i >= pos; i--) {
      if (options.end) {
        options.end(stack[i].tag, start, end);
      }
    }
  
    // Remove the open elements from the stack
    stack.length = pos;
    lastTag = pos && stack[pos - 1].tag;
}

這裏首先找到棧中對應的開始標籤的索引pos,再從該索引開始到棧頂的所以元素調用options.end這個函數

 end: function end() {
  // pop stack
  stack.length -= 1;
  currentParent = stack[stack.length - 1];
},

把棧頂元素出棧,因爲這個元素已經匹配到結束標籤了,再把當前父元素更改。終於走完了,把html的內容循環完,最終return root 這個root就是我們所要得到的AST

這只是Vue的冰山一角,文中有什麼不對的地方請大家幫忙指正,本人最近也一直在學習Vue的源碼,希望能夠拿出來與大家一起分享經驗,接下來會繼續更新後續的源碼,如果覺得有幫忙請給個Star哈

github地址爲:https://github.com/zwStar/vue-ast 歡迎各位star或issues

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章