單向綁定非常簡單,就是把Model綁定到View囊咏,當我們用JavaScript代碼更新Model時恕洲,View就會自動更新。
有單向綁定匆笤,就有雙向綁定研侣。如果用戶更新了View,Model的數(shù)據(jù)也自動被更新了炮捧,這種情況就是雙向綁定庶诡。
這么個能讓人從dom操作解放出來渾身通泰的東西,能不研究一下它的原理咆课?
Vue源碼的英文解釋很詳細末誓。以下代碼,僅僅用于原理的說明书蚪。
參考滴滴商業(yè)FED
閱讀順序建議粗略過代碼喇澡,對照著思路再看代碼。
兩個核心
在研究之前殊校,得先明白了Vue實現(xiàn)數(shù)據(jù)綁定的兩個核心理念晴玖,即:
-
Object.defineProperty()
監(jiān)聽數(shù)據(jù)的變動 - 觀察者(發(fā)布-訂閱者)模式
數(shù)據(jù)對應的邏輯操作
它們的關系又是如何?
一句話描述,一個頁面在多處訂閱使用了同一個數(shù)據(jù)呕屎,用defineProperty監(jiān)聽其改變让簿,并由發(fā)布者通知 訂閱者去更新它所持有的數(shù)據(jù)。
關鍵字get/set
使用 Object.defineProperty() 的 get/set
對傳入new Vue({})所有數(shù)據(jù)對象做一個數(shù)據(jù)監(jiān)聽秀睛,用于在屬性獲取(get)和設置(set)時尔当,添加對應的邏輯。
// ---------------數(shù)據(jù)監(jiān)聽----------------------
observe = function(value){
// 是否監(jiān)聽
if(!value || typeof value !== 'object'){
return
}
return new Observer(value)
}
// ------
class Observer{
constructor(value){
this.value = value
this.walk(value)
}
walk(value){ //監(jiān)聽數(shù)據(jù)的所有屬性
Object.keys(value).forEach(key => this.convert(key, value[key]))
}
convert(key, val){
defineReactive(this.value, key, val)
}
}
defineReactive = function(obj, key, val){
var dep = new Dep()
// 給當前屬性的值添加監(jiān)聽
var chlidOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=> {
//判斷是否有watcher需要添加
if(Dep.target){
dep.addSub(Dep.target)
}
// -------
return val
},
set: (newVal) => {
if(val === newVal) return
val = newVal
// -------
// 對新值進行監(jiān)聽
chlidOb = observe(newVal)
// 通知所有訂閱者更新數(shù)據(jù)
dep.notify()
}
})
}
將一些的枝枝葉葉去掉蹂安,剩下的Object.defineProperty
就可以對一個數(shù)據(jù)監(jiān)聽了椭迎。就不重復貼代碼了。
觀察者(發(fā)布-訂閱者)模式
WHY 為什么要用這個模式田盈?
觀察者模式是開發(fā)基于行為的大型應用程序的有力手段畜号。在一次瀏覽器會話期間,應用程序中可能會斷斷續(xù)續(xù)地發(fā)生幾十次缠黍,幾百次甚至上千次各種事件弄兜。你可以消減為事件注冊監(jiān)聽器的次數(shù),讓可觀察者對象借助一個事件監(jiān)聽器替你處理各種行為并將信息委托給它的所有訂閱者瓷式,從而降低內(nèi)存消耗和提高互動性能替饿。這樣一來,就不用沒完沒了地為同樣的元素增添新的事件監(jiān)聽器贸典。這樣有利于減少系統(tǒng)開銷并提高程序的可維護性视卢。(JavaScript設計模式)
頻繁的數(shù)據(jù)操作與此模式非常的契合
觀察者模式實質(zhì)就是你可以對程序中某個對象的狀態(tài)進行觀察,并在其發(fā)生改變時能得到通知廊驼。
觀察者模式存在兩個角色:
- 觀察者(發(fā)布者)
- 被觀察者(訂閱者)
發(fā)布者:
- [] 一個用來管理訂閱者的數(shù)組
- addSub() 添加訂閱者
- notify() 用于發(fā)布消息据过,通知訂閱者有新的訂閱信息
// ------------------------------------
class Dep{
constructor(){
this.subs = [] //管理訂閱者隊列
}
addSub(sub){
// 去重復
var alreadyExists = this.subs.some( (el) => {
return el === sub
})
if (!alreadyExists) {
this.subs.push(sub)
}
}
notify(){
// 通知所有的訂閱者(Watcher),觸發(fā)訂閱者的相應邏輯處理
this.subs.forEach((sub) => sub.update())
}
}
訂閱者:
- value 自身的值妒挎,在這里是用來保存發(fā)布者發(fā)布過來的值
- updata() 接收發(fā)布者更新通知
// ----------------------------------
class Watcher{
constructor(vm, expOrFn, cb){
this.vm = vm // 整個實例
this.cb = cb // 當數(shù)據(jù)更新時想要做的事情
this.expOrFn = expOrFn // 被訂閱的數(shù)據(jù)
this.val = this.get() // 獲取訂閱的值作為自身的值
}
// 更新數(shù)據(jù)
update(){
this.run()
}
run(){
const val = this.get()
if(val !== this.val){
this.val = val;
this.cb.call(this.vm)
}
}
get(){
// 當前訂閱者(Watcher)讀取發(fā)布者的值
Dep.target = this
const exp = this.expOrFn //獲取鍵名绳锅,用來定位到是哪一個發(fā)布者
var val = this.vm._data[exp] //獲取發(fā)布者的值
Dep.target = null
return val;
}
}
如何整合
在整理完這些核心點之后該,我們擁有了零件酝掩。接下來該如何組裝起來呢鳞芙?
既然用的是觀察者模式
誰是發(fā)布者?怎么添加訂閱期虾?
首先原朝,我們監(jiān)聽的每一個數(shù)據(jù)都應該是一個發(fā)布者,這樣就可以在數(shù)據(jù)發(fā)生改變的時候通知到各個訂閱者镶苞。那么喳坠,就可以在初始化該數(shù)據(jù)監(jiān)聽的時候(defineReactive),在函數(shù)里面var dep = new Dep()
茂蚓。
defineReactive = function(obj, key, val){
var dep = new Dep()
...
}
那訂閱者呢壕鹉,前面說過剃幌,在get/set
的時候添加對應的邏輯,這就派上用場了御板。
get:
我們可以在調(diào)用get時锥忿,往當前的發(fā)布者dep中添加訂閱者。注意怠肋!是添加。
/* */
在這一處有一個比較繞的一個點淹朋,就是訂閱者的創(chuàng)建笙各。
訂閱者的創(chuàng)建應該伴隨的是在頁面的某個地方需要用到這個數(shù)據(jù),可以說是出現(xiàn)一個 {{}}
础芍,這時就有一個watcher杈抢。
這時候回過頭來看watcher類,在new Watcher(this, expOrFn, cb)
時仑性,為了初始化訂閱者的值惶楼,調(diào)用了get()this.val = this.get() // 獲取訂閱的值作為自身的值
,而且在watcher的參數(shù)里面诊杆,可以知道需要獲取的是哪一個發(fā)布者的值
const exp = this.expOrFn//獲取鍵名歼捐,定位到是哪一個發(fā)布者
var val = this.vm._data[exp] //獲取發(fā)布者的值
自然而然的,這一步觸發(fā)了發(fā)布者的get()晨汹,然后我們再看Dep.target = this
豹储,我們就可以將整個watcher添加進訂閱者的隊列了。
set:
set需要做的就簡單得多了淘这,當發(fā)布者的數(shù)據(jù)發(fā)生改變時剥扣,會調(diào)用set,在這將新值更新完之后铝穷,就該通知該發(fā)布者的所有訂閱者更新信息钠怯。
保留了添加訂閱者和更新發(fā)布者數(shù)據(jù)兩個功能。
class Vue{
constructor(options = {}){
// 簡化了$options的處理
this.$options = options
// 簡化了對data的處理
let data = this._data = this.$options.data
// 將所有data最外層屬性代理到Vue實例上
Object.keys(data).forEach(key => this._proxy(key))
// 監(jiān)聽數(shù)據(jù)
console.log('listen data :')
observe(data)
}
// 對外暴露調(diào)用訂閱者的接口曙聂,內(nèi)部主要在指令中使用訂閱者
// ------------------{{}}---------------------
$watch(expOrFn, cb){
//
new Watcher(this, expOrFn, cb)
}
// -------------------------------------------
_proxy(key){
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get: () => this._data[key],
set: (val) => {
//更新數(shù)據(jù)
this._data[key] = val
}
})
}
}
// test-----------
var t = new Vue({
data: {
name: 'NAME'
}
})
t.$watch('name', () => console.log('cb:the one'))
t.$watch('name', () => console.log('cb:the second'))
t.name = 'ghjk'
到這Vue雙向數(shù)據(jù)綁定的簡單邏輯基本也就完成了晦炊,還剩下的是同界面的解析交互了。
相信在現(xiàn)在的前端環(huán)境筹陵,雙向數(shù)據(jù)綁定幾乎是選擇框架的一種標準了刽锤。
粗略算起來,研究了小半個星期的Vue雙向數(shù)據(jù)綁定朦佩,略有所得并思,做個記錄,不足或有錯之處语稠,望指出宋彼。