面试官问我啥是AST?我....

316 阅读3分钟

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。

一、分词/语法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token),考虑程序var a = 2这段程序通常会被分解为下面这些词法单元:var、a、=、2。

二、解析/语法分析(Parsing)

这个过程是将词法单元流(数组)转换成一个元素逐级嵌套所组成的代表了程序语法结构的树,这个树被称之为抽象语法树(Abstract Syntax Tree)

三、代码生成

将AST转换成可执行代码的过程被称为代码生成。简单来说就是有某种方法可以将var a = 2;的AST转化为一组机器指令,用来创建一个叫做a的变量,并将一个值存储在a中。

 <div id='app'>
     <p>hello {{ name }}</p>
     world
 </div>

转换为下面的render函数

_c('div', {id:'app'}, _c('p', undefined, _v('hello', + _s(name))), _v('world'))

源码:gitee.com/guonan01/pe…

Vue中Parser html实现

parser-html.js

// 模板编译的正则表达式
 // Regular Expressions for parsing tags and attributes
 // copy from packages\vue-template-compiler\browser.js
 // 匹配标签伤的属性 id="app" id='app' id=app
 var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
 var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
 // 标签:abc-aaa
 var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z]*";
 // <aaa:bbb>
 var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
 // 标签开头的正则,捕获的内容是标签名
 var startTagOpen = new RegExp(("^<" + qnameCapture));
 // 匹配开始标签的结束符 >
 var startTagClose = /^\s*(\/?)>/;
 // 匹配结尾标签: </div>
 var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
 var doctype = /^<!DOCTYPE [^>]+>/i;
 var defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{ name }}
 

 let root = null    // AST语法树的树根
 let currentParent  // 标识当前父节点
 let stack = []     // 栈,用于存储父节点
 const ELEMENT_TYPE = 1 // 元素类型
 const TEXT_TYPE = 3  // 文本类型
 function createASTElement(tagName, attrs) {
    return {
      tag: tagName,
      type: ELEMENT_TYPE,
      children: [],
      attrs,
      parent: null  
    }
 }  


/**
 * 解析开始标签
 * @param {*} tagName 
 * @param {*} attrs 
 */
 function start (tagName, attrs) {
    // 遇到开始标签就创建一个AST元素
    let element = createASTElement(tagName, attrs)
    if(!root){
      root = element
    }
    currentParent = element // 把当前元素标记成AST树的父节点
    stack.push(element) // 把当前元素放入栈中
 }


 /**
  * 处理文本
  * @param {text} text 
  */
 function chars(text) {
    text = text.replace(/\s/g, '')
    if(text){
      currentParent.children.push({
        type: TEXT_TYPE,
        text
      })
    }
 }


 /**
  * 闭合标签
  * @param {} tagName 
  */
 // eg. <div><p></p></div>
 function end(tagName) {
    // 拿到的是AST对象
    let element = stack.pop()
    // 标识当前标签是属于这个div的子元素
    currentParent = stack[stack.length - 1]
    if(currentParent){
      element.parent = currentParent
      currentParent.children.push(element) //实现了树的父子关系
    }
 }


 export function parseHTML (html) {
    // 循环解析html
    while(html){
      let textEnd = html.indexOf('<')
      if(textEnd == 0){
        // 如果当前索引为0,则是一个标签【开始标签,或者结束标签】
        let 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
        }
      }
      
      let text;
      if(textEnd >= 0) {
        text = html.substring(0, textEnd)
      }
      if(text){
        advance(text.length)
        chars(text)
      }
    }

    // 截取
    function advance(n) {
      html = html.substring(n)
    }

    // 解析开始标签
    function parseStartTag() {
      let start = html.match(startTagOpen)
      // 匹配开始标签
      if(start) {
        let match = {
          tagName: start[1],
          attrs: []
        }
        advance(start[0].length)
        let end,attr
        // 匹配属性
        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] || ''
          })
        }
        
        // 匹配结束标签
        if(end){
          advance(end[0].length)
          return match
        }
      }
      
    }
    return root
 }

render函数的字符串拼接

compiler/index.js

 import { parseHTML } from './parser-html' 

 function genProps(attrs) {
   let str = "";
   for(let i = 0; i < attrs.length; i++) {
     let attr = attrs[i]
     if(attr.name === 'style'){
       let obj = {}
        attr.value.split(';').forEach(item => {
          let [key, value] = item.split(":")
          if(typeof value === 'string') {
            obj[key] = value.trim()
          }
        })
        attr.value = obj
     }
     str += `${attr.name}:${JSON.stringify(attr.value)},`
   }
   return `{${str.slice(0, -1)}}`
 }

 // _c('div', {id:'app', style: {color: red; background: blue;}}, _c('p', undefined, _v('hello', + _s(name))), _v('world'))
 function generate(el) {
    let code = `_c("${el.tag}", ${
      el.attrs.length ? genProps(el.attrs) : 'undefined' // undefined => {}
    })

    `

    return code
 }
 
 
 // AST语法树:用对象来描述js的原生语法,虚拟DOM:用对象描述dom节点
export function compileToFunction (template) {
  // 解析HTML字符串,将HTML字符串转换成AST语法树对象
  let root = parseHTML(template)
  
  // 需要将ast语法树生成最终的render函数,也就是字符串的拼接(模板引擎)
  let code = generate(root)
  console.log('code===', code)

  // <div id='app'><p>hello {{ name }}</p>world</div>
  // _c('div', {id:'app'}, _c('p', undefined, _v('hello', + _s(name))), _v('world'))
  return function render(){

  }
}

code=== _c("div", {id:"app",style:{"color":"red"," background":"blue"}})

源码:gitee.com/guonan01/pe…