您好,登錄后才能下訂單哦!
監聽數據對象變化,最容易想到的是建立一個需要監視對象的表,定時掃描其值,有變化,則執行相應操作,不過這種實現方式,性能是個問題,如果需要監視的數據量大的話,每掃描一次全部的對象,需要的時間很長。當然,有些框架是采用的這種方式,不過他們用非常巧妙的算法提升性能,這不在我們的討論范圍之類。
Vue 中數據對象的監視,是通過設置 ES5 的新特性(ES7 都快出來了,ES5 的東西倒也真稱不得新)Object.defineProperty() 中的 set、get 來實現的。
目標
與官方文檔第一個例子相似,不過也有簡化,因為這篇只是介紹下數據對象的監聽,不涉及文本解析,所以文本解析相關的直接舍棄了:
<div id="app"></div> var app = new Vue({ el: 'app', data: { message: 'Hello Vue!' } });
瀏覽器顯示:
Hello Vue!
在控制臺輸入諸如:
app.message = 'Changed!'
之類的命令,瀏覽器顯示內容會跟著修改。
Object.defineProperty
引用 MDN 上的定義:
Object.defineProperty()方法會直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 并返回這個對象。
與此相生相伴的還有一個 Object.getOwnPropertyDescriptor():
Object.getOwnPropertyDescriptor() 返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不需要從原型鏈上進行查找的屬性)
下面的例子用一種比較簡單、直觀的方式來設置 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); // 調用 getter,12 被壓入 dep,此時 dep 值為 [12] a.a; // 調用 setter,輸出 dep ([12]) a.a = 24; // 調用 getter,24 被壓入 dep,此時 dep 值為 [12, 24] a.a;
Observer
簡單說過 Object.defineProperty 之后,就要開始扯 Observer 了。observer,中文解釋為“觀察者”,觀察什么東西呢?觀察對象屬性值的變化。故此,所謂 observer,就是給對象的所有屬性加上 getter、setter,如果對象的屬性還有屬性,比如說 {a: {a: {a: 'a'}}},則通過遞歸給其屬性的屬性也加上 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; // 遞歸的方式實現給屬性的屬性添加 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 通過設置數據對象的 getter、setter 來達到監聽數據變化的目的。數據被獲取,被設置、被修改,都能監聽到,且能做出相應的動作。
現在還有一個問題就是,誰讓你監聽的?
這個發出指令的就是 Watcher,只有 Watcher 獲取數據才觸發相應的操作;同樣,修改數據時,也只執行 Watcher 相關操作。
那如何講 Observer、Watcher 兩者關聯起來呢?全局變量!這個全局變量,只有 Watcher 才做修改,Observer 只是讀取判斷,根據這個全局變量的值不同而判斷是否 Watcher 對數據進行讀取,這個全局變量可以附加在 dep 上:
dep.target = null;
根據以上所述,簡單整理下,代碼如下:
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 調用的 getter dep.target = this; // 調用 getter,觸發相應響應 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; // 遞歸的方式實現給屬性的屬性添加 getter、setter var childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { var value = getter ? getter.call(obj) : val; // 如果是 Watcher 監聽的,就把 Watcher 對象壓入 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); // 按序執行 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;
上面基本實現了數據的監聽,bug 肯定有不少,不過只是一個粗糙的 demo,只是想展示一個大概的流程,沒有扣到非常細致。
Dep
上面幾個例子,dep 是個全局的數組,但凡 new 一個 Watcher,dep 中就要多一個 Watcher 實例,這時候不管哪個 data 更新,所有的 Watcher 實例的 update 都會執行,這是不可接受的。
Dep 抽象出來,單獨搞一個構造函數,不放在全局,就能解決了:
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 將上面的代碼改寫下就好了(當然,此處的 Dep 代碼也不完全,只是一個大概的意思罷了)。
Vue 實例代理 data 對象
官方文檔中有這么一句話:
每個 Vue 實例都會代理其 data 對象里所有的屬性。
var data = { a: 1 }; var vm = new Vue({data: data}); vm.a === data.a // -> true // 設置屬性也會影響到原始數據 vm.a = 2 data.a // -> 2 // ... 反之亦然 data.a = 3 vm.a // -> 3
這種代理看起來很麻煩,其實也是可以通過 Object.defineProperty 來實現的:
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]; }, // 設置值的時候直接設置 vm.data[key] 的值 set: function(val) { vm.data[key] = val; } }; }
捏出一個 Vue,實現最初目標
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 源碼分析之如何實現 observer 和 watcher
vue早期源碼學習系列之一:如何監聽一個對象的變化
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。