如何模拟vue实现一个watcher
首先需要了解一下Object.defineProperty
,看一个示例。
var obj = {};
Object.defineProperty(obj, 'name', {
value: 'zhangsan'
});
上述例子,给对象obj
添加一个name
属性,值为zhangsan
。
在defineProperty
的第三个参数中,可以添加多个属性描述:
value
:属性的值(不用多说了)writable
:如果为false
,属性的值就不能被重写,只能为只读了configurable
:总开关,一旦为false
,就不能再设置他的(value
,writable
,configurable
)enumerable
:是否能在for...in
循环中遍历出来或在Object.keys
中列举出来。get
:获取值时执行的函数set
:赋值是执行的函数
需要注意的是,第三个参数的对象中,value
属性不能和get
/set
同时存在。实现属性值变动的监测,就是从get
和set
这两个属性上着手,比如如下的例子,设置值和获取值时都会打印一段文本内容。
var obj = {};
Object.defineProperty(obj, 'name', {
get: function() {
return '获取值:zhangsan';
},
set: function (newVal) {
console.log('设置值:' + newVal);
return newVal;
}
});
obj.name = 'lisi';
console.log(obj.name);
//输出:
//设置值:lisi
//获取值:zhangsan
Object.defineProperty在MDN上的介绍(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
最终希望实现的效果
var v = new Vue({
data: {
//properties...
}
})
v.$watch('someProp',function() {
//watch callback
})
当$watch
的属性值变化时,让其触发watch中的回调。
那么实现思路就有了,在属性值set的时候,让其发送数据发生变化的通知给Watcher
,然后Watcher
触发$watch
中设置的回调,当属性值get的时候,将属性值再返回回来。
- 实现一个
Observer
,让其为对象设置的每一个属性都添加上get
和set
方法,以便于我们获得控制权。 实现一个
Watcher
,当数据发生变化时,立即通知Watcher
,让Watcher
进行相应的操作。使用
Dep
来进行通知处理,它连接Watcher
与Observer
,当Observer
监测到数据变化,使用Dep
来通知Watcher
。
首先实现一个Observer
,递归遍历对象,为属性添加set/get
。
export default class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(value) {
Object.keys(value).forEach(key => {
this.convert(key, value[key]);
})
}
convert(key, val) {
defineReactive(this.value, key, val);
}
}
在defineReactive
中为属性添加set/get
,当设置值时使用Dep
的notify
方法来通知Watcher
,当获取值时,判断来源,如果属于watcher
监听范围内的属性,将其Dep.target
(即当前属性的Watcher
对象)添加到Dep
实例的订阅数组中,这个数组中维护着当前watch的Watcher
对象集合。
export function defineReactive(obj, key, val) {
let childObj = observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
childObj = observe(newVal);
dep.notify();
}
})
}
export function observe(value) {
if (!value || typeof value !== 'object') {
return;
}
return (new Observer(value));
}
然后是Dep
,可以把它看做是服务于Observer
的订阅系统。Watcher
订阅某个Observer
的Dep
,当Observer
观察的数据发生变化时,通过Dep
通知各个已经订阅的Watcher
。
Dep
提供了几个接口:
addSub
: 接收的参数为Watcher
实例,并把Watcher
实例存入记录依赖的数组中removeSub
: 与addSub
对应,作用是将Watcher
实例从记录依赖的数组中移除depend
:Dep.target
上存放这当前需要操作的Watcher
实例,调用depend
会调用该Watcher
实例的addDep
方法,addDep
的功能可以看下面对Watcher
的介绍notify
: 通知依赖数组中所有的watcher
进行更新操作
export default class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}
depend() {
if(Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
Dep.target = null;
在Watcher
中,当获取属性值时,首先将Dep.target
设置为当前watcher对象,那么可以在属性get
的时候,可以判断这个属性值的获取来源是哪里,如果来自于Watcher
,则会带有当前的watcher对象。
export default class Watcher {
constructor(vm, expOrFn, cb) {
this.expOrFn = expOrFn;
this.cb = cb;
this.vm = vm;
this.value = this.get();
}
get() {
Dep.target = this;
const value = this.vm.$data[this.expOrFn];
Dep.target = null;
return value;
}
update() {
this.run();
}
addDep(dep) {
dep.addSub(this);
}
run() {
const value = this.get();
if (value !== this.value) {
this.value = value;
this.cb.call(this.vm);
}
}
}
最后是vue的实现,将属性值也遍历添加到根节点上,通过vue当前实例.属性
这种形式也可以获取到属性值。
export default class Vue {
constructor(options = {}) {
this.$options = options;
let data = this.$data = this.$options.data;
Object.keys(data).forEach(key => this._proxy(key));
observe(data);
}
$watch(expOrFn, cb) {
new Watcher(this, expOrFn, cb);
}
_proxy(key) {
const self = this;
Object.defineProperty(self, key, {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return self.$data[key];
},
set: function proxySetter(newVal) {
self.$data[key] = newVal;
}
})
}
}
最后测试代码
const vm = new Vue({
data: {
a: 1,
b: {
c: 2
}
}
});
vm.$watch('a', () => console.log(`a当前的值为:${vm.a}`));
setTimeout(() => {
vm.a = 3;
}, 2000);
setTimeout(() => {
vm.a = 15;
}, 5000);
在控制台运行可以看到当a
的值变化时,能够执行$watch
的回调。
- 本博客所有文章除特别声明外,均可转载和分享,转载请注明出处!
- 本文地址:https://www.leevii.com/?p=962