哔哔哔
在前文中,我们实现了一个Observer,这一节我们就来讲讲dep和watcher的实现,依然如前,在文章末尾我编写了一个js的简单测试版供你边测试边读懂这一节的文字。
先讲dep
dep就是我们的依赖收集器,通过前文的observer你就会知道,observer观察的是一个对象|数组内的所有数据,因此,dep也是管理observer内观察的对象|数组内的所有数据所各自对应的多个依赖(watcher)
说实话有点绕,所以让我们直接开始
创建
class depNext {
subs: Map<string, Array<Watcher>>;
constructor() {
this.subs = new Map();
}
addSub(prop, target) {
...
}
// 添加一个依赖
depend(prop) {
...
}
// 通知所有依赖更新
notify(prop) {
...
}
}
我们要管理对象内所有数据各自对应的所有依赖,则首先得有一个合适的数据结构,Map就很合适,比如
//observer使obj可响应化后
let obj={
a:1
b:2
c:3
}
//dep中的map就长这样,这里是伪代码
let map={
a:[wathcer1,watcher2.....]
b:[wathcer3,watcher4.....]
c:[wathcer5,watcher6.....]
}
那么要如何构建这样的Map呢?我们编写我们的addSub逻辑
我们首先获取Map中有没有创建prop:[watcher1.....]这样的数据结构,(后文我们称这个prop:[watcher1.....],为映射数组)没有则直接创建,如下:
addSub(prop, target) {
const sub = this.subs.get(prop);
if (!sub) {
this.subs.set(prop, [target]);
return;
}
sub.push(target);
}
在数据被get的时候,会调用dep.depend(),depend的逻辑也非常简单,检测全局变量target有没有被赋值依赖,有的话就根据prop让addSub添加依赖。
depend(prop) {
if (window.target) {
this.addSub(prop, window.target);
}
}
最后只剩下notify了,notify的逻辑就是通知prop的映射数组内的所有依赖去完成更新。
notify(prop) {
const watchers = this.subs.get(prop);
if(!watchers)return;
for (let i = 0, l = watchers.length; i < l; i++) {
watchers[i].update();
}
}
}
构建完后的dep长这样
class depNext {
subs: Map<string, Array<Watcher>>;
constructor() {
this.subs = new Map();
}
addSub(prop, target) {
const sub = this.subs.get(prop);
if (!sub) {
this.subs.set(prop, [target]);
return;
}
sub.push(target);
}
// 添加一个依赖
depend(prop) {
if (window.target) {
this.addSub(prop, window.target);
}
}
// 通知所有依赖更新
notify(prop) {
const watchers = this.subs.get(prop);
if(!watchers)return;
for (let i = 0, l = watchers.length; i < l; i++) {
watchers[i].update();
}
}
}
再说watcher
watcher就是我们的“依赖”,在数据更新后,observer会通知dep更新相关数据的依赖,依赖会执行其上的callback回调函数来更新视图。
首先先上骨架:
class Watcher {
vm:VM
cb:Function;
getter:any;
value:any;
constructor (vm,initVal,expOrFn,cb) {
this.vm = vm; //vue实例
this.cb = cb; //要执行的回调
if(isType(expOrFn,'String'))this.getter = parsePath(expOrFn)//先不用管parsePath是什么
else if(isType(expOrFn,'Function'))this.getter=expOrFn
this.value = this.get() //收集依赖
this.value=initVal //设定初始值
}
get () {
//...收集依赖
}
update () {
//...依赖更新
}
}
先从核心依赖收集讲起
前面我们讲过dep,在depend方法中收集依赖
depend(prop) {
if (window.target) {
this.addSub(prop, window.target);
}
}
再联系一下前文observer在数据被获取时,会通知dep执行depend收集依赖,
那么我们watcher的get方法就呼之欲出了!
get () {
window.target = this;
let value = this.getter(this.vm.$data)
window.target = undefined;
return value
}
我们先在全局target赋值当前依赖,然后再获取一下跟依赖相关的数据,这时候observer就会执行后续的依赖收集流程。
那么,你一定会有疑惑,getter是什么?
parsePath方法
在constructor中
if(isType(expOrFn,'String'))this.getter = parsePath(expOrFn)
else if(isType(expOrFn,'Function'))this.getter=expOrFn
如果getter本身就是获取数据的函数,那么就直接赋值,如果是形如"obj.a"的字符串,那么用parsePath方法将其变成一个获取$data选项中obj:{a:'xxxx'}数据的方法
为后面文本解析器的讲解做铺垫,我现在就举一个更详细的例子,
当complier解析到 “小明的年龄是{{obj.a}}岁” 这样的文本节点时,他就会生成一个watcher,此时传参的expOrFn就是obj.a。
function parsePath(path) {
const bailRE = /[^\w.$]/;
const segments = path.split(".");
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
if (bailRE.test(segments[i])) {
//this.arr[0] this[arr[0]]
const match = segments[i].match(/(\w+)\[(.+)\]/);
obj = obj[match[1]];
obj = obj[match[2]];
continue;
}
obj = obj[segments[i]];
}
return obj;
};
parsePath函数简单,它会返回一个函数,这个函数会按照expOrFn中a.b.c的顺序从参数obj中取出这个数据,这时候observer就会执行后续的依赖收集流程了!
最后说说依赖更新
更简单了,就是触发回调嘛~
update () {
const oldValue = this.value
this.value = this.getter(this.vm.$data)
this.cb.call(this.vm, this.value, oldValue)
}
最终成果
let target=null;
function def(obj, key, val, enumerable = false) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true,
});
}
class ObserverNext {
constructor(key, value, parent) {
this.$key = key;
this.$value = value;
this.$parent = parent;
this.dep = new Dep();
def(value, "__ob__", this);
this.walk(value);
this.detect(value, parent);
}
walk(obj) {
for (const [key, val] of Object.entries(obj)) {
if (typeof val == "object") {
//同时判断数组和对象
new ObserverNext(key, val, obj);
}
}
}
detect(val, parent) {
const dep = this.dep;
const key = this.$key;
const proxy = new Proxy(val, {
get(obj, property) {
if (!obj.hasOwnProperty(property)) {
return;
}
dep.depend(property);
return obj[property];
},
set(obj, property, value) {
obj[property] = value;
dep.notify(property);
if (parent.__ob__) parent.__ob__.dep.notify(key);
return true;
},
});
parent[key] = proxy;
}
}
class Dep {
constructor() {
this.subs = new Map();
}
addSub(prop, target) {
const sub = this.subs.get(prop);
if (!sub) {
this.subs.set(prop, [target]);
return;
}
sub.push(target);
}
// 添加一个依赖
depend(prop) {
if (target) {
this.addSub(prop, target);
}
}
// 通知所有依赖更新
notify(prop) {
const watchers = this.subs.get(prop);
if (!watchers) return;
for (let i = 0, l = watchers.length; i < l; i++) {
watchers[i].update();
}
}
}
class Watcher {
constructor (vm,initVal,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get() //收集依赖
this.value=initVal
}
get () {
target = this;
let value = this.getter(this.vm.data)
target = undefined;
return value
}
update () {
const oldValue = this.value
// this.value = this.get() //更新时不要触发getter否则会收集依赖
this.value = this.getter(this.vm.data)
this.cb.call(this.vm, this.value, oldValue)
}
}
function parsePath(path) {
const bailRE = /[^\w.$]/;
const segments = path.split(".");
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
if (bailRE.test(segments[i])) {
//this.arr[0] this[arr[0]]
const match = segments[i].match(/(\w+)\[(.+)\]/);
obj = obj[match[1]];
obj = obj[match[2]];
continue;
}
obj = obj[segments[i]];
}
return obj;
};
}
const vm = {
data: {
attr1: {
a: 1,
b: 2,
c: 3,
},
array: [1, 2, 3],
},
};
new ObserverNext('data',vm.data,vm);
new Watcher(vm,'{{attr1,a}}','attr1.a',(val,oldVal)=>{
console.log('依赖更新','@当前值:'+val,'@旧值:'+oldVal);
})
vm.data.attr1.a=2
你再想啊,如果我们面对不同节点然后改变传入watcher的回调,是不是就成了?下一篇文章要讲的解析器就是这么个道理
归档
# 手摸手教你实现一个简单vue(1)响应式原理
# 手摸手教你实现一个简单vue(2)上手编写observer
# 手摸手教你实现一个简单vue(3)上手编写dep和watcher