響應(yīng)式框架原理3(Object.defineProperty VS Proxy)
接著之前的繼續(xù)
模板編譯原理
了解了如何監(jiān)聽數(shù)據(jù)的變化,接下來以類 Vue 框架為例,看一個(gè)典型的用法:
<body>
<div id="app">
<h1>{{stage}}標(biāo)題:{{course.title}}</h1>
<p>作者:{{course.author}}</p>
<p>時(shí)間:{{course.publishTime}} </p>
</div>
<script>
let vue = new Vue({
ele: '#app',
data: {
stage: '簡書',
course: {
title: '響應(yīng)式框架原理記錄',
author: '看到請叫我不要熬夜',
publishTime: '2020年3月3日'
},
}
})
</script>
</body>
模版變量使用了 {{ }} 的表達(dá)方式輸出模版變量。最終輸出的 HTML 內(nèi)容應(yīng)該被合適的數(shù)據(jù)進(jìn)行填充替換,因此還需要一步編譯過程,該過程任何框架或類庫中都是相通的,比如 React 中的 JSX,也是編譯為 React.createElement,并在生成虛擬 DOM 時(shí)進(jìn)行數(shù)據(jù)填充。
簡化過程,將模版內(nèi)容輸出為真實(shí)的HTML過程:
<div id="app">
<h1>{{stage}}標(biāo)題:{{course.title}}</h1>
<p>作者:{{course.author}}</p>
<p>時(shí)間:{{course.publishTime}} </p>
</div>
模版編譯實(shí)現(xiàn)
使用正則 + 遍歷,有時(shí)也需要一些算法知識,現(xiàn)在的場景只需要對 #app 節(jié)點(diǎn)下內(nèi)容進(jìn)行替換,通過正則識別出模版變量,獲取對應(yīng)的數(shù)據(jù)即可:
const data = {
stage: '簡書',
course: {
title: '響應(yīng)式框架原理記錄',
author: '看到請叫我不要熬夜',
publishTime: '2020年3月3日'
},
}
compile(document.querySelector('#app'), data)
function compile(el, data) {
let fragment = document.createDocumentFragment()
while (child = el.firstChild) {
fragment.appendChild(child)
}
// 對el里面的內(nèi)容進(jìn)行替換
function replace(fragment) {
Array.from(fragment.childNodes).forEach(node => {
let textContent = node.textContent
let reg = /\{\{(.*?)\}\}/g
if (node.nodeType === 3 && reg.test(textContent)) {
const nodeTextContent = node.textContent
const replaceText = () => {
node.textContent = nodeTextContent.replace(reg, (matched, placeholder) => {
return placeholder.split('.').reduce((prev, Key) => {
return prev[Key]
}, data)
})
}
replaceText()
}
// 如果還有子節(jié)點(diǎn),繼續(xù)遞歸 replace
if (node.childNodes && node.childNodes.length) {
replace(node)
}
})
}
replace(fragment)
el.appendChild(fragment)
return el
}

使用 fragment 變量儲(chǔ)存生成的真實(shí) HTML 節(jié)點(diǎn)內(nèi)容。通過 replace 方法對 {{變量}} 進(jìn)行數(shù)據(jù)替換,同時(shí) {{變量}} 的表達(dá)只會(huì)出現(xiàn)在 nodeType === 3 的文本類型節(jié)點(diǎn)中,因此對于符合 node.nodeType === 3 && reg.test(textContent) 條件的情況,進(jìn)行數(shù)據(jù)獲取和填充。我們借助字符串 replace 方法第二個(gè)參數(shù)進(jìn)行一次性替換,此時(shí)對于形如 {{data.course.title}} 的深層數(shù)據(jù),通過 reduce 方法,獲得正確的值。
因?yàn)?DOM 結(jié)構(gòu)可能是多層的,所以對存在子節(jié)點(diǎn)的節(jié)點(diǎn),依然使用遞歸進(jìn)行 replace 替換。
編譯過程比較簡單,沒有考慮到邊界情況,只是單純完成模版變量到真實(shí) DOM 的轉(zhuǎn)換,你品,細(xì)品
雙向綁定實(shí)現(xiàn)
上面例子實(shí)現(xiàn)是單向的,數(shù)據(jù)變化引發(fā)視圖變化,接下來在頁面中存一個(gè)輸入框,觸發(fā)數(shù)據(jù)變化:
測試:<input type="text" v-model="inputValue"/>
思路:在模板編譯過程中,對存在v-model屬性的node進(jìn)行事件監(jiān)聽,在輸入框輸入時(shí)候,改變v-model對應(yīng)的數(shù)據(jù)(inputValue),增加compile中replace方法邏輯,對于 node.nodeType === 1 的DOM類型。
const data = {
stage: '簡書',
inputValue: '',
course: {
title: '響應(yīng)式框架原理記錄',
author: '看到請叫我不要熬夜',
publishTime: '2020年3月3日'
},
}
compile(document.querySelector('#app'), data)
function compile(el, data) {
let fragment = document.createDocumentFragment()
while (child = el.firstChild) {
fragment.appendChild(child)
}
// 對el里面的內(nèi)容進(jìn)行替換
function replace(fragment) {
Array.from(fragment.childNodes).forEach(node => {
let textContent = node.textContent
let reg = /\{\{(.*?)\}\}/g
if (node.nodeType === 3 && reg.test(textContent)) {
const nodeTextContent = node.textContent
const replaceText = () => {
node.textContent = nodeTextContent.replace(reg, (matched, placeholder) => {
return placeholder.split('.').reduce((prev, Key) => {
return prev[Key]
}, data)
})
}
replaceText()
}
if(node.nodeType === 1) {
let attributesArray = node.attributes
Array.from(attributesArray).forEach(attr => {
let attributeName = attr.name
let attributeValue = attr.value
if(name.includes('v-')) {
node.value = data[attributeValue]
}
node.addEventListener('input', e => {
let newVal = e.target.value
data[attributeValue] = newVal
console.log(data)
})
})
}
// 如果還有子節(jié)點(diǎn),繼續(xù)遞歸 replace
if (node.childNodes && node.childNodes.length) {
replace(node)
}
})
}
replace(fragment)
el.appendChild(fragment)
return el
}

