Vue實現(xiàn)文本編譯詳情
Vue實現(xiàn)文本編譯詳情
模板編譯
在數(shù)據(jù)劫持中,我們完成了Vue中data選項中數(shù)據(jù)的初始操作。這之后需要將html字符串編譯為render函數(shù),其核心邏輯如下:
有render函數(shù)的情況下會直接使用傳入的render函數(shù),而在沒有render函數(shù)的情況下,需要將template編譯為render函數(shù)。
具體邏輯如下:
- 獲取
template字符串 - 將
template字符串解析為ast抽象語法樹 - 將
ast抽象語法樹生成代碼字符串 - 將字符串處理為
render函數(shù)賦值給vm.$options.render
獲取template字符串
在進(jìn)行template解析之前,會進(jìn)行一系列的條件處理,得到最終的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方法進(jìn)行掛載
// el選項如果不存在,需要手動調(diào)用vm.$mount方法來進(jìn)行組件的掛載
if (el) {
vm.$mount(el);
}
};
Vue.prototype.$mount = function (el) {
el = query(el);
const vm = this;
const options = vm.$options;
if (!options.render) { // 有render函數(shù),優(yōu)先處理render函數(shù)
let template = options.template;
// 沒有template,使用el.outerHTML作為template
if (!template && el) {
template = el.outerHTML;
}
options.render = compileToFunctions(template);
}
};
}當(dāng)我們得到最終的template后,需要調(diào)用compileToFunctions將template轉(zhuǎn)換為render函數(shù)。在compileToFunctions中就是模板編譯的主要邏輯。
創(chuàng)建src/compiler/index.js文件,其代碼如下:
export function compileToFunctions (template) {
// 將html解析為ast語法樹
const ast = parseHtml(template);
// 通過ast語法樹生成代碼字符串
const code = generate(ast);
// 將字符串轉(zhuǎn)換為函數(shù)
return new Function(`with(this){return $[code]}`);
}解析html
當(dāng)拿到對應(yīng)的html字符串后,需要通過正則來將其解析為ast抽象語法樹。簡單來說就是將html處理為一個樹形結(jié)構(gòu),可以很好的表示每個節(jié)點(diǎn)的父子關(guān)系。
下面是一段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', // 標(biāo)簽名
attrs: [{ name: 'id', value: 'app' }], // 屬性數(shù)組
type: 1, // type:1 是元素,type: 3 是文本
parent: null, // 父節(jié)點(diǎn)
children: [] // 孩子節(jié)點(diǎn)
}html的解析邏輯如下:
- 通過正則匹配開始標(biāo)簽的開始符號、匹配標(biāo)簽的屬性、匹配開始標(biāo)簽結(jié)束符號、匹配文本、匹配結(jié)束標(biāo)簽
while循環(huán)html字符串,每次刪除掉已經(jīng)匹配的字符串,直到html為空字符串時,說明整個文本匹配完成- 通過棧數(shù)據(jù)結(jié)構(gòu)來記錄所有正在處理的標(biāo)簽,并且根據(jù)標(biāo)簽的入棧出棧順序生成樹結(jié)構(gòu)
代碼中通過advance函數(shù)來一點(diǎn)點(diǎn)刪除被匹配的字符串,其邏輯比較簡單,只是對字符串進(jìn)行了截?。?/p>
// 刪除匹配的字符串
function advance (length) {
html = html.slice(length);
}
首先處理開始標(biāo)簽和屬性。
以<開頭的字符串為開始標(biāo)簽或結(jié)束標(biāo)簽,通過正則匹配開始標(biāo)簽,可以通過分組得到標(biāo)簽名。之后循環(huán)匹配標(biāo)簽的屬性,直到匹配到結(jié)尾標(biāo)簽。在這過程中要將匹配到的字符串通過advance進(jìn)行刪除。
export function parseHtml (html) {
function parseStartTag () {
const start = html.match(startTagOpen);
if (start) {
const match = { tag: start[1], attrs: [] };
// 開始解析屬性,直到標(biāo)簽閉合
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中書寫模板時可能開始和結(jié)束會有空白
html = html.trim();
while (html) {
// 開始和結(jié)束標(biāo)簽都會以 < 開頭
const textEnd = html.indexOf('<');
if (textEnd === 0) {
// 處理開始標(biāo)簽
const startTag = parseStartTag();
if (startTag) {
start(startTag.tag, startTag.attrs);
}
// some code ...
}
// some code...
}
return root;
}在獲得開始標(biāo)簽的標(biāo)簽名和屬性后,通過start函數(shù),可以生成樹根以及每一個入棧標(biāo)簽對應(yīng)ast元素并確定父子關(guān)系:
// 樹 + 棧
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 {
// 記錄父子關(guān)系
currentParent.children.push(element);
element.parent = currentParent;
}
currentParent = element;
stack.push(element);
}以一段簡單的html為例,我們畫圖看下其具體的出棧入棧邏輯:
<div id="app">
<h2>
hello world
<span> xxx </span>
</h2>
</div>
通過對象的引用關(guān)系,最終便能得到一個樹形結(jié)構(gòu)對象root。
解析完開始標(biāo)簽后,剩余的文本起始字符串可能為:
- 下一個開始標(biāo)簽
- 文本內(nèi)容
- 結(jié)束標(biāo)簽
如果仍然是開始標(biāo)簽,會重復(fù)上述邏輯。如果是文本內(nèi)容,<字符的索引會大于0,只需要將[0, textEnd)之間的文本截取出來放到父節(jié)點(diǎn)的children中即可:
export function parseHtml (html) {
// 樹 + 棧
let root, currentParent;
const stack = [];
function char (text) {
// 替換所有文本中的空格
text = text.replace(/\s/g, '');
if (currentParent && text) {
// 將文本放到對應(yīng)的父節(jié)點(diǎn)的children數(shù)組中,其type為3,標(biāo)簽type為1
currentParent.children.push({
type: 3,
text,
parent: currentParent
});
}
}
while (html) {
// some code ...
// < 在之后的位置,說明要處理的是文本內(nèi)容
if (textEnd > 0) { // 處理文本內(nèi)容
let text = html.slice(0, textEnd);
if (text) {
char(text);
advance(text.length);
}
}
}
return root;
}最后來處理結(jié)束標(biāo)簽。
匹配到結(jié)束標(biāo)簽時要將stack中最后一個元素出棧,更新currentParent,直到stack中的元素為空時。就得到了完整的ast抽象語法樹:
export function parseHtml (html) {
// 樹 + 棧
let root, currentParent;
const stack = [];
// 每次處理好前一個,最后將所有元素作為子元素push到root節(jié)點(diǎn)中
function end (tag) { // 在結(jié)尾標(biāo)簽匹配時可以確立父子關(guān)系
stack.pop();
currentParent = stack[stack.length - 1];
}
while (html) {
// 開始和結(jié)束標(biāo)簽都會以 < 開頭
const textEnd = html.indexOf('<');
if (textEnd === 0) {
// some code ...
// 處理結(jié)尾標(biāo)簽
const endTagMatch = html.match(endTag);
if (endTagMatch) {
end(endTagMatch[1]);
advance(endTagMatch[0].length);
}
}
// some code ...
}
return root;
}到這里我們拿到了一個樹形結(jié)構(gòu)對象ast,接下來要根據(jù)這個樹形結(jié)構(gòu),遞歸生成代碼字符串
生成代碼字符串
先看下面一段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]})轉(zhuǎn)換為render函數(shù),而在render函數(shù)執(zhí)行時通過call來將this指向vm 。所以代碼字符串中的函數(shù)和變量都會從vm上進(jìn)行查找。
下面是代碼字符串中用到的函數(shù)的含義:
_c: 創(chuàng)建虛擬元素節(jié)點(diǎn)createVElement_v: 創(chuàng)建虛擬文本節(jié)點(diǎn)createTextVNode_s:stringify對傳入的值執(zhí)行JSON.stringify
接下來開始介紹如何將ast樹形對象處理為上邊介紹到code。
創(chuàng)建src/compiler/generate.js文件,需要解析的內(nèi)容如下:
- 標(biāo)簽
- 屬性
- 遞歸處理
children - 文本
標(biāo)簽處理比較簡單,直接獲取ast.tag即可。
屬性在代碼字符串中是以對象的格式存在,而在ast中是數(shù)組的形式。這里需要遍歷數(shù)組,并將其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來進(jìn)行處理
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 : ''})`;
}在用,拼接對象時,也可以先將每一部分放到數(shù)組中,通過數(shù)組的join方法用,來拼接為字符串。
標(biāo)簽和屬性之后的參數(shù)都為孩子節(jié)點(diǎn),要以函數(shù)參數(shù)的形式用,進(jìn)行拼接,最終在生成虛擬節(jié)點(diǎn)時會通過...擴(kuò)展運(yùn)算符將其處理為一個數(shù)組:
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];
// 將生成結(jié)果放到數(shù)組中
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é)點(diǎn)時,需要判斷每一項的類型,如果是元素會繼續(xù)執(zhí)行generate方法來生成元素對應(yīng)的代碼字符串,如果是文本,需要通過genText方法來進(jìn)行處理:
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的大小關(guān)系
// match.index === lastIndex時,說明此時是{{}}中的內(nèi)容,前邊沒有字符串
if (match.index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, match.index)));
}
// 然后將括號內(nèi)的元素放到數(shù)組中
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標(biāo)識,每次匹配完之后,都會將lastIndex移動到下一次開始匹配的位置。最終匹配完所有的{{}} 文本后,match=null并且lastIndex=0,終止循環(huán)。
在{{}}中的文本需要放到_s() 中,每段文本都會放到數(shù)組tokens中,最后將每段文本通過+拼接起來。最終在render函數(shù)執(zhí)行時,會進(jìn)行字符串拼接操作,然后展示到頁面中。
代碼中用到的lastIndex和match.index的含義分別如下:
lastIndex: 字符串下次開始匹配的位置對應(yīng)的索引match.index: 匹配到的字符串在原字符串中的索引
其匹配邏輯如下圖所示:

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

