往期
前言
本文分为入门和进阶两部分,建议有经验的读者直接阅读进阶部分。
本文主要参考了vue这一开源库,若读者阅读过它的源码可以直接跳过本文 :)
入门
关于访问对象的某个属性
既然是入门,还是先提一下Vue.prototype.$watch
的几种用法
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');
关于观察者模式
先让我们看看维基百科是怎么说的:
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.
也就是说subject用来维护依赖列表, 每个依赖都是一个observer。当依赖列表中的某一项发生了变化,就自动通知subject自身状态的变更。
让我们先拷贝一下上篇文章的内容, 注意注释里的内容!
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
函数中,如何传递给依赖, 并在被监听对象该属性变化时调用呢?
我们可以利用一个全局变量(在这里我们称它为DepTarget),在访问变量的时候设置为$watch
函数的callback, 并将这个callback存到一个集合里,访问结束后置空。同时需要注意的是,每个$watch
函数应该只对应一个观察者(依赖)
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);
进阶
关于Watcher和Dep
对于上述的问题,我们需要抽象出两个类: 一个用来存储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);
好了,以上就是关于如何监听一个对象某个属性的全部内容。