01- vdom およびテンプレートのコンパイル ソース コード

コンポーネントのレンダリングのプロセス

テンプレート --> ast --> レンダリング --> vDom --> リアル Dom --> ページ

ランタイムコンパイラーとランタイムのみの違い - プログラマーが求めた

コンパイル手順

テンプレートのコンパイルは Vue の中核部分です。Vue のコンパイル原理の全体的なロジックは主に 3 つの部分に分かれており、3 つのステップに分かれていると言えます。

ステップ 1: テンプレート文字列を要素 AST に変換する ( parse )

ステップ 2: AST 上の静的ノード マーキング。主に仮想 DOM レンダリングの最適化 ( optimizer optimize )に使用されます。

ステップ 3: 要素 AST を使用してレンダー関数コード文字列を生成する (コード ジェネレーター生成)

 コンパイルされた AST 構造

テンプレートテンプレート:

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

AST 抽象構文ツリー:

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时才有这个属性,表示表达式的内容
      }]
    }
  ]
}

生成: AST を直接実行できる JavaScript 文字列に変換します。

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

注 1: 通常の開発では、コンパイル済みバージョン (ランタイムのみ) を含まない Vue バージョンを使用し、開発環境でエラーを報告するためにオプションでテンプレート オプションを直接渡します。

注 2: ここで渡されるテンプレート オプションは、.vue ファイルのテンプレートと混同しないでください。vue 単一ファイル コンポーネントのテンプレートは、vue-loader によって処理される必要があります。

渡した el または template オプションは、テンプレート解析の一貫性を維持するために、最終的にレンダリング関数に解析されます。

次のコード実装は、entry-runtime-with-compiler.js にあり、ランタイムのみのバージョンと区別する必要があります。

1. テンプレートのコンパイルエントリ

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;
			}
		}
	}
}
  • 最初に状態を初期化します ( initState(vm) )
  • 次に、 initMixin関数でel があるかどうかを判断し、ある場合はテンプレートのレンダリングのためにvm.$mount(vm.$options.el)を直接呼び出します。ない場合は手動で呼び出します。
  • Vue.prototype.$mountメソッド内でrender属性の有無を判定し、存在する場合はテンプレートに値を代入し、renderとtemplateがないがel属性がある場合は、テンプレートをアウターに直接代入するel が配置されている HTML 構造 (つまり、el 自体は親要素ではありません)。
  • 処理された テンプレートテンプレートを、 compileToFunctionsを通じて レンダリング関数に変換します。

私たちの主な懸念は、$mount メソッドが最終的に処理されたテンプレートをレンダリング関数に変換することです。

2. テンプレート変換コアメソッド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 → レンダリング関数

ここでは、次の 3 つの手順で HTML 文字列を render 関数に変換する必要があります。

(1) parse関数はHTMLコードをAST構文ツリーに変換します。

        AST は、コード自体によって形成されるツリー構造を記述するために使用されます。HTML だけでなく、css や js の構文も記述することができます。webpack、babel、eslint など、多くのライブラリで AST が使用されています。

(2)最適化機能は 静的ノードを最適化します (主に仮想 DOM レンダリングの最適化に使用されます)

        まず AST を走査し、AST の静的ノードをマークし (つまり、ノードは決して変更されません)、いくつかの特別な処理を実行します。例えば:

  • ここでは意味が無いため、静的ノードの v-once ディレクティブを削除しました。
  • 静的ノードのキー属性を削除します。静的ノードは決して変更されないため、キー属性は必要ありません。
  • いくつかの静的ノードを 1 つのノードに結合して、レンダリングされるノードの数を減らします。たとえば、隣接するテキスト ノードと要素ノードを 1 つの要素ノードにマージできます。

(3)生成関数はAST を通じてコードを再生成します。

最終的に生成されるコードは、render 関数と同じである必要があります。

このような構造:

_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
  • _c はノードを作成する createElement を表します
  • _v は、テキスト ノードを作成する createTextVNode を表します。
  • _s は、オブジェクトを文字列に解析するための toString を表します。
     

テンプレート エンジンの実装原則は、 with + new Function です。with 構文を使用してスコープを this に変更し、次に render 関数を呼び出してこれを変更し、コード内の変数値を容易にします。

関数を解析し、HTML を解析して 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;
}

関数を生成し、ast をレンダリング関数構造に変換します。

// 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;
}

コード文字列生成レンダリング関数

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

Vue テンプレート AST の詳細な説明 - 記事チュートリアル - Wenjiang Blog

スライド検証ページ

手書きの Vue2.0 ソースコード (2) - テンプレートのコンパイル原理|技術解説_MOOC ノート

フロントエンド Brother Shark の個人ホームページ - 記事 - ナゲッツ

フロントエンド・アドバンスト高収入必見・定番記事・ナゲッツ

Vue テンプレートの解析から正規表現を学ぶ

おすすめ

転載: blog.csdn.net/iaz999/article/details/132128864