本文分爲入門和進階兩部分,建議有經驗的讀者直接閱讀進階部分。vue
本文主要參考了vue這一開源庫,若讀者閱讀過它的源碼能夠直接跳過本文 :)瀏覽器
既然是入門,仍是先提一下Vue.prototype.$watch
的幾種用法app
const vm = Vue({
data() {
return {
b: true,
o: { name: 'obj' },
a: ['a', 'b', 'c'],
odeep: {
path: {
name: 'obj deep',
value: [],
},
},
};
},
watch: {
// 若是b的值改變了,打印改變前與改變後的值
b(val, oldVal) {
console.warn(val, oldVal);
},
// 若是o.name的值改變了,打印改變前與改變後的值
'o.name': {
handler(val, oldVal) {
console.warn(val, oldVal);
},
},
},
created() {
// 深度監聽: 若是odeep.path.name/odeep.path.value的值改變了,打印odeep.path改變前與改變後的值
this.$watch('odeep.path', (val, oldVal) => {
console.warn(val, oldVal);
}, { deep: true });
},
});
複製代碼
如何去經過諸如o.name
的字符串訪問到vm.o.name
呢? vm['o.name']
固然是不行的,須要寫成vm['o']['name']
這樣的形式。函數
function parsePath(path) {
if (!/[\w.]$/.test(path)) {
// 爲何要返回一個帶參數的函數呢? 提早告訴你,是爲了觸發被監聽對象的get方法(還記得上一篇文章的內容嗎)
return function(obj) {};
}
const segs = path.split('.');
// 想知道這裏爲何不用forEach嗎,試試在forEach裏使用return吧
return function(obj) {
for (let i = 0; i < segs.length; i += 1) {
if (!obj) {
return;
}
obj = obj[segs[i]];
}
return obj;
};
}
const obj = {
o: { name: 'a' },
};
console.assert(parsePath('o.name')(obj) === 'a');
複製代碼
先讓咱們看看維基百科是怎麼說的:post
The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.ui
也就是說subject用來維護依賴列表, 每一個依賴都是一個observer。當依賴列表中的某一項發生了變化,就自動通知subject自身狀態的變動。this
讓咱們先拷貝一下上篇文章的內容, 注意註釋裏的內容!spa
function defineReactive(obj, key, val) {
if (isPlainObject(val)) {
observe(val);
} else if (Array.isArray(val)) {
dealAugment(val, dep);
observeArray(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 將依賴加入依賴列表
return val;
},
set(newVal) {
if (val !== newVal) {
val = newVal;
if (isPlainObject(newVal)) {
observe(newVal);
} else if (Array.isArray(newVal)) {
dealAugment(newVal, dep);
observeArray(newVal);
}
// 依賴通知subject自身狀態的改變,即調用callback
}
},
});
}
複製代碼
可是callback在$watch
函數中,如何傳遞給依賴, 並在被監聽對象該屬性變化時調用呢?prototype
咱們能夠利用一個全局變量(在這裏咱們稱它爲DepTarget),在訪問變量的時候設置爲$watch
函數的callback, 並將這個callback存到一個集合裏,訪問結束後置空。同時須要注意的是,每一個$watch
函數應該只對應一個觀察者(依賴)code
let DepTarget = null;
function $watch(obj, path, cb) {
DepTarget = cb;
// 訪問obj,自動調用get方法實現依賴注入
parsePath(path)(obj);
DepTarget = null;
}
function defineReactive(obj, key, val) {
const deps = [];
if (isPlainObject(val)) {
observe(val);
} else if (Array.isArray(val)) {
// 傳遞dep,在push等函數觸發時notify, 可是咱們沒法訪問到舊的value值
dealAugment(val, deps, val);
observeArray(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (DepTarget) {
// 將callback存入集合
deps.push(DepTarget);
}
return val;
},
set(newVal) {
if (val !== newVal) {
val = newVal;
if (isPlainObject(newVal)) {
observe(newVal);
} else if (Array.isArray(newVal)) {
dealAugment(newVal, deps, val);
observeArray(newVal);
}
// 依賴通知subject自身狀態的改變,即調用callback
deps.forEach((cb) => {
if (typeof cb === 'function') {
cb(val);
}
});
}
},
});
}
function dealAugment(val, deps, val) {
const arrayMethods = Object.create(Array.prototype);
// 咱們以push方法爲例
arrayMethods.push = function mutator(...args) {
[].push.apply(this, args);
// 依賴通知subject自身狀態的改變,即調用callback
deps.forEach((cb) => {
if (typeof cb === 'function') {
cb(val);
}
});
};
// 若是瀏覽器實現了__proto__, 覆蓋原型對象
if ('__proto__' in {}) {
val.__proto__ = arrayMethods;
} else {
// 要是瀏覽器沒有實現__proto__, 覆蓋對象自己的該方法
Object.defineProperty(val, 'push', {
value: arrayMethods['push'],
enumerable: true,
});
}
}
複製代碼
讓咱們試一試
const obj = {
b: true,
o: { name: 'obj' },
a: ['a', 'b', 'c'],
odeep: {
path: {
name: 'obj deep',
value: [],
},
},
};
// observe等函數的實現請查看上一篇文章, 或是文章末尾的完整示例
observe(obj);
$watch(obj, 'b', (val, oldVal) => {
console.warn('b watched: ', val, oldVal);
});
$watch(obj, 'a', (val, oldVal) => {
console.warn('a watched: ',val, oldVal);
});
$watch(obj, 'odeep.path.value', (val, oldVal) => {
console.warn('odeep.path.value watched: ',val, oldVal);
});
setTimeout(() => {
// 固然不會有什麼問題, 不過你也發現了,咱們只能訪問到當前的value值,因此咱們須要一個對象來存儲舊的value值
obj.b = false;
obj.a.push('d');
obj.o.name = 'new obj';
obj.odeep.path.value.push(1);
}, 1000);
複製代碼
對於上述的問題,咱們須要抽象出兩個類: 一個用來存儲value值和callback(以及傳遞舊的value值),咱們把它稱爲watcher; 另外一個用來添加/存儲watcher,咱們把它稱爲dep。
let DepTarget = null;
class Dep {
constructor() {
this.watchers = new Set();
}
add(watcher) {
this.watchers.add(watcher);
}
notify() {
this.watchers.forEach((watcher) => {
// 依賴通知自身狀態的改變, 即調用callback
watcher.update();
});
}
}
class Watcher {
constructor(obj, path, cb) {
this.obj = obj;
this.path = path;
this.cb = cb;
this.value = this.get();
}
get() {
DepTarget = this;
// 訪問obj,自動調用get方法實現依賴注入
const val = parsePath(this.path)(this.obj);
DepTarget = null;
return val;
}
update() {
const val = this.get();
// 對於當前狀態改變的被監聽屬性纔會觸發callback
if (val !== this.value) {
const oldVal = this.value;
this.value = val;
// 傳遞val和oldVal
this.cb(val, oldVal);
}
}
}
function defineReactive(obj, key, val) {
const dep = new Dep();
if (isPlainObject(val)) {
observe(val);
} else if (Array.isArray(val)) {
// 傳遞dep,在push等函數觸發時notify
dealAugment(val, dep);
observeArray(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (DepTarget) {
dep.add(DepTarget);
}
return val;
},
set(newVal) {
if (val !== newVal) {
val = newVal;
if (isPlainObject(newVal)) {
observe(newVal);
} else if (Array.isArray(newVal)) {
dealAugment(newVal, dep);
observeArray(newVal);
}
dep.notify();
}
},
});
}
function dealAugment(val, dep) {
const arrayMethods = Object.create(Array.prototype);
// 咱們以push方法爲例
arrayMethods.push = function mutator(...args) {
[].push.apply(this, args);
dep.notify();
};
// 若是瀏覽器實現了__proto__, 覆蓋原型對象
if ('__proto__' in {}) {
val.__proto__ = arrayMethods;
} else {
// 要是瀏覽器沒有實現__proto__, 覆蓋對象自己的該方法
Object.defineProperty(val, 'push', {
value: arrayMethods['push'],
enumerable: true,
});
}
}
複製代碼
好了,這樣咱們就實現了監聽對象某個屬性的變化。
對於深度監聽,思路其實也是同樣的: 訪問obj,自動調用get方法實現依賴注入,咱們只須要遍歷訪問對象的全部屬性便可。
class Watcher {
constructor(obj, path, cb, opts = {}) {
this.obj = obj;
this.path = path;
this.cb = cb;
this.deep = !!opts.deep;
this.value = this.get();
}
get() {
DepTarget = this;
const val = parsePath(this.path)(this.obj);
if (this.deep) {
// 若當前路徑屬性值爲對象,訪問其全部屬性
traverse(val);
}
DepTarget = null;
return val;
}
update() {
const val = this.get();
if (val !== this.value || isObject(val)) {
const oldVal = this.value;
this.value = val;
this.cb(val, oldVal);
}
}
}
function traverse(val) {
let i = 0;
if (Array.isArray(val)) {
i = val.length;
while (i--) { traverse(val[i]); }
} else if (isPlainObject(val)) {
const keys = Object.keys(val);
i = keys.length;
while (i--) { traverse(val[keys[i]]); }
}
}
複製代碼
let DepTarget = null;
class Dep {
constructor() {
this.watchers = new Set();
}
add(watcher) {
this.watchers.add(watcher);
}
notify() {
this.watchers.forEach((watcher) => {
watcher.update();
});
}
}
class Watcher {
constructor(obj, path, cb, opts = {}) {
this.obj = obj;
this.path = path;
this.cb = cb;
this.deep = !!opts.deep;
this.value = this.get();
}
get() {
DepTarget = this;
const val = parsePath(this.path)(this.obj);
if (this.deep) {
traverse(val);
}
DepTarget = null;
return val;
}
update() {
const val = this.get();
if (val !== this.value || isObject(val)) {
const oldVal = this.value;
this.value = val;
this.cb(val, oldVal);
}
}
}
function parsePath(path) {
if (!/[\w.]$/.test(path)) {
return function(obj) {};
}
const segs = path.split('.');
return function(obj) {
for (let i = 0; i < segs.length; i += 1) {
if (!obj) {
return;
}
obj = obj[segs[i]];
}
return obj;
};
}
function traverse(val) {
let i = 0;
if (Array.isArray(val)) {
i = val.length;
while (i--) { traverse(val[i]); }
} else if (isPlainObject(val)) {
const keys = Object.keys(val);
i = keys.length;
while (i--) { traverse(val[keys[i]]); }
}
}
function isObject(val) {
const type = typeof val;
return val != null && (type === 'object' || type === 'function');
}
function isPlainObject(obj) {
return ({}).toString.call(obj) === '[object Object]';
}
function defineReactive(obj, key, val) {
const dep = new Dep();
if (isPlainObject(val)) {
observe(val);
} else if (Array.isArray(val)) {
dealAugment(val, dep);
observeArray(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (DepTarget) {
dep.add(DepTarget);
}
return val;
},
set(newVal) {
if (val !== newVal) {
val = newVal;
if (isPlainObject(newVal)) {
observe(newVal);
} else if (Array.isArray(newVal)) {
dealAugment(newVal, dep);
observeArray(newVal);
}
dep.notify();
}
},
});
}
function dealAugment(val, dep) {
const arrayMethods = Object.create(Array.prototype);
// 咱們以push方法爲例
arrayMethods.push = function mutator(...args) {
[].push.apply(this, args);
dep.notify();
};
// 若是瀏覽器實現了__proto__, 覆蓋原型對象
if ('__proto__' in {}) {
val.__proto__ = arrayMethods;
} else {
// 要是瀏覽器沒有實現__proto__, 覆蓋對象自己的該方法
Object.defineProperty(val, 'push', {
value: arrayMethods['push'],
enumerable: true,
});
}
}
function observeArray(obj) {
obj.forEach((el) => {
if (isPlainObject(el)) {
observe(el);
} else if (Array.isArray(el)) {
observeArray(el);
}
});
}
function observe(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
const obj = {
b: true,
o: { name: 'obj' },
a: ['a', 'b', 'c'],
odeep: {
path: {
name: 'obj deep',
value: [],
},
},
};
observe(obj);
new Watcher(obj, 'b', (val, oldVal) => {
console.warn('b watched: ', val, oldVal);
});
new Watcher(obj, 'a', (val, oldVal) => {
console.warn('a watched: ',val, oldVal);
});
new Watcher(obj, 'odeep.path.value', (val, oldVal) => {
console.warn('odeep.path.value watched: ',val, oldVal);
});
new Watcher(obj, 'odeep', (val, oldVal) => {
console.warn('odeep watched: ',val, oldVal);
}, { deep: true });
setTimeout(() => {
obj.b = false;
obj.a.push('d');
obj.o.name = 'b';
obj.odeep.path.value.push(1);
obj.odeep.path.name = 'new obj deep';
}, 1000);
複製代碼
好了,以上就是關於如何監聽一個對象某個屬性的所有內容。