Hand to hand teaching you to achieve a simple vue, start writing dep and watcher

Beep, beep, beep

In the previous article, we implemented an Observer. In this section, we will talk about the implementation of dep and watcher. As before, at the end of the article, I wrote a simple beta version of js for you to read the text of this section while testing.

Let's talk about dep first

dep is our dependency collector. You will know from the above observer that observer observes all data in an object array. Therefore, dep also manages multiple dependencies corresponding to all data in the object array observed in observer
To be honest, it's a little windy, so let's start directly

establish

class depNext {
  subs: Map<string, Array<Watcher>>;
  constructor() {
    this.subs = new Map();
  }

  addSub(prop, target) {
     ...
  }
  // Add a dependency
  depend(prop) {
   ...
  }
  // Notify all dependent updates
  notify(prop) {
   ...
  }
}

If we want to manage all the dependencies corresponding to all the data in the object, we must first have an appropriate data structure. Map is very appropriate, for example

//After observer makes obj responsive
let obj={
  a:1
  b:2
  c:3
}
//The map in dep looks like this. Here is pseudo code
let map={
  a:[wathcer1,watcher2.....]
  b:[wathcer3,watcher4.....]
  c:[wathcer5,watcher6.....]
}

So how to build such a Map? We write our addSub logic
Let's first get whether prop:[watcher1.....] has been created in the Map Such a data structure, (hereinafter we call this prop:[watcher1.....], If it is a mapping array), it will be created directly, as follows:

  addSub(prop, target) {
    const sub = this.subs.get(prop);
    if (!sub) {
      this.subs.set(prop, [target]);
      return;
    }
    sub.push(target);
  }

When the data is got, dep.depend () will be called. The logic of depend is also very simple. Check whether the global variable tar get is assigned dependency. If so, ask addSub to add dependency according to prop.

  depend(prop) {
    if (window.target) {
      this.addSub(prop, window.target);
    }
  }

Finally, only notify is left. The logic of notify is to notify all dependencies in the mapping array of prop to complete the update.

  notify(prop) {
    const watchers = this.subs.get(prop);
    if(!watchers)return;
    for (let i = 0, l = watchers.length; i < l; i++) {
      watchers[i].update();
    }
  }
}

The dep after construction is as long as this

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);
  }
  // Add a dependency
  depend(prop) {
    if (window.target) {
      this.addSub(prop, window.target);
    }
  }
  // Notify all dependent updates
  notify(prop) {
    const watchers = this.subs.get(prop);
    if(!watchers)return;
    for (let i = 0, l = watchers.length; i < l; i++) {
      watchers[i].update();
    }
  }
}

And watcher

The watcher is our "dependency". After the data is updated, the observer will notify dep to update the dependency of relevant data, and the dependency will execute the callback function on it to update the view.
Start with the skeleton:

class Watcher {
    vm:VM
    cb:Function;
    getter:any;
    value:any;
    
    constructor (vm,initVal,expOrFn,cb) {
      this.vm = vm;     //vue instance
      this.cb = cb;     //Callback to execute
      if(isType(expOrFn,'String'))this.getter = parsePath(expOrFn)//Never mind what parsePath is
      else if(isType(expOrFn,'Function'))this.getter=expOrFn
      this.value = this.get() //Collection dependency
      this.value=initVal      //Set initial value
    }
    get () {
        //... Collection dependency
    }
    update () {
       //... Dependency update
    }
  }

Let's start with core dependency collection

We talked about dep earlier, collecting dependencies in the depend method

depend(prop) {
    if (window.target) {
      this.addSub(prop, window.target);
    }
  }

Contact the above again. When the data is obtained, the observer will notify dep to execute the dependent collection dependency,
Then the get method of our watcher is ready to come out!

    get () {
      window.target = this;
      let value = this.getter(this.vm.$data)
      window.target = undefined;
      return value
    }

We first assign the current dependency to the global target, and then obtain the data related to the dependency. At this time, observer will execute the subsequent dependency collection process.
Then, you must have doubts. What is getter?

parsePath method

In constructor

if(isType(expOrFn,'String'))this.getter = parsePath(expOrFn)
else if(isType(expOrFn,'Function'))this.getter=expOrFn

If the getter itself is a function to obtain data, it will be assigned directly. If it is a string shaped like "obj.a", then use the parsePath method to turn it into a method to obtain obj:{a:'xxxx'} data in the $data option

To pave the way for the later explanation of the text parser, I will now give a more detailed example,
When the complier parses the text node "Xiao Ming's age is {{obj.a}}", it will generate a watcher. At this time, the expOrFn of the parameter is 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;
  };

The parsePath function is simple. It will return a function. This function will take the data from the parameter obj in the order of a.b.c in expOrFn. At this time, observer will execute the subsequent dependency collection process!

Finally, let's talk about dependency update

It's easier to trigger a callback~

update () {
      const oldValue = this.value
      this.value = this.getter(this.vm.$data)
      this.cb.call(this.vm, this.value, oldValue)
    }

Final results

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") {
        //Judge arrays and objects at the same time
        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);
  }
  // Add a dependency
  depend(prop) {
    if (target) {
      this.addSub(prop, target);
    }
  }
  // Notify all dependent updates
  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() //Collection dependency
    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() / / do not trigger the getter when updating, otherwise the dependency will be collected
    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('Dependency update','@Current value:'+val,'@Old value:'+oldVal);
})
vm.data.attr1.a=2

Think again. If we face different nodes and change the callback of the incoming watcher, will it be? This is the reason for the parser to be discussed in the next article

file

#Touch hands to teach you to realize a simple vue (1) responsive principle
#Touch hands to teach you to implement a simple vue (2) start writing observer
#Touch hands to teach you to implement a simple vue (3) start writing dep and watcher

Keywords: Vue

Added by designationlocutus on Thu, 03 Feb 2022 06:11:10 +0200