资讯专栏INFORMATION COLUMN

vue分析之template模板解析AST

2bdenny / 3138人阅读

摘要:注意看注释很粗很简单,我就是一程序员姓名,年龄,请联系我吧是否保留注释定义分隔符,默认为对于转成,则需要先获取,对于这部分内容,做一个简单的分析,具体的请自行查看源码。其中的负责修改以及截取剩余模板字符串。

通过查看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结构。

  

很粗

很简单,我就是一程序员

姓名:{{name}},年龄:{{age}}, 请联系我吧

对于转成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: "
", if (typeof template === "string") { // 如果为"#template" if (template.charAt(0) === "#") { let tpl = query(template); template = tpl ? tpl.innerHTML : ""; } // 否则不做处理,如:"
" } else if (template.nodeType) { // 如果模板为DOM节点,如:template: document.querySelector("#template") // 比如: 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) } } }


可以看出:在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(`^]*>`)
  // 匹配注释
  const comment = /^");
            if (commentEnd >= 0) {
              if (opt.shouldKeepComment && opt.comment) {
                // 保存注释内容
                opt.comment(html.substring(4, commentEnd))
              }
              // 调整index以及html
              advance(commentEnd + 3);
              continue;
            }
          }
          // 处理 html条件注释, 如

          // 处理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("([sS]*?)(]*>)", "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);
          // 处理结束标签//
          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) {
      // 结构:["
        // 调整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等
        // 形如: 

// 与parseEndTag中tagName为p时相对应,处理

,添加

// 处理结果:

parseEndTag(lastTag); } if (canBeLeftOpenTag(tagName) && lastTag === tagName) { // 如果标签闭合标签可以省略"/" // 形如:
  • // 处理结果:
  • 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] } } // 判断是不是自闭和标签,如
    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") {
          // 处理: 
    if (opt.start) { opt.start(tagName, [], true) } } else if (tagName === "p") { // 处理上面说的情况:

    if (opt.start) { opt.start(tagName, [], false); } if (opt.end) { opt.end(); } } }

    parseEndTag中,处理结束标签时,需要一层层往外,在stack中找到当前标签最近的相同标签,获取stack中的位置,如果标签匹配正确,一般为stack中的最后一个(否则缺少结束标签),如果匹配成功,将栈中的匹配标签出栈,并重新设置lastTag为栈中的最后一个。
    注意:需要特殊处理br或p标签,标签在stack中找不到对应的匹配标签,需要多带带保存到AST结构中,而

    标签主要是为了处理特殊情况,和之前开始标签中处理相关,此时会多一个

    标签,在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 + "((?:.|
    )+?)" + 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
        }
      }

    最后,附上以上原理简略分析图:

      

    很粗

    很简单,我就是一程序员

    姓名:{{name}},年龄:{{age}}, 请联系我吧

    解析流程如下: 分析过程: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的源码,希望能够拿出来与大家一起分享经验,接下来会继续更新后续的源码,如果觉得有需要可以相互交流。

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/95814.html

    相关文章

    • Vue编译器AST抽象语法树源码分析

       直接进入核心现在说说baseCompile核心代码:  //`createCompilerCreator`allowscreatingcompilersthatusealternative   //parser/optimizer/codegen,e.gtheSSRoptimizingcompiler.   //Herewejustexportadefaultcompilerusingthede...

      3403771864 评论0 收藏0
    • Vue原理】Compile - 源码版 Parse 主要流程

      写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧 【Vue原理】Compile - 源码版 之 Parse 主要流程 本文难度较繁琐,需要耐心观看,如果你对 compile 源码暂时...

      Forest10 评论0 收藏0
    • vue源码阅读数据渲染过程

      摘要:图在中应用三数据渲染过程数据绑定实现逻辑本节正式分析从到数据渲染到页面的过程,在中定义了一个的构造函数。一、概述 vue已是目前国内前端web端三分天下之一,也是工作中主要技术栈之一。在日常使用中知其然也好奇着所以然,因此尝试阅读vue源码并进行总结。本文旨在梳理初始化页面时data中的数据是如何渲染到页面上的。本文将带着这个疑问一点点追究vue的思路。总体来说vue模版渲染大致流程如图1所...

      AlphaGooo 评论0 收藏0
    • Vue原理】Compile - 源码版 optimize 标记静态节点

      摘要:一旦我们检测到这些子树,我们可以把它们变成常数,这样我们就不需要了在每次重新渲染时为它们创建新的节点在修补过程中完全跳过它们。否则,吊装费用将会增加好处大于好处,最好总是保持新鲜。 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,...

      Soarkey 评论0 收藏0
    • Vue 2.0源码学习

      摘要:今年的月日,的版本正式发布了,其中核心代码都进行了重写,于是就专门花时间,对的源码进行了学习。本篇文章就是源码学习的总结。实现了并且将静态子树进行了提取,减少界面重绘时的对比。的最新源码可以去获得。 Vue2.0介绍 从去年9月份了解到Vue后,就被他简洁的API所吸引。1.0版本正式发布后,就在业务中开始使用,将原先jQuery的功能逐步的进行迁移。 今年的10月1日,Vue的2...

      Joyven 评论0 收藏0

    发表评论

    0条评论

    最新活动
    阅读需要支付1元查看
    <