Skip to content

模板编译

vue初次渲染=>先初始化数据=>将模版进行编译=>变成render()=>生成虚拟节点=>变成真实的dom=>放到页面 vue模版编译

  • template
  • render
  • el 注意el(必须有)

优先级

js
import { initState } from "./initState";
export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    let vm = this;
    vm.$options = options;
    // 初始化状态
    initState(vm);
    //渲染模版
    //1.如果有el走且有template 则用template
    //2.需要将模板编译成render函数
    //3.如果有el但没有template 则用outerHTML
    //4.render函数
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
  //  $mount 挂载方法
  Vue.prototype.$mount = function (el) {
    const vm = this;
    el = document.querySelector(el); //获取元素
    vm.$el = el;
    let options = vm.$options;
    if (!options.render) {
      // 没有render 需要模板编译
      let template = options.template;
      //3----------
      if (!template && el) {
        //获取html
        el = el.outerHTML;
      } else {
      }
    }
  };
}

创建ast语法树

js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //标签名称
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //<span:xx>
const startTagOpen = new RegExp(`^<${qnameCapture}`); //标签开头的正则,捕获的内容上标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //匹配标签结尾的</div>
const attribute =
  /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; //匹配属性的
const startTagClose = /^\s*(\/?)>/; //匹配标签结尾的>
const defaultTagRE = /\{\{((?:.|\r|\n)+?)\}\}/g; //匹配{{}}
//el=<div id="app"><div>{{msg}}</div></div>
export function compileToFunction(el) {
  // 1. 处理模板
  parseHTML(el);
}

//遍历

//创建一个ast对象
function createASTElement(tag, attrs) {
  return {
    tag, //元素
    attrs,
    type: 1, //元素类型
    children: [],
    parent: null,
  };
}
let root; //根元素
let currentParent; //当前父元素
let stack = []; //数据结构 栈
function start(tag, attrs) {
  let element = createASTElement(tag, attrs);
  if (!root) {
    root = element;
  }
  currentParent = element;
  stack.push(element);//-----入栈
}
//获取文本
function charts(text) {
    text=text.replace(/\s/g,'')
    if(text){
        currentParent.children.push({
            type:3,
            text
        })
    }
}
//结束标签
function end(tag) {
  let element = stack.pop();//出栈
  currentParent = stack[stack.length - 1];
  if(currentParent){//元素的闭合
    element.parent = currentParent;
    currentParent.children.push(element);
  }
}
//解析html
function parseHTML(html) {
  //开始标签
  //文本
  //结束标签
  while (html) {
    //判断标签
    let textEnd = html.indexOf("<");
    if (textEnd === 0) {
      //开始标签
      const 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);
      charts(text);
    }
  }
  function parseStartTag() {
    const start = html.match(startTagOpen);
    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
      };
      //匹配到开始标签,截取掉
      advance(start[0].length);
      let attr, 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);
  }
  return root
}

//ast语法树
/**
 * {
 *  tag:'div',
 *  attrs:[{id:'app'}],
 *  children:[{tag:null,text:'hello'},{tag:null,text:'world'}]
 * }
 */

![[Pasted image 20240630000138.png]]

将ast语法树变成render函数字符串

js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/compile/index.js
import { parseHTML } from "./parseAst.js";
import { generate } from "./generate.js";
export function compileToFunction(el) {
  // 1.把模板变成ast语法树
  let ast = parseHTML(el);
  console.log(ast, 2222);
  //2. ast语法树生成render函数
  //2.1 ast语法树变成字符串
  //2.2 字符串变成函数
  let code = generate(ast);
  //3. render函数变成虚拟dom
  console.log(code, 3333);
}
js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/compile/generate.js
export function generate(el) {
  /**
   * _c:解析标签
   * _v:解析文本
   * _s:解析变量
   */

  /**
   * 1.先检查元素标签,
   * 2.先判断是否需要解析属性,然后解析属性
   */
  let children = genChildren(el);
  let code = `_c('${el.tag}',${el.attrs.length ? `${genProps(el.attrs)}` : "undefined"}${children ? `,${children}` : ""})`;
  console.log(code);
  return code;
}

/**
 * {type: 3, text: 'hello{{msg}}'}
 * {tag: 'h1', attrs:[], type: 1, children: [{type: 3, text: '1111'}], parent: {},tag:'h1',type:1}
 */

//处理子集1
function genChildren(el) {
  let children = el.children;
  if (children) {
    return children.map((child) => gen(child)).join(",");
  }
}

/**
 * 没解析的时候是
 * [
 *      {name: 'id', value: 'app'},
 *      {name: 'class', value: 'title name value'},
 *      {name: 'style', value: 'color: red; font-size: 20px'}
 * ]
 */

//处理属性,只解析样式然后其他的都是拼接的
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, val] = item.split(":");
        obj[key] = val;
      });
      attr.value = obj;
    }
    //拼接
    str += `${attr.name}:${JSON.stringify(attr.value)},`;
  }
  return `{${str.slice(0, -1)}}`; //移除最后一个逗号
}

const defaultTagRE = /\{\{((?:.|\r|\n)+?)\}\}/g; //匹配{{}}
//处理子集2
function gen(node) {
  /**
   * 1是元素
   * 3是文本
   */
  if (node.type === 1) {
    debugger;
    return generate(node);
  } else {
    let text = node.text;
    if (!defaultTagRE.test(text)) {
      //没有差值表达式
      return `_v(${JSON.stringify(text)})`;
    }
    let tokens = [];
    //TODO:正则匹配每次匹配需要重置为0
    /**
     * 什么情况需要把正则的lastIndex归0
     * 1. 重新使用正则表达式进行新的匹配:当你想要重新开始匹配时,如果之前的匹配操作没有完成,lastIndex可能不会自动归零。这种情况下,手动将lastIndex归零是必要的。
     * 2。 在不同字符串之间切换匹配:如果你用同一个正则表达式对象去匹配不同的字符串,为了避免lastIndex干扰新的字符串匹配,需要手动重置lastIndex。
     * 3. 使用String.prototype.replace或String.prototype.split等方法后:虽然这些方法内部会处理lastIndex,但如果在这些方法之间有手动操作正则表达式,最好确保lastIndex是重置的。
     * 4. 避免意外匹配结果:当多次执行exec方法或在循环中使用正则表达式进行匹配时,如果不重置lastIndex,可能会得到意外的匹配结果。
     */
    let lastIndex = (defaultTagRE.lastIndex = 0); //重置正则
    let match, index;
    while ((match = defaultTagRE.exec(text))) {
      index = match.index;
      //为什么需要判断index>lastIndex
      //因为正则匹配是贪婪匹配,会尽可能多的匹配
      if (index > lastIndex) {
        //如果index>lastIndex说明没有匹配到,需要把中间的文本push到tokens中
        tokens.push(JSON.stringify(text.slice(lastIndex, index)));
      }
      tokens.push(`_s(${match[1].trim()})`);
      lastIndex = index + match[0].length;
      if (lastIndex < text.length) {
        tokens.push(JSON.stringify(text.slice(lastIndex)));
      }
      return `_v(${tokens.join("+")})`;
    }
  }
}
/**
 * render(){
 *  return _c('div',{id:app},_v('hello'+_s(msg)),_c)
 * }
 */

[!question] 正则匹配每次匹配需要重置为0

  1. 重新使用正则表达式进行新的匹配:当你想要重新开始匹配时,如果之前的匹配操作没有完成,lastIndex可能不会自动归零。这种情况下,手动将lastIndex归零是必要的。
  2. 在不同字符串之间切换匹配:如果你用同一个正则表达式对象去匹配不同的字符串,为了避免lastIndex干扰新的字符串匹配,需要手动重置lastIndex。
  3. 使用String.prototype.replace或String.prototype.split等方法后:虽然这些方法内部会处理lastIndex,但如果在这些方法之间有手动操作正则表达式,最好确保lastIndex是重置的。
  4. 避免意外匹配结果:当多次执行exec方法或在循环中使用正则表达式进行匹配时,如果不重置lastIndex,可能会得到意外的匹配结果。

将render字符串变为函数

js
import { parseHTML } from "./parseAst.js";
import { generate } from "./generate.js";
export function compileToFunction(el) {
  // 1.把模板变成ast语法树
  let ast = parseHTML(el);
  console.log(ast, 2222);
  //2. ast语法树生成render函数
  //2.1 ast语法树变成字符串
  let code = generate(ast);
  //2.2 字符串变成函数
  //
  let render = new Function(`with(this){return ${code}}`)
  console.log(render, 1111);
  //3. render函数变成虚拟dom
}

[!question] with的用法

将render函数变为虚拟DOM

[!cite]

  1. render函数变为vnode
  2. vnode变为真实dom
  3. 生命周期
  1. 将render函数return出去
js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/compile/index.js
import { parseHTML } from "./parseAst.js";
import { generate } from "./generate.js";
export function compileToFunction(el) {
  // 1.把模板变成ast语法树
  let ast = parseHTML(el);
  console.log(ast, 2222);
  //2. ast语法树生成render函数
  //2.1 ast语法树变成字符串
  let code = generate(ast);
  //2.2 字符串变成函数
  //
  let render = new Function(`with(this){return ${code}}`)
  return render
  //3. render函数变成虚拟dom
}

在挂载的方法接收render方法,执行挂载组件的方法mounetComponent

js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/init.js
import { initState } from "./initState";
import {compileToFunction} from './compile/index'
import { mounetComponent } from "./lifecycle"
export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    let vm = this;
    vm.$options = options;
    // 初始化状态
    initState(vm);
    //渲染模版
    //1.如果有el走且有template 则用template
    //2.需要将模板编译成render函数
    //3.如果有el但没有template 则用outerHTML
    //4.render函数
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
  //  $mount 挂载方法
  Vue.prototype.$mount = function (el) {
    const vm = this;
    el = document.querySelector(el); //获取元素
    vm.$el = el;
    let options = vm.$options;
    if (!options.render) {
      // 没有render 需要模板编译
      let template = options.template;
      //3----------
      if (!template && el) {
        //获取html
        el = el.outerHTML;
        //变成ast语法树
        let render = compileToFunction(el);
        //将render函数编程vnode
        //vonde变为真实DOM放到页面上去
        options.render = render;
      } else {
      }
    }
    mounetComponent(vm, el);
  };
}

执行mounetComponent方法,其中需要注册render方法,在初始化js中注册render方法

js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/lifecycle.js
//生命周期文件
export function mounetComponent(vm,el){
    //vm._render将render函数变为vnode
    //vm._update将vnode变为真实dom
    //render函数=》vnode=》真实dom
    console.log(vm)
    vm._update(vm._render())
}
//初始化生命周期
export function lifecycleMixin(Vue){
    Vue.prototype._update = function(vnode){

    }
}

注册render方法

js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/index.js
import {initMixin} from './init'
import {lifecycleMixin} from './lifecycle'
import {renderMixin} from './vnode/index'
function Vue(options){
    this._init(options)
}
initMixin(Vue)
lifecycleMixin(Vue)//添加生命周期
renderMixin(Vue)//添加render

export default Vue

挂载render函数,转成vnode(虚拟dom)

js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/vnode/index.js
export function renderMixin(Vue) {
  Vue.prototype._c = function () {
    //标签
    //创建标签
    return createElement(...arguments);
  };
  Vue.prototype._v = function (text) {
    //文本
    return createText(text);
  };
  Vue.prototype._s = function (val) {
    //变量
    return val == null
      ? ""
      : typeof val === "object"
      ? JSON.stringify(val)
      : val;
  };
  Vue.prototype._render = function () {
    //render函数变为vnode
    let vm = this;
    let render = vm.$options.render;
    let vnode = render.call(vm);
    return vnode;
  };
}
//创建元素
function createElement(tag, data = {}, ...children) {
  return vnode(tag, data, data.key, children);
}

function vnode(tag, data, key, children, text) {
  return {
    tag,
    data,
    key,
    children,
    text,
  };
}

//创建文本
function createText(text) {
  return vnode(undefined, undefined, undefined,undefined, text);
}

将虚拟dom变为真实dom