手摸手教你实现一个简单vue(3)上手编写dep和watcher

1,057 阅读2分钟

哔哔哔

在前文中,我们实现了一个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