博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
vue分析之template模板解析AST
阅读量:6263 次
发布时间:2019-06-22

本文共 15726 字,大约阅读时间需要 52 分钟。

通过查看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(`^<\\/${qnameCapture}[^>]*>`)  // 匹配注释  const comment = /^

定义标签结构:

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条件注释, 如
// 处理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]*?)(
]*>)', '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结构中,而</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    }  }

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

    很粗

    很简单,我就是一程序员

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

    转载地址:http://vckpa.baihongyu.com/

    你可能感兴趣的文章
    .NET Entity Framework入门操作
    查看>>
    iOS-集成微信支付和支付宝支付
    查看>>
    SAP
    查看>>
    读掘金小册组件精讲总结2
    查看>>
    MVC项目中怎样用JS导出EasyUI DataGrid为Excel
    查看>>
    制作个人开发IDE
    查看>>
    给架构师骂了
    查看>>
    ajax提交form表单资料详细汇总
    查看>>
    Excel——使用INDEX和SMALL实现条件筛选
    查看>>
    c#迭代器 转载
    查看>>
    JQuery与JavaScript
    查看>>
    Jmeter--正则表达式提取器
    查看>>
    设置Slider Control 控件的取值范围
    查看>>
    struts2 启动tomcat时报错:org.apache.catalina.core.StandardContext filterStart
    查看>>
    asp.net导入后台代码
    查看>>
    java web dev知识积累
    查看>>
    Flex 经纬度匹配正则表达式
    查看>>
    在SSIS包中使用 Checkpoint从失败处重新启动包[转]
    查看>>
    为什么开通博客?
    查看>>
    深入浅出Mybatis系列(四)---配置详解之typeAliases别名(mybatis源码篇)
    查看>>