vue2源码-五、将模板编译解析成AST语法树1

发布时间 2023-04-14 21:52:57作者: 楸枰~

将模板编译成ast语法树

  1. complileToFunction方法

    vue数据渲染:template模板->ast语法树->render函数,模板编译的最终结果结果就是render函数。

    complileToFunction方法中,生成render函数,需要以下两个核心步骤:

    • 通过parserHTML方法:将模板(templatehtml)内容编译成ast语法树
    • 通过codegen方法:根据ast语法树生成为render函数
    export function complileToFunction(template) {
      // 1.就是将template转化为ast语法树
      let ast = parseHTML(template)
      // 2.生成render方法(render方法执行后的返回结果就是虚拟DOM),使用ast生成render函数
      // 模板引擎的实现原理就是with + new Function
      let code = codegen(ast)
    
      code = `with(this){return ${code}}`
      let render = new Function(code)
      return render
    }
    
  2. parserHTML方法

    parserHTML方法:将模板(templatehtml)编译成为ast语法树

    注意:parserHTML方法的入参template,指的是<template>标签内部的内容并不包括<template>标签本身。

    Vue初始化时:

    • 如果options选项选项中设置了template,将优先使用template内容作为模板
    • 如果options选项没有设置template,将采用元素内容作为html模板:<div id="app"></div>

    主要使用的正则匹配。七个正则匹配。

    • 匹配标签号
    • 匹配命名空间标签名
    • 匹配开始标签-开始部分
    • 匹配结束标签
    • 匹配属性
    • 匹配开始标签-闭合部分
    • 匹配插值表达式
    export function parseHTML(html) {
      const ELEMENT_TYPE = 1
      const TEXT_TYPE = 3
      const stack = [] // 用于存放元素的
      let currenrParent // 指向栈的最后一个
      let root
    
      // 最终转化为一颗抽象语法树
      function createASTElement(tag, attrs) {
        return {
          tag,
          type: ELEMENT_TYPE,
          children: [],
          attrs,
          parent: null,
        }
      }
    
      // 利用栈构造一棵树
      function start(tag, attrs) {
        let node = createASTElement(tag, attrs) // 创造一个ast节点
        // 看一下是否为空树
        if (!root) {
          root = node // 如果为空则当前是树的根节点
        }
        if (currenrParent) {
          node.parent = currenrParent // 只赋予了parent属性
          currenrParent.children.push(node) // 还需要让父亲记住自己
        }
    
        stack.push(node)
        currenrParent = node // currenrParent为栈中的最后一个
      }
        
      // 文本 
      function chars(text) {
        text = text.replace(/\s/g, '')
        // 文本直接放到当前指向的节点中
        text &&
          currenrParent.children.push({
            type: TEXT_TYPE,
            text,
            parent: currenrParent,
          })
      }
      
      // 结束
      function end(tag) {
        stack.pop() // 弹出最后一个,校验标签是否合法
        currenrParent = stack[stack.length - 1]
      }
    
      function advance(n) {
        html = html.substring(n)
      }
      function parseStartTag() {
        // 1. 匹配开始标签
        const start = html.match(startTagOpen)
        if (start) {
          // 2. 构造怕匹配结果对象:包含标签名和属性
          const match = {
            tagName: start[1], // 标签名
            attrs: [], // 属性
          }
          // 3. 截取匹配到的结果
          advance(start[0].length)
          
          // 如果不是开始标签的结束,就一直匹配下去
          // 4. 开始解析标签的属性 id="app" a=1 b=2>
          let attr   // 是否匹配开始标签的结束符号 > 或 />
          let end    // 存储属性匹配的结果
          // 匹配并获取属性,放入 match.attrs 数组
          while (
            !(end = html.match(startTagClose)) &&
            (attr = html.match(attribute))
          ) {
            // 4.1 截取掉已匹配的结果
            advance(attr[0].length)
            // 4.2 将匹配到的属性记录到数组 match.attrs(属性对象包含属性名和属性值)
            match.attrs.push({
              name: attr[1],
              value: attr[3] || attr[4] || attr[5],
            })
          }
          // 4.3 将匹配到的属性记录到数组 match.attrs(属性对象包含属性名和属性值)
          if (end) {
            // <div id="app" 处理完成,需要连同关闭符号 > 一起截取掉,截取掉
            advance(end[0].length)
          }
          //  4.4 开始标签处理完成后,返回匹配结果:tagName 标签名 + attrs 属性对象
          return match
        }
        return false // 不是开始标签
      }
      while (html) {
        // 如果textEnd为0,说明是一个开始标签或者结束标签
        // 如果textEnd>0说明就是文本的结束位置
        // 解析标签or文本,判断html的第一个字符,是否为 < 尖角号
        let textEnd = html.indexOf('<') // 如果索引是0则说明是一个标签
        if (textEnd === 0) {
          // 解析开始标签,返回匹配结果,即标签名。
          const startTagMatch = parseStartTag()
          if (startTagMatch) {
            // 解析到的开始标签,传递标签名和属性
            start(startTagMatch.tagName, startTagMatch.attrs)
            continue
          }
          // 如果开始标签没有匹配到,有可能是结束标签 </div>
          let endTageMatch = html.match(endTag)
          if (endTageMatch) {
            // 删除已匹配完成的部分
            advance(endTageMatch[0].length)
            // 匹配到开始标签,向外传递标签名和属性
            end(endTageMatch[1])
            continue
          }
        }
    	// 如果是文本:将文本内容取出来并发射出去,并从html片段中截取掉
        if (textEnd > 0) {
          let text = html.substring(0, textEnd) // 文本内容
          if (text) {
            // 向外传递文本
            chars(text)
            advance(text.length) // 解析到的文本
          }
        }
      }
      return root
    }