コンポーネントのレンダリングのプロセス
テンプレート --> 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 ノート