Vue實(shí)現(xiàn)文本編譯詳情
Vue實(shí)現(xiàn)文本編譯詳情
模板編譯
在數(shù)據(jù)劫持中,我們完成了Vue
中data
選項(xiàng)中數(shù)據(jù)的初始操作。這之后需要將html
字符串編譯為render
函數(shù),其核心邏輯如下:
有render
函數(shù)的情況下會(huì)直接使用傳入的render
函數(shù),而在沒(méi)有render
函數(shù)的情況下,需要將template
編譯為render
函數(shù)。
具體邏輯如下:
- 獲取
template
字符串 - 將
template
字符串解析為ast
抽象語(yǔ)法樹(shù) - 將
ast
抽象語(yǔ)法樹(shù)生成代碼字符串 - 將字符串處理為
render
函數(shù)賦值給vm.$options.render
獲取template字符串
在進(jìn)行template
解析之前,會(huì)進(jìn)行一系列的條件處理,得到最終的template
,其處理邏輯如下:
在src/init.js
中書(shū)寫(xiě)如下代碼:
/** * 將字符串處理為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)存在,會(huì)將el通過(guò)vm.$mount方法進(jìn)行掛載 // el選項(xiàng)如果不存在,需要手動(dòng)調(diào)用vm.$mount方法來(lái)進(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; // 沒(méi)有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語(yǔ)法樹(shù) const ast = parseHtml(template); // 通過(guò)ast語(yǔ)法樹(shù)生成代碼字符串 const code = generate(ast); // 將字符串轉(zhuǎn)換為函數(shù) return new Function(`with(this){return $[code]}`); }
解析html
當(dāng)拿到對(duì)應(yīng)的html
字符串后,需要通過(guò)正則來(lái)將其解析為ast
抽象語(yǔ)法樹(shù)。簡(jiǎn)單來(lái)說(shuō)就是將html
處理為一個(gè)樹(shù)形結(jié)構(gòu),可以很好的表示每個(gè)節(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
的解析邏輯如下:
- 通過(guò)正則匹配開(kāi)始標(biāo)簽的開(kāi)始符號(hào)、匹配標(biāo)簽的屬性、匹配開(kāi)始標(biāo)簽結(jié)束符號(hào)、匹配文本、匹配結(jié)束標(biāo)簽
while
循環(huán)html
字符串,每次刪除掉已經(jīng)匹配的字符串,直到html
為空字符串時(shí),說(shuō)明整個(gè)文本匹配完成- 通過(guò)棧數(shù)據(jù)結(jié)構(gòu)來(lái)記錄所有正在處理的標(biāo)簽,并且根據(jù)標(biāo)簽的入棧出棧順序生成樹(shù)結(jié)構(gòu)
代碼中通過(guò)advance
函數(shù)來(lái)一點(diǎn)點(diǎn)刪除被匹配的字符串,其邏輯比較簡(jiǎn)單,只是對(duì)字符串進(jìn)行了截?。?/p>
// 刪除匹配的字符串 function advance (length) { html = html.slice(length); }
首先處理開(kāi)始標(biāo)簽和屬性。
以<
開(kāi)頭的字符串為開(kāi)始標(biāo)簽或結(jié)束標(biāo)簽,通過(guò)正則匹配開(kāi)始標(biāo)簽,可以通過(guò)分組得到標(biāo)簽名。之后循環(huán)匹配標(biāo)簽的屬性,直到匹配到結(jié)尾標(biāo)簽。在這過(guò)程中要將匹配到的字符串通過(guò)advance
進(jìn)行刪除。
export function parseHtml (html) { function parseStartTag () { const start = html.match(startTagOpen); if (start) { const match = { tag: start[1], attrs: [] }; // 開(kāi)始解析屬性,直到標(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中書(shū)寫(xiě)模板時(shí)可能開(kāi)始和結(jié)束會(huì)有空白 html = html.trim(); while (html) { // 開(kāi)始和結(jié)束標(biāo)簽都會(huì)以 < 開(kāi)頭 const textEnd = html.indexOf('<'); if (textEnd === 0) { // 處理開(kāi)始標(biāo)簽 const startTag = parseStartTag(); if (startTag) { start(startTag.tag, startTag.attrs); } // some code ... } // some code... } return root; }
在獲得開(kāi)始標(biāo)簽的標(biāo)簽名和屬性后,通過(guò)start
函數(shù),可以生成樹(shù)根以及每一個(gè)入棧標(biāo)簽對(duì)應(yīng)ast
元素并確定父子關(guān)系:
// 樹(shù) + 棧 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); }
以一段簡(jiǎn)單的html
為例,我們畫(huà)圖看下其具體的出棧入棧邏輯:
<div id="app"> <h2> hello world <span> xxx </span> </h2> </div>
通過(guò)對(duì)象的引用關(guān)系,最終便能得到一個(gè)樹(shù)形結(jié)構(gòu)對(duì)象root
。
解析完開(kāi)始標(biāo)簽后,剩余的文本起始字符串可能為:
- 下一個(gè)開(kāi)始標(biāo)簽
- 文本內(nèi)容
- 結(jié)束標(biāo)簽
如果仍然是開(kāi)始標(biāo)簽,會(huì)重復(fù)上述邏輯。如果是文本內(nèi)容,<
字符的索引會(huì)大于0,只需要將[0, textEnd)
之間的文本截取出來(lái)放到父節(jié)點(diǎn)的children
中即可:
export function parseHtml (html) { // 樹(shù) + 棧 let root, currentParent; const stack = []; function char (text) { // 替換所有文本中的空格 text = text.replace(/\s/g, ''); if (currentParent && text) { // 將文本放到對(duì)應(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 ... // < 在之后的位置,說(shuō)明要處理的是文本內(nèi)容 if (textEnd > 0) { // 處理文本內(nèi)容 let text = html.slice(0, textEnd); if (text) { char(text); advance(text.length); } } } return root; }
最后來(lái)處理結(jié)束標(biāo)簽。
匹配到結(jié)束標(biāo)簽時(shí)要將stack
中最后一個(gè)元素出棧,更新currentParent
,直到stack
中的元素為空時(shí)。就得到了完整的ast
抽象語(yǔ)法樹(shù):
export function parseHtml (html) { // 樹(shù) + 棧 let root, currentParent; const stack = []; // 每次處理好前一個(gè),最后將所有元素作為子元素push到root節(jié)點(diǎn)中 function end (tag) { // 在結(jié)尾標(biāo)簽匹配時(shí)可以確立父子關(guān)系 stack.pop(); currentParent = stack[stack.length - 1]; } while (html) { // 開(kāi)始和結(jié)束標(biāo)簽都會(huì)以 < 開(kāi)頭 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; }
到這里我們拿到了一個(gè)樹(shù)形結(jié)構(gòu)對(duì)象ast
,接下來(lái)要根據(jù)這個(gè)樹(shù)形結(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"))`
最終會(huì)將上述代碼通過(guò)new Function(with(this) { return $[code]})
轉(zhuǎn)換為render
函數(shù),而在render
函數(shù)執(zhí)行時(shí)通過(guò)call
來(lái)將this
指向vm
。所以代碼字符串中的函數(shù)和變量都會(huì)從vm
上進(jìn)行查找。
下面是代碼字符串中用到的函數(shù)的含義:
_c
: 創(chuàng)建虛擬元素節(jié)點(diǎn)createVElement
_v
: 創(chuàng)建虛擬文本節(jié)點(diǎn)createTextVNode
_s
:stringify
對(duì)傳入的值執(zhí)行JSON.stringify
接下來(lái)開(kāi)始介紹如何將ast
樹(shù)形對(duì)象處理為上邊介紹到code
。
創(chuàng)建src/compiler/generate.js
文件,需要解析的內(nèi)容如下:
- 標(biāo)簽
- 屬性
- 遞歸處理
children
- 文本
標(biāo)簽處理比較簡(jiǎn)單,直接獲取ast.tag
即可。
屬性在代碼字符串中是以對(duì)象的格式存在,而在ast
中是數(shù)組的形式。這里需要遍歷數(shù)組,并將其name
和value
處理為對(duì)象的鍵和值。需要注意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(','); // 可以對(duì)對(duì)象使用JSON.stringify來(lái)進(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 : ''})`; }
在用,
拼接對(duì)象時(shí),也可以先將每一部分放到數(shù)組中,通過(guò)數(shù)組的join
方法用,
來(lái)拼接為字符串。
標(biāo)簽和屬性之后的參數(shù)都為孩子節(jié)點(diǎn),要以函數(shù)參數(shù)的形式用,
進(jìn)行拼接,最終在生成虛擬節(jié)點(diǎn)時(shí)會(huì)通過(guò)...
擴(kuò)展運(yùn)算符將其處理為一個(gè)數(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用','拼接起來(lái) 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)時(shí),需要判斷每一項(xiàng)的類型,如果是元素會(huì)繼續(xù)執(zhí)行generate
方法來(lái)生成元素對(duì)應(yīng)的代碼字符串,如果是文本,需要通過(guò)genText
方法來(lái)進(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)) { // 這里的先后順序如何確定? 通過(guò)match.index和lastIndex的大小關(guān)系 // match.index === lastIndex時(shí),說(shuō)明此時(shí)是{{}}中的內(nèi)容,前邊沒(méi)有字符串 if (match.index > lastIndex) { tokens.push(JSON.stringify(text.slice(lastIndex, match.index))); } // 然后將括號(hào)內(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
中會(huì)利用lastIndex
以及match.index
來(lái)循環(huán)處理每一段文本。由于正則添加了g
標(biāo)識(shí),每次匹配完之后,都會(huì)將lastIndex
移動(dòng)到下一次開(kāi)始匹配的位置。最終匹配完所有的{{}}
文本后,match=null
并且lastIndex=0
,終止循環(huán)。
在{{}}
中的文本需要放到_s()
中,每段文本都會(huì)放到數(shù)組tokens
中,最后將每段文本通過(guò)+
拼接起來(lái)。最終在render
函數(shù)執(zhí)行時(shí),會(huì)進(jìn)行字符串拼接操作,然后展示到頁(yè)面中。
代碼中用到的lastIndex
和match.index
的含義分別如下:
lastIndex
: 字符串下次開(kāi)始匹配的位置對(duì)應(yīng)的索引match.index
: 匹配到的字符串在原字符串中的索引
其匹配邏輯如下圖所示:
在上邊的邏輯完成后,會(huì)得到最終的code
,下面需要將code
處理為render
函數(shù)。
生成render函數(shù)
在js
中,new Function
可以通過(guò)字符串來(lái)創(chuàng)建一個(gè)函數(shù)。利用我們之前生成的字符串再結(jié)合new Function
便可以得到一個(gè)函數(shù)。
而字符串中的變量最終會(huì)到vm
實(shí)例上進(jìn)行取值,with
可以指定變量的作用域,下面是一個(gè)簡(jiǎn)單的例子:
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)了文章開(kāi)始時(shí)這行代碼的邏輯:
vm.$options.render = compileFunctions(template)
結(jié)語(yǔ)
文本中代碼主要涉及的知識(shí)如下:
- 通過(guò)棧+樹(shù)這倆種數(shù)據(jù)結(jié)構(gòu),通過(guò)正則將
html
解析為樹(shù) - 利用正則表達(dá)式來(lái)進(jìn)行字符串的匹配實(shí)現(xiàn)相應(yīng)的邏輯
文章中介紹到的整個(gè)邏輯,也是Vue
在文本編譯過(guò)程中的核心邏輯。希望小伙伴在讀完本文之后,可以對(duì)Vue
如何解析template
有更深的理解,并可以嘗試閱讀其源碼。
到此這篇關(guān)于Vue實(shí)現(xiàn)文本編譯詳情的文章就介紹到這了,更多相關(guān)Vue文本編譯內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue3中update:modelValue的使用與不生效問(wèn)題解決
現(xiàn)在vue3的使用越來(lái)越普遍了,vue3這方面的學(xué)習(xí)我們要趕上,下面這篇文章主要給大家介紹了關(guān)于vue3中update:modelValue的使用與不生效問(wèn)題的解決方法,需要的朋友可以參考下2022-03-03vuejs2.0運(yùn)用原生js實(shí)現(xiàn)簡(jiǎn)單的拖拽元素功能示例
本篇文章主要介紹了vuejs2.0運(yùn)用原生js實(shí)現(xiàn)簡(jiǎn)單的拖拽元素功能示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02基于vue.js實(shí)現(xiàn)分頁(yè)查詢功能
這篇文章主要為大家詳細(xì)介紹了基于vue.js實(shí)現(xiàn)分頁(yè)查詢功能,vue.js實(shí)現(xiàn)數(shù)據(jù)庫(kù)分頁(yè),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12Vue零基礎(chǔ)入門(mén)之模板語(yǔ)法與數(shù)據(jù)綁定及Object.defineProperty方法詳解
這篇文章主要介紹了Vue初學(xué)基礎(chǔ)中的模板語(yǔ)法、數(shù)據(jù)綁定、Object.defineProperty方法等基礎(chǔ),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09highCharts提示框中顯示當(dāng)前時(shí)間的方法
今天小編就為大家分享一篇關(guān)于highCharts提示框中顯示當(dāng)前時(shí)間的方法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-01-01laravel5.3 vue 實(shí)現(xiàn)收藏夾功能實(shí)例詳解
這篇文章主要介紹了laravel5.3 vue 實(shí)現(xiàn)收藏夾功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-01-01antd?Vue實(shí)現(xiàn)Login登錄頁(yè)面布局案例詳解?附帶驗(yàn)證碼驗(yàn)證功能
這篇文章主要介紹了antd?Vue實(shí)現(xiàn)Login登錄頁(yè)面布局案例詳解附帶驗(yàn)證碼驗(yàn)證功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05vue3+ant?design的form數(shù)組表單校驗(yàn)方法
這篇文章主要介紹了vue3+ant?design的form數(shù)組表單,如何校驗(yàn),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-09-09