vue Template 转换为 ast 语法树的实现方法

本文详细介绍了Vue在编译过程中如何将template转化为AST抽象语法树,并利用虚拟DOM提升性能。Vue的mount过程涉及template到AST的转换,然后生成render函数,最后构建vnode。解析HTML字符串,通过正则匹配开始和结束标签,创建AST元素,构建出DOM的抽象表示,用于优化DOM操作。同时,文章阐述了虚拟DOM的工作原理和提高性能的关键步骤。
摘要由CSDN通过智能技术生成
  • 抽象语法树介绍

vue在mount过程中,会将template编译成ast抽象语法树,是源代码的抽象语法结构的树状表现形式

虚拟dom的实现思路

vue 中使用了虚拟dom(vitrualdom),来模拟dom树,通过操作dom树数据来操作dom提高了dom操作的性能

具体流程

     template  转换成 ast抽象语法树 ast抽象语法树再转换成 render函数  最终返回一个vnode

具体代码

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // aa-aa
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //aa:aa
/* 
  ?: 匹配不捕获
*/
const defautTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 可以匹配到标签名  [1]
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //[0] 标签的结束名字
//    style="xxx"   style='xxx'  style=xxx
const attribute =
  /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配属性的 aaa="aaa"  aa='aaa'  a =  aaa
const startTagClose = /^\s*(\/?)>/; //匹配标签结束的
// ast 语法树

// let obj = {
//     tag: 'div',
//         type: 1,//nodeType
//         attrs: [
//             {
//                 style:"color:red"
//             }
//     ],
//     children: [
//         {
//             tag: 'span',
//             type: 1,
//             attrs: [],
//             children,
//             parent
//         }
//     ],
//     parent:null
// }
//vue3里面支持多个根元素(外层加了一个空元素)vue2中只有一个根节点

export function parseHTML(html) {
  function createASTElement(tag, attrs) {
    return {
      tag,
      type: 1,
      children: [],
      attrs,
      parent,
    };
  }
  let root = null;
  let currentParent;
  let stack = [];// //这里和上面的parse函数一样用到stack这个数组 不过这里的stack只是为了了简单存放标签名 为了和结束标签进行匹配的作用
  //根据开始标签,结束标签 ,文本内容 生成一个ast语法树
  function start(tagName, attrs) {//tagName  标签名 attrs 传入的属性名
    debugger;
    let element = createASTElement(tagName, attrs);//创建ast 元素
    if (!root) {
      //没有树根 是最外层元素
      root = element;
    }
    currentParent = element; //更新父元素
    stack.push(element);
  }
  function end(tagName) {
    debugger;
    let element = stack.pop(); //遇到结束标签 删掉
    currentParent = stack[stack.length - 1];
    if (currentParent) {
      element.parent = currentParent;
      currentParent.children.push(element);
    }
  }
  function chars(text) {
    debugger;
    text = text.replace(/\s/g, "");
    if (text) {
      currentParent.children.push({
        type: 3,
        text,
      });
    }
  }
  function advance(n) {
    debugger;
    html = html.substring(n);
  }
  function parseStartTag() {
    debugger;
    const start = html.match(startTagOpen);//通过正则匹配开始标签的具体信息
    if (start.length>0) {
      let match = {
        tagName: start[1],//标签名
        attrs: [],//属性
      };
      advance(start[0].length);//截取掉开始标签,即删除掉开始标签,然后在处理剩下的标签
      console.log(html, match);
      let end, attr;
      while (
        !(end = html.match(startTagClose)) &&//不是闭合标签
        (attr = html.match(attribute))// 当前标签的属性赋值给 attr
      ) {
        // advance(attr[0].length);
        console.log("95", attr, html);
        match.attrs.push({
          name: attr[1],//attr[1] 是属性名
          value: attr[3] || attr[4] || attr[5] || true, //属性值
        });
        console.log("attrs", match);
        advance(attr[0].length);//将标签中的属性字符串删除掉
      }
      if (end) {//在处理完标签的开始标签和属性之后并删除之后,删除 闭合标签 >
        advance(end[0].length);
        return match;
      }
    }
  }
    while (html) {
      // 传入的template 的html片段
      debugger;
      let textEnd = html.indexOf("<");
      if (textEnd == 0) {
        // textEnd的值为0 表示 是开始标签
        // 先解析开始 然后解析结束 调用解析开始标签的 parseStartTag()
        let startTagMatch = parseStartTag();
        console.log("startTagMatch", startTagMatch);
        console.log(html);
        if (startTagMatch) {
          // 开始标签
          console.log("开始", startTagMatch.tagName);
          start(startTagMatch.tagName, startTagMatch.attrs);
          continue;
        }
        // 结束标签
        let endTagMatch = html.match(endTag);
        if (endTagMatch) {
          advance(endTagMatch[0].length);

          end(endTagMatch[1]);
          continue;
        }
      }
      let text;
      if (textEnd > 0) {
        //开始解析文本
        text = html.substring(0, textEnd);
      }
      if (text) {
        advance(text.length);
        chars(text);
        console.log(text);
      }
      break;
    }
    return root;
  }

while (html) {
    // 传入的template 的html片段
    debugger;
    let textEnd = html.indexOf("<");
    if (textEnd == 0) {
      // textEnd的值为0 表示 是开始标签
      // 先解析开始 然后解析结束 调用解析开始标签的 parseStartTag()
      let startTagMatch = parseStartTag();
      console.log("startTagMatch", startTagMatch);
      console.log(html);
      if (startTagMatch) {
        // 开始标签
        console.log("开始", startTagMatch.tagName);
        start(startTagMatch.tagName, startTagMatch.attrs);
        continue;
      }
      // 结束标签
      let endTagMatch = html.match(endTag);
      if (endTagMatch) {
        advance(endTagMatch[0].length);

        end(endTagMatch[1]);
        continue;
      }
    }
    let text;
    if (textEnd > 0) {
      //开始解析文本
      text = html.substring(0, textEnd);
    }
    if (text) {
      advance(text.length);
      chars(text);
      console.log(text);
    }
    break;
  }
  return root;
}
  1. 判断传入的html字符串开始是不是 < 开始标签 如果是就调用 parseStartTag 方法解析 

parseStartTag 解析开始标签 

function parseStartTag() {
    debugger;
    const start = html.match(startTagOpen);//通过正则匹配开始标签的具体信息
    if (start.length>0) {
      let match = {
        tagName: start[1],//标签名
        attrs: [],//属性
      };
      advance(start[0].length);//截取掉开始标签,即删除掉开始标签,然后在处理剩下的标签
      console.log(html, match);
      let end, attr;
      while (
        !(end = html.match(startTagClose)) &&//不是闭合标签
        (attr = html.match(attribute))// 当前标签的属性赋值给 attr
      ) {
        // advance(attr[0].length);
        console.log("95", attr, html);
        match.attrs.push({
          name: attr[1],//attr[1] 是属性名
          value: attr[3] || attr[4] || attr[5] || true, //属性值
        });
        console.log("attrs", match);
        advance(attr[0].length);//将标签中的属性字符串删除掉
      }
      if (end) {//在处理完标签的开始标签和属性之后并删除之后,删除 闭合标签 >
        advance(end[0].length);
        return match;
      }
    }
  }

 具体实现思路

通过html.match()解析开始标签的数据

  •  如果start的长度大于0 说明开始标签解析成功 
  • 就去start的索引为1的数据作为 标签的名字,并且定义一个attrs属性数组
  • 将调用advance字符串的开始标签删除,然继续处理剩下的属性和闭合标签字符串
  • 如果剩下的字符串的开头不是闭合标签并且是属性,将属性解析
  • 并将解析出来属性值组成一个属性对象并添加到match的属性数组中
  • 并删除属性字符串,继续下边的处理
  • 如果是闭合标签 删除掉
  • 并将 match 对象返回

返回的match数据

 start 方法

  function start(tagName, attrs) {//tagName  标签名 attrs 传入的属性名
    debugger;
    let element = createASTElement(tagName, attrs);//创建ast 元素
    if (!root) {
      //没有树根 是最外层元素
      root = element;
    }
    currentParent = element; //更新父元素
    stack.push(element);
  }
  1. 使用 createASTElement 创建ast元素

  2. 判断有没有根元素,如果没有就将最外层作为root根元素
  3. 并将创建的ast元素作为当前的父元素,也就是更新父元素
  4. 将当前元素添加到 stack 栈中

end 方法

  function end(tagName) {
    debugger;
    let element = stack.pop(); //遇到结束标签 删掉
    currentParent = stack[stack.length - 1];//更新当前的父亲节点
    if (currentParent) {
      element.parent = currentParent;
      currentParent.children.push(element);
    }
  }

chars 处理文本的方法

  function chars(text) {
    debugger;
    text = text.replace(/\s/g, "");
    if (text) {
      currentParent.children.push({
        type: 3,
        text,
      });
    }
  }

入宫是文本 直接将 空格、制表符、换页符 替换问 空字符 并且设置元素类型为 文本节点添加到currentParent的 children 中

advance 删除字符串

  function advance(n) {
    debugger;
    html = html.substring(n);
  }
  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值