3. 获取html模板创建ast树

54 阅读2分钟

前言

image.png 看vue2官网的生命周期图可以得到结论: vue初次渲染 => 先初始化数据 => 将模板进行编译 => 变成render() => 生成虚拟节点 => 变成真实DOM=>放到页面

vue中模板编译的方式:

  1. render
  2. tmplate
  3. el (这个必须要有)

执行顺序是 render => tmplate => el

所新增代码以及文件的目录结构:


- index.html
- src
    - compile // 编译相关(主要作用:就是把模板转换成render函数,在render函数中创建虚拟DOM)
        -index.js  
- init.js

获取html及创建ast树

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">{{msg}}123<h1>6666</h1></div>
  <script src="dist/vue.js"></script>
  <script>
    let vm= new Vue({
      el:"#app",
      data(){
        console.log(this,"@@");
        return {
          msg:"hello world",
          arr:[{name:"小明",age:18}]
        }
      },


    })
    console.log(vm,"这是vm");
  </script>
</body>
</html>

src/init.js:

import { initState } from "./initState";
import { compileToFunction } from "./compile/index";
/**
 * @description 初始化vue
 * @param {Object} Vue
 * @returns {void}
 */
export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    let vm = this
    vm.$options = options
    // 初始化状态
    initState(vm)
    // 渲染模板
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
  // 创建$mount 进行模板编译
  Vue.prototype.$mount = function (el) {
    let vm = this
     // 获取dom元素
    el = document.querySelector(el)
    let options = vm.$options
    // 没有render函数
    if (!options.render) { 
      let template = options.template
      if (!template && el) { 
        // 获取html
        el = el.outerHTML
        // 转换成ast语法树  vnode(虚拟dom)   [ast语法树是能操作js和css 虚拟dom只能操作一个节点]
        let ast = compileToFunction(el)

      }
    }
    
  }
}

src/compile/index.js:


let root // 表示根元素
let createParent // 当前元素的父元素
let stack = [] // 数据结构  栈 
const attribute =
  /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 获取属性
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*` // 获取标签名称
// const qnameCapture = `((?:${ncname}\\:)?${ncname})` // 
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 标签开头的正则 捕获的内容是标签名
const startTagClose = /^\s*(\/?)>/ // 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
/***
 * @description 生成ast语法树
 * @param {string}  template
 * 
 */
export function compileToFunction(template) {
  // 解析HTML
  let ast = parseHTML(template)
}
function parseHTML(html) {
  // 遍历 html 为空就结束
  while (html) {
    // 判断标签
    let textEnd = html.indexOf('<');
    let text 
    if (textEnd === 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
      }
     
    }
    // 文本
    if (textEnd > 0) {
      // 获取到文本内容
      text = html.substring(0, textEnd)
      
    }
    if (text) { 
      // 删除文本
      advance(text.length)
      charts(text)
    }
  }
  function parseStartTag() {
    // 匹配开始标签
    let start = html.match(startTagOpen);
    if (!start) { 
      return
    }
    let match = {
      tagName: start[1],
      attrs: [],
    }
    // 删除开始标签
    advance(start[0].length)
    // 遍历属性
    let attr 
    let end
    while (!(end= html.match(startTagClose)) && (attr = html.match(attribute))){
      match.attrs.push({ name:attr[1],value:attr[3]||attr[4]||attr[5] })
      advance(attr[0].length)
    }
    if (end) { 
      advance(end[0].length)
      return match
    }
  }
  function advance(n) { 
    html = html.substring(n)
  }
  console.log("最后的root",root);
  return root
}


// 遍历开始的标签 
function start(tagName,attrs) { 
  let element = createASTElement(tagName, attrs)
  if (!root) { 
    root=element
  }
  createParent = element
  stack.push(element)
}

// 遍历文本标签
function charts(text) { 
  // 空格
  text = text.replace(/\s/g, ' ')
  if (text) {
    createParent.children.push({type:3,text:text})
  }
}

// 遍历结束标签
function end(tagName) { 
  let element = stack.pop() // 获取最后的元素进行出栈
  createParent=stack[stack.length-1]
  if (createParent) { // 元素闭合
    element.parent = createParent.tag
    createParent.children.push(element)
  }
}

function createASTElement(tag,attrs) { 
  return {
    tag,  // 表示元素
    attrs, // 表示属性
    children: [], // 是否有子节点
    type: 1,
    parent: null,
  }
}

最后解析出来的ast树:

image.png