This is the fourth day of my participation in Gwen Challenge.

preface

Vue. Js also provides $set and $delete methods because Object and Array change detection has some drawbacks. In this article, we will take a closer look at the implementation of $watch, $set, and $delete.

$watch

usage

Observe a change in the evaluation result of an expression or function on a Vue instance. The callback takes new and old values. The expression accepts only simple key paths. For more complex expressions, replace them with a function.

vm.$watch(expOrFn, callback, [options]);
Copy the code
vm.$watch('a.b.c'.function(newVal, oldVal){
/ /...
});
Copy the code

Options: deep

To detect changes in values within an object, specify deep: true in the option argument. Note that listening for array changes does not need to do this.

vm.$watch('someObject', callback, {
  deep: true
})
vm.someObject.nestedValue = 123
// callback is fired
Copy the code

Options: immediate

Specifying immediate: true in the option argument triggers the callback immediately with the current value of the expression

$watch('a', callback, {immediate: true}) // Trigger the callback immediately with the current value of 'a'Copy the code

Realize the principle of

Vm.$watch is essentially a wrapper around Watcher.

Vue.prototype.$watch = function(expOrFn, cb, options){
    const vm = this;
    options = options || {};
    const watcher = new Watcher(vm, expOrFn, cn, options);
    if (options.immediate){
        cb.call(vm, watcher.value)
    }
    return function unwatchFn() { watcher.teardown(); }}Copy the code

ExpOrFn, which we have seen previously, can be a url shaped like ‘A.B.C’ keyPath, or it can be a function. Executing $watch returns an unwatch function that calls watcher.teardown() internally.

This method is to cancel listening, which has not been implemented before. To actually cancel listening, you remove the Watcher from the DEP. So how do we do that?

export default class Watcher {
    constructor(vm, expOrFn, cb){
        this.vm = vm;
        this.deps = []; / / new
        this.depIds = new Set(a);/ / new
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
        }
        this.cb = cb;
        this.value = this.get();
    }
    
    addDep(dep) {
        const id = dep.id;
        if (!this.depIds.has(id)){
            this.depIds.add(id);
            this.deps.push(dep);
            dep.addSub(this)}}}Copy the code

We added a list of DEPs to the Watcher to keep track of which DEPs collected the Watcher. We use depIds to determine that if the current Watcher is already subscribed to the Dep, the subscription will not be repeated. This.ddepids. add records that the current Watcher has subscribed to Dep.

With the addDep method added by Watcher, the logic for collecting dependencies in Dep also needs to change:

let uid = 0;
export default class Dep {
  constructor() {
    this.id = uid++;
    this.subs = [];
  }

  depend() {
    if (window.target) {
      window.target.addDep(this); }}}Copy the code

The Dep records which Watchers need to be notified, and the Watcher also records which DEPs it will be notified by. Watcher and Dep, they’re many-to-many.

Why is it many-to-many?

If the expOrFn parameter in Watcher is an expression, then only one Dep is recorded. However, if expOrFn is a function that uses multiple data internally, then Watcher will record multiple DEPs.

this.$watch(function(){
    return this.surname + this.firstName
})
Copy the code

Now we can implement teardown:

teardown(){
    let i = this.deps.length;
    while(i--){
        this.deps[i].removeSub(this)}}Copy the code

The implementation of deep

export default class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm;
    if (options) {
      this.deep = !! options.deep; }else {
      this.deep = false;
    }

    this.deps = [];
    this.depIds = new Set(a);this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get();
  }

  get() {
    window.target = this;
    let value = this.getter.call(this.vm, this.vm);
    if (this.deep) {
      traverse(value);
    }
    window.target = undefined;
    returnvalue; }}Copy the code

We need to use all the children of the traverse recursive value to trigger their collection dependencies before window.target = undefined.

const seenObjects = new Set(a);export function traverse(val) {
  _traverse(val, seenObjects);
  seenObjects.clear();
}

function _traverse(val, seen) {
  let i, keys;
  const isA = Array.isArray(val);
  if((! isA && isObject(val)) ||Object.isFrozen(val)) {
    return;
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id;
    if (seen.has(depId)) {
      return;
    }
    seen.add(depId);
  }

  if (isA) {
    i = val.length;
    while(i--) { __traverse(val[i], seen); }}else {
    keys = Object.keys(val);
    while(i--) { __traverse(val[keys[i]], seen); }}}Copy the code

$set

usage

vm.$set(target, key, value);
Copy the code

As we’ve learned before, only changes to existing attributes are tracked, not new attributes.

Vm.$set is designed to solve this problem. It can also convert newly added attributes into reactive ones.

Realize the principle of

Array processing

function set(target, key, val) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    returnval; }}Copy the code

The Object of processing

1. The key already exists
function set(target, key, val) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
  / / new
  if (key intarget && ! (keyin Object.prototype)) {
    target[key] = val;
    returnval; }}Copy the code
2. Key is new
function set(target, key, val) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }

  if (key intarget && ! (keyin Object.prototype)) {
    target[key] = val;
    return val;
  }
  / / new
  const ob = target.__ob__;
  if (target.__isVue ||(ob && ob.vmCount)) return val;
  if(! ob) { target[key] = val;return val;
  }
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}
Copy the code

First, we try to get the __ob__ attribute of value.

If it does not exist, it is a normal object. If it does, it indicates that it is reactive and needs to call defineReative to convert the new property into a getter/setter.

Finally, target’s dependencies are notified.

$delete

usage

vm.$delete(target, key)
Copy the code

Realize the principle of

Array processing

export function del(target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    returnval; }}Copy the code

The Object of processing

1. The key to exist
export function del(target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return val;
  }
  / / new
  const ob = target.__ob__;
  if (target.__isVue ||(ob && ob.vmCount)) return;
  delete target[key];
  ob.dep.notify();
}
Copy the code
2. The key does not exist
export function del(target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return val;
  }
  const ob = target.__ob__;
  / / new
  if(! hasOwn(target.key)) {return;
  }
  delete target[key];
  ob.dep.notify();
}
Copy the code
3. Not a responsive object
export function del(target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return val;
  }
  const ob = target.__ob__;
  if(! hasOwn(target.key)) {return;
  }
  delete target[key];
  / / new
  if(! ob)return;
  ob.dep.notify();
}
Copy the code

conclusion

$watch/$set/$deleteHow is it done

$watch is a wrapper around the form of the Watcher call. At the same time, we also know that Watcher and Dep are many-to-many.

$set = Array (); $delete = Array (); $delete = Array (); $set = Array (); $delete = Array ();