第十四节: 模板编译之 template->AST树

83 阅读2分钟

vue的模板编译的过程

  1. 获取template
  2. template -> AST树
  3. AST树 -> render函数
  4. render函数 -> 虚拟节点
  5. 设置patch -> 打补丁到真实的DOM

AST Abstract syntax tree 抽象语法树 源代码的抽象语法结构的树状描述

思路

/*
<div id="app" style="color: red;font-size: 20px;">
  你好, {{ name }}
  <span class="text" style="color: green">{{ age }}</span>
</div>
type: 1元素
type: 3文本节点

匹配完了就删除 match来匹配
删除完了继续匹配
<div
id="app" style="color: red;font-size: 20px;"
>
你好, {{ name }}
<span
class="text"
style="color: green"
>
{{ age }}
</span>
</div>
*/
  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app" style="color: red;font-size: 20px;">
    你好, {{ name }}
    <span class="text" style="color: green">{{ age }}</span>
  </div>
  <script src="dist/vue.js"></script>
  <script>
    // umd -> window.Vue
    // console.log(Vue);
    const vm = new Vue({
      el: "#app",
      data() {
        return {
          name: "蒋小白",
          age: 18,
          msg: 'vue zzz',
          a: {
            b: 10
          },
          list: [1,2, 3]
        }
      },
      props: {},
      watch: {},
      computed: {}
    })
    // .$mount('#app');
  </script>
</body>
</html>

入口文件

import { initMixin } from './init'
function Vue(options) {
  // 初始化
  this.__init(options);
}

initMixin(Vue);//初始化
export default Vue

初始化文件

import { initState } from './initState.js';
import { compileToFunction } from './compile/index.js'
// 初始化业务
export function initMixin(Vue) {
  // 初始化
  Vue.prototype.__init = function(options) {
    let vm = this;// this -> vue实例
    vm.$options = options;
    // 初始化状态
    initState(vm);
    
//------------------------- 上面的代码见前几节的学习笔记 ----------------
    // 渲染模板 el
    if(vm.$options.el) {
      // 执行挂载函数 Vue.prototype.$mount原型上的函数
      vm.$mount(vm.$options.el);
    }
  }

  // 创建 $mount
  Vue.prototype.$mount = function(el) {
    // el < template < render 优先级别比较
    let vm = this,
        options = vm.$options;
    el = document.querySelector(el);//获取元素
    vm.$el = el;//为什么在实例上挂载,就是方便以后传实例可以获取所有的东西

    if(!options.render) {//没有render
      // 获取template属性
      let template = options.template;
      if(!template && el) {// 没有template && 有el
        // 获取包括自己的html
        template = el.outerHTML;// <div id="app">{{msg}}</div>
        // 变成 ast语法树
      }

      //作用 template -> ast树  -> 转成render函数
      let render = compileToFunction(template);
      // 将render挂载到options上面
      options.render = render;
    }
  }
}

处理 tempalte -> ast树的入口文件 compile/index.js

import { parseHtmlToAst } from './astParser';

// 进行模板编译的
function compileToFunction(html) {
  // html -> 转化成Ast
  const ast = parseHtmlToAst(html);// 源码叫 parseHTML
}

export {
  compileToFunction
}

tempalte -> ast树功能文件 compile/astParser.js

// id="app" id='app' id=app
// const attribute = /^\s*([^\s"'<div>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<div>`]+)))?/;
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 匹配标签名
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// 匹配特殊的 <my-header></my-header> <m:header></m:header>
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 匹配标签头 </div
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 匹配标签尾 >  />
const startTagClose = /^\s*(\/?)>/;
// 匹配</div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);

// html -> 转化成Ast
function parseHtmlToAst(html) {
  let text,
      root,
      currentParent,
      stack = [];

  while(html) {
    // 先找 <
    let textEnd = html.indexOf('<');
    if(textEnd === 0) {//说明是第一项
      const startTagMatch = parseStartTag();
      if(startTagMatch) {
        start(startTagMatch.tagName, startTagMatch.attrs);
        continue;
      }

      // 匹配如 </div>
      const endTagMatch = html.match(endTag);
      if(endTagMatch) {
        advance(endTagMatch[0].length);
        end(endTagMatch[1]);
        continue;
      }
    }

    // < 不在第一位,说明 -> 文本 + <
    if(textEnd > 0) {
      // 获取文本
      text = html.substring(0, textEnd);
    }

    if(text) {
      // 删掉text
      advance(text.length);
      // 处理文本数据格式
      chars(text);
    }
  }

  function parseStartTag() {
    const start = html.match(startTagOpen);//匹配 如 <div
    let end,
        attr;

    if(start) {
      const match = {
        tagName: start[1],
        attrs: [],//属性还没处理
      }
      // 截取后面的的部分,去掉匹配的部门
      advance(start[0].length);
      // 没有匹配> 匹配的属性
      while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5],
          // 写法不同 值可能出现在 3 4 5 项上的一项上
        })
        // 截取掉匹配的部分
        advance(attr[0].length);
      }

      if(end) {
        advance(end[0].length);
        return match;
      }
    }
  }

  // 截取字符串
  function advance(n) {
    html = html.substring(n);
  }

  // stack [div, span]
  function start(tagName, attrs) {
    // 组装数据
    const element = createASTElement(tagName, attrs);
    // 如果root没有值说明这一项是根元素
    if(!root) {
      root = element;
    }
    // => 记录当前的 "元素"
    currentParent = element;
    stack.push(element);//添加到数组里面去  [div] -> [div, span]
  }

  function end(tagName) {
    // span
    const element = stack.pop();
    // div
    currentParent = stack[stack.length - 1];
    if(currentParent) {
      // span -> parent -> div
      element.parent = currentParent;
      // div -> children -> span
      currentParent.children.push(element);
    }
  }

  // 处理文本字符串
  function chars(text) {
    // 文本去掉空格
    text = text.trim();
    if(text.length > 0) {
      // 当前元素添加文本
      currentParent.children.push({
        type: 3,
        text
      })
    }
  }

  // 元素类型的
  function createASTElement(tagName, attrs) {
    return {
      tag: tagName,
      type: 1,
      attrs,
      children: [],
      parent
    }
  }
  console.log(root);
  return root;
}

export {
  parseHtmlToAst
}
  • 处理的AST树数据格式如下: image.png