vue剖析之template模板剖析AST

經由歷程檢察vue源碼,可以曉得Vue源碼中運用了假造DOM(Virtual Dom),假造DOM構建閱歷 template編譯成AST語法樹 -> 再轉換為render函數 終究返回一個VNode(VNode就是Vue的假造DOM節點) 。
本文經由歷程對Vue源碼中的AST轉化部份舉行簡樸提取,返回靜態的AST構造(不斟酌兼容性及屬性的詳細剖析)。並終究依據一個實例的template轉化為終究的AST構造。

什麼是AST

在Vue的mount歷程當中,template會被編譯成AST語法樹,AST是指籠統語法樹(abstract syntax tree或許縮寫為AST),或許語法樹(syntax tree),是源代碼的籠統語法構造的樹狀表現情勢。

代碼剖析

起首、定義一個簡樸的html DOM構造、个中包括比較罕見的標籤、文本以及解釋,用來天生AST構造。

  <div id="app" class="demo">
    <!-- 注重看解釋 -->
    <p>
      <b>很粗</b>
    </p>
    很簡樸,我就是一程序員
    <br/>
    <h1>
      姓名:{{name}},歲數:{{age}},
      請聯絡我吧
    </h1>
  </div>
  <script>
    var vm = new Vue({
      el: '#app',
      // template: '#template',
      // template: 'string template',
      // template: document.querySelector('#template'),
      data () {
        return {
          name: 'Jeffery',
          age: '26'
        }
      },
      comments: true, // 是不是保留解釋
      // delimiters: ['{', '}'] // 定義分隔符,默以為"{{}}"
    })
  </script>

關於轉成AST,則須要先獵取template,關於這部份內容,做一個簡樸的剖析,詳細的請自行檢察Vue源碼。
詳細目次請參考: ‘/src/platforms/web/entry-runtime-with-compiler’
從vue官網中曉得,vue供應了兩個版本,完整版和只包括運行時版,差別是完整版包括編譯器,就是將template模板編譯成AST,再轉化為render函數的歷程,因而只包括運行時版必需供應render函數。
注重:此處處置懲罰比較簡樸,只是為了獵取template,以便用於天生AST。

  function Vue (options) {
    // 假如沒有供應render函數,則處置懲罰template,不然直接運用render函數
    if (!options.render) {
      let template = options.template;
      // 假如供應了template模板
      if (template) {
        // template: '#template',
        // template: '<div></div>',
        if (typeof template === 'string') {
          // 假如為'#template'
          if (template.charAt(0) === '#') {
            let tpl = query(template);
            template = tpl ? tpl.innerHTML : '';
          }
          // 不然不做處置懲罰,如:'<div></div>'
        } else if (template.nodeType) {
          // 假如模板為DOM節點,如:template: document.querySelector('#template')
          // 比方:<script type="text/x-template" id="template"></script>
          template = template.innerHTML;
        }
      } else if (options.el) {
        // 假如沒有模板,則運用el
        template = getOuterHTML(query(options.el));
      }

      if (template) {   
        // 將template模板編譯成AST(此處省略一系列函數、參數處置懲罰歷程,詳細見下圖及源碼)
        let ast = null;
        ast = parse(template, options);
        console.log(ast)
      }
    }
  }

《vue剖析之template模板剖析AST》
可以看出:在options中,vue默許先運用render函數,假如沒有供應render函數,則會運用template模板,末了再運用el,經由歷程剖析模板編譯AST,終究轉化為render。
个中函數以下:

  function query (el) {
    if (typeof el === 'string') {
      var selected = document.querySelector(el);
      if (!selected) {
        console.error('Cannot find element: ' + el);
      }
      return selected;
    }
    return el;
  }

  function getOuterHTML (el) {
    if (el.outerHTML) {
      return el.outerHTML;
    } else {
      var dom = document.createElement('div');
      dom.appendChild(el.cloneNode(true));
      return dom.innerHTML;
    }
  }

關於定義組件模板情勢,可以參考下這篇文章

說了這麼多,也不空話了,下面重點引見template編譯成AST的歷程。
依據源碼,先定義一些基礎東西要領,以及對相干html標籤舉行分類處置懲罰等。

  // script、style、textarea標籤
  function isPlainTextElement (tag) {
    let tags = {
      script: true,
      style: true,
      textarea: true
    }
    return tags[tag]
  }

  // script、style標籤
  function isForbiddenTag (tag) {
    let tags = {
      script: true,
      style: true
    }
    return tags[tag]
  }

  // 自閉和標籤
  function isUnaryTag (tag) {
    let strs = `area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr`;
    let tags = makeMap(strs);
    return tags[tag];
  }

  // 完畢標籤可以省略"/"
  function canBeLeftOpenTag (tag) {
    let strs = `colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source`;
    let tags = makeMap(strs);
    return tags[tag];
  }

  // 段落標籤
  function isNonPhrasingTag (tag) {
    let strs = `address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track`;
    let tags = makeMap(strs);
    return tags[tag];
  }

  // 構造:如
    # {
    #   script: true,
    #   style: true
    # }
  function makeMap(strs) {
    let tags = strs.split(',');
    let o = {}
    for (let i = 0; i < tags.length; i++) {
      o[tags[i]] = true;
    }
    return o;
  }

定義正則以下:

  // 婚配屬性
  const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
  const ncname = '[a-zA-Z_][\\w\\-\\.]*'
  const qnameCapture = `((?:${ncname}\\:)?${ncname})`
  // 婚配最先標籤最先部份
  const startTagOpen = new RegExp(`^<${qnameCapture}`)
  // 婚配最先標籤完畢部份
  const startTagClose = /^\s*(\/?)>/
  // 婚配完畢標籤
  const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
  // 婚配解釋
  const comment = /^<!\--/
  // 婚配默許的分隔符 "{{}}"
  const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g

定義標籤構造:

  function createASTElement (tag, attrs, parent) {
    // attrs:
    # [
    #   {
    #     name: 'id',
    #     value: 'app'
    #   },
    #   {
    #     name: 'class',
    #     value: 'demo'
    #   }
    # ]
    let attrsMap = {}
    for (let i = 0, len = attrs.length; i < len; i++) {
      attrsMap[attrs[i].name] = attrs[i].value;
    }
    // attrsMap:
    # {
    #   id: 'app',
    #   class: 'demo'
    # }
    return {
      type: 1,
      tag,
      attrsList: attrs,
      attrsMap: attrsMap,
      parent,
      children: []
    }
  }

重要的parse詳細代碼以下:

  function parse (template, options) {
    let root; // 終究返回的AST
    let currentParent; // 設置當前標籤的父節點
    let stack = []; // 保護一個棧,保留剖析歷程當中的最先標籤,用於婚配完畢標籤
    // 剖析模板的詳細完成
    parseHTML(template, {
      expectHTML: true, 
      shouldKeepComment: options.comments, // 是不是保留解釋
      delimiters: options.delimiters, // 自定義的分隔符
      start (tag, attrs, unary) {(
        // 處置懲罰最先標籤,剖析的最先標籤入棧,設置children以及parent等(个中的屬性剖析請檢察源碼)
        let element = createASTElement(tag, attrs, currentParent);

        // 假如tag為script/style標籤,設置屬性,返回的AST中不含該標籤元素構造
        if (isForbiddenTag(tag)) {
          element.forbidden = true;
          console.error('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.')
        }

        // 設置根元素節點
        if (!root) {
          root = element;
        }

        // 設置元素的父節點,將當前元素的增加到父節點的children中
        if (currentParent && !element.forbidden) {
          currentParent.children.push(element);
          element.parent = currentParent;
        }

        // 假如不是自閉和標籤(沒有對應的完畢標籤),則須要將當前tag入棧,用於婚配完畢標籤時,挪用end要領婚配近來的標籤,同時設置父節點為當前元素
        if (!unary) {
          currentParent = element;
          stack.push(element);
        }
      },
      end () {
         // 將婚配完畢的標籤出棧,修正父節點為之前上一個元素
        let element = stack.pop();
        currentParent = stack[stack.length - 1];
      },
      chars (text) {
        // 保留文本
        if (!currentParent) {
          console.error('Component template requires a root element, rather than just text.');
        } else {
          const children = currentParent.children;
          if (text) {
            let res;
            // 假如文本節點包括表達式
            if (res = parseText(text, opt.delimiters)) {
              children.push({
                type: 2,
                expression: res.expression,
                tokens: res.tokens,
                text
              })
            } else {
              children.push({
                type: 3,
                text
              })
            }
          }
        }
      },
      comment (text) {
        // 保留解釋
        if (currentParent) {
          currentParent.children.push({
            type: 3,
            text,
            isComment: true
          })
        }
      }
    })
    return root;
  }

從上面的可以看出:在parse函數中,重要用來剖析template模板,構成AST構造,天生一個終究的root根元素,並返回。
而關於標籤、文本、解釋type也是差別的。
个中:
標籤:type為1
含有表達式文本:type為2
不含表達式文本:type為3
解釋: type為3,同時isComment為true

同時,options參數對象上增加了start、end、chars和comment四個要領,用來處置懲罰當婚配到最先標籤、完畢標籤、文本以及解釋時,婚配對應的最先標籤,設置響應的currentParent以及parent等,天生成AST。

當挪用parseHTML后,會在處置懲罰標籤的差別狀況下,挪用對應的這四個要領。

在start中:每次處置懲罰最先標籤時,會設置一個root節點(只會設置一次),當標籤而且不是自閉合標籤時(沒有對應的完畢標籤),到場stack中,並將當前元素設置為currentParent,一層層往內婚配,終究的currentParent為最內層的元素標籤,並將當前元素保留到為currentParent的children中及parent為currentParent。
在end中:在stack中找到近來的雷同標籤(棧中的末了一個),設置為currentParent,並出棧,一層層往外婚配。
形如: html:<div><p></p></div> stack:[‘div’, ‘p’] pop: p => pop: div
而關於chars和comment,則分別是保留文本以及解釋到對應的currentParent的children中。

个中parseHTML:

  // 定義幾個全局變量
  let stack = []; // 保留最先標籤tag,和上面相似
  let lastTag; // 保留前一個標籤,相似於currentParent
  let index = 0; // template最先剖析索引
  let html; // 盈餘的template模板
  let opt; // 保留對options的援用,輕易挪用start、end、chars、comment要領
  function parseHTML (template, options) {
    html = template;
    opt = options;
    // 不停輪迴剖析html,直到為""
    while(html) {
      // 假如標籤tag不是script/style/textarea
      if (!lastTag || !isPlainTextElement(lastTag)) {
        // 剛最先或tag不為script/style/textarea
        let textEnd = html.indexOf('<');
        if (textEnd === 0) {
          // html以"<"最先
          
          // 處置懲罰html解釋
          if (html.match(comment)) {
            let commentEnd = html.indexOf('-->');
            if (commentEnd >= 0) {
              if (opt.shouldKeepComment && opt.comment) {
                // 保留解釋內容
                opt.comment(html.substring(4, commentEnd))
              }
              // 調解index以及html
              advance(commentEnd + 3);
              continue;
            }
          }
          // 處置懲罰 html前提解釋, 如<![if !IE]>

          // 處置懲罰html聲明Doctype

          // 處置懲罰最先標籤startTaga
          const startTagMatch = parseStartTag();
          if (startTagMatch) {
            handleStartTag(startTagMatch);
            continue;
          }

          // 婚配完畢標籤endTag
          const endTagMatch = html.match(endTag);
          if (endTagMatch) {
            // 調解index以及html
            advance(endTagMatch[0].length);
            // 處置懲罰完畢標籤
            parseEndTag(endTagMatch[1]);
            continue;
          }
        }
        let text;
        if (textEnd > 0) {
          // html為純文本,須要斟酌文本中含有"<"的狀況,此處省略,請自行檢察源碼
          text = html.slice(0, textEnd);
           // 調解index以及html
          advance(textEnd);
        }
        if (textEnd < 0) {
          // htlml以文本最先
          text = html;
          html = '';
        }
        // 保留文本內容
        if (opt.chars) {
          opt.chars(text);
        }
      } else {
        // tag為script/style/textarea
        let stackedTag = lastTag.toLowerCase();
        let tagReg = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i');

        // 簡樸處置懲罰下,詳情請檢察源碼
        let match = html.match(tagReg);
        if (match) {
          let text = match[1];
          if (opt.chars) {
            // 保留script/style/textarea中的內容
            opt.chars(text);
          }
          // 調解index以及html
          advance(text.length + match[2].length);
          // 處置懲罰完畢標籤</script>/</style>/</textarea>
          parseEndTag(stackedTag);
        }
      }
    }
  }

定義advance:

  // 修正模板不停剖析后的位置,以及截取模板字符串,保留未剖析的template
  function advance (n) {
    index += n;
    html = html.substring(n)
  }

在parseHTML中,可以看到:經由歷程不停輪迴,修正當前未知的索引index以及不停截取html模板,並分狀況處置懲罰、剖析,直到末了剩下空字符串為止。
个中的advance擔任修正index以及截取盈餘html模板字符串。
下面重要看看剖析最先標籤和完畢標籤:

  function parseStartTag () {
    let start = html.match(startTagOpen);
    if (start) {
      // 構造:["<div", "div", index: 0, groups: undefined, input: "..."]
      let match = {
        tagName: start[1],
        attrs: [],
        start: index
      }

       // 調解index以及html
      advance(start[0].length);

      // 輪迴婚配屬性
      let end, attr;
      while (!(end = html.match(startTagClose))&& (attr = html.match(attribute))) {
        // 構造:["id="app"", "id", "=", "app", undefined, undefined, groups: undefined, index: 0, input: "..."]
        advance(attr[0].length);
        match.attrs.push(attr);
      }
      // 婚配到最先標籤的完畢位置
      if (end) {
        match.unarySlash = end[1]; // end[1]婚配的是"/",如<br/>
        // 調解index以及html
        advance(end[0].length)
        match.end = index;
        return match;
      }
    }
  }

在parseStartTag中,將最先標籤處置懲罰成特定的構造,包括標署名、一切的屬性名,最先位置、完畢位置及是不是是自閉和標籤。
構造如:{
tagName,
attrs,
start,
end,
unarySlash
}

  function handleStartTag(match) {
    const tagName = match.tagName;
    const unarySlash = match.unarySlash;
    
    if (opt.expectHTML) {
      
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        // 假如p標籤包括了段落標籤,如div、h1、h2等
        // 形如: <p><h1></h1></p>
        // 與parseEndTag中tagName為p時相對應,處置懲罰</p>,增加<p>
        // 處置懲罰結果: <p></p><h1></h1><p></p>
        parseEndTag(lastTag);
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        // 假如標籤閉合標籤可以省略"/"
        // 形如:<li><li>
        // 處置懲罰結果: <li></li>
        parseEndTag(tagName);
      }
    }

    // 處置懲罰屬性構造(name和vulue情勢)
    let attrs = [];
    attrs.length = match.attrs.length;
    for (let i = 0, len = match.attrs.length; i < len; i++) {
      attrs[i] = {
        name: match.attrs[i][2],
        value: match.attrs[i][3]
      }
    }
    // 推斷是不是是自閉和標籤,如<br>
    let unary = isUnaryTag(tagName) || !!unarySlash;

    // 假如不是自閉合標籤,保留到stack中,用於endTag婚配,
    if (!unary) {
      stack.push({
        tag: tagName,
        lowerCasedTag: tagName.toLowerCase(),
        attrs: attrs
      })
      // 從新設置上一個標籤
      lastTag = tagName;
    }

    if (opt.start) {
      opt.start(tagName, attrs, unary)
    }
  }

將最先標籤處置懲罰成特定構造后,再經由歷程handleStartTag,將attrs進一步處置懲罰,成name、value構造情勢。
構造如:attrs: [
{

name: 'id',
value: 'app'

}
]
堅持和之前處置懲罰一致,非自閉和標籤時,從外標籤往內標籤,一層層入棧,須要保留到stack中,並設置lastTag為當前標籤。

  function parseEndTag (tagName) {
    let pos = 0;

    // 婚配stack中最先標籤中,近來的婚配標籤位置
    if (tagName) {
      tagName = tagName.toLowerCase();
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === tagName) {
          break;
        }
      }
    }

    // 假如可以婚配勝利
    if (pos >= 0) {
      let i = stack.length - 1;
      if (i > pos || !tagName) {
        console.error(`tag <${stack[i - 1].tag}> has no matching end tag.`)
      }
      // 假如婚配準確: pos === i
      if (opt.end) {
        opt.end();
      }
      // 將婚配勝利的最先標籤出棧,並修正lastTag為之前的標籤
      stack.length = pos;
      lastTag = pos && stack[stack.length - 1].tagName;
    } else if (tagName === 'br') {
      // 處置懲罰: </br>
      if (opt.start) {
        opt.start(tagName, [], true)
      }
    } else if (tagName === 'p') {
      // 處置懲罰上面說的狀況:<p><h1></h1></p>
      if (opt.start) {
        opt.start(tagName, [], false);
      }
      if (opt.end) {
        opt.end();
      }
    }
  }

parseEndTag中,處置懲罰完畢標籤時,須要一層層往外,在stack中找到當前標籤近來的雷同標籤,獵取stack中的位置,假如標籤婚配準確,平常為stack中的末了一個(不然缺乏完畢標籤),假如婚配勝利,將棧中的婚配標籤出棧,並從新設置lastTag為棧中的末了一個。
注重:須要特別處置懲罰br或p標籤,標籤在stack中找不到對應的婚配標籤,須要零丁保留到AST構造中,而</p>標籤重要是為了處置懲罰特別狀況,和之前最先標籤中處置懲罰相干,此時會多一個</p>標籤,在stack中近來的標籤不是p,也須要零丁保留到AST構造中。

差點忘了另有一個parseText函數。
个中parseText:

  function parseText (text, delimiters) {
    let open;
    let close;
    let resDelimiters;
    // 處置懲罰自定義的分隔符
    if (delimiters) {
      open = delimiters[0].replace(regexEscapeRE, '\\$&');
      close = delimiters[1].replace(regexEscapeRE, '\\$&');
      resDelimiters = new RegExp(open + '((?:.|\\n)+?)' + close, 'g');
    }
    const tagRE = delimiters ? resDelimiters : defaultTagRE;
    // 沒有婚配,文本中不含表達式,返回
    if (!tagRE.test(text)) {
      return;
    }
    const tokens = []
    const rawTokens = [];

    let lastIndex = tagRE.lastIndex = 0;
    let index;
    let match;
    // 輪迴婚配本文中的表達式
    while(match = tagRE.exec(text)) {
      index = match.index;

      if (index > lastIndex) {
        let value = text.slice(lastIndex, index);
        tokens.push(JSON.stringify(value));
        rawTokens.push(value)
      }
      // 此處須要處置懲罰過濾器,暫不處置懲罰,請檢察源碼
      let exp = match[1].trim();
      tokens.push(`_s(${exp})`);
      rawTokens.push({'@binding': exp})
      lastIndex = index + match[0].length;
    }
    if (lastIndex < text.length) {
      let value = text.slice(lastIndex);
      tokens.push(JSON.stringify(value));
      rawTokens.push(value);
    }
    return {
      expression: tokens.join('+'),
      tokens: rawTokens
    }
  }

末了,附上以上道理簡單剖析圖:

  <div id="app" class="demo">
    <!-- 注重看解釋 -->
    <p>
        <b>很粗</b>
    </p>
    很簡樸,我就是一程序員
    <br/>
    <h1>
      姓名:{{name}},歲數:{{age}},
      請聯絡我吧
    </h1>
  </div>

剖析流程以下:

剖析歷程:tagName    stack1      lastTag    currentParent    stack2       root     children       parent    操縱

  div       div        [div]       div          div         [div]          div       div:[p]      null        入棧
  comment  解釋 ---> 保留到currentParent.children中
  p       p        [div,p]       p              p        [div,p]       div       p:[b]          div       入棧
  b        b        [div,p,b]      b            b           [div,p,b]      div     b:[text]      p         入棧
  /b       b        [div,p]        p            p           [div,p]        div     ---            ---       出棧
  /p       p        [div]          div          div         [div]          div       ---            ---       出棧
  text    文本 ---> 經由處置懲罰后,保留到currentParent.children中
  h1       h1       [div,h1]       h1           h1          [div,h1]       div    h1:[text]      div        入棧
  text    文本 ---> 經由處置懲罰后,保留到currentParent.children中
  /h1      h1       [div]         div           div         [div]          div    ---            ---       出棧
  /div     div      []            null          null        []             div    ---            ---       出棧

終究:root = div:[p,h1]

終究AST構造以下:
《vue剖析之template模板剖析AST》
《vue剖析之template模板剖析AST》

以上是我依據vue源碼剖析,抽出來的簡樸的template轉化AST,文中如有什麼不對的地方請人人幫助斧正,本人近來也一直在進修Vue的源碼,願望可以拿出來與人人一同分享履歷,接下來會繼承更新後續的源碼,假如以為有須要可以互相交換。

    原文作者:Jeffery
    原文地址: https://segmentfault.com/a/1190000015432258
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞