vue.js動態(tài)數(shù)據(jù)綁定學(xué)習(xí)筆記
對于vue.js的動態(tài)數(shù)據(jù)綁定,經(jīng)過反復(fù)地看源碼和博客講解,總算能夠理解它的實(shí)現(xiàn)了,心累~ 分享一下學(xué)習(xí)成果,同時也算是做個記錄。完整代碼GitHub地址:https://github.com/hanrenguang/Dynamic-data-binding。也可以到倉庫的 README 閱讀本文,容我厚臉皮地求 star,求 follow。
整體思路
不知道有沒有同學(xué)和我一樣,看著vue的源碼卻不知從何開始,真叫人頭大。硬生生地看了observer, watcher, compile這幾部分的源碼,只覺得一臉懵逼。最終,從這里得到啟發(fā),作者寫得很好,值得一讀。
關(guān)于動態(tài)數(shù)據(jù)綁定呢,需要搞定的是 Dep , Observer , Watcher , Compile 這幾個類,他們之間有著各種聯(lián)系,想要搞懂源碼,就得先了解他們之間的聯(lián)系。下面來理一理:
- Observer 所做的就是劫持監(jiān)聽所有屬性,當(dāng)有變動時通知 Dep
- Watcher 向 Dep 添加訂閱,同時,屬性有變化時,Observer 通知 Dep,Dep 則通知 Watcher
- Watcher 得到通知后,調(diào)用回調(diào)函數(shù)更新視圖
- Compile 則是解析所綁定元素的 DOM 結(jié)構(gòu),對所有需要綁定的屬性添加 Watcher 訂閱
由此可以看出,當(dāng)屬性發(fā)生變化時,是由Observer -> Dep -> Watcher -> update view,Compile 在最開始解析 DOM 并添加 Watcher 訂閱后就功成身退了。
從程序執(zhí)行的順序來看的話,即 new Vue({}) 之后,應(yīng)該是這樣的:先通過 Observer 劫持所有屬性,然后 Compile 解析 DOM 結(jié)構(gòu),并添加 Watcher 訂閱,再之后就是屬性變化 -> Observer -> Dep -> Watcher -> update view,接下來就說說具體的實(shí)現(xiàn)。
從new一個實(shí)例開始談起
網(wǎng)上的很多源碼解讀都是從 Observer 開始的,而我會從 new 一個MVVM實(shí)例開始,按照程序執(zhí)行順序去解釋或許更容易理解。先來看一個簡單的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<div class="test">
<p>{{user.name}}</p>
<p>{{user.age}}</p>
</div>
<script type="text/javascript" src="hue.js"></script>
<script type="text/javascript">
let vm = new Hue({
el: '.test',
data: {
user: {
name: 'Jack',
age: '18'
}
}
});
</script>
</body>
</html>
接下來都將以其為例來分析。下面來看一個簡略的 MVVM 的實(shí)現(xiàn),在此將其命名為 hue。為了方便起見,為 data 屬性設(shè)置了一個代理,通過 vm._data 來訪問 data 的屬性顯得麻煩且冗余,通過代理,可以很好地解決這個問題,在注釋中也有說明。添加完屬性代理后,調(diào)用了一個 observe 函數(shù),這一步做的就是 Observer 的屬性劫持了,這一步具體怎么實(shí)現(xiàn),暫時先不展開。先記住他為 data 的屬性添加了 getter 和 setter。
function Hue(options) {
this.$options = options || {};
let data = this._data = this.$options.data,
self = this;
Object.keys(data).forEach(function(key) {
self._proxyData(key);
});
observe(data);
self.$compile = new Compile(self, options.el || document.body);
}
// 為 data 做了一個代理,
// 訪問 vm.xxx 會觸發(fā) vm._data[xxx] 的getter,取得 vm._data[xxx] 的值,
// 為 vm.xxx 賦值則會觸發(fā) vm._data[xxx] 的setter
Hue.prototype._proxyData = function(key) {
let self = this;
Object.defineProperty(self, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return self._data[key];
},
set: function proxySetter(newVal) {
self._data[key] = newVal;
}
});
};
再往下看,最后一步 new 了一個 Compile,下面我們就來講講 Compile。
Compile
new Compile(self, options.el || document.body) 這一行代碼中,第一個參數(shù)是當(dāng)前 Hue 實(shí)例,第二個參數(shù)是綁定的元素,在上面的示例中為class為 .test 的div。
關(guān)于 Compile,這里只實(shí)現(xiàn)最簡單的 textContent 的綁定。而 Compile 的代碼沒什么難點(diǎn),很輕易就能讀懂,所做的就是解析 DOM,并添加 Watcher 訂閱。關(guān)于 DOM 的解析,先將根節(jié)點(diǎn) el 轉(zhuǎn)換成文檔碎片 fragment 進(jìn)行解析編譯操作,解析完成后,再將 fragment 添加回原來的真實(shí) DOM 節(jié)點(diǎn)中。來看看這部分的代碼:
function Compile(vm, el) {
this.$vm = vm;
this.$el = this.isElementNode(el)
? el
: document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype.node2Fragment = function(el) {
let fragment = document.createDocumentFragment(),
child;
// 也許有同學(xué)不太理解這一步,不妨動手寫個小例子觀察一下他的行為
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
};
Compile.prototype.init = function() {
// 解析 fragment
this.compileElement(this.$fragment);
};
以上面示例為例,此時若打印出 fragment,可觀察到其包含兩個p元素:
<p>{{user.name}}</p>
<p>{{user.age}}</p>
下一步就是解析 fragment,直接看代碼及注釋吧:
Compile.prototype.compileElement = function(el) {
let childNodes = Array.from(el.childNodes),
self = this;
childNodes.forEach(function(node) {
let text = node.textContent,
reg = /\{\{(.*)\}\}/;
// 若為 textNode 元素,且匹配 reg 正則
// 在上例中會匹配 '{{user.name}}' 及 '{{user.age}}'
if (self.isTextNode(node) && reg.test(text)) {
// 解析 textContent,RegExp.$1 為匹配到的內(nèi)容,在上例中為 'user.name' 及 'user.age'
self.compileText(node, RegExp.$1);
}
// 遞歸
if (node.childNodes && node.childNodes.length) {
self.compileElement(node);
}
});
};
Compile.prototype.compileText = function(node, exp) {
// this.$vm 即為 Hue 實(shí)例,exp 為正則匹配到的內(nèi)容,即 'user.name' 或 'user.age'
compileUtil.text(node, this.$vm, exp);
};
let compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
bind: function(node, vm, exp, dir) {
// 獲取更新視圖的回調(diào)函數(shù)
let updaterFn = updater[dir + 'Updater'];
// 先調(diào)用一次 updaterFn,更新視圖
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 添加 Watcher 訂閱
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
// 根據(jù) exp,獲得其值,在上例中即 'vm.user.name' 或 'vm.user.age'
_getVMVal: function(vm, exp) {
let val = vm;
exp = exp.trim().split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
}
};
let updater = {
// Watcher 訂閱的回調(diào)函數(shù)
// 在此即更新 node.textContent,即 update view
textUpdater: function(node, value) {
node.textContent = typeof value === 'undefined'
? ''
: value;
}
};
正如代碼中所看到的,Compile 在解析到 {{xxx}} 后便添加了 xxx 屬性的訂閱,即 new Watcher(vm, exp, callback)。理解了這一步后,接下來就需要了解怎么實(shí)現(xiàn)相關(guān)屬性的訂閱了。先從 Observer 開始談起。
Observer
從最簡單的情況來考慮,即不考慮數(shù)組元素的變化。暫時先不考慮 Dep 與 Observer 的聯(lián)系。先看看 Observer 構(gòu)造函數(shù):
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype.walk = function(data) {
const keys = Object.keys(data);
// 遍歷 data 的所有屬性
for (let i = 0; i < keys.length; i++) {
// 調(diào)用 defineReactive 添加 getter 和 setter
defineReactive(data, keys[i], data[keys[i]]);
}
};
接下來通過 Object.defineProperty 方法給所有屬性添加 getter 和 setter,就達(dá)到了我們的目的。屬性有可能也是對象,因此需要對屬性值進(jìn)行遞歸調(diào)用。
function defineReactive(obj, key, val) {
// 對屬性值遞歸,對應(yīng)屬性值為對象的情況
let childObj = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
// 直接返回屬性值
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
// 值發(fā)生變化時修改閉包中的 val,
// 保證在觸發(fā) getter 時返回正確的值
val = newVal;
// 對新賦的值進(jìn)行遞歸,防止賦的值為對象的情況
childObj = observe(newVal);
}
});
}
最后補(bǔ)充上 observe 函數(shù),也即 Hue 構(gòu)造函數(shù)中調(diào)用的 observe 函數(shù):
function observe(val) {
// 若 val 是對象且非數(shù)組,則 new 一個 Observer 實(shí)例,val 作為參數(shù)
// 簡單點(diǎn)說:是對象就繼續(xù)。
if (!Array.isArray(val) && typeof val === "object") {
return new Observer(val);
}
}
這樣一來就對 data 的所有子孫屬性(不知有沒有這種說法。。)都進(jìn)行了“劫持”。顯然到目前為止,這并沒什么用,或者說如果只做到這里,那么和什么都不做沒差別。于是 Dep 上場了。我認(rèn)為理解 Dep 與 Observer 和 Watcher 之間的聯(lián)系是最重要的,先來談?wù)?Dep 在 Observer 里做了什么。
Observer & Dep
在每一次 defineReactive 函數(shù)被調(diào)用之后,都會在閉包中新建一個 Dep 實(shí)例,即 let dep = new Dep()。Dep 提供了一些方法,先來說說 notify 這個方法,它做了什么事?就是在屬性值發(fā)生變化的時候通知 Dep,那么我們的代碼可以增加如下:
function defineReactive(obj, key, val) {
let childObj = observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
childObj = observe(newVal);
// 發(fā)生變動
dep.notify();
}
});
}
如果僅考慮 Observer 與 Dep 的聯(lián)系,即有變動時通知 Dep,那么這里就算完了,然而在 vue.js 的源碼中,我們還可以看到一段增加在 getter 中的代碼:
// ...
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
}
// ...
這個 depend 方法呢,它又做了啥?答案是為閉包中的 Dep 實(shí)例添加了一個 Watcher 的訂閱,而 Dep.target 又是啥?他其實(shí)是一個 Watcher 實(shí)例,???一臉懵逼,先記住就好,先看一部份的 Dep 源碼:
// 標(biāo)識符,在 Watcher 中有用到,先不用管
let uid = 0;
function Dep() {
this.id = uid++;
this.subs = [];
}
Dep.prototype.depend = function() {
// 這一步相當(dāng)于做了這么一件事:this.subs.push(Dep.target)
// 即添加了 Watcher 訂閱,addDep 是 Watcher 的方法
Dep.target.addDep(this);
};
// 通知更新
Dep.prototype.notify = function() {
// this.subs 的每一項(xiàng)都為一個 Watcher 實(shí)例
this.subs.forEach(function(sub) {
// update 為 Watcher 的一個方法,更新視圖
// 沒錯,實(shí)際上這個方法最終會調(diào)用到 Compile 中的 updaterFn,
// 也即 new Watcher(vm, exp, callback) 中的 callback
sub.update();
});
};
// 在 Watcher 中調(diào)用
Dep.prototype.addSub = function(sub) {
this.subs.push(sub);
};
// 初始時引用為空
Dep.target = null;
也許看到這還是一臉懵逼,沒關(guān)系,接著往下。大概有同學(xué)會疑惑,為什么要把添加 Watcher 訂閱放在 getter 中,接下來我們來說說這 Watcher 和 Dep 的故事。
Watcher & Dep
先讓我們回顧一下 Compile 做的事,解析 fragment,然后給相應(yīng)屬性添加訂閱:new Watcher(vm, exp, cb)。new 了這個 Watcher 之后,Watcher 怎么辦呢,就有了下面這樣的對話:
Watcher:hey Dep,我需要訂閱 exp 屬性的變動。
Dep:這我可做不到,你得去找 exp 屬性中的 dep,他能做到這件事。
Watcher:可是他在閉包中啊,我無法和他聯(lián)系。
Dep:你拿到了整個 Hue 實(shí)例 vm,又知道屬性 exp,你可以觸發(fā)他的 getter 啊,你在 getter 里動些手腳不就行了。
Watcher:有道理,可是我得讓 dep 知道是我訂閱的啊,不然他通知不到我。
Dep:這個簡單,我?guī)湍?,你每次觸發(fā) getter 前,把你的引用告訴 Dep.target 就行了。記得辦完事后給 Dep.target 置空。
于是就有了上面 getter 中的代碼:
// ...
get: function() {
// 是否是 Watcher 觸發(fā)的
if (Dep.target) {
// 是就添加進(jìn)來
dep.depend();
}
return val;
}
// ...
現(xiàn)在再回頭看看 Dep 部分的代碼,是不是好理解些了。如此一來, Watcher 需要做的事情就簡單明了了:
function Watcher(vm, exp, cb) {
this.$vm = vm;
this.cb = cb;
this.exp = exp;
this.depIds = new Set();
// 返回一個用于獲取相應(yīng)屬性值的函數(shù)
this.getter = parseGetter(exp.trim());
// 調(diào)用 get 方法,觸發(fā) getter
this.value = this.get();
}
Watcher.prototype.get = function() {
const vm = this.$vm;
// 將 Dep.target 指向當(dāng)前 Watcher 實(shí)例
Dep.target = this;
// 觸發(fā) getter
let value = this.getter.call(vm, vm);
// Dep.target 置空
Dep.target = null;
return value;
};
Watcher.prototype.addDep = function(dep) {
const id = dep.id;
if (!this.depIds.has(id)) {
// 添加訂閱,相當(dāng)于 dep.subs.push(this)
dep.addSub(this);
this.depIds.add(id);
}
};
function parseGetter(exp) {
if (/[^\w.$]/.test(exp)) {
return;
}
let exps = exp.split(".");
return function(obj) {
for (let i = 0; i < exps.length; i++) {
if (!obj)
return;
obj = obj[exps[i]];
}
return obj;
};
}
最后還差一部分,即 Dep 通知變化后,Watcher 的處理,具體的函數(shù)調(diào)用流程是這樣的:dep.notify() -> sub.update(),直接上代碼:
Watcher.prototype.update = function() {
this.run();
};
Watcher.prototype.run = function() {
let value = this.get();
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
// 調(diào)用回調(diào)函數(shù)更新視圖
this.cb.call(this.$vm, value, oldVal);
}
};
結(jié)語
到這就算寫完了,本人水平有限,若有不足之處歡迎指出,一起探討。
參考資料
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- vue.js數(shù)據(jù)綁定的方法(單向、雙向和一次性綁定)
- vue.js數(shù)據(jù)綁定操作詳解
- Vue.js實(shí)現(xiàn)雙向數(shù)據(jù)綁定方法(表單自動賦值、表單自動取值)
- Vue.js+Layer表格數(shù)據(jù)綁定與實(shí)現(xiàn)更新的實(shí)例
- 實(shí)現(xiàn)非常簡單的js雙向數(shù)據(jù)綁定
- JavaScript數(shù)據(jù)綁定實(shí)現(xiàn)一個簡單的 MVVM 庫
- JavaScript中雙向數(shù)據(jù)綁定詳解
- JavaScript觀察者模式(publish/subscribe)原理與實(shí)現(xiàn)方法
- JavaScript觀察者模式(經(jīng)典)
- JavaScript設(shè)計模式之觀察者模式(發(fā)布者-訂閱者模式)
- 原生javascript實(shí)現(xiàn)類似vue的數(shù)據(jù)綁定功能示例【觀察者模式】
相關(guān)文章
Vue實(shí)現(xiàn)簡單基礎(chǔ)的圖片裁剪功能
這篇文章主要為大家詳細(xì)介紹了如何利用Vue2實(shí)現(xiàn)簡單基礎(chǔ)的圖片裁剪功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2022-09-09
Vue使用localStorage存儲數(shù)據(jù)的方法
這篇文章主要為大家詳細(xì)介紹了Vue使用localStorage存儲數(shù)據(jù)的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-05-05
vue實(shí)現(xiàn)兩個區(qū)域滾動條同步滾動
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)兩個區(qū)域滾動條同步滾動,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-12-12
解決Vue.js Devtools未檢測到Vue實(shí)例的問題
在開發(fā)Vue.js應(yīng)用時,Vue.js Devtools是一個不可或缺的調(diào)試工具,然而,有時我們可能會遇到“Vue.js not detected”的提示,這意味著Vue.js Devtools未能成功識別和連接到我們的Vue應(yīng)用,本文將詳細(xì)解析這個問題,并提供相應(yīng)的解決步驟與代碼示例,需要的朋友可以參考下2024-01-01
Echarts實(shí)現(xiàn)一張圖現(xiàn)切換不同的X軸(實(shí)例代碼)
這篇文章主要介紹了Echarts 如何實(shí)現(xiàn)一張圖現(xiàn)切換不同的X軸,通過動圖給大家展示效果,實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-11-11
Vue使用正則校驗(yàn)文本框?yàn)檎麛?shù)
這篇文章主要介紹了Vue使用正則校驗(yàn)文本框?yàn)檎麛?shù)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10

