Vue源碼解讀之AST語法樹(一)

什麼是AST

AST是指抽象語法樹(abstract syntax tree),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式。Vue在mount過程當中,template會被編譯成AST語法樹。
而後,通過generate(將AST語法樹轉化成render function字符串的過程)獲得render函數,返回VNode。VNode是Vue的虛擬DOM節點,裏面包含標籤名、子節點、文本等信息,關於VNode的學習來自:https://blog.csdn.net/qq_3626...
javascript

<div id="test">
    請輸入:<input type="text" v-model="message"><br/>
</div>

parse()

var stack = [];
  var preserveWhitespace = options.preserveWhitespace !== false;
  var root;
  var currentParent;
  var inVPre = false;
  var inPre = false;
  var warned = false;

  function warnOnce (msg){
  }
  function closeElement (element){
  }
  //調用parseHTML,這裏對options的內容省略
  parseHTML(template,options);

定義一些變量,root用於存放AST樹根節點,currentParent存放當前父元素,stack用來輔助樹創建的棧。接着調用parseHTML函數進行轉化,傳入template和options。
options的結構以下:html

clipboard.png

parseHTML()

parseHTML內容大綱java

clipboard.png

last = html;
    //確認html不是相似<script>,<style>這樣的純文本標籤
    if (!lastTag || !isPlainTextElement(lastTag)) {
      var textEnd = html.indexOf('<');//判斷html字符串是否以<開頭
      if (textEnd === 0) {
        // 這裏的Comment是Vue定義的正則表達式,判斷html是否是<!-- -->註釋
        //var comment = /^<!\--/;
        if (comment.test(html)) {
          var commentEnd = html.indexOf('-->');

          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              options.comment(html.substring(4, commentEnd));
            }
            advance(commentEnd + 3);
            continue
          }
        }
        //判斷是否處理向下兼容的註釋,相似<![if !IE]>
        //var conditionalComment = /^<!\[/;
        if (conditionalComment.test(html)) {
          var conditionalEnd = html.indexOf(']>');

          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2);
            continue
          }
        }

        //獲取<!DOCTYPE開頭的標籤內容
        // var doctype = /^<!DOCTYPE [^>]+>/i;
        var doctypeMatch = html.match(doctype);
        if (doctypeMatch) {
          advance(doctypeMatch[0].length);
          continue
        }

        //判斷此段html是否結束標籤
        // var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
        // var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
        // var ncname = '[a-zA-Z_][\\w\\-\\.]*';
        var endTagMatch = html.match(endTag);
        if (endTagMatch) {
          var curIndex = index;
          advance(endTagMatch[0].length);
          parseEndTag(endTagMatch[1], curIndex, index);
          continue
        }

        // 匹配開始標籤,獲取match對象
        var startTagMatch = parseStartTag();
        if (startTagMatch) {
          handleStartTag(startTagMatch);
          if (shouldIgnoreFirstNewline(lastTag, html)) {
            advance(1);
          }
          continue
        }
        var text = (void 0), rest = (void 0), next = (void 0);
      if (textEnd >= 0) {
        rest = html.slice(textEnd);
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
            // 處理文本中的<字符
          next = rest.indexOf('<', 1);
          if (next < 0) { break }
          textEnd += next;
          rest = html.slice(textEnd);
        }
        text = html.substring(0, textEnd);
        advance(textEnd);
      }

      if (textEnd < 0) {
        text = html;
        html = '';
      }

      if (options.chars && text) {
        options.chars(text);
      }
    } else {
      //代碼省略
    }

    if (html === last) {
      //代碼省略
    }
      }

clipboard.png

parseHTML使用while循環對傳進來的html進行解析。首先獲取<標籤索引var textEnd = html.indexOf('<');若是textEnd爲0 說明是標籤<xx>或者</xx>,再用正則匹配是否爲<!---->註釋標籤,若是不是,再判斷是否爲向下兼容放入註釋,如<![if !IE]>(詳情見),若是不是,再判斷是否已<!DOCTYPE開頭的文檔頭部標籤,若是不是,再判斷當前是否結束標籤</xx>。var endTagMatch = html.match(endTag); 匹配不到那麼就是開始標籤,調用parseStartTag()函數解析。正則表達式

parseStartTag()

function parseStartTag () {
    var start = html.match(startTagOpen);
    if (start) {
      var match = {
        tagName: start[1],//標籤名,本文的例子div
        attrs: [],
        start: index//0
      };//定義match對象
      advance(start[0].length);//index=4,html=" id="test">...
      var end, attr;
      //match對象的attrs
      //index=14
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length);
        match.attrs.push(attr);
      }
      // 在第二次while循環後 end匹配到結束標籤 => ['>','']
      if (end) {
        match.unarySlash = end[1];
        advance(end[0].length);
        match.end = index;
        return match
      }
    }
  }

parseStartTag()構建一個match對象,對象裏面包含標籤名(tagName),標籤屬性(attrs),<左開始標籤的位置(start),>右開始標籤的位置(end)。本文的例子,程序第一次進入該函數,因此tagName:div,start:0,end:14 match數組

function advance (n) {
    index += n;
    html = html.substring(n);
  }

advance函數將局部變量index日後推 並切割字符串。app

handleStartTag()

function handleStartTag (match) {
    var tagName = match.tagName;
    var unarySlash = match.unarySlash;

    if (expectHTML) {
    //段落式元素
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        parseEndTag(lastTag);
      }
      // 能夠省略閉合標籤
      if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
        parseEndTag(tagName);
      }
    }

    var unary = isUnaryTag$$1(tagName) || !!unarySlash;

    var l = match.attrs.length;
    var attrs = new Array(l);
    //解析html屬性值{name:'id',value:'test'}的格式
    for (var i = 0; i < l; i++) {
      var args = match.attrs[i];
      if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
        if (args[3] === '') { delete args[3]; }
        if (args[4] === '') { delete args[4]; }
        if (args[5] === '') { delete args[5]; }
      }
      var value = args[3] || args[4] || args[5] || '';
      var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
        ? options.shouldDecodeNewlinesForHref
        : options.shouldDecodeNewlines;
      attrs[i] = {
        name: args[1],
        // 處理轉義字符
        value: decodeAttr(value, shouldDecodeNewlines)
      };
    }
// 將切割出來的字符串轉換爲AST
    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);
    }
  }

在該函數中,對match進行了二次處理,根據標籤名、屬性生成一個新對象,push到最開始的stack數組中。
因爲匹配的是起始標籤,因此也會以這個標籤名結束,此處的lastTag就是div。
函數最後調用了parse內部聲明的方法startide

function start (tag, attrs, unary) {
      //檢查命名空間是不是svg或者math
      var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);

      // handle IE svg bug
      /* istanbul ignore if */
      if (isIE && ns === 'svg') {
        attrs = guardIESVGBug(attrs);
      }

      //建立element元素,element其實就是{type: 1,
      //tag: "div",
      //attrsList: [{name: "id", value: "test"}]],
      //attrsMap: makeAttrsMap(attrs), //parent:undefined
      //children: []}的一個對象
      var element = createASTElement(tag, attrs, currentParent);
      if (ns) {
        element.ns = ns;
      }
    
      //排除script,style標籤
      if (isForbiddenTag(element) && !isServerRendering()) {
        element.forbidden = true;
        "development" !== 'production' && warn$2(
          'Templates should only be responsible for mapping the state to the ' +
          'UI. Avoid placing tags with side-effects in your templates, such as ' +
          "<" + tag + ">" + ', as they will not be parsed.'
        );
      }

      // apply pre-transforms
      for (var i = 0; i < preTransforms.length; i++) {
      //若html裏面有v-model等指令,經過preTransforms進行轉換
        element = preTransforms[i](element, options) || element;
      }

      if (!inVPre) {
      // 判斷是否有v-pre屬性
        processPre(element);
        if (element.pre) {
          inVPre = true;
        }
      }
      //判斷標籤名是否是pre
      if (platformIsPreTag(element.tag)) {
        inPre = true;
      }
      if (inVPre) {
        processRawAttrs(element);
      } else if (!element.processed) {
        // 處理v-for
        processFor(element);
        // 處理v-if
        processIf(element);
        // 處理v-once
        processOnce(element);
        // element-scope stuff
        processElement(element, options);
      }

      // 樹結構的root節點處理
      if (!root) {
        root = element;
        checkRootConstraints(root);
      } else if (!stack.length) {
        // allow root elements with v-if, v-else-if and v-else
        if (root.if && (element.elseif || element.else)) {
          checkRootConstraints(element);
          addIfCondition(root, {
            exp: element.elseif,
            block: element
          });
        } else {
          warnOnce(
            "Component template should contain exactly one root element. " +
            "If you are using v-if on multiple elements, " +
            "use v-else-if to chain them instead."
          );
        }
      }
      if (currentParent && !element.forbidden) {
        if (element.elseif || element.else) {
          processIfConditions(element, currentParent);
        } else if (element.slotScope) { // scoped slot
          currentParent.plain = false;
          var name = element.slotTarget || '"default"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
        } else {
          currentParent.children.push(element);
          element.parent = currentParent;
        }
      }
      if (!unary) {
        currentParent = element;
        stack.push(element);
      } else {
        closeElement(element);
      }
    }

對標籤名進行校驗,同時對屬性進行更細緻的處理,如v-pre,v-for,v-if等。最後調用processElement(element, options)對當前的樹節點元素進行處理,具體以下:svg

processKey(element);
  // 檢測是不是空屬性節點
  element.plain = !element.key && !element.attrsList.length;
 // 處理:ref或v-bind:ref屬性
  processRef(element);
  //處理標籤名爲slot的狀況
  processSlot(element);
  // 處理is或v-bind:is屬性
  processComponent(element);
  for (var i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element;
  }
  //處理屬性
  processAttrs(element);

start()生成element對象,再鏈接元素的parent和children節點,最後push到棧中,此時棧中第一個元素生成。結構以下:函數

clipboard.png

接下來開始第二次循環,html變成了 請輸入:<input type="text" v-model="message">

</div>,所以此次解析的是文字:'請輸入',具體代碼分析在下一次~~~學習

相關文章
相關標籤/搜索