likes
comments
collection
share

Vue源码学习之实现模板转化成ast语法树(下)

作者站长头像
站长
· 阅读数 11

接上文解析模板参数完成以后,我们就成功拿到模板可以开始进行处理了,那么接下来就开始对compileToFcuntion()开始耕种。

第一步 编写正则表达式

在我们拿到模板之后,我们需要对标签、文本、属性、表达式、字符串等进行匹配,那就需要用到我们的正则表达式,如下:

// compiler/index.js

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
console.log(startTagOpen);
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const startTagClose = /^\s*(\/?)>/;
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

输出一下,用正则可视化工具来看一下规则如下:

  • startTagOpen:开始为<,然后里面是匹配名字div或者带命名空间的div:xxx等这样的开始标签的名字

Vue源码学习之实现模板转化成ast语法树(下)

  • endTag:结束标签</xxx>,最终匹配到的分组就是结束标签的名字

Vue源码学习之实现模板转化成ast语法树(下)

  • attribute:用来匹配属性的,属性前面可以有一些空白white space,不能是None of内的符号,中间是=,左右于两边可以有空白字符,左右单引号中间不是单引号,左右双引号中间不是双引号,也就是说第一个分组就是属性的key,value是分组3/4/5

Vue源码学习之实现模板转化成ast语法树(下)

  • startTagClose:匹配到的就是一个反斜杠/,可能是</div>或者<br/>

Vue源码学习之实现模板转化成ast语法树(下)

  • defaultTagRE:匹配的是双大括号{{}},两边是大括号,中间是换行或者回车以及任何字符

Vue源码学习之实现模板转化成ast语法树(下)

第二步 将模板转换成ast语法树

有了上面的正则之后,我们就需要去对模板进行匹配,然后将匹配到的字符串转换成ast语法树。

// compiler/index.js
// 对模板进行编译处理

......

function parseHTML(html) {
  const ELEMENT_TYPE = 1
  const TEXT_TYPE = 3
  const stack = [] // 用于存放元素
  let currentParent; // 指向栈中最后一个
  let root
  function createdASTElement(tag, attrs) {
    return {
      tag,
      type: ELEMENT_TYPE,
      children: [],
      attrs,
      parent: null
    }
  }
  // 开始标签
  function start(tag, attrs) {
    let node = createdASTElement(tag, attrs) // 创造一个ast节点
    if (!root) { // 看一下是否为空树
      root = node // 如果为空则表示当前是树的根节点
    }
    if (currentParent) {
      node.parent = currentParent // 只赋了parent属性
      currentParent.children.push(node) // 还需要让父亲记住自己
    }
    stack.push(node)
    currentParent = node  //currentParent为栈中的最后一个
  }
  // 文本
  function chars(text) { // 文本直接放到当前指向的节点中
    text = text.replace(/\s/g,'')
    && currentParent.children.push({
      type: TEXT_TYPE,
      text,
      parent: currentParent
    })
  }
  // 结束标签
  function end(tag) {
    stack.pop() // 弹出最后一个
    currentParent = stack[stack.length - 1]
  }
  function advance(n) {
    html = html.substring(n)
  }
  function parseStartTag() {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1], // 标签名
        attrs: []
      }
      advance(start[0].length)
      // 如果不是开始标签的结束就一直匹配下去
      let attr, end
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length)
        match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] || true })
      }
      if (end) {
        advance(end.length)
      }
      return match
    }
    return false // 不是开始标签
  }
  while (html) { // html最开始肯定是一个 <
    // 如果textEnd为0说明是一个开始标签或者结束标签
    // 如果textEnd大于0则说明是文本结束的位置
    let textEnd = html.indexOf('<') // 如果indexOf的索引是0则说明是个标签
    if (textEnd === 0) {
      const starttagMatch = parseStartTag()  //开始标签的匹配
      if (starttagMatch) { // 解析到的开始标签
        start(starttagMatch.tagName, starttagMatch.attrs)
        continue
      }
      let endTagmatch = html.match(endTag)
      if (endTagmatch) {
        advance(endTagmatch[0].length)
        end(endTagmatch[1])
        continue
      }
    }
    if (textEnd > 0) {
      let text = html.substring(0, textEnd) // 文本内容
      if (text) {
        chars(text)
        advance(text.length) // 解析到的文本
      }
    }
  }
  console.log(root);
  return root
}
export function compileToFcuntion(template) {
  // 1.将template转换为ast语法树
  let ast = parseHTML(template)
  // 2.生成render方法 render方法执行后的结果就是虚拟DOM
  console.log(template);
}

思路:

  1. 通过 parseHTML(template)方法传入模板,将模板转换成ast语法树,解析开始标签、结束标签等内容,通过这个方法就会返回一个ast语法树。
  2. parseHTML()方法中,每解析一个标签则在模板的字符串中将其截取掉,直到为空,所以可以写一个while循环来完成
  3. html肯定是<开头的,所以我们直接用indexOf方法来进行判断:如果textEnd为0说明是一个开始标签或者结束标签,如果textEnd大于0则说明是文本结束的位置。
  4. 如果textEnd等于0就认为他是开始标签,那就用parseStartTag方法来解析开始标签。如下图接下来就可以塞到match里面。 Vue源码学习之实现模板转化成ast语法树(下)
  5. 追加完成之后就需要去解析属性,那我们就需要先将已经匹配完成的标签进行删除,通过advance(start[0].length)来进行截取,如下图就说明截取成功了,截取成功之后就可以去匹配属性

Vue源码学习之实现模板转化成ast语法树(下)

  1. 在匹配属性的过程中,只要不是开始标签的结束就可以一直进行匹配,所以可以通过while,如果不是开始标签的结束就一直匹配下去,并且将每次拿到的属性进行保存,每次匹配完在将其截掉,最后还有一个>符号,所以在匹配结束以后end可能就会有值,所以再把end删掉

Vue源码学习之实现模板转化成ast语法树(下) 7. 紧接着我们需要将属性解析出来放到attr里面通过match.attrs.push,最后再将matchreturn出去

Vue源码学习之实现模板转化成ast语法树(下) 8. 然后就可以处理文本内容,当textEnd>0说明有文本,继续进行截取。

Vue源码学习之实现模板转化成ast语法树(下) 9. 整个过程就是遇到开始标签处理开始标签,遇到文本处理文本,遇到结束标签处理结束标签,当最后循环能够终止输出为空,就说明处理成功了 9. 现在还没有替换文本只是把字符串删掉了,那就还需要几个方法将它们暴露出去,也可以用htmlparser2去解析html

Vue源码学习之实现模板转化成ast语法树(下) 9. 最终我们想要转换成一棵抽象语法树,树肯定需要有层级关系,那我们就需要构建父子关系,我们可以做一个栈形结构,刚开始匹配到标签的时候将这个标签放进去,之后当匹配下一个标签的时候,就知道这个标签是栈中最后一个标签的儿子,当遇到结束标签的时候就将栈中最后一个弹出去,通过这种方式来模拟出父子关系:

  1. 知道方法之后,就开始定义元素类型ELEMENT_TYPE、文本类型TEXT_TYPE,栈stack,指针currentParent,根节点root
  2. 当遇到开始标签的时候就创建一个AST元素,用createdASTElement方法,然后还需要去判断产生的节点是否是根节点root,如果没有根节点这个节点就是根节点。然后将这个节点放进栈中,同时还需要当前这个指针指向栈中的最后一个。如果currentParent有值就需要让当前节点的父亲等于currentParent,并将node值赋给currentParent.children
  3. 当遇到文本的时候,这个文本就是当前元素的孩子,直接找到currentParent.children放进去
  4. 当遇到结束标签的时候,直接弹出栈中最后一个,然后更新currentParent指针,在end中也可以进行校验标签是否合法。

这个时候输出发现children是空的,可以在开始的时候chars执行的时候将空格去掉text = text.replace(/\s/g,'') Vue源码学习之实现模板转化成ast语法树(下) 再次执行输出如下:

Vue源码学习之实现模板转化成ast语法树(下)

最终

Vue源码学习之实现模板转化成ast语法树(下) 到此,利用栈形结构,我们这棵树就构建完成了。