你是否對MVVM多少有點(diǎn)不解?
你是奇怪Jquery忽然成為“過時(shí)”的技術(shù)镣丑?
你是否想寫出一個(gè)類似Vue的簡單框架宣谈?
只要你會(huì)用原生JS, 不需要掌握Vue肄梨,react等高深技能阻荒,本文換種角度讓你窮死理解現(xiàn)代前端框架。
0. 關(guān)于UI狀態(tài)同步
有沒有想過众羡,為何使用現(xiàn)代前端框架侨赡?
React, Vue, Angular等提供很有意思的東西粱侣,如組件化羊壹,第三方UI組件,單網(wǎng)頁支持齐婴,腳手加等工具油猫。然而這些不是根本原因,《現(xiàn)代js 框架存在的根本原因》 給出本質(zhì)原因:
現(xiàn)代前端框架支持UI狀態(tài)同步尔店。
所謂UI狀態(tài)同步是指瀏覽器能實(shí)時(shí)顯示JS中的數(shù)據(jù)眨攘,比如js中 name: '張三'
,則瀏覽器頁面中顯示張三
如果js中 `naee = '李四’, 則頁面自動(dòng)變?yōu)槔钏?/p>
在以前Jquery以前的時(shí)代嚣州。想實(shí)現(xiàn)這一操作困難重重鲫售,需要不斷的更新dom, 不僅性能首限该肴,而且零碎的dom操作代碼容易導(dǎo)致代碼混亂情竹。如何決這個(gè)問題呢? 按前端發(fā)展的歷程匀哄,分為四步:
- 觀察者
- 臟檢查
- 描述屬性符
- 代理
1. 觀察者
你沒看錯(cuò)秦效,這里的觀察者就是N大設(shè)計(jì)模式中的觀察者模式雏蛮。在網(wǎng)頁中,觀察者即UI中顯示的內(nèi)容阱州,被觀察者就是JS中存儲(chǔ)的數(shù)據(jù)挑秉。
JS中存儲(chǔ)的數(shù)據(jù)通常會(huì)在頁面多個(gè)地方顯示, 一個(gè)被觀察者可以對應(yīng)多個(gè)觀察者苔货。
我們需要JS中數(shù)據(jù)的變動(dòng)引起頁面的變動(dòng)犀概,即被觀察者變動(dòng),引起對應(yīng)的觀察者A夜惭、觀察者B等變動(dòng)姻灶,這就是觀察者模式。
實(shí)現(xiàn)此過程很簡單诈茧,把被觀察者存在一個(gè)稱為訂閱池的數(shù)組中产喉,觀察者變動(dòng)時(shí)循環(huán)遍歷訂閱池?cái)?shù)組,更新觀察者即可敢会。
1.1 最簡單的例子
下面用最簡單的例子展示觀察者曾沈。該例子簡單到用日志輸出console.log() 代表UI變動(dòng)。
var uiName1 = function (val) {
console.log('Name#1 become:' + val)
};
var uiName2 = function (val) {
console.log('Name#2 become:' + val)
};
var subjects = [];
subjects.push(uiName1);
subjects.push(uiName2);
function set (val) {
subjects.forEach(item => {
item(val)
})
}
- 建立第一個(gè)觀察者uiName1走触, 表示頁面中有一個(gè)地方顯示姓名晦譬。
var uiName1 = function (val) {
console.log('Name#1 become:' + val)
};
2.建立第二個(gè)觀察者uiName1, 表示頁面中另一個(gè)地方顯示姓名互广。
...
var uiName2 = function (val) {
console.log('Name#2 become:' + val)
};
- 創(chuàng)建一個(gè)數(shù)組敛腌,表示訂閱池 (有些地方也寫作watchers)。訂閱池是本文最重要的三個(gè)概念之一惫皱。
...
var subjects = []
- 將兩個(gè)觀察者放入訂閱池中
...
subjects.push(uiName1);
subjects.push(uiName2);
- 寫一個(gè)set函數(shù)表示觀察者的變動(dòng)像樊, 參數(shù)
val
表示姓名值
...
function set (val) {
subjects.forEach(item => {
item(val)
})
}
打開瀏覽器命令行,打入set('Tom')
旅敷,便可看到Name發(fā)生變化
1.2 多個(gè)被觀察者
上面的例子中只有一個(gè)被觀察者Name生棍,而實(shí)際中有多個(gè)被觀察數(shù)據(jù)。修改上例媳谁,增加一個(gè)被觀察者Age
var uiName1 = function (val) {
console.log('Name#1 become:' + val)
};
var uiName2 = function (val) {
console.log('Name#2 become:' + val)
};
// 增加一個(gè)新的被觀察者Age
var uiAge = function (val) {
console.log('Age become:' + val)
};
// 改造訂閱池
var subjects = {
name: [uiName1, uiName2],
age: [uiAge]
};
// 改造set函數(shù)
function set (key, val) {
subjects[key].forEach(item => {
item(val)
})
}
- 增加一個(gè)新的被觀察者Age涂滴,通樣用
console.log
表示變動(dòng)
...
var uiAge = function (val) {
console.log('Age become:' + val)
};
...
- 改造訂閱池,用對象的Key表示被觀察者晴音,Value為相應(yīng)的觀察者
...
var subjects = {
name: [uiName1, uiName2],
age: [uiAge]
};
...
- 改造set函數(shù)柔纵,遍歷指定被觀察者的觀察者, 參數(shù)
key
表示被觀察者name
和age
,val
表示樣變成的值
...
function set (key, val) {
subjects[key].forEach(item => {
item(val)
})
}
...
在命令行控制臺輸入set('name','Tom')
, 會(huì)發(fā)現(xiàn)只有name
發(fā)生改變, 輸入set('age',18)
z則只有age
發(fā)生改變
1.3在頁面顯示
總用命令行日志代碼UI是不行的锤躁,我們把觀察者模式用于網(wǎng)頁搁料。將上例中的
var uiName1 = function (val) {
console.log('Name#1 become:' + val)
};
var uiName2 = function (val) {
console.log('Name#2 become:' + val)
};
的觀察者用下方的網(wǎng)頁模版表示:
<p> {{name}} </p>
<p> {{name}} </p>
<p> {{age}} </p>
這種模版非常優(yōu)雅,為簡單起見,我們用如下方式呈現(xiàn)數(shù)據(jù):
<p my-value='name'> </p>
<p my-value='name'> </p>
<p my-value='age'> </p>
我們需要將模版轉(zhuǎn)為上例中觀察者郭计,這一個(gè)過程叫做模版解析compile, 這是本文最重要的三個(gè)概念之二
為確定渲染的范圍霸琴,增加id='app'
,全部的html代碼如下:
<html>
<head></head>
<body>
<p my-value='name'> </p>
<p my-value='name'> </p>
<p my-value='age'> </p>
</body>
</html>
下面增加JS部分的代碼。
- 首先聲明JS的數(shù)據(jù)昭伸,也就是前端框架常說的狀態(tài)State:
var data = {
name: 'mike',
age: 1
};
- 創(chuàng)建訂閱池和set函數(shù)梧乘,和上例幾乎一樣。只是需要把需要變的值賦值給
data
var subjects = {};
function set(key, val) {
data[key] = val
subjects[key].forEach(item=> {
item()
})
}
- 下面需做模版解析庐杨,即把模版解析成觀察者宋下。 參數(shù)id表示只解析
id
(‘a(chǎn)pp’
)范圍內(nèi)的html代碼:
function compile (id) {
}
compile('app')
- 下面我們補(bǔ)充
comile()
解析函數(shù)
4.1 獲取節(jié)點(diǎn)的全部子元素,nodes 的值為[<p my-value='name'> </p>, <p my-value='age'> </p>...]
function compile (id) {
var nodes = document.getElementById(id).children;
}
4.2 遍歷子節(jié)點(diǎn)辑莫,node 的值為<p my-value='name'> </p>
,<p my-value='age'> </p>
等
function compile (id) {
var nodes = document.getElementById(id).children;
for (let i = 0; i < nodes.length; i ++ ) {
let node = nodes[i];
}
}
4.3 如果包含屬性my-value
則獲取該值,property
的值為 name
或 age
罩引,表示被觀察者各吨。
...
let node = nodes[i]
if (node.hasAttribute('my-value')) {
let property = node.getAttribute('my-value');
...
4.4 如果訂閱池中沒有被觀察者則放入被觀察者
...
let property = node.getAttribute('my-value');
if (!subjects.hasOwnProperty(property)) {
subjects[property] = []
}
4.5 推入觀察者至訂閱池
...
if (!subjects.hasOwnProperty(property)) {
subjects[property] = []
}
subjects[property].push(()=>{
node.innerHTML = data[property]
})
...
4.6 修改Dom的顯示
...
node.innerHTML = data[property]
...
完整JS代碼如下
var data = {
name: 'mike',
age: 1
};
var subjects = {};
compile('app')
function set(key, val) {
data[key] = val
subjects[key].forEach(item=> {
item()
})
}
function compile(id) {
var nodes = document.getElementById(id).children;
for (let i = 0; i < nodes.length; i ++ ) {
let node = nodes[i];
if (node.hasAttribute('my-value')) {
let property = node.getAttribute('my-value');
if (!subjects.hasOwnProperty(property)) {
subjects[property] = []
}
subjects[property].push(()=>{
node.innerHTML = data[property]
})
node.innerHTML = data[property]
}
}
}
打開頁面顯示如下:
在命令行輸入set('name', 'Jim')
會(huì)發(fā)現(xiàn)頁面相應(yīng)改變
輸入`set('age', 99) 會(huì)改變年齡
1.4 使用者
emberJs, 微信小程序,react等都在使用觀察者模式袁铐,利用set
揭蜒,setData
或類似函數(shù)更新數(shù)據(jù),很常見剔桨。
2.臟檢查
觀察者通過模版解析和訂閱池實(shí)現(xiàn)了UI狀態(tài)同步屉更,然而想更新被觀察者,需要手動(dòng)的調(diào)用set(key, value)
函數(shù)洒缀,并不方便瑰谜,如果JS中的狀態(tài)變化能自動(dòng)調(diào)用set函數(shù)就好啦堰怨。
為解決這個(gè)痛點(diǎn)痢法,Angular1.0 提出臟檢查這一概念。當(dāng)觸發(fā)了某些條件卦碾,比如頁面加載完成饺饭,用戶點(diǎn)擊渤早,或者一些數(shù)據(jù)發(fā)生改變后,會(huì)遍歷所有的數(shù)據(jù)進(jìn)行檢查瘫俊,如果發(fā)現(xiàn)有變化的地方則更新鹊杖。
Angular雖然對臟檢查做了很多優(yōu)化,深入了解可以閱讀angular臟檢查原理及偽代碼實(shí)現(xiàn)扛芽。但由于經(jīng)常要遍歷全部數(shù)據(jù)骂蓖,對現(xiàn)在的大型網(wǎng)頁應(yīng)用而言,效率太慢胸哥。當(dāng)Angular維護(hù)的狀態(tài)達(dá)到數(shù)百后涯竟,可能會(huì)出現(xiàn)卡頓現(xiàn)象。
3 屬性描述符
如何能搞效的進(jìn)行UI狀態(tài)同步? 屬性描述符(或稱為對象定義屬性)defineProperty庐船,給出答案银酬。我們利用defineProperty的getter 和 setter劫持?jǐn)?shù)據(jù)對象,當(dāng)數(shù)據(jù)變動(dòng)時(shí)會(huì)自動(dòng)調(diào)用setter中的方法筐钟,進(jìn)而改變頁面揩瞪。
Object.defineProperty
可以豐富對象的取值和賦值操作,語法如下:
Object.defineProperty(obj, prop, descriptor)
obj是目標(biāo)對象篓冲, prop是屬性名即鍵值李破,descriptor是目標(biāo)屬性所擁有的特性。返回值是被傳遞給函數(shù)的對象壹将, 簡言之一個(gè)對象嗤攻。具體語法參見理解Object.defineProperty的作用
再看下面的例子
var data = {} // 被劫持的對象
Object.defineProperty(data, ‘name’, {
enumerable: true, // 可枚舉
configurable: true, // 可忽略
get () { // 攔截取值
return val
},
set (newVal) { // 攔截賦值操作
val = newVal
console.log('我被劫持了')
}
})
當(dāng)你在命令行執(zhí)行 data.name = 'Tom'
時(shí),會(huì)發(fā)現(xiàn)輸出一條日志我被劫持了
3.1 用defineProperty做UI狀態(tài)同步
仍然用之前的代碼诽俯,只是增加對象的劫持操作
<div id=‘a(chǎn)pp’>
<p> {{name}} </p>
<p> {{name}} </p>
<p> {{age}} </p>
</div>
<script>
var data = {
name: 'mike',
age: 1
};
var subjects = {}
compile(‘a(chǎn)pp’)
obverser(data) // 注意這里妇菱,我媽劫持data啦
function set(key, val) {/* 同前例.. */}
function compile(el) {* 同前例.. */}
function obverser(data) {
// 注意這里,補(bǔ)全obverse函數(shù)
}
</script>
注意obverser(data)
這一行暴区,obverse劫持對象闯团,這是本文三個(gè)重點(diǎn)之三,參數(shù)data
是被劫持的數(shù)據(jù)仙粱。
obverser()
函數(shù)寫起來也很簡單房交,首先遍歷data的每一個(gè)屬性。Object.keys
能把對象的鍵轉(zhuǎn)為一個(gè)數(shù)組如['name','age']
, forEach
遍歷這個(gè)數(shù)組伐割。
...
function obverser(data) {
Object.keys(data).forEach(key=>{
let value = data[key]
之后添加getter選項(xiàng)候味,直接返回?cái)?shù)據(jù)的值即可。
...
function obverser(data) {
Object.keys(data).forEach(key=>{
let value = data[key]
Object.defineProperty(data, key, {
get () {
return value
},
最后增加getter攔截函數(shù)
...
Object.defineProperty(data, key, {
get () {
return value
},
set (newValue) {
if (value != newValue) { // 只有在賦不同值后才起作用隔心,避免循環(huán)調(diào)用
// console.log('我被劫持啦')
value = newValue
set(key, value) // 以前需手動(dòng)寫的set函數(shù)负溪,現(xiàn)在可以自動(dòng)運(yùn)行
}
}
})
})
}
至此完工,打開瀏覽器看看效果济炎。
在Console中輸入data.name= 'Jim'
試試川抡? 看,不需要手動(dòng)寫set函數(shù)
再輸入data.age = 99
改下年齡
3.2 小結(jié)
再理下思路须尚,不外乎三點(diǎn):
- UI中崖堤,對模版進(jìn)行解析compile,產(chǎn)生觀察者
- JS中耐床,對狀態(tài)state進(jìn)行劫持(或稱作觀察)observer密幔,產(chǎn)生被觀察者
-
通過訂閱池Watchers進(jìn)行連接
本文的例子非常簡單,只解釋概念撩轰,如果繼續(xù)完善下去胯甩,比如增加對表單onChange
事件的監(jiān)聽昧廷,可以做出一個(gè)類似Vue的MVVM的框架,有興趣可以閱讀《剖析vue實(shí)現(xiàn)原理偎箫,自己動(dòng)手實(shí)現(xiàn)mvvm》
這就是《一種基于訪問器劫持的前端數(shù)據(jù)雙向綁定實(shí)現(xiàn)方法》木柬,你沒看錯(cuò),這竟然被注冊成專利淹办,有興趣可以深入閱讀《雙向綁定也能申請專利》
Vue眉枕,Angular 2以后的版本,以及國人出品的框架avalon在使用這種技術(shù)
4代理
事情往往并不完美怜森,屬性描述符defineProperty也是如此速挑。在聲明對象屬性后,defineProperty才能對該屬性進(jìn)行劫持副硅,于是vue中我們還需要寫this.$set(data, key,val)
以添加新的屬性姥宝。本節(jié)講的代理將能完美解決definePropery的缺點(diǎn)。
代理Proxy, 作為ES6的新特性可能會(huì)遇到瀏覽器兼容問題恐疲。又由于profill的降級對代理幾乎沒用伶授,很少有人將代理用于時(shí)間開發(fā)中,相信隨著現(xiàn)代瀏覽器的普及這一現(xiàn)狀將得到改變流纹。使用代理Proxy前最好檢查下瀏覽器的兼容問題,參加《Can I use proxy ?》
4.1 簡明代理語法
Proxy代理违诗, 可以理解在目標(biāo)對象之前架設(shè)一層“攔截”漱凝,外界對該對象的訪問,都必須先通過這層攔截诸迟,因此提供了一種機(jī)制茸炒,可以對外界的訪問進(jìn)行過濾和改寫。
比如阵苇,用proxy攔截取值操作:
var proxy = new Proxy({}, {
get: function () { // 攔截取值壁公,類似getter
return 1;
}
}
proxy.name // 1
proxy.book //1
Proxy的寫法都如此, 語法如下:
var proxy = new Proxy(target, handler)
new Proxy
表示生成一個(gè)Proxy
實(shí)例绅项, taget參數(shù)表示所要攔截的目標(biāo)對象紊册。目標(biāo)對象可以是js中的對象,數(shù)組快耿,函數(shù)甚至另一個(gè)代理囊陡。handler參數(shù)也是一個(gè)對象,用來定制攔截行為掀亥。返回值是Proxy對象, new Proxy
是穩(wěn)定操作撞反,不會(huì)對target有任何影響。
常見handler 的有:
- get: 攔截取值
- set: 攔截賦值
- deleteProperty: 攔截刪除
- apply: 攔截函數(shù)執(zhí)行
- defineProperty: 攔截defineProperty操作
更多Proxy操作可閱讀《ECMAScript6入門》
代理給JS編程打開了一扇門搪花,靈活快速遏片,可稱是對JS的“元編程”嘹害。代理的用途很廣泛,比如表單驗(yàn)證吮便,圖片懶加載笔呀,異步隊(duì)列,等等线衫,有興趣可以閱讀(《使用 Javascript 原生的 Proxy 優(yōu)化應(yīng)用》)[https://juejin.im/post/5a3cb0846fb9a044fb07f36c]
4.2 狀態(tài)同步代理版
用代理做UI狀態(tài)同步非常簡單凿可,我們還是用上例的代碼,只需修改observer
函數(shù)即可授账。
<div id=‘a(chǎn)pp’>
<p> {{name}} </p>
<p> {{name}} </p>
<p> {{age}} </p>
<p value='phone'> </p> <!-- phone 是用來做什么的枯跑? 最后說 -->
</div>
<script>
var data = {
name: 'mike',
age: 1
};
var subjects = {}
compile(‘a(chǎn)pp’)
obverser(data) // 注意這里,我媽劫持data啦
function set(key, val) { /* 同前例.. */ }
function compile(el) { /* 同前例.. */ }
function obverser(state) {
// 注意這里白热,重寫obverser函數(shù)
}
</script>
1.創(chuàng)建代理敛助, 注意為避免變量重復(fù),這里把函數(shù)參數(shù)改為state
function obverser(state) {
data = new Proxy(data, {
})
}
- 攔截取值操作
function obverser(target) {
data = new Proxy(target, {
get (target, property) {
return target[property]
},
})
}
其中targert
表示目標(biāo)對象屋确, property
表示目標(biāo)對象的屬性, return target[property]
相當(dāng)于把原對象的值直接返回
- 攔截賦值操作
function obverser(target) {
data = new Proxy(target, {
get (target, property) {
return target[property]
},
set (target, property, newValue) {
target[property] = newValue
set(property, newValue)}
})
}
其中targert
表示目標(biāo)對象纳击, property
表示目標(biāo)對象的屬性, newValue
顧名思義是新設(shè)置的值攻臀。target[property] = newValue
是賦值操作焕数。 set(property, newValue)
就是前面眾多例子中的set()
函數(shù)。
至此刨啸,結(jié)束堡赔。
打開網(wǎng)頁看下效果,注意日志中清晰簡潔的呈現(xiàn)數(shù)據(jù)设联。
輸入data.name = 'Jim'
, 會(huì)發(fā)現(xiàn)名字由Mike變?yōu)镴im
輸入data.age= 99
, 會(huì)發(fā)現(xiàn)年齡由99變?yōu)?
輸入data.phone= 15442258
, 會(huì)發(fā)頁面多出了電話號碼善已。
請注意在
var data = {
name: 'mike',
age: 1
};
data的聲明中我們并沒有寫phone, 只是在模版中寫有
<p value='phone'> </p> <!-- phone 是用來做什么的离例? 現(xiàn)在說 -->
可以看到代理可以對沒有聲明的屬性進(jìn)行監(jiān)聽换团,完美解決描述屬性符的問題。
附: 完整代碼
<html lang="en">
<head>
<meta charset="UTF-8">
<title>5代理</title>
</head>
<body>
<div id ='app'>
<p value='name'> </p>
<p value='name'> </p>
<p value='age'> </p>
<p value='phone'> </p>
</div>
<script>
var data = {
name: 'mike',
age: 1
};
var subjects = {};
function set(key, val) {
subjects[key].forEach(item=> {
item()
})
}
function compile(el) {
var nodes = document.getElementById(el).children;
for (let i = 0; i < nodes.length; i ++ ) {
let node = nodes[i];
if (node.hasAttribute('value')) {
let property = node.getAttribute('value');
if (!subjects.hasOwnProperty(property)) {
subjects[property] = []
}
subjects[property].push(()=>{
node.innerHTML = data[property]
});
node.innerHTML = data[property] || ''
}
}
}
compile('app');
obverser(data);
function obverser(state) {
data = new Proxy(state, {
get (target, property) {
return target[property]
},
set (target, property, newValue) {
target[property] = newValue;
set(property, newValue);
}
})
}
</script>
</body>
</html>
4.3 更進(jìn)一步:雙向綁定
我們用代理做狀態(tài)同步宫蛆,再進(jìn)一步艘包,我們可以用代理做雙向綁定。實(shí)現(xiàn)原理很簡單耀盗,仍用前例的代碼辑甜,只是更改compile函數(shù),增加對輸入框的監(jiān)聽
node.addEventListener('input', () => {
// 利用代理的set攔截袍冷。
// 相當(dāng)于在瀏覽器console中輸入data.name = 'Jim'
data[property] = node.value
})
看下效果磷醋。
這部分內(nèi)容已經(jīng)超出本文的范圍,有興趣可以直接閱讀下面的代碼和注釋胡诗。本例可能是行數(shù)最少的雙向綁定代碼邓线,只是比前面的例子增加了幾行代碼淌友。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>6代理雙向綁定</title>
</head>
<body>
<div id ='app'>
<p value='name'> </p>
<input type="text" model = 'age'> <!-- 增加文本輸入框 -->
<p value='age'> </p>
</div>
<script>
var data = {
name: 'mike',
age: 1
};
var subjects = {};
function set(key) {
subjects[key].forEach(item=> {
item()
})
}
/*
* 只重寫compile函數(shù),其余JS代碼均沒變化
* */
function compile(el) {
var nodes = document.getElementById(el).children;
// 遍歷子節(jié)點(diǎn)骇陈,同前例
for (let i = 0; i < nodes.length; i ++ ) {
let node = nodes[i];
// 為模版綁定值震庭,同前例讓js的數(shù)據(jù)顯示在頁面上
if (node.hasAttribute('value')) {
let property = node.getAttribute('value');
if (!subjects.hasOwnProperty(property)) {
subjects[property] = []
}
subjects[property].push(()=>{
node.innerHTML = data[property]
});
node.innerHTML = data[property] || ''
// 新增部分: 當(dāng)遇到`model'屬性,表示雙向綁定,
} else if (node.hasAttribute('model')) {
// 為模版綁定值你雌,讓js的數(shù)據(jù)顯示在頁面上
let property = node.getAttribute('model');
if (!subjects.hasOwnProperty(property)) {
subjects[property] = []
}
subjects[property].push(()=>{
node.value = data[property]
});
node.value = data[property] || ''
// 關(guān)鍵:增加監(jiān)聽器联。當(dāng)文本框變動(dòng)時(shí)觸發(fā)
node.addEventListener('input', () => {
/*
* 關(guān)鍵中的關(guān)鍵: 利用代理的set攔截。
* 相當(dāng)于在瀏覽器console中輸入data.name = 'Jim'
*/
data[property] = node.value
})
}
}
}
compile('app');
obverser(data);
function obverser(state) {
data = new Proxy(state, {
get (target, property) {
return target[property]
},
set (target, property, newValue) {
target[property] = newValue;
set(property);
}
})
}
</script>
</body>
</html>
4.4 再進(jìn)一步婿崭,再進(jìn)一步
本文的例子很簡單拨拓,不大可能用于實(shí)踐,還有很多工作要做氓栈。
如果要寫如下的嵌套模版怎么辦渣磷?給Compile加層遞歸循環(huán)吧?
<div id ='app'>
<p>
姓名:
<span value='name'> </span>
</p>
<p>
年齡:
<input type="text" model = 'age'>
</p>
<p>
年齡:
<span value='age'> </span>
</p>
</div>
下方compile函數(shù)又丑又長怎么辦? 向Vue一樣 用watcher和dap改造吧授瘦!
...
node.innerHTML = data[property] || ''
} else if (node.hasAttribute('model')) {
let property = node.getAttribute('model');
if (!subjects.hasOwnProperty(property)) {
subjects[property] = []
}
subjects[property].push(()=>{
...
有興趣閱讀《用proxy實(shí)現(xiàn)一個(gè)更優(yōu)雅Vue》
5.番外篇: 虛擬渲染
即使用代理進(jìn)行雙向綁定醋界,也需要操作DOM,而操作DOM是耗時(shí)不高效的提完。
React另辟蹊徑形纺,不用代理,使用尋渲染做UI狀態(tài)同步徒欣。
詳細(xì)原理可閱讀《如何理解虛擬DOM?》逐样,大致原理如下:
- 創(chuàng)建虛擬DOM,即把HTML中的模版轉(zhuǎn)為js顯示帚称。比如:
// html代碼
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
//轉(zhuǎn)為JS
var tree = h('ul', {id: 'list'}, [
h('li', {class: 'item'}, ['Item 1']),
h('li', {class: 'item'}, ['Item 2']),
h('li', {class: 'item'}, ['Item 3'])
])
- 通過
render
渲染函數(shù)將虛擬DOM轉(zhuǎn)為真正的DOM并加載在頁面上
var root = tree.render()
document.body.appendChild(root)
- 如果JS發(fā)生改變,直接生成新的尋DOM秽澳,比如更改Item的名稱
var newTree = h('ul', {id: 'list'}, [
h('li', {class: 'item'}, ['A']),
h('li', {class: 'item'}, ['B']),
h('li', {class: 'item'}, ['C])
])
- 用DIff算法比較新就DOM樹闯睹,并將不同點(diǎn)存在變量pathces中
var patches = diff(tree, newTree)])
// patches 內(nèi)容類似如下:
[{node: 'li', old: 'Item 1, new 'A'} , {node: ...} ....]
- 在真正的DOM樹中變更
patch(root, patches)
// DOM將變?yōu)?
<ul id='list'>
<li class='item'>A</li>
<li class='item'>B</li>
<li class='item'>C</li>
</ul>
至此,結(jié)束担神。
6. 結(jié)語
UI狀態(tài)同步簡史楼吃,有兩條科技線。一條是通過觀察者模式對數(shù)據(jù)的觀察妄讯,一條是虛擬函數(shù)用JS代替HTML孩锡。
而到今天,兩條科技線早已相互結(jié)合亥贸,互相吸取優(yōu)點(diǎn)躬窜。于是有了今天的React, Vue等。
不過故事仍沒結(jié)束炕置,在UI狀態(tài)同步的路上荣挨,優(yōu)化無止境男韧。
本文源于公司的一次內(nèi)部分享