vue源码--手写实现AST抽象语法树

344 阅读1分钟

vue将模板字符串渲染为DOM的过程

一、模板编译

  1. parse方法,将 template 中的代码解析成AST抽象语法树;(本文主要实现parse方法)
  2. optimize 方法,优化AST抽象语法树,防止重复渲染;
  3. generate函数,将AST抽象语法树生成render函数字符串(h函数);

二、渲染DOM

  1. h函数,生成虚拟节点
  2. patch方法,diff算法,并渲染为真实DOM

手写实现简版parse方法,将模板字符串转换为AST

简版不考虑以下情况:

  1. 自结束标签(有兴趣的伙伴可以自行尝试编写)
  2. 文本节点位于结束标签与开始标签之间,比如:<div></div>文本节点<h3></h3>

最终效果:

实现思路:

  1. 遍历模板字符串,利用栈的思想
  2. 如果是开始标签,处理属性信息,向栈中推入该标签的信息,如:{ tag:'div', attrs:[], type:1, children: [] }
  3. 如果是文本节点,向栈顶元素的children属性中推入文本节点
  4. 如果是结束标签,弹栈,并将弹栈的结果,推入到当前栈顶元素的children属性中,如果弹栈后,栈空了,说明已经遍历完一个根元素了

实现代码:

parse.js 解析模板字符串

import parseAttr from "./parseAttr"  // 引入解析属性字符串的方法
// parse函数
export default function (template) {
  let i = 0
  let lastTem = '' // 记录剩下的模板字符串
  let stack = []  // 栈
  // 匹配收集开始标签,并收集除标签名以外的attr
  let startReg = /^<([a-z]+[1-6]?)(\s.*?)?>/
  // 匹配收集结束标签
  let endReg = /^</([a-z]+[1-6]?)>/
  // 匹配收集文字
  let wordReg = /^>(.*?)<\//
  // 记录结果,模板字符串可以不止一个根标签,都搜集到children中
  let res = { tag: 'template', children: [] }
  while (i < template.length) {
    lastTem = template.substring(i)  // 获取剩下的模板字符串
    if (startReg.test(lastTem)) {   // 如果是开始标签
      let match = lastTem.match(startReg)
      let startTag = match[1]    // 开始标签
      let attrs = match[2]       // 属性字符串
      let attrsArr = []          // 收集属性的数组
      if (attrs) {
        attrsArr = parseAttr(attrs)    // 将属性字符串变为数组
      }
      // 将标签信息推入栈数组
      stack.push({ tag: startTag, attrs: attrsArr, type: 1, children: [] })
      i += match[0].length - 1     // length-1,是为了能收集到标签后的文字
    } else if (endReg.test(lastTem)) {   // 如果是结束标签
      let endTag = lastTem.match(endReg)[1]   // 结束标签
      if (endTag == stack[stack.length - 1].tag) {
        let top = stack.pop()   // 栈顶元素
        if (stack.length == 0) {
          // 栈中没有元素时,表示遍历完了一个根元素
          res.children.push(top)
        } else {
          // 将栈顶元素,推入上一个元素的children属性中
          stack[stack.length - 1].children.push(top)
        }
      } else {
        // 收集的结束标签与栈顶元素的tag不相等
        throw new Error(`${stack[stack.length - 1].tag}没有结束标签`)
      }
      i += endTag.length + 3
    } else if (wordReg.test(lastTem)) {   // 文本节点
      let word = lastTem.match(wordReg)[1]
      // 将文本节点推入栈顶元素的children中
      stack[stack.length - 1].children.push({ text: word, type: 3 })
      i += word.length
    } else {   // 如果是其他情况(比如是标签间空格),就不做任何处理
      i++
    }
  }
  return res
}

parseAttr.js 解析属性字符串

思路:
  1. 遍历属性字符串,使用一个变量记录是否在引号内
  2. 如果遇到在空格,且不在引号内的空格,从当前位置切断并存储到数组中
  3. 利用"="将数组中每个字符串转换为{name:xxx,value:xxx}的对象格式
// parseAttr函数 解析attr
export default function (attrStr) {
  let attrs = attrStr.trim()  // 去除前后空格
  let res = []  // 记录结果
  let spaceIn = false  // 记录是否在引号内,默认不在引号内
  let pos = 0   // 记录断点
  for (let i = 0; i < attrs.length; i++) {
    let char = attrs[i]
    if (char == '"') {
      spaceIn = !spaceIn // 遇到引号,spaceIn取反
    } else if (char == ' ' && !spaceIn) { // 遇到空格,且不在引号内
      // 从引号处切断,并存储到res数组中
      res.push(attrs.substring(pos, i).trim())
      pos = i   // 将断点设置为当前索引
    }
  }
  // 将最后一项推到结果数组中
  res.push(attrs.substring(pos).trim())
  // 使用map方法,将数组中的每一项变为{name:xxx,value:xxx}的格式
  return res.map(item => {
    let o = item.match(/(.*?)="(.*?)"/)
    return {
      name: o[1],
      value: o[2]
    }
  })
}

index.js 测试代码

import parse from "./parse";
// 两个同级的ul标签
let template = `
<ul class="hide box" id="box">
  <li>A</li>
  <li>B</li>
</ul>
<ul class="show box" id="box2">
  <li>C</li>
  <li>D</li>
</ul>
`
console.log(parse(template))

结果: