Vue 【进阶】- AST 抽象语法树

1. AST 简介

在开发Vue的时候编译器会将模板语法编译成正常的HTML语法,而直接编译的时候是非常困难的,因此此时会借助AST抽象语法树进行周转,进而变为正常的HTML语法,使编译工作变得更加简单。

抽象语法树的本质上是一个JS对象,Vue在审视所有HTML结构时是以字符串的新式进行的,最终将其解析为JS对象。AST抽象语法树服务于模板编译,将一种语法翻译为另一种语法。在Vue中将模板语法编译为HTML语法,自己作为中转站。

2. 抽象语法树和虚拟节点的关系

AST 只会在 编译阶段 出现,用来描述 template 模板
虚拟DOM 只在运行时出现,用来描述 dom, render 的时候产生新的 虚拟dom

首次渲染是很消耗性能的,所以,用运行时版本,使用webpack -> vue-loader 可以将我们的 vue 文件在打包编译时就转成了一个个 render 函数

为什么 AST 不可以直接当 virtual DOM 呢?为什么我们要写 render 函数,而不是直接写虚拟 DOM?
vue 的响应式机制是数据改变时重新执行 render 函数,生成新的虚拟 dom,然后 diff 比对、渲染。如果直接编译成虚拟 dom,数据改变时如何更新虚拟 dom?AST 重新生成虚拟 dom?做不到呀,因为代码没变。

其实 AST 和 virtual DOM 的样子都差不多

AST的样子

虚拟dom的样子

3. 手撸解析为 AST 的过程

[https://www.cnblogs.com/caijinghong/p/16918948.html]建议先看这篇栈相关处理得算法

index.js

import parse from './parse'

var template = `
  <div>
    <h3>你好</h3>
    <ul>
      <li>A</li>
      <li>B</li>
      <li>C</li>
    </ul>
  </div>
`

console.log(parse(template))

parse.js

// parse, 主函数
export default function(template) {
  template = template.replace(/\s*/g,'')
  // console.log(template)
  // 指针
  let index = 0,
  // 剩余
  tail = template,
  // 栈
  stack = [], 
  // 匹配头标签<div>
  RegExp1 = /^\<(\w+)\>/,
  // 匹配尾部 content</div>和前面内容
  RegExp2 = /(.*?)(\<\/\w+\>)/;

  while(index < template.length) {
    if(RegExp1.test(tail)) {
      stack.push({
        tag: RegExp.$1,
        type: 1,
        children: [] 
      })

      index += RegExp.$1.length + 2
      tail = template.substring(index)
    } else if(RegExp2.test(tail) && !RegExp1.test(tail)){
      let endstack = stack[stack.length - 1]

      if(RegExp.$1) {
        endstack.children.push({
          text: RegExp.$1,
          type: 3
        })
      }

      let child = stack.pop()
      // 最后的返回值
      if(!stack.length) return child

      stack[stack.length - 1].children.push(child)

      index += (RegExp.$1.length + RegExp.$2.length)
      tail = template.substring(index)
    }
  }
}

最后得 AST 树

加入属性版

var template = `
  <div>
    <h3 class="test" name="666" style="color:red">你好</h3>
    <ul>
      <li>A</li>
      <li>B</li>
      <li>C</li>
    </ul>
  </div>
`

parse.js

import handleAttr from './handleAttr'

// parse, 主函数
export default function(template) {
  // 去掉首尾
  template = template.replace(/(^\s*|\s*$)/g,'')
  // 指针
  let index = 0,
  // 剩余
  tail = template,
  // 栈
  stack = [], 
  // 匹配头标签<div>,还能匹配属性props
  RegExp1 = /^\<(\w+)(\s*.*?)\>/,
  // 匹配尾部 content</div>和前面内容
  RegExp2 = /(.*?)(\<\/\w+\>)/;

  while(index < template.length) {
    if(RegExp1.test(tail)) {
      let obj = { tag: RegExp.$1, type: 1, children: [] }
      // 有 attrs
      if(RegExp.$2) {
        obj.attrs = [handleAttr(RegExp.$2)]
      }
      stack.push(obj)

      // 加上'<>'的长度
      index += (RegExp.$1.length + 2 + RegExp.$2.length)
      // 并且清掉剩下的头部的空格
      tail = template.substring(index).replace(/(^\s*)/g,'') 
      // 加上空格的长度
      index += RegExp.$1.length
    } else if(RegExp2.test(tail) && !RegExp1.test(tail)){
      let endstack = stack[stack.length - 1]

      if(RegExp.$1) {
        endstack.children.push({
          text: RegExp.$1,
          type: 3
        })
      }

      let child = stack.pop()
      // 最后的返回值
      if(!stack.length) return child

      stack[stack.length - 1].children.push(child)

      index += (RegExp.$1.length + RegExp.$2.length)
      tail = template.substring(index).replace(/(^\s*)/g,'')
      index += RegExp.$1.length
    }
  }
}

handleAttr.js 处理属性对象

export default function(attrs) {
  let obj = {}
  let attrsToArr = attrs.split(' ')

  attrsToArr
    .filter(item => item)
    .forEach(item => {
      let arrData =item.split('=')
      obj[arrData[0]] = arrData[1]
    })

  return obj
}

最后处理得如下

属性的写法改进

原来的写法 split 通过空格来处理得数组有个缺陷, <h3 class="test name" name="666" style="color:red">你好</h3>,这样处理得数组就不对,所以进行改进

方法一(指针法)

一个变量作为保存结果的作用,每次成功匹配变回覆盖
遍历到等号前的字符串去前空格后(/(^\s*)/g),便可变量作为对象的key,清空变量
匹配到'"'则开始保存值value在此遇到'"'结束

方法2️⃣(正则)
handleAttr.js

export default function(attrs) {
  let obj = {}
  let attrsToArr = attrs.match(/(\w+\=\"[\w|\s|\:]+\")+/g)

  console.log(attrs, attrsToArr)
 

  attrsToArr
    .forEach(item => {
      let arrData =item.split('=')
      obj[arrData[0]] = arrData[1]
    })

  return obj
}

大功告成!!!

posted on 2022-11-20 02:09  京鸿一瞥  阅读(351)  评论(0编辑  收藏  举报