Vue實(shí)現(xiàn)文本編譯詳情
Vue實(shí)現(xiàn)文本編譯詳情
模板編譯
在數(shù)據(jù)劫持中,我們完成了Vue
中data
選項(xiàng)中數(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選項(xiàng)存在,會將el通過vm.$mount方法進(jìn)行掛載 // el選項(xiàng)如果不存在,需要手動調(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)時,需要判斷每一項(xiàng)的類型,如果是元素會繼續(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
實(shí)例上進(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ù)的功能,實(shí)現(xiàn)了文章開始時這行代碼的邏輯:
vm.$options.render = compileFunctions(template)
結(jié)語
文本中代碼主要涉及的知識如下:
- 通過棧+樹這倆種數(shù)據(jù)結(jié)構(gòu),通過正則將
html
解析為樹 - 利用正則表達(dá)式來進(jìn)行字符串的匹配實(shí)現(xiàn)相應(yīng)的邏輯
文章中介紹到的整個邏輯,也是Vue
在文本編譯過程中的核心邏輯。希望小伙伴在讀完本文之后,可以對Vue
如何解析template
有更深的理解,并可以嘗試閱讀其源碼。
到此這篇關(guān)于Vue實(shí)現(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-03vuejs2.0運(yùn)用原生js實(shí)現(xiàn)簡單的拖拽元素功能示例
本篇文章主要介紹了vuejs2.0運(yùn)用原生js實(shí)現(xiàn)簡單的拖拽元素功能示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-02-02Vue零基礎(chǔ)入門之模板語法與數(shù)據(jù)綁定及Object.defineProperty方法詳解
這篇文章主要介紹了Vue初學(xué)基礎(chǔ)中的模板語法、數(shù)據(jù)綁定、Object.defineProperty方法等基礎(chǔ),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09laravel5.3 vue 實(shí)現(xiàn)收藏夾功能實(shí)例詳解
這篇文章主要介紹了laravel5.3 vue 實(shí)現(xiàn)收藏夾功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-01-01antd?Vue實(shí)現(xiàn)Login登錄頁面布局案例詳解?附帶驗(yàn)證碼驗(yàn)證功能
這篇文章主要介紹了antd?Vue實(shí)現(xiàn)Login登錄頁面布局案例詳解附帶驗(yàn)證碼驗(yàn)證功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-05-05vue3+ant?design的form數(shù)組表單校驗(yàn)方法
這篇文章主要介紹了vue3+ant?design的form數(shù)組表單,如何校驗(yàn),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-09-09