【vuejs深入三】vue源碼解析之二 htmlParse解析器的實現


寫在前面

  一個好的架構需要經過血與火的歷練,一個好的工程師需要經過無數項目的摧殘。

  昨天博主分析了一下在vue中,最為基礎核心的api,parse函數,它的作用是將vue的模板字符串轉換成ast,從而構建vnode,構建指令,實現virtual dom,然后在這基礎之上實現雙向綁定等。【vuejs深入二】vue源碼解析之一,基礎源碼結構和htmlParse解析器

  今天博主就來詳細的實現一個擁有核心功能的htmlParse函數,看看它內部的實現邏輯,了解它是怎么樣去解析一個vue模板的。

小目標

  我們最終的目標是將html轉換成ast對象,那么首先我們定一個小目標:

<div id="div1"></div>

  我希望將上面的html解析成ast格式,類似於下面:

{
  "tag":"div",
  "attrs":[
            {  "id":"div1" }
   ],
   "children":[],
   "type":1
}    

       最終想要達成的第一個小目標是可以將div標簽字符串輸出成這樣一個object格式,tag表示標簽名稱,attrs表示屬性,children表示這個div所有的子節點,type的話表示節點的類型,我們今天只三個類型:

1.元素類型,也就是標簽類型,所有用<tag attr=""></tag>這樣的標簽。2.變量text,現在我們實現一個{{text}}的變量轉換,它其實是一個節點。3.普通文本,普通文本包括普通文字和空格、換行。

 

基本結構

  基本結構的設計決定的代碼能擴展多遠,如果一開始結構設計錯誤,最后在新加入的功能無法嵌入的時候,那就只有重構一條路可以走了。

  首先理清楚我們的思路。

  匹配單個字符》匹配標簽》匹配屬性》匹配文本》匹配結束標簽

  然后,你想啊,html標簽都是有開始,有結束的。那么這里問題就來了,可以想到的方式,解析一個標簽的開始與結束吧,例如我們使用正則匹配開始標簽<div id='div1'> 然后找到結束標簽</div>,這樣是不是就可以解析div里面的內容了?

       難。

      開始標簽比較好找,結束標簽就惡心了,例如 <div><div></div></div> ,,完了,怎么區分嵌套關系?第一個<div>到底匹配哪一個結束標簽?

     這個思路是錯的,很難。

  那么我們換個思路,如果我們單個字符匹配呢,

 例如我們匹配一個 <div><div></div></div>,

   ok 腦補步驟

 1。匹配到 <   匹配到這個字符我就可以認為,后面的要么是開始標簽,要么是結束標簽。

    2。用正則匹配從<到后面的字符,如果是開始標簽,現在記錄一下,啊,我遇到了一個開始標簽<div>  順便用正則記錄attrs

    3. 現在我們匹配走走走。。。走到<div></div></div>   

    4.又匹配到一個 < 老步驟啊。

    5.發現是開始標簽,再次記錄,啊,我又遇到一個開始標簽  <div>  順便用正則記錄attrs

   6. 現在我們匹配走走走。。。走到</div></div>  

   7. 又匹配到一個 < 老步驟啊。

   8.發現是一個結束標簽</div> ,嗯?結束標簽!它是誰的結束標簽?想一想。。。。。。應該是最后一個遇到的開始標簽吧。  第一個遇到的結束標簽不就是最后一個開始標簽的結束么?

   9.啊,結束了一個。 

   10.再匹配,再完成。

    恩。。。思路清晰了有沒有,來實現走一個:

  //轉化HTML至AST對象
  function parse(template){
    var currentParent; //當前父節點
    var root; //最終生成的AST對象
    var stack = []; //插入棧
    var startStack = []; //開始標簽棧
    var endStack = [];  //結束標簽棧
    //console.log(template);
    parseHTML(template,{
      start:function start(targetName,attrs,unary,start,end,type,text){//標簽名 ,attrs,是否結束標簽,文本開始位置,文本結束位置,type,文本,
        var element = {   //我們想要的對象
          tag:targetName,
          attrsList:attrs,
          parent:currentParent,  //需要記錄父對象吧
          type:type,
          children:[]
        }
        if(!root){ //根節點哈
          root = element;
        }
        if(currentParent && !unary){ //有父節點並且不是結束標簽?
          currentParent.children.push(element);  //插入到父節點去
          element.parent = currentParent;  //記錄父節點
        }
        if (!unary) {  //不是結束標簽?
            if(type == 1){
               currentParent = element;//不是結束標簽,當前父節點就要切換到現在匹配到的這個開始標簽哈,后面再匹配到
               startStack.push(element);  //推入開始標簽棧
            }
             stack.push(element);  //推入總棧
         }else{
           endStack.push(element);  //推入結束標簽棧
           currentParent = startStack[endStack.length-1].parent;   //結束啦吧當前父節點切到上一個開始標簽,這能理解吧,當前這個已經結束啦
         }
         //console.log(stack,"currentstack")
      },
      end:function end(){

      },
      chars:function chars(){

      }
    });
    console.log(root,"root");
    return root;
  };
  /**
   * Not type-checking this file because it's mostly vendor code.
   */

  /*!
   * HTML Parser By John Resig (ejohn.org)
   * Modified by Juriy "kangax" Zaytsev
   * Original code by Erik Arvidsson, Mozilla Public License
   * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
   */

  // Regular Expressions for parsing tags and attributes
  var singleAttrIdentifier = /([^\s"'<>/=]+)/;
  var singleAttrAssign = /(?:=)/;
  var singleAttrValues = [
    // attr value double quotes
    /"([^"]*)"+/.source,
    // attr value, single quotes
    /'([^']*)'+/.source,
    // attr value, no quotes
    /([^\s"'=<>`]+)/.source
  ];
  var attribute = new RegExp(
    '^\\s*' + singleAttrIdentifier.source +
    '(?:\\s*(' + singleAttrAssign.source + ')' +
    '\\s*(?:' + singleAttrValues.join('|') + '))?'
  );
  // could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
  // but for Vue templates we can enforce a simple charset
  var ncname = '[a-zA-Z_][\\w\\-\\.]*';
  var qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')';
  var startTagOpen = new RegExp('^<' + qnameCapture);
  var startTagClose = /^\s*(\/?)>/;
  var endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>');
  var doctype = /^<!DOCTYPE [^>]+>/i;
  var comment = /^<!--/;
  var conditionalComment = /^<!\[/;

//偷懶哈  上面的正則是我在vue上拿下來的,這個后期可以研究,下面的話簡單的寫兩個用用,和vue原版的是有一些差別的

    //{{變量}}

  var varText = new RegExp('{{' + ncname + '}}');
  //空格與換行符
  var space = /^\s/;
  var checline = /^[\r\n]/;
    /**
      type 1普通標簽
      type 2代碼
      type 3普通文本
    */
  function parseHTML(html,options){
    var stack = []; //內部也要有一個棧
    var index = 0;  //記錄的是html當前找到那個索引啦
    var last; //用來比對,當這些條件都走完后,如果last==html 說明匹配不到啦,結束while循環
    var isUnaryTag = false;

    while(html){
      last = html;
      var textEnd = html.indexOf('<');
      if(textEnd === 0){ //這一步如果第一個字符是<那么就只有兩種情況,1開始標簽  2結束標簽
        //結束標簽
        var endTagMatch = html.match(endTag); //匹配
        if(endTagMatch){
          console.log(endTagMatch,"endTagMatch");
          isUnaryTag = true;
          var start = index;
          advance(endTagMatch[0].length); //匹配完要刪除匹配到的,並且更新index,給下一次匹配做工作
          options.start(null,null,isUnaryTag,start,index,1);
          continue;
        }
        //初始標簽
        var startMatch = parseStartTag();
        if(startMatch){
          parseStartHandler(startMatch);//封裝處理下
          console.log(stack,"startMatch");
          continue;
        }
      }

      if(html === last){
        console.log(html,"html");
       break;
      }
    }
    function advance (n) {
      index += n;
      html = html.substring(n);
    }
    //處理起始標簽 主要的作用是生成一個match 包含初始的attr標簽
    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))) {//在endClose之前尋找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進行二次處理,生成對象推入棧
    function parseStartHandler(match){
      var _attrs = new Array(match.attrs.length);
      for(var i=0,len=_attrs.length;i<len;i++){  //這兒就是找attrs的代碼哈
        var args = match.attrs[i];
        var value = args[3] || args[4] || args[5] || '';
        _attrs[i] = {
          name:args[1],
          value:value
        }
      }
      stack.push({tag: match.tagName,type:1, lowerCasedTag: match.tagName.toLowerCase(), attrs: _attrs}); //推棧
      options.start(match.tagName, _attrs,false, match.start, match.end,1);  //匹配開始標簽結束啦。
    }

  }

我們執行   parse("<div id='test1'><div></div></div>");  大功告成哈哈哈哈哈  呃。

    

神馬,你還想問我細節問題?

正好給你培養一下讀代碼的能力哈,思路有了,代碼有了,拉下去調試調試哈。當然博主在下一章還會詳細介紹的。

寫在后面

  mvvm框架和webpack的出現確實改變了前端的開發方式,使得學習前端變成了一門有着深入學問的課題。在我們日常開發中應該不斷地學習,歸納,總結,尋找新的思想,對原有的代碼有好的補充和好的改進。

       寫的不好,謝謝大家觀看。 后續有空會新增更多關於開發的知識分享。  

       如果你有什么疑問,你可以聯系我,或者在下方評論。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM