Vue實現文本編譯詳情
Vue實現文本編譯詳情
模板編譯
在數據劫持中,我們完成了Vue中data選項中數據的初始操作。這之后需要將html字符串編譯為render函數,其核心邏輯如下:
有render函數的情況下會直接使用傳入的render函數,而在沒有render函數的情況下,需要將template編譯為render函數。
具體邏輯如下:
- 獲取
template字符串 - 將
template字符串解析為ast抽象語法樹 - 將
ast抽象語法樹生成代碼字符串 - 將字符串處理為
render函數賦值給vm.$options.render
獲取template字符串
在進行template解析之前,會進行一系列的條件處理,得到最終的template,其處理邏輯如下:

在src/init.js中書寫如下代碼:
/**
* 將字符串處理為dom元素
* @param el
* @returns {Element|*}
*/
function query (el) {
if (typeof el === 'string') {
return document.querySelector(el);
}
return el;
}
function initMixin (Vue) {
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options;
initState(vm);
const { el } = options;
// el選項存在,會將el通過vm.$mount方法進行掛載
// el選項如果不存在,需要手動調用vm.$mount方法來進行組件的掛載
if (el) {
vm.$mount(el);
}
};
Vue.prototype.$mount = function (el) {
el = query(el);
const vm = this;
const options = vm.$options;
if (!options.render) { // 有render函數,優(yōu)先處理render函數
let template = options.template;
// 沒有template,使用el.outerHTML作為template
if (!template && el) {
template = el.outerHTML;
}
options.render = compileToFunctions(template);
}
};
}當我們得到最終的template后,需要調用compileToFunctions將template轉換為render函數。在compileToFunctions中就是模板編譯的主要邏輯。
創(chuàng)建src/compiler/index.js文件,其代碼如下:
export function compileToFunctions (template) {
// 將html解析為ast語法樹
const ast = parseHtml(template);
// 通過ast語法樹生成代碼字符串
const code = generate(ast);
// 將字符串轉換為函數
return new Function(`with(this){return $[code]}`);
}解析html
當拿到對應的html字符串后,需要通過正則來將其解析為ast抽象語法樹。簡單來說就是將html處理為一個樹形結構,可以很好的表示每個節(jié)點的父子關系。
下面是一段html,以及表示它的ast:
<body>
<div id="app">
hh
<div id="aa" style="font-size: 18px;">hello {{name}} world</div>
</div>
<script>
const vm = new Vue({
el: '#app',
data () {
return {
name: 'zs',
};
},
});
</script>
</body>
const ast = {
tag: 'div', // 標簽名
attrs: [{ name: 'id', value: 'app' }], // 屬性數組
type: 1, // type:1 是元素,type: 3 是文本
parent: null, // 父節(jié)點
children: [] // 孩子節(jié)點
}html的解析邏輯如下:
- 通過正則匹配開始標簽的開始符號、匹配標簽的屬性、匹配開始標簽結束符號、匹配文本、匹配結束標簽
while循環(huán)html字符串,每次刪除掉已經匹配的字符串,直到html為空字符串時,說明整個文本匹配完成- 通過棧數據結構來記錄所有正在處理的標簽,并且根據標簽的入棧出棧順序生成樹結構
代碼中通過advance函數來一點點刪除被匹配的字符串,其邏輯比較簡單,只是對字符串進行了截?。?/p>
// 刪除匹配的字符串
function advance (length) {
html = html.slice(length);
}
首先處理開始標簽和屬性。
以<開頭的字符串為開始標簽或結束標簽,通過正則匹配開始標簽,可以通過分組得到標簽名。之后循環(huán)匹配標簽的屬性,直到匹配到結尾標簽。在這過程中要將匹配到的字符串通過advance進行刪除。
export function parseHtml (html) {
function parseStartTag () {
const start = html.match(startTagOpen);
if (start) {
const match = { tag: start[1], attrs: [] };
// 開始解析屬性,直到標簽閉合
advance(start[0].length);
let end = html.match(startTagClose);
let attr = html.match(attribute);
// 循環(huán)處理屬性
while (!end && attr) {
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5]
});
advance(attr[0].length);
end = html.match(startTagClose);
attr = html.match(attribute);
}
if (end) {
advance(end[0].length);
}
return match;
}
}
// 注意:在template中書寫模板時可能開始和結束會有空白
html = html.trim();
while (html) {
// 開始和結束標簽都會以 < 開頭
const textEnd = html.indexOf('<');
if (textEnd === 0) {
// 處理開始標簽
const startTag = parseStartTag();
if (startTag) {
start(startTag.tag, startTag.attrs);
}
// some code ...
}
// some code...
}
return root;
}在獲得開始標簽的標簽名和屬性后,通過start函數,可以生成樹根以及每一個入棧標簽對應ast元素并確定父子關系:
// 樹 + 棧
function createASTElement (tag, attrs) {
return {
tag,
type: 1,
attrs,
children: [],
parent: null
};
}
let root, currentParent;
const stack = [];
function start (tag, attrs) {
const element = createASTElement(tag, attrs);
if (!root) {
root = element;
} else {
// 記錄父子關系
currentParent.children.push(element);
element.parent = currentParent;
}
currentParent = element;
stack.push(element);
}以一段簡單的html為例,我們畫圖看下其具體的出棧入棧邏輯:
<div id="app">
<h2>
hello world
<span> xxx </span>
</h2>
</div>
通過對象的引用關系,最終便能得到一個樹形結構對象root。
解析完開始標簽后,剩余的文本起始字符串可能為:
- 下一個開始標簽
- 文本內容
- 結束標簽
如果仍然是開始標簽,會重復上述邏輯。如果是文本內容,<字符的索引會大于0,只需要將[0, textEnd)之間的文本截取出來放到父節(jié)點的children中即可:
export function parseHtml (html) {
// 樹 + 棧
let root, currentParent;
const stack = [];
function char (text) {
// 替換所有文本中的空格
text = text.replace(/\s/g, '');
if (currentParent && text) {
// 將文本放到對應的父節(jié)點的children數組中,其type為3,標簽type為1
currentParent.children.push({
type: 3,
text,
parent: currentParent
});
}
}
while (html) {
// some code ...
// < 在之后的位置,說明要處理的是文本內容
if (textEnd > 0) { // 處理文本內容
let text = html.slice(0, textEnd);
if (text) {
char(text);
advance(text.length);
}
}
}
return root;
}最后來處理結束標簽。
匹配到結束標簽時要將stack中最后一個元素出棧,更新currentParent,直到stack中的元素為空時。就得到了完整的ast抽象語法樹:
export function parseHtml (html) {
// 樹 + 棧
let root, currentParent;
const stack = [];
// 每次處理好前一個,最后將所有元素作為子元素push到root節(jié)點中
function end (tag) { // 在結尾標簽匹配時可以確立父子關系
stack.pop();
currentParent = stack[stack.length - 1];
}
while (html) {
// 開始和結束標簽都會以 < 開頭
const textEnd = html.indexOf('<');
if (textEnd === 0) {
// some code ...
// 處理結尾標簽
const endTagMatch = html.match(endTag);
if (endTagMatch) {
end(endTagMatch[1]);
advance(endTagMatch[0].length);
}
}
// some code ...
}
return root;
}到這里我們拿到了一個樹形結構對象ast,接下來要根據這個樹形結構,遞歸生成代碼字符串
生成代碼字符串
先看下面一段html字符串生成的代碼字符串是什么樣子的:
<body>
<div id="app">
hh
<div id="aa" style="color: red;">hello {{name}} world</div>
</div>
<script>
const vm = new Vue({
el: '#app',
data () {
return {
name: 'zs',
};
},
});
</script>
</body>最終得到的代碼字符串如下:
const code = `_c("div",{id:"app"},_v("hh"),_c("div"),{id:"aa",style:{color: "red"}},_v("hello"+_s(name)+"world"))`最終會將上述代碼通過new Function(with(this) { return $[code]})轉換為render函數,而在render函數執(zhí)行時通過call來將this指向vm 。所以代碼字符串中的函數和變量都會從vm上進行查找。
下面是代碼字符串中用到的函數的含義:
_c: 創(chuàng)建虛擬元素節(jié)點createVElement_v: 創(chuàng)建虛擬文本節(jié)點createTextVNode_s:stringify對傳入的值執(zhí)行JSON.stringify
接下來開始介紹如何將ast樹形對象處理為上邊介紹到code。
創(chuàng)建src/compiler/generate.js文件,需要解析的內容如下:
- 標簽
- 屬性
- 遞歸處理
children - 文本
標簽處理比較簡單,直接獲取ast.tag即可。
屬性在代碼字符串中是以對象的格式存在,而在ast中是數組的形式。這里需要遍歷數組,并將其name和value處理為對象的鍵和值。需要注意style屬性要特殊處理
function genAttrs (attrs) {
if (attrs.length === 0) {
return 'undefined';
}
let str = '';
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
if (attr.name === 'style') {
const styleValues = attr.value.split(',');
// 可以對對象使用JSON.stringify來進行處理
attr.value = styleValues.reduce((obj, item) => {
const [key, val] = item.split(':');
obj[key] = val;
return obj;
}, {});
}
str += `${attr.name}:${JSON.stringify(attr.value)}`;
if (i !== attrs.length - 1) {
str += ',';
}
}
return `{${str}}`;
}
// some code ...
export function generate (el) {
const children = genChildren(el.children);
return `_c("${el.tag}", ${genAttrs(el.attrs)}${children ? ',' + children : ''})`;
}在用,拼接對象時,也可以先將每一部分放到數組中,通過數組的join方法用,來拼接為字符串。
標簽和屬性之后的參數都為孩子節(jié)點,要以函數參數的形式用,進行拼接,最終在生成虛擬節(jié)點時會通過...擴展運算符將其處理為一個數組:
function gen (child) {
if (child.type === 1) {
// 將元素處理為代碼字符串并返回
return generate(child);
} else if (child.type === 3) {
return genText(child.text);
}
}
// 將children處理為代碼字符串并返回
function genChildren (children) { // 將children用','拼接起來
const result = [];
for (let i = 0; i < children.length; i++) {
const child = children[i];
// 將生成結果放到數組中
result.push(gen(child));
}
return result.join(',');
}
export function generate (el) {
const children = genChildren(el.children);
return `_c("${el.tag}", ${genAttrs(el.attrs)}${children ? ',' + children : ''})`;
}在生成孩子節(jié)點時,需要判斷每一項的類型,如果是元素會繼續(xù)執(zhí)行generate方法來生成元素對應的代碼字符串,如果是文本,需要通過genText方法來進行處理:
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
function genText (text) {
if (!defaultTagRE.test(text)) {
return `_v(${JSON.stringify(text)})`;
}
// <div id="aa">hello {{name}} xx{{msg}} hh <span style="color: red" class="bb">world</span></div>
const tokens = [];
let lastIndex = defaultTagRE.lastIndex = 0;
let match;
while (match = defaultTagRE.exec(text)) {
// 這里的先后順序如何確定? 通過match.index和lastIndex的大小關系
// match.index === lastIndex時,說明此時是{{}}中的內容,前邊沒有字符串
if (match.index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, match.index)));
}
// 然后將括號內的元素放到數組中
tokens.push(`_s(${match[1].trim()})`);
lastIndex = defaultTagRE.lastIndex;
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
return `_v(${tokens.join('+')})`;
}genText中會利用lastIndex以及match.index來循環(huán)處理每一段文本。由于正則添加了g標識,每次匹配完之后,都會將lastIndex移動到下一次開始匹配的位置。最終匹配完所有的{{}} 文本后,match=null并且lastIndex=0,終止循環(huán)。
在{{}}中的文本需要放到_s() 中,每段文本都會放到數組tokens中,最后將每段文本通過+拼接起來。最終在render函數執(zhí)行時,會進行字符串拼接操作,然后展示到頁面中。
代碼中用到的lastIndex和match.index的含義分別如下:
lastIndex: 字符串下次開始匹配的位置對應的索引match.index: 匹配到的字符串在原字符串中的索引
其匹配邏輯如下圖所示:

在上邊的邏輯完成后,會得到最終的code,下面需要將code處理為render函數。
生成render函數
在js中,new Function可以通過字符串來創(chuàng)建一個函數。利用我們之前生成的字符串再結合new Function便可以得到一個函數。
而字符串中的變量最終會到vm實例上進行取值,with可以指定變量的作用域,下面是一個簡單的例子:
const obj = { a: 1, b: 2 }
with (obj) {
console.log(a) // 1
console.log(b) // 2
}利用new Function和with的相關特性,可以得到如下代碼:
const render = new Function(`with(this){return $[code]}`)到這里,我們便完成了compileToFunctions函數的功能,實現了文章開始時這行代碼的邏輯:
vm.$options.render = compileFunctions(template)
結語
文本中代碼主要涉及的知識如下:
- 通過棧+樹這倆種數據結構,通過正則將
html解析為樹 - 利用正則表達式來進行字符串的匹配實現相應的邏輯
文章中介紹到的整個邏輯,也是Vue在文本編譯過程中的核心邏輯。希望小伙伴在讀完本文之后,可以對Vue如何解析template有更深的理解,并可以嘗試閱讀其源碼。
到此這篇關于Vue實現文本編譯詳情的文章就介紹到這了,更多相關Vue文本編譯內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue3中update:modelValue的使用與不生效問題解決
現在vue3的使用越來越普遍了,vue3這方面的學習我們要趕上,下面這篇文章主要給大家介紹了關于vue3中update:modelValue的使用與不生效問題的解決方法,需要的朋友可以參考下2022-03-03
Vue零基礎入門之模板語法與數據綁定及Object.defineProperty方法詳解
這篇文章主要介紹了Vue初學基礎中的模板語法、數據綁定、Object.defineProperty方法等基礎,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-09-09
antd?Vue實現Login登錄頁面布局案例詳解?附帶驗證碼驗證功能
這篇文章主要介紹了antd?Vue實現Login登錄頁面布局案例詳解附帶驗證碼驗證功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-05-05

