Vue源码解析之Template转化为AST的实现方法


Posted in Javascript onDecember 14, 2018

什么是AST

在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式。

Virtual Dom

Vue的一个厉害之处就是利用Virtual DOM模拟DOM对象树来优化DOM操作的一种技术或思路。

Vue源码中虚拟DOM构建经历 template编译成AST语法树 -> 再转换为render函数 最终返回一个VNode(VNode就是Vue的虚拟DOM节点)

本文通过对源码中AST转化部分进行简单提取,因为源码中转化过程还需要进行各种兼容判断,非常复杂,所以笔者对主要功能代码进行提取,用了300-400行代码完成对template转化为AST这个功能。下面用具体代码进行分析。

function parse(template) {
    var currentParent;  //当前父节点
    var root;      //最终返回出去的AST树根节点
    var stack = [];
    parseHTML(template, {
      start: function start(tag, attrs, unary) {
        ......
      },
      end: function end() {
       ......
      },
      chars: function chars(text) {
        ......
      }
    })
    return root
  }

第一步就是调用parse这个方法,把template传进来,这里假设template为 <div id="app"><span>{{message}}</span></div>

然后声明3个变量

currentParent -> 存放当前父元素,root -> 最终返回出去的AST树根节点,stack -> 一个栈用来辅助树的建立

接着调用parseHTML函数进行转化,传入template和options(包含3个方法 start,end,chars 等下用到这3个函数再进行解释)接下来先看parseHTML这个方法

function parseHTML(html, options) {
    var stack = [];  //这里和上面的parse函数一样用到stack这个数组 不过这里的stack只是为了简单存放标签名 为了和结束标签进行匹配的作用
    var isUnaryTag$$1 = isUnaryTag;  //判断是否为自闭合标签
    var index = 0;
    var last;
    while (html) {
      //第一次进入while循环时,由于字符串以<开头,所以进入startTag条件,并进行AST转换,最后将对象弹入stack数组中
      last = html;
      var textEnd = html.indexOf('<');
      if (textEnd === 0) {   // 此时字符串是不是以<开头
        // End tag:
        var endTagMatch = html.match(endTag);
        if (endTagMatch) {
          var curIndex = index;
          advance(endTagMatch[0].length);
          parseEndTag(endTagMatch[1], curIndex, index);
          continue
        }

        // Start tag:  // 匹配起始标签
        var startTagMatch = parseStartTag();  //处理后得到match
        if (startTagMatch) {
          handleStartTag(startTagMatch);
          continue
        }
      }

      // 初始化为undefined 这样安全且字符数少一点
      var text = (void 0), rest = (void 0), next = (void 0);
      if (textEnd >= 0) {   // 截取<字符索引 => </div> 这里截取到闭合的<
        rest = html.slice(textEnd); //截取闭合标签
        // 处理文本中的<字符
        // 获取中间的字符串 => {{message}}
        text = html.substring(0, textEnd); //截取到闭合标签前面部分
        advance(textEnd);        //切除闭合标签前面部分

      }
      // 当字符串没有<时
      if (textEnd < 0) {
        text = html;
        html = '';
      }
      // // 处理文本
      if (options.chars && text) {
        options.chars(text);
      }
    }
  }

函数进入while循环对html进行获取<标签索引 var textEnd = html.indexOf('<');如果textEnd === 0 说明当前是标签<xxx>或者</xxx> 再用正则匹配是否当前是结束标签</xxx>。var endTagMatch = html.match(endTag); 匹配不到那么就是开始标签,调用parseStartTag()函数解析。

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))) { 
      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对象,对象里面包含tagName(标签名),attrs(标签的属性),start(<左开始标签在template中的位置),end(>右开始标签在template中的位置) 如template = <div id="app"><div><span>{{message}}</span></div></div> 程序第一次进入该函数 匹配的是div标签 所以tagName就是div
start:0 end:14 如图:

Vue源码解析之Template转化为AST的实现方法

接着把match返回出去 作为调用handleStartTag的参数

var startTagMatch = parseStartTag();  //处理后得到match
if (startTagMatch) {
  handleStartTag(startTagMatch);
  continue
}

接下来看handleStartTag这个函数:

function handleStartTag(match) {
  var tagName = match.tagName;
  var unary = isUnaryTag$$1(tagName) //判断是否为闭合标签 
  var l = match.attrs.length;
  var attrs = new Array(l);
  for (var i = 0; i < l; i++) {
    var args = match.attrs[i];
    var value = args[3] || args[4] || args[5] || '';
    attrs[i] = {
      name: args[1],
      value: value
    };
  }
  if (!unary) {
    stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs});
    lastTag = tagName;
  }
  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end);
  }
  }

函数中分为3部分 第一部分是for循环是对attrs进行转化,我们从上一步的parseStartTag()得到的match对象中的attrs属性如图

Vue源码解析之Template转化为AST的实现方法

当时attrs是上面图这样子滴 我们通过这个循环把它转化为只带name 和 value这2个属性的对象 如图:

Vue源码解析之Template转化为AST的实现方法

接着判断如果不是自闭合标签,把标签名和属性推入栈中(注意 这里的stack这个变量在parseHTML中定义,作用是为了存放标签名 为了和结束标签进行匹配的作用。)接着调用最后一步 options.start 这里的options就是我们在parse函数中 调用parseHTML是传进来第二个参数的那个对象(包含start end chars 3个方法函数) 这里开始看options.start这个函数的作用:

start: function start(tag, attrs, unary) {
  var element = {
    type: 1,
    tag: tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    parent: currentParent,
    children: []
  };
  processAttrs(element);
  if (!root) {
    root = element;
  } 
  if(currentParent){
    currentParent.children.push(element);
    element.parent = currentParent;
  }
  if (!unary) {
    currentParent = element;
    stack.push(element);
  }
}

这个函数中 生成element对象 再连接元素的parent 和 children节点 最终push到栈中

此时栈中第一个元素生成 如图:

Vue源码解析之Template转化为AST的实现方法

完成了while循环的第一次执行,进入第二次循环执行,这个时候html变成<span>{{message}}</span></div> 接着截取到<span> 处理过程和第一次一致 经过这次循环stack中元素如图:

Vue源码解析之Template转化为AST的实现方法

Vue源码解析之Template转化为AST的实现方法

接着继续执行第三个循环 这个时候是处理文本节点了 {{message}}

// 初始化为undefined 这样安全且字符数少一点
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) {   // 截取<字符索引 => </div> 这里截取到闭合的<
  rest = html.slice(textEnd); //截取闭合标签
  // 处理文本中的<字符
  // 获取中间的字符串 => {{message}}
  text = html.substring(0, textEnd); //截取到闭合标签前面部分
  advance(textEnd);        //切除闭合标签前面部分
}
// 当字符串没有<时
if (textEnd < 0) {
  text = html;
  html = '';
}
// 另外一个函数
if (options.chars && text) {
  options.chars(text);
}

这里的作用就是把文本提取出来 调用options.chars这个函数 接下来看options.chars

chars: function chars(text) {
  if (!currentParent) {  //如果没有父元素 只是文本
    return
  }

  var children = currentParent.children; //取出children
  // text => {{message}}
  if (text) {
    var expression;
    if (text !== ' ' && (expression = parseText(text))) {
      // 将解析后的text存进children数组
      children.push({
        type: 2,
        expression: expression,
        text: text
      });
    } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
      children.push({
        type: 3,
        text: text
      });
    }
  }
}
})

这里的主要功能是判断文本是{{xxx}}还是简单的文本xxx,如果是简单的文本 push进父元素的children里面,type设置为3,如果是字符模板{{xxx}},调用parseText转化。如这里的{{message}}转化为 _s(message)(加上_s是为了AST的下一步转为render函数,本文中暂时不会用到。) 再把转化后的内容push进children。

Vue源码解析之Template转化为AST的实现方法

又走完一个循环了,这个时候html = </span></div> 剩下2个结束标签进行匹配了

var endTagMatch = html.match(endTag);
  if (endTagMatch) {
    var curIndex = index;
    advance(endTagMatch[0].length);
    parseEndTag(endTagMatch[1], curIndex, index);
    continue
  }

接下来看parseEndTag这个函数 传进来了标签名 开始索引和结束索引

function parseEndTag(tagName, start, end) {
  var pos, lowerCasedTagName;
  if (tagName) {
    lowerCasedTagName = tagName.toLowerCase();
  }
  // Find the closest opened tag of the same type
  if (tagName) { // 获取最近的匹配标签
    for (pos = stack.length - 1; pos >= 0; pos--) {
      // 提示没有匹配的标签
      if (stack[pos].lowerCasedTag === lowerCasedTagName) {
        break
      }
    }
  } else {
    // If no tag name is provided, clean shop
    pos = 0;
  }
  
  if (pos >= 0) {
    // Close all the open elements, up the stack
    for (var i = stack.length - 1; i >= pos; i--) {
      if (options.end) {
        options.end(stack[i].tag, start, end);
      }
    }
  
    // Remove the open elements from the stack
    stack.length = pos;
    lastTag = pos && stack[pos - 1].tag;
}

这里首先找到栈中对应的开始标签的索引pos,再从该索引开始到栈顶的所以元素调用options.end这个函数

end: function end() {
  // pop stack
  stack.length -= 1;
  currentParent = stack[stack.length - 1];
},

把栈顶元素出栈,因为这个元素已经匹配到结束标签了,再把当前父元素更改。终于走完了,把html的内容循环完,最终return root 这个root就是我们所要得到的AST

Vue源码解析之Template转化为AST的实现方法

这只是Vue的冰山一角,文中有什么不对的地方请大家帮忙指正,本人最近也一直在学习Vue的源码,希望能够拿出来与大家一起分享经验,接下来会继续更新后续的源码,如果觉得有帮忙请给个Star哈

github地址为:https://github.com/zwStar/vue-ast 欢迎各位star或issues

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
关于js获取radio和select的属性并控制的代码
May 12 Javascript
nullJavascript中创建对象的五种方法实例
May 07 Javascript
JavaScript的instanceof运算符学习教程
Jun 08 Javascript
微信小程序 setData使用方法及常用错误解决办法
May 11 Javascript
解析Angular 2+ 样式绑定方式
Jan 15 Javascript
vue addRoutes实现动态权限路由菜单的示例
May 15 Javascript
JavaScript作用域、闭包、对象与原型链概念及用法实例总结
Aug 20 Javascript
Vue 组件注册实例详解
Feb 23 Javascript
Angular请求防抖处理第一次请求失效问题
May 17 Javascript
Electron 调用命令行(cmd)
Sep 23 Javascript
countUp.js实现数字动态变化效果
Oct 17 Javascript
解决vue单页面应用进入页面加载所有 js 的问题
Aug 12 Javascript
JavaScript模板引擎实现原理实例详解
Dec 14 #Javascript
Angular2 自定义表单验证器的实现方法
Dec 14 #Javascript
JavaScript模板引擎应用场景及实现原理详解
Dec 14 #Javascript
详解React 服务端渲染方案完美的解决方案
Dec 14 #Javascript
JS/HTML5游戏常用算法之路径搜索算法 A*寻路算法完整实例
Dec 14 #Javascript
JS实现的A*寻路算法详解
Dec 14 #Javascript
详解vue项目接入微信JSSDK的坑
Dec 14 #Javascript
You might like
IIS环境下快速安装、配置和调试PHP5.2.0
2006/12/17 PHP
php面向对象中static静态属性和静态方法的调用
2015/02/08 PHP
我整理的PHP 7.0主要新特性
2016/01/07 PHP
PHP Ajax实现无刷新附件上传
2016/08/17 PHP
幻宇的层模拟窗口效果-提供演示和下载
2007/01/20 Javascript
Javascript 跨域访问解决方案
2009/02/14 Javascript
EXTJS内使用ACTIVEX控件引起崩溃问题的解决方法
2010/03/31 Javascript
写js时遇到的一些小问题
2010/12/06 Javascript
Javascript继承(上)——对象构建介绍
2012/11/08 Javascript
浅谈javascript中的DOM方法
2015/07/16 Javascript
JS实现弹性菜单效果代码
2015/09/07 Javascript
学习JavaScript设计模式之状态模式
2016/01/08 Javascript
javascript实现表单验证
2016/01/29 Javascript
Angularjs中如何使用filterFilter函数过滤
2016/02/06 Javascript
jquery.multiselect多选下拉框实现代码
2016/11/11 Javascript
JavaScript 巧学巧用
2017/05/23 Javascript
layui table数据修改的回显方法
2019/09/04 Javascript
python实现二叉树的遍历
2017/12/11 Python
Python中循环引用(import)失败的解决方法
2018/04/22 Python
Python使用wget实现下载网络文件功能示例
2018/05/31 Python
celery4+django2定时任务的实现代码
2018/12/23 Python
详解如何修改python中字典的键和值
2020/09/29 Python
详解WebSocket跨域问题解决
2018/08/06 HTML / CSS
Fashion Eyewear美国:英国线上设计师眼镜和太阳镜的零售商
2016/08/15 全球购物
印尼在线精品店:Berrybenka.com
2016/10/22 全球购物
英国花园药房: The Garden Pharmacy
2017/12/28 全球购物
澳大利亚领先的美容护肤品零售商之一:SkincareStore
2018/01/22 全球购物
德国家用电器购物网站:Premiumshop24
2019/08/22 全球购物
西班牙在线药店:DosFarma
2020/03/28 全球购物
Java提供了哪些企业应用编程接口
2015/02/13 面试题
经济系大学生求职信
2013/10/01 职场文书
合作意向书模板
2014/03/31 职场文书
小学总务工作总结
2015/08/13 职场文书
使用css样式设计一个简单的html登陆界面的实现
2021/03/30 HTML / CSS
详解Python requests模块
2021/06/21 Python
PHP中多字节字符串操作实例详解
2021/08/23 PHP