Vue監(jiān)聽(tīng)數(shù)據(jù)對(duì)象變化源碼
監(jiān)聽(tīng)數(shù)據(jù)對(duì)象變化,最容易想到的是建立一個(gè)需要監(jiān)視對(duì)象的表,定時(shí)掃描其值,有變化,則執(zhí)行相應(yīng)操作,不過(guò)這種實(shí)現(xiàn)方式,性能是個(gè)問(wèn)題,如果需要監(jiān)視的數(shù)據(jù)量大的話(huà),每掃描一次全部的對(duì)象,需要的時(shí)間很長(zhǎng)。當(dāng)然,有些框架是采用的這種方式,不過(guò)他們用非常巧妙的算法提升性能,這不在我們的討論范圍之類(lèi)。
Vue 中數(shù)據(jù)對(duì)象的監(jiān)視,是通過(guò)設(shè)置 ES5 的新特性(ES7 都快出來(lái)了,ES5 的東西倒也真稱(chēng)不得新)Object.defineProperty() 中的 set、get 來(lái)實(shí)現(xiàn)的。
目標(biāo)
與官方文檔第一個(gè)例子相似,不過(guò)也有簡(jiǎn)化,因?yàn)檫@篇只是介紹下數(shù)據(jù)對(duì)象的監(jiān)聽(tīng),不涉及文本解析,所以文本解析相關(guān)的直接舍棄了:
<div id="app"></div>
var app = new Vue({
el: 'app',
data: {
message: 'Hello Vue!'
}
});
瀏覽器顯示:
Hello Vue!
在控制臺(tái)輸入諸如:
app.message = 'Changed!'
之類(lèi)的命令,瀏覽器顯示內(nèi)容會(huì)跟著修改。
Object.defineProperty
引用 MDN 上的定義:
Object.defineProperty()方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)已經(jīng)存在的屬性, 并返回這個(gè)對(duì)象。
與此相生相伴的還有一個(gè) Object.getOwnPropertyDescriptor():
Object.getOwnPropertyDescriptor() 返回指定對(duì)象上一個(gè)自有屬性對(duì)應(yīng)的屬性描述符。(自有屬性指的是直接賦予該對(duì)象的屬性,不需要從原型鏈上進(jìn)行查找的屬性)
下面的例子用一種比較簡(jiǎn)單、直觀的方式來(lái)設(shè)置 setter、getter:
var dep = [];
function defineReactive(obj, key, val) {
// 有自定義的 property,則用自定義的 property
var property = Object.getOwnPropertyDescriptor(obj, key);
if(property && property.configurable === false) {
return;
}
var getter = property && property.get;
var setter = property && property.set;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
var value = getter ? getter.call(obj) : val;
dep.push(value);
return value;
},
set: function(newVal) {
var value = getter ? getter.call(obj) : val;
// set 值與原值相同,則不更新
if(newVal === value) {
return;
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
console.log(dep);
}
});
}
var a = {};
defineReactive(a, 'a', 12);
// 調(diào)用 getter,12 被壓入 dep,此時(shí) dep 值為 [12]
a.a;
// 調(diào)用 setter,輸出 dep ([12])
a.a = 24;
// 調(diào)用 getter,24 被壓入 dep,此時(shí) dep 值為 [12, 24]
a.a;
Observer
簡(jiǎn)單說(shuō)過(guò) Object.defineProperty 之后,就要開(kāi)始扯 Observer 了。observer,中文解釋為“觀察者”,觀察什么東西呢?觀察對(duì)象屬性值的變化。故此,所謂 observer,就是給對(duì)象的所有屬性加上 getter、setter,如果對(duì)象的屬性還有屬性,比如說(shuō) {a: {a: {a: 'a'}}},則通過(guò)遞歸給其屬性的屬性也加上 getter、setter:
function Observer(value) {
this.value = value;
this.walk(value);
}
Observer.prototype.walk = function(obj) {
var keys = Object.keys(obj);
for(var i = 0; i < keys.length; i++) {
// 給所有屬性添加 getter、setter
defineReactive(obj, keys[i], obj[keys[i]]);
}
};
var dep = [];
function defineReactive(obj, key, val) {
// 有自定義的 property,則用自定義的 property
var property = Object.getOwnPropertyDescriptor(obj, key);
if(property && property.configurable === false) {
return;
}
var getter = property && property.get;
var setter = property && property.set;
// 遞歸的方式實(shí)現(xiàn)給屬性的屬性添加 getter、setter
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
var value = getter ? getter.call(obj) : val;
dep.push(value);
return value;
},
set: function(newVal) {
var value = getter ? getter.call(obj) : val;
// set 值與原值相同,則不更新
if(newVal === value) {
return;
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 給新賦值的屬性值的屬性添加 getter、setter
childOb = observe(newVal);
console.log(dep);
}
});
}
function observe(value) {
if(!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
Watcher
Observer 通過(guò)設(shè)置數(shù)據(jù)對(duì)象的 getter、setter 來(lái)達(dá)到監(jiān)聽(tīng)數(shù)據(jù)變化的目的。數(shù)據(jù)被獲取,被設(shè)置、被修改,都能監(jiān)聽(tīng)到,且能做出相應(yīng)的動(dòng)作。
現(xiàn)在還有一個(gè)問(wèn)題就是,誰(shuí)讓你監(jiān)聽(tīng)的?
這個(gè)發(fā)出指令的就是 Watcher,只有 Watcher 獲取數(shù)據(jù)才觸發(fā)相應(yīng)的操作;同樣,修改數(shù)據(jù)時(shí),也只執(zhí)行 Watcher 相關(guān)操作。
那如何講 Observer、Watcher 兩者關(guān)聯(lián)起來(lái)呢?全局變量!這個(gè)全局變量,只有 Watcher 才做修改,Observer 只是讀取判斷,根據(jù)這個(gè)全局變量的值不同而判斷是否 Watcher 對(duì)數(shù)據(jù)進(jìn)行讀取,這個(gè)全局變量可以附加在 dep 上:
dep.target = null;
根據(jù)以上所述,簡(jiǎn)單整理下,代碼如下:
function Watcher(data, exp, cb) {
this.data = data;
this.exp = exp;
this.cb = cb;
this.value = this.get();
}
Watcher.prototype.get = function() {
// 給 dep.target 置值,告訴 Observer 這是 Watcher 調(diào)用的 getter
dep.target = this;
// 調(diào)用 getter,觸發(fā)相應(yīng)響應(yīng)
var value = this.data[this.exp];
// dep.target 還原
dep.target = null;
return value;
};
Watcher.prototype.update = function() {
this.cb();
};
function Observer(value) {
this.value = value;
this.walk(value);
}
Observer.prototype.walk = function(obj) {
var keys = Object.keys(obj);
for(var i = 0; i < keys.length; i++) {
// 給所有屬性添加 getter、setter
defineReactive(obj, keys[i], obj[keys[i]]);
}
};
var dep = [];
dep.target = null;
function defineReactive(obj, key, val) {
// 有自定義的 property,則用自定義的 property
var property = Object.getOwnPropertyDescriptor(obj, key);
if(property && property.configurable === false) {
return;
}
var getter = property && property.get;
var setter = property && property.set;
// 遞歸的方式實(shí)現(xiàn)給屬性的屬性添加 getter、setter
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
var value = getter ? getter.call(obj) : val;
// 如果是 Watcher 監(jiān)聽(tīng)的,就把 Watcher 對(duì)象壓入 dep
if(dep.target) {
dep.push(dep.target);
}
return value;
},
set: function(newVal) {
var value = getter ? getter.call(obj) : val;
// set 值與原值相同,則不更新
if(newVal === value) {
return;
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 給新賦值的屬性值的屬性添加 getter、setter
childOb = observe(newVal);
// 按序執(zhí)行 dep 中元素的 update 方法
for(var i = 0; i < dep.length; i++) {
dep[i].update();
}
}
});
}
function observe(value) {
if(!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
var data = {a: 1};
new Observer(data);
new Watcher(data, 'a', function(){console.log('it works')});
data.a =12;
data.a =14;
上面基本實(shí)現(xiàn)了數(shù)據(jù)的監(jiān)聽(tīng),bug 肯定有不少,不過(guò)只是一個(gè)粗糙的 demo,只是想展示一個(gè)大概的流程,沒(méi)有扣到非常細(xì)致。
Dep
上面幾個(gè)例子,dep 是個(gè)全局的數(shù)組,但凡 new 一個(gè) Watcher,dep 中就要多一個(gè) Watcher 實(shí)例,這時(shí)候不管哪個(gè) data 更新,所有的 Watcher 實(shí)例的 update 都會(huì)執(zhí)行,這是不可接受的。
Dep 抽象出來(lái),單獨(dú)搞一個(gè)構(gòu)造函數(shù),不放在全局,就能解決了:
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function(sub) {
this.subs.push(sub);
};
Dep.prototype.notify = function() {
var subs = this.subs.slice();
for(var i = 0; i < subs.length; i++) {
subs[i].update();
}
}
利用 Dep 將上面的代碼改寫(xiě)下就好了(當(dāng)然,此處的 Dep 代碼也不完全,只是一個(gè)大概的意思罷了)。
Vue 實(shí)例代理 data 對(duì)象
官方文檔中有這么一句話(huà):
每個(gè) Vue 實(shí)例都會(huì)代理其 data 對(duì)象里所有的屬性。
var data = { a: 1 };
var vm = new Vue({data: data});
vm.a === data.a // -> true
// 設(shè)置屬性也會(huì)影響到原始數(shù)據(jù)
vm.a = 2
data.a // -> 2
// ... 反之亦然
data.a = 3
vm.a // -> 3
這種代理看起來(lái)很麻煩,其實(shí)也是可以通過(guò) Object.defineProperty 來(lái)實(shí)現(xiàn)的:
function Vue(options) {
var data = this.data = options.data;
var keys = Object.keys(data);
var i = keys.length;
while(i--) {
proxy(this, keys[i];
}
}
function proxy(vm, key) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
// 直接獲取 vm.data[key] 的值
get: function() {
return vm.data[key];
},
// 設(shè)置值的時(shí)候直接設(shè)置 vm.data[key] 的值
set: function(val) {
vm.data[key] = val;
}
};
}
捏出一個(gè) Vue,實(shí)現(xiàn)最初目標(biāo)
var Vue = (function() {
var Watcher = function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get();
};
Watcher.prototype.get = function get() {
Dep.target = this;
var value = this.vm._data[this.exp];
Dep.target = null;
return value;
};
Watcher.prototype.addDep = function addDep(dep) {
dep.addSub(this);
};
Watcher.prototype.update = function update() {
this.run();
};
Watcher.prototype.run = function run() {
this.cb.call(this.vm);
}
var Dep = function Dep() {
this.subs = [];
};
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub);
};
Dep.prototype.depend = function depend() {
if(Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify() {
var subs = this.subs.slice();
for(var i = 0; i < subs.length; i++) {
subs[i].update();
}
};
Dep.target = null;
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.walk(value);
};
Observer.prototype.walk = function walk(obj) {
var keys = Object.keys(obj);
for(var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]]);
}
};
function defineReactive(obj, key, val) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if(property && property.configurable === false) {
return;
}
var getter = property && property.get;
var setter = property && property.set;
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if(Dep.target) {
dep.depend();
if(childOb) {
childOb.dep.depend();
}
}
return value;
},
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val;
if(newVal === value) {
return;
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = observe(newVal);
dep.notify();
}
});
}
function observe(value) {
if(!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
function Vue(options) {
var vm = this;
this._el = options.el;
var data = this._data = options.data;
var keys = Object.keys(data);
var i = keys.length;
while(i--) {
proxy(this, keys[i]);
}
observe(data);
var elem = document.getElementById(this._el);
elem.innerHTML = vm.message;
new Watcher(this, 'message', function() {
elem.innerHTML = vm.message;
});
}
function proxy(vm, key) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function proxyGetter() {
return vm._data[key];
},
set: function proxySetter(val) {
vm._data[key] = val;
}
});
}
return Vue;
})();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script type="text/javascript" src="vue.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/javascript">
var app = new Vue({
el: 'app',
data: {
message: 'aaaaaaaaaaaaa'
}
});
</script>
</body>
</html>
參考資料:
vue 源碼分析之如何實(shí)現(xiàn) observer 和 watcher
vue早期源碼學(xué)習(xí)系列之一:如何監(jiān)聽(tīng)一個(gè)對(duì)象的變化
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Vue監(jiān)聽(tīng)數(shù)據(jù)的原理詳解
- vue自定義鍵盤(pán)信息、監(jiān)聽(tīng)數(shù)據(jù)變化的方法示例【基于vm.$watch】
- vue中的watch監(jiān)聽(tīng)數(shù)據(jù)變化及watch中各屬性的詳解
- Vue監(jiān)聽(tīng)數(shù)據(jù)渲染DOM完以后執(zhí)行某個(gè)函數(shù)詳解
- vue2.0$nextTick監(jiān)聽(tīng)數(shù)據(jù)渲染完成之后的回調(diào)函數(shù)方法
- Vue之監(jiān)聽(tīng)數(shù)據(jù)的原理詳解
相關(guān)文章
vue增加強(qiáng)緩存和版本號(hào)的實(shí)現(xiàn)方法
這篇文章主要介紹了vue增加強(qiáng)緩存和版本號(hào)的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
vue component 中引入less文件報(bào)錯(cuò) Module build failed
這篇文章主要介紹了vue component 中引入less文件報(bào)錯(cuò) Module build failed的解決方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-04-04
vue.js單頁(yè)面應(yīng)用實(shí)例的簡(jiǎn)單實(shí)現(xiàn)
本篇文章主要介紹了vue.js單頁(yè)面應(yīng)用實(shí)例的簡(jiǎn)單實(shí)現(xiàn),使用單頁(yè)應(yīng)用,沒(méi)有頁(yè)面切換,就沒(méi)有白屏阻塞,可以大大提高 H5 的性能,達(dá)到接近原生的流暢體驗(yàn)。2017-04-04
詳解Vue CLI3 多頁(yè)應(yīng)用實(shí)踐和源碼設(shè)計(jì)
這篇文章主要介紹了詳解Vue CLI3 多頁(yè)應(yīng)用實(shí)踐和源碼設(shè)計(jì),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
vue3關(guān)于時(shí)間顯示格式化的問(wèn)題
這篇文章主要介紹了vue3關(guān)于時(shí)間顯示格式化的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
VUE設(shè)置和清除定時(shí)器的方式及遇到的問(wèn)題
?最近需要再頁(yè)面里做個(gè)倒計(jì)時(shí),發(fā)現(xiàn)用clearInterval()清除定時(shí)器失效,下面這篇文章主要給大家介紹了關(guān)于VUE設(shè)置和清除定時(shí)器的方式及遇到的問(wèn)題的相關(guān)資料,需要的朋友可以參考下2022-10-10
Vue引入路徑正確但一直報(bào)錯(cuò)問(wèn)題:Already included file name&n
這篇文章主要介紹了Vue引入路徑正確但一直報(bào)錯(cuò):Already included file name ‘××ב differs from file name ‘××ב only in casing.具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
vue使用動(dòng)態(tài)組件實(shí)現(xiàn)TAB切換效果完整實(shí)例
在實(shí)際項(xiàng)目開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到選項(xiàng)卡切換,對(duì)于一個(gè)前端工程師來(lái)說(shuō),組件化/模塊化開(kāi)發(fā)是一種必備的行為規(guī)范,下面這篇文章主要給大家介紹了關(guān)于vue使用動(dòng)態(tài)組件實(shí)現(xiàn)TAB切換效果的相關(guān)資料,需要的朋友可以參考下2023-05-05
使用vue構(gòu)建移動(dòng)應(yīng)用實(shí)戰(zhàn)代碼
本篇文章主要介紹了使用vue構(gòu)建移動(dòng)應(yīng)用實(shí)戰(zhàn)代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08

