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控制iframe的高度/宽度让其自适应内容
Apr 09 Javascript
jQuery的bind()方法使用详解
Jul 15 Javascript
jqGrid中文文档之选项设置
Dec 02 Javascript
JavaScript 实现的checkbox经典实例分享
Oct 16 Javascript
一句jQuery代码实现返回顶部效果(简单实用)
Dec 28 Javascript
使用gulp搭建本地服务器并实现模拟ajax
Apr 05 Javascript
JS隐藏号码中间4位代码实例
Apr 09 Javascript
Koa从零搭建到Api实现项目的搭建方法
Jul 30 Javascript
微信小程序封装分享与分销功能过程解析
Aug 13 Javascript
使用js实现一个简单的滚动条过程解析
Sep 10 Javascript
javascript实现前端成语点击验证
Jun 24 Javascript
jQuery实现可以扩展的日历
Dec 01 jQuery
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
PHP文件大小格式化函数合集
2014/03/10 PHP
分享一段php获取linux服务器状态的代码
2014/05/27 PHP
PHP获取数组中指定的一列实例
2017/12/27 PHP
PHP实现倒计时功能
2020/11/16 PHP
javascript 日历提醒系统( 兼容所有浏览器 )
2009/04/07 Javascript
javascript实现yield的方法
2013/11/06 Javascript
JavaScript实现同一页面内两个表单互相传值的方法
2015/08/12 Javascript
js窗口关闭提示信息(兼容IE和firefox)
2015/10/23 Javascript
JavaScript 2048 游戏实例代码(简单易懂)
2016/03/25 Javascript
Javascript基础知识盲点总结之函数
2016/05/15 Javascript
jQuery中事件与动画的总结分享
2016/05/24 Javascript
JS+CSS3实现超炫的散列画廊特效
2016/07/16 Javascript
浅谈js多维数组和hash数组定义和使用
2016/07/27 Javascript
AngularJs Understanding the Model Component
2016/09/02 Javascript
BootStrap注意事项小结(五)表单
2017/03/10 Javascript
JS 60秒后重新发送验证码的实例讲解
2017/07/26 Javascript
本地搭建微信小程序服务器的实现方法
2017/10/27 Javascript
vue-next/runtime-core 源码阅读指南详解
2019/10/25 Javascript
JavaScript数组排序的六种常见算法总结
2020/08/18 Javascript
Vue3不支持Filters过滤器的问题
2020/09/24 Javascript
[16:43]Heroes19_剃刀(完美)
2014/10/31 DOTA
[06:33]DOTA2亚洲邀请赛小组赛第二日 TOP10精彩集锦
2015/01/31 DOTA
python 处理微信对账单数据的实例代码
2019/07/19 Python
python 队列基本定义与使用方法【初始化、赋值、判断等】
2019/10/24 Python
Django 创建后台,配置sqlite3教程
2019/11/18 Python
Pandas时间序列基础详解(转换,索引,切片)
2020/02/26 Python
Python GUI编程学习笔记之tkinter控件的介绍及基本使用方法详解
2020/03/30 Python
Python Selenium操作Cookie的实例方法
2021/02/28 Python
伊利莎白雅顿官网:Elizabeth Arden
2016/10/10 全球购物
捷克鲜花配送:Florea.cz
2018/10/29 全球购物
生日寿宴答谢词
2014/01/19 职场文书
学历公证书范本
2014/04/09 职场文书
投标保密承诺书
2014/05/19 职场文书
超市收银员岗位职责
2015/04/07 职场文书
红歌会主持词
2015/07/02 职场文书
员工考勤管理制度
2015/08/06 职场文书