01- vdom and template compilation source code

The process of component rendering

template --> ast --> render --> vDom --> real Dom --> page

The difference between Runtime-Compiler and Runtime-Only - Programmer Sought

Compilation steps

Template compilation is a core part of Vue. The overall logic of the Vue compilation principle is mainly divided into three parts, or it can be said to be divided into three steps. The context is as follows:

Step 1: Convert template strings to element ASTs ( parse )

Step 2: Static node marking on AST, mainly used for virtual DOM rendering optimization ( optimizer optimize )

Step 3: Use element ASTs to generate render function code strings ( code generator generate )

 The compiled AST structure

template template:

<div class="box">
  <p>{
   
   {name}}</p>
</div>

AST abstract syntax tree:

ast: {
  tag: "div" //  元素标签名
  type: 1,  // 元素节点类型 1标签 2包含字面量表达式的文本节点 3普通文本节点或注释节点
  staticRoot: false, // 是否静态根节点
  static: false,  // 是否静态节点
  plain: true, 
  parent: undefined,
  attrsList: [], // 标签节点的属性名和值的对象集合
  attrsMap: {}, // 和attrsList类似,不同在它是以键值对保存属性名和值
  children: [
    {
      tag: "p"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: {tag: "div", ...},
      attrsList: [],
      attrsMap: {},
      children: [{
        type: 2,
        text: "{
   
   {name}}",
        static: false,
        expression: "_s(name)"  // type为2时才有这个属性,表示表达式的内容
      }]
    }
  ]
}

generate, which converts AST into a JavaScript string that can be executed directly

with(this) {
    return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])
}

Note 1: In normal development, we use the Vue version without a compiled version (runtime-only) and directly pass in the template option in options to report an error in the development environment

Note 2: The template option passed in here should not be confused with the template in the .vue file. The template of the vue single-file component needs to be processed by vue-loader

The el or template option we pass in will be parsed into a render function in the end so as to maintain the consistency of template parsing

The following code implementation is in entry-runtime-with-compiler.js, which needs to be distinguished from the runtime-only version

1. Template compilation entry

export function initMixin (Vue) {
	Vue.prototype._init = function (options) {
		const vm = this;
		vm.$options = options;
		initState(vm);
		// 如果有 el 属性,进行模板渲染
		if (vm.$options.el) {
			vm.$mount(vm.$options.el)
		}
	}

	Vue.prototype.$mount = function (el) {
		const vm = this;
		const options = vm.$options;
		el = document.querySelector(el);
		// 不存在 render 属性的几种情况
		if (!options.render) {
			// 不存在 render 但存在 template
			let template = options.template;
			// 不存在 render 和 template 但是存在 el 属性,直接将模板赋值到 el 所在的外层 html 结构 (就是 el 本身,并不是父元素)
			if (!template && el) {
				template = el.outerHTML;
			}
			// 最后把处理好的 template 模板转化成 render 函数
			if (template) {
				const render = compileToFunctions(template);
				options.render = render;
			}
		}
	}
}
  • Initialize the state first, initState(vm) ;
  • Then in the initMixin function, judge whether there is el , if there is, directly call vm.$mount(vm.$options.el) for template rendering, if not, call it manually;
  • In the Vue.prototype.$mount method, judge whether there is a render attribute, and assign a value to the template if it exists. If there is no render and template but there is an el attribute, directly assign the template to the outer HTML structure where el is located (that is, el itself, is not the parent element);
  • Convert the processed  template template into a render function through  compileToFunctions .

Our main concern is that the $mount method will eventually convert the processed template template into a render function

2. Template conversion core method compileToFunctions

export function compileToFunctions (template) {
	// html → ast → render函数
	// 第一步 将 html 字符串转换成 ast 语法树
	let ast = parse(template);
	// 第二步 优化静态节点
	if (mergeOptions.optimize !== false) {
		optimize(ast, options);
	}
	// 第三步 通过 ast 重新生成代码
	let code = generate(ast);
	// 使用 with 语法改变作用域为 this, 之后调用 render 函数可以使用 call 改变 this,方便 code 里面的变量取值。
	let renderFn = new Function(`with(this){return ${code}}`);
	return renderFn;
}

html → ast → render-function

Here you need to turn the html string into a render function in three steps:

(1) parse function converts HTML code into AST syntax tree

        AST is used to describe the tree structure formed by the code itself. It can describe not only HTML, but also css and js syntax; many libraries use AST, such as webpack, babel, eslint, etc.

(2) The optimize function  optimizes static nodes (mainly used for virtual DOM rendering optimization)

        First traverse the AST, mark the static nodes of the AST (that is, the nodes will never change) and do some special processing. For example:

  • Removed the v-once directive for static nodes, as it doesn't make sense here.
  • Remove the key attribute of static nodes. Because static nodes never change, no key attribute is needed.
  • Combine some static nodes into one node to reduce the number of nodes rendered. For example adjacent text nodes and element nodes can be merged into one element node.

(3) The generate function regenerates the code through AST

The final generated code needs to be the same as the render function

A structure like this:

_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
  • _c stands for createElement to create a node
  • _v stands for createTextVNode to create a text node
  • _s stands for toString to parse the object into a string
     

The implementation principle of the template engine is with + new Function , use the with syntax to change the scope to this, and then call the render function to change this to facilitate the variable value in the code.

parse function, parse html and generate ast

// src/compiler/parse.js

// 以下为源码的正则  对正则表达式不清楚的同学可以参考小编之前写的文章(前端进阶高薪必看 - 正则篇);
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束  >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性  形如 id="app"

let root, currentParent; //代表根节点 和当前父节点
// 栈结构 来表示开始和结束标签
let stack = [];
// 标识元素和文本type
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;
// 生成ast方法
function createASTElement(tagName, attrs) {
  return {
    tag: tagName,
    type: ELEMENT_TYPE,
    children: [],
    attrs,
    parent: null,
  };
}

// 对开始标签进行处理
function handleStartTag({ tagName, attrs }) {
  let element = createASTElement(tagName, attrs);
  if (!root) {
    root = element;
  }
  currentParent = element;
  stack.push(element);
}

// 对结束标签进行处理
function handleEndTag(tagName) {
  // 栈结构 []
  // 比如 <div><span></span></div> 当遇到第一个结束标签</span>时 会匹配到栈顶<span>元素对应的ast 并取出来
  let element = stack.pop();
  // 当前父元素就是栈顶的上一个元素 在这里就类似div
  currentParent = stack[stack.length - 1];
  // 建立parent和children关系
  if (currentParent) {
    element.parent = currentParent;
    currentParent.children.push(element);
  }
}

// 对文本进行处理
function handleChars(text) {
  // 去掉空格
  text = text.replace(/\s/g, "");
  if (text) {
    currentParent.children.push({
      type: TEXT_TYPE,
      text,
    });
  }
}

// 解析标签生成ast核心
export function parse(html) {
  while (html) {
    // 查找<
    let textEnd = html.indexOf("<");
    // 如果<在第一个 那么证明接下来就是一个标签 不管是开始还是结束标签
    if (textEnd === 0) {
      // 如果开始标签解析有结果
      const startTagMatch = parseStartTag();
      if (startTagMatch) {
        // 把解析好的标签名和属性解析生成ast
        handleStartTag(startTagMatch);
        continue;
      }

      // 匹配结束标签</
      const endTagMatch = html.match(endTag);
      if (endTagMatch) {
        advance(endTagMatch[0].length);
        handleEndTag(endTagMatch[1]);
        continue;
      }
    }

    let text;
    // 形如 hello<div></div>
    if (textEnd >= 0) {
      // 获取文本
      text = html.substring(0, textEnd);
    }
    if (text) {
      advance(text.length);
      handleChars(text);
    }
  }

  // 匹配开始标签
  function parseStartTag() {
    const start = html.match(startTagOpen);

    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
      };
      //匹配到了开始标签 就截取掉
      advance(start[0].length);

      // 开始匹配属性
      // end代表结束符号>  如果不是匹配到了结束标签
      // attr 表示匹配的属性
      let end, attr;
      while (
        !(end = html.match(startTagClose)) &&
        (attr = html.match(attribute))
      ) {
        advance(attr[0].length);
        attr = {
          name: attr[1],
          value: attr[3] || attr[4] || attr[5], //这里是因为正则捕获支持双引号 单引号 和无引号的属性值
        };
        match.attrs.push(attr);
      }
      if (end) {
        //   代表一个标签匹配到结束的>了 代表开始标签解析完毕
        advance(1);
        return match;
      }
    }
  }
  //截取html字符串 每次匹配到了就往前继续匹配
  function advance(n) {
    html = html.substring(n);
  }
  //   返回生成的ast
  return root;
}

generate function, convert ast into render function structure

// src/compiler/codegen.js

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 {
   
   {  }} 捕获花括号里面的内容

function gen(node) {
  // 判断节点类型
  // 主要包含处理文本核心
  // 源码这块包含了复杂的处理  比如 v-once v-for v-if 自定义指令 slot等等  咱们这里只考虑普通文本和变量表达式{
   
   {}}的处理

  // 如果是元素类型
  if (node.type == 1) {
    //   递归创建
    return generate(node);
  } else {
    //   如果是文本节点
    let text = node.text;
    // 不存在花括号变量表达式
    if (!defaultTagRE.test(text)) {
      return `_v(${JSON.stringify(text)})`;
    }
    // 正则是全局模式 每次需要重置正则的lastIndex属性  不然会引发匹配bug
    let lastIndex = (defaultTagRE.lastIndex = 0);
    let tokens = [];
    let match, index;

    while ((match = defaultTagRE.exec(text))) {
      // index代表匹配到的位置
      index = match.index;
      if (index > lastIndex) {
        //   匹配到的{
   
   {位置  在tokens里面放入普通文本
        tokens.push(JSON.stringify(text.slice(lastIndex, index)));
      }
      //   放入捕获到的变量内容
      tokens.push(`_s(${match[1].trim()})`);
      //   匹配指针后移
      lastIndex = index + match[0].length;
    }
    // 如果匹配完了花括号  text里面还有剩余的普通文本 那么继续push
    if (lastIndex < text.length) {
      tokens.push(JSON.stringify(text.slice(lastIndex)));
    }
    // _v表示创建文本
    return `_v(${tokens.join("+")})`;
  }
}

// 处理attrs属性
function genProps(attrs) {
  let str = "";
  for (let i = 0; i < attrs.length; i++) {
    let attr = attrs[i];
    // 对attrs属性里面的style做特殊处理
    if (attr.name === "style") {
      let obj = {};
      attr.value.split(";").forEach((item) => {
        let [key, value] = item.split(":");
        obj[key] = value;
      });
      attr.value = obj;
    }
    str += `${attr.name}:${JSON.stringify(attr.value)},`;
  }
  return `{${str.slice(0, -1)}}`;
}

// 生成子节点 调用gen函数进行递归创建
function getChildren(el) {
  const children = el.children;
  if (children) {
    return `${children.map((c) => gen(c)).join(",")}`;
  }
}
// 递归创建生成code
export function generate(el) {
  let children = getChildren(el);
  let code = `_c('${el.tag}',${
    el.attrs.length ? `${genProps(el.attrs)}` : "undefined"
  }${children ? `,${children}` : ""})`;
  return code;
}

code string generation render function

export function compileToFunctions (template) {

	let ast = parseHTML(template);

	let code = genCode(ast);
	// 将模板变成 render 函数,通过 with + new Function 的方式让字符串变成 JS 语法来执行
	const render = new Function(`with(this){return ${code}}`);

	return render;
}

 https://juejin.cn/spost/7267858684667035704

Detailed Explanation of Vue Template AST - Article Tutorial - Wenjiang Blog

slide verification page

Handwritten Vue2.0 source code (2) - Template Compilation Principle|Technical Comments_MOOC Notes

front-end Brother Shark's personal homepage- Articles- Nuggets

Front-end advanced high-paying must-see-regular articles- Nuggets

Learn regular expressions from vue template parsing

Guess you like

Origin blog.csdn.net/iaz999/article/details/132128864