[Vue2.0 Source Code Learning] Example Methods - Data-related Methods

0. Preface

There are 3 instance methods related to data, which are vm.$set, , vm.$deleteand vm.$watch. They are stateMixinmounted on the prototype in the function Vue, the code is as follows:

import {
    
     set, del } from "../observer/index";

export function stateMixin(Vue) {
    
    
  Vue.prototype.$set = set;
  Vue.prototype.$delete = del;
  Vue.prototype.$watch = function(expOrFn, cb, options) {
    
    };
}

After the function is executed stateMixin, Vuethe above three instance methods will be mounted on the prototype.

Next, let's analyze the internal principles of these three data-related instance methods.

1. vm.$watch

1.1 Usage review

Before introducing the internal principle of the method, let's review its usage based on the official documentation example.

vm.$watch(expOrFn, callback, [options]);
  • Parameters :

    • {string | Function} expOrFn

    • {Function | Object} callback

    • {Object} [options]
      
      • {boolean} deep
      • {boolean} immediate
  • return value :{Function} unwatch

  • Usage :

    An expression or computed property function that observes Vueinstance changes. The parameters obtained by the callback function are the new value and the old value. Expressions only accept supervised key paths. For more complex expressions, use a function instead.

    NOTE: When mutating (not replacing) an object or array, the old value will be the same as the new value, since their references point to the same object/array. VueA copy of the value before mutation is not kept.

  • Example :

    // 键路径
    vm.$watch("a.b.c", function(newVal, oldVal) {
          
          
      // 做点什么
    });
    
    // 函数
    vm.$watch(
      function() {
          
          
        // 表达式 `this.a + this.b` 每次得出一个不同的结果时
        // 处理函数都会被调用。
        // 这就像监听一个未被定义的计算属性
        return this.a + this.b;
      },
      function(newVal, oldVal) {
          
          
        // 做点什么
      }
    );
    

    vm.$watchReturns a cancel observer function to stop firing callbacks:

    var unwatch = vm.$watch("a", cb);
    // 之后取消观察
    unwatch();
    
  • option: deep

    In order to discover changes in the object's internal values, it can be specified in the options parameter deep: true. Note that monitoring array changes does not require this.

    vm.$watch("someObject", callback, {
          
          
      deep: true
    });
    vm.someObject.nestedValue = 123;
    // callback is fired
    
  • Option: immediate

    Specifying in the options argument immediate: truewill trigger the callback immediately with the current value of the expression:

    vm.$watch("a", callback, {
          
          
      immediate: true
    });
    // 立即以 `a` 的当前值触发回调
    

    Note that with immediatethe option, you cannot unlisten the given property on the first callback.

    // 这会导致报错
    var unwatch = vm.$watch(
      "value",
      function() {
          
          
        doSomething();
        unwatch();
      },
      {
          
           immediate: true }
    );
    

    If you still want to call an unlisten function inside the callback, you should check the availability of its function first:

    var unwatch = vm.$watch(
      "value",
      function() {
          
          
        doSomething();
        if (unwatch) {
          
          
          unwatch();
        }
      },
      {
          
           immediate: true }
    );
    

1.2 Internal Principles

$watchThe definition of is located in the source code src/core/instance/state.js, as follows:

Vue.prototype.$watch = function(expOrFn, cb, options) {
    
    
  const vm: Component = this;
  if (isPlainObject(cb)) {
    
    
    return createWatcher(vm, expOrFn, cb, options);
  }
  options = options || {
    
    };
  options.user = true;
  const watcher = new Watcher(vm, expOrFn, cb, options);
  if (options.immediate) {
    
    
    cb.call(vm, watcher.value);
  }
  return function unwatchFn() {
    
    
    watcher.teardown();
  };
};

It can be seen that $watchthe code of the method is not much, and the logic is not very complicated.

Inside the function, first determine whether the incoming callback function is an object, like the following form:

vm.$watch("a.b.c", {
    
    
  handler: function(val, oldVal) {
    
    
    /* ... */
  },
  deep: true
});

If the callback function passed in is an object, it means that the user passed in the second parameter callback function cband the third parameter option optionstogether. At this time, createWatcherthe function is called. The function is defined as follows:

function createWatcher(vm, expOrFn, handler, options) {
    
    
  if (isPlainObject(handler)) {
    
    
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === "string") {
    
    
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options);
}

cbIt can be seen that inside the function, the callback function and parameters are stripped from the object passed in by the user options, and then the method is called in a conventional way $watchand the stripped parameters are inserted.

Then get the value passed in by the user options. If the user does not pass it in, it will be assigned a default empty object, as follows:

options = options || {
    
    };

$watchAn instance will be created inside the method watcher. Since the instance is $watchcreated by the user manually calling the method, attributes are optionsadded and assigned a value to distinguish the instance created by the user from the instance created internally , as follows:usertruewatcherVuewatcher

options.user = true;

Next, pass in the parameters to create an watcherinstance, as follows:

const watcher = new Watcher(vm, expOrFn, cb, options);

Then judge if the user optionsspecifies in the option immediateparameter true, then immediately trigger the callback with the current value of the observed data, as follows:

if (options.immediate) {
    
    
  cb.call(vm, watcher.value);
}

Finally, a cancel observation function is returned unwatchFnto stop triggering the callback. as follows:

return function unwatchFn() {
    
    
  watcher.teardown();
};

This cancel observation function unwatchFnactually calls the method watcherof the instance teardown, so let's take a look at teardownhow this method is implemented. Its code is as follows:

export default class Watcher {
    
    
  constructor(/* ... */) {
    
    
    // ...
    this.deps = [];
  }
  teardown() {
    
    
    let i = this.deps.length;
    while (i--) {
    
    
      this.deps[i].removeSub(this);
    }
  }
}

In the previous article introducing change detection, we said that whoever reads the data means that whoever relies on the data, then who will exist in the dependency list of the data, and will be notified when the data changes who. In other words, if anyone does not want to rely on this data, then just delete whoever is from the dependency list of this data.

In the above code, watcherwhen an instance is created, the observed data will be read. Reading the data means that it depends on the data, so the watcherinstance will exist in the data dependency list, and watcherthe instance also records which data it depends on. In addition, we also said that each data has its own dependency manager dep. watcherThe instance records which data it depends on. In fact, it depstores the dependency manager of these data in the properties watcherof the instance this.deps = []. When the observation is cancelled, watcherthe instance does not want to rely on these data, then traverse the dependency manager of these data recorded by yourself, and tell these data to delete me from your dependency list.

for example:

vm.$watch(
  function() {
    
    
    return this.a + this.b;
  },
  function(newVal, oldVal) {
    
    
    // 做点什么
  }
);

For example, in the above watcherexample, it observes data aand data b, then it depends on data aand data b, then this watcherinstance exists in the dependency manager of data and data a, and these two dependency managers are also recorded in the properties of the instance , that is ,bdepAdepBwatcherdepsthis.deps=[depA,depB]

When canceling the observation, it traverses this.deps, and let each dependency manager call its removeSubmethod to watcherdelete this instance from its own dependency list.

There is one last question below, when the attribute optionsin the option parameter isdeep , how to achieve in-depth observation?true

First of all, let's take a look at what is in-depth observation. If there is the following observed data:

obj = {
    
    
  a: 2
};

The so-called in-depth observation means that objwe will be notified when the object changes, and obj.awe should also be notified when the property changes. Simply put, it is to observe the change of the internal value of the object.

It is not difficult to realize this function. We know that if we want to notify us when the data changes, then we only need to become a dependency of this data, because when the data changes, all its dependencies will be notified, so how to become a data dependency? , very simple, just read the data. That is to say, we only need to recursively read all the values ​​inside the object watcherwhen creating an instance , then this instance will be added to the dependency list of all values ​​in the object, and then when any value in the object changes when you will be notified.objwatcher

After having a preliminary idea, let's see how it is implemented in the code. We know that watcherwhen an instance is created, the method Watcherin the class will be executed getto read the observed data, as follows:

export default class Watcher {
    
    
  constructor(/* ... */) {
    
    
    // ...
    this.value = this.get();
  }
  get() {
    
    
    // ...
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
    
    
      traverse(value);
    }
    return value;
  }
}

It can be seen that in getthe method, if the input deepis true, the function will be called traverse, and in the source code, there is a very vivid comment on this step:

"touch" every property so they are all tracked as dependencies for deep watching

“触摸”每个属性,以便将它们全部作为深度监视的依赖项进行跟踪

The so-called "touching" each attribute means reading each attribute once? Ha ha

Back to the code, traversethe function definition is as follows:

const seenObjects = new Set();

export function traverse(val: any) {
    
    
  _traverse(val, seenObjects);
  seenObjects.clear();
}

function _traverse(val: any, seen: SimpleSet) {
    
    
  let i, keys;
  const isA = Array.isArray(val);
  if (
    (!isA && !isObject(val)) ||
    Object.isFrozen(val) ||
    val instanceof VNode
  ) {
    
    
    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);
    i = keys.length;
    while (i--) _traverse(val[keys[i]], seen);
  }
}

It can be seen that this function is actually a recursive traversal process, recursively traversing and reading all the internal values ​​​​of the observed data.

First judge the incoming valtype, if it is not Arrayor object, or has been frozen, then return directly and exit the program. as follows:

const isA = Array.isArray(val);
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    
    
  return;
}

Then get it valand dep.idstore it in the created collection seen, because the collection has a natural deduplication effect compared to the data, so as to ensure that dep.idthere is no duplication of the stored data, and it will not cause repeated collection dependencies, as follows:

if (val.__ob__) {
    
    
  const depId = val.__ob__.dep.id;
  if (seen.has(depId)) {
    
    
    return;
  }
  seen.add(depId);
}

Next, judge if it is an array, loop the array, and recursively call each item in the array _traverse; if it is an object, take out all the objects key, then perform a read operation, and then recurse the internal value, as follows:

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

In this way, after recursively reading all the values ​​in the observed data, this watcherinstance will be added to the dependency list of all values ​​in the object, and then you will be notified when any value in the object changes up.

2. vm.$set

vm.$setis an aliasVue.set for the global and is used in the same way.

2.1 Usage review

Before introducing the internal principle of the method, let's review its usage based on the official documentation example.

vm.$set(target, propertyName / index, value);
  • Parameters :

    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value
  • Return value : the set value.

  • Usage :

    Add a property to the reactive object and make sure the new property is also reactive and triggers a view update. It must be used to add new properties to reactive objects, since Vuethere is no way to detect normal new properties (eg this.myObject.newProperty = 'hi')

  • Note : The object cannot be Vuean instance, or Vuethe root data object of an instance.

2.2 Internal Principles

Remember when we introduced data change detection, we said that for objecttype data, when we objectadd a new pair to the data key/valueor delete a pair of existing ones key/value, Vueit is impossible to observe; and for Arraytype data, when we When the data in the array is modified through the array subscript, Vueit is also unobservable;

It is precisely because of this problem that these two methods Vueare designed to solve this problem. Let's take a look at the internal implementation principle of the method first.setdeleteset

setThe definition of the method is located in the source code src/core/observer/index.js, as follows:

export function set(target, key, val) {
    
    
  if (
    process.env.NODE_ENV !== "production" &&
    (isUndef(target) || isPrimitive(target))
  ) {
    
    
    warn(
      `Cannot set reactive property on undefined, null, or primitive value: ${
      
      (target: any)}`
    );
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    
    
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
  if (key in target && !(key in Object.prototype)) {
    
    
    target[key] = val;
    return val;
  }
  const ob = (target: any).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    
    
    process.env.NODE_ENV !== "production" &&
      warn(
        "Avoid adding reactive properties to a Vue instance or its root $data " +
          "at runtime - declare it upfront in the data option."
      );
    return val;
  }
  if (!ob) {
    
    
    target[key] = val;
    return val;
  }
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}

It can be seen that the logic inside the method is not complicated, it just makes different processing according to different situations.

Firstly, judge whether the incoming type targetis undefinedor nullis the original type in a non-production environment, and if so, throw a warning, as follows:

if (
  process.env.NODE_ENV !== "production" &&
  (isUndef(target) || isPrimitive(target))
) {
    
    
  warn(
    `Cannot set reactive property on undefined, null, or primitive value: ${
      
      (target: any)}`
  );
}

Then judge if the input targetis an array and the input keyis a valid index, then take the current array length and keythe maximum value of the two as the new length of the array, and then use the array method to add the value corresponding to splicethe input index into the array. Pay attention here, why use the method? Remember when we introduced the change detection method of type data, we said that the method of the array has been rewritten by the interceptor we created, that is to say, when the method is used to add an element to the array, the element will be automatically changed into responsive. as follows:keyvalspliceArraysplicesplice

if (Array.isArray(target) && isValidArrayIndex(key)) {
    
    
  target.length = Math.max(target.length, key);
  target.splice(key, 1, val);
  return val;
}

If the input targetis not an array, it is treated as an object.

First, determine whether the incoming attribute keyalready exists target. If it exists, it indicates that this operation is not a new attribute, but a simple modification of the existing attribute value. Then only modify the attribute value, as follows:

if (key in target && !(key in Object.prototype)) {
    
    
  target[key] = val;
  return val;
}

The attribute obtained next target, __ob__as we said, whether the attribute is truea sign of targetwhether it is a responsive object, and then judges if it tragteis Vuean instance, or Vuethe root data object of an instance, then throws a warning and exits the program, as follows:

const ob = (target: any).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
    
    
  process.env.NODE_ENV !== "production" &&
    warn(
      "Avoid adding reactive properties to a Vue instance or its root $data " +
        "at runtime - declare it upfront in the data option."
    );
  return val;
}

Then judge that if obthe property is false, then it targetis not a responsive object, then we only need to simply add new properties to it, without converting the new properties into responsive, as follows:

if (!ob) {
    
    
  target[key] = val;
  return val;
}

Finally, if targetit is an object and it is responsive, then call defineReactivethe method to add the new attribute value to targetit, defineReactivethen after the new attribute is added, it will be converted into a responsive type, and finally the dependency update will be notified, as follows:

defineReactive(ob.value, key, val);
ob.dep.notify();

The above is setthe internal principle of the method. Its logic flow chart is as follows:

insert image description here

3. vm.$delete

vm.$deleteis an aliasVue.delete for the global and its usage is the same.

3.1 Usage review

Before introducing the internal principle of the method, let's review its usage based on the official documentation example.

vm.$delete(target, propertyName / index);
  • Parameters :

    • {Object | Array} target
    • {string | number} propertyName/index

    Array + index usage is only supported in version 2.2.0+.

  • Usage :

    Delete an attribute of an object. If the object is reactive, make sure deletion triggers an update of the view. This method is mainly used to avoid Vuethe limitation of not being able to detect that the attribute is deleted, but you should rarely use it.

    Working with arrays is also supported in 2.2.0+.

  • Note : The target object cannot be an Vueinstance or Vuethe root data object of an instance.

3.2 Internal Principles

deleteThe method is used to solve Vuethe limitation of not being able to detect that the attribute is deleted. The definition of this method is located in the source code src/core.observer/index.js, as follows:

export function del(target, key) {
    
    
  if (
    process.env.NODE_ENV !== "production" &&
    (isUndef(target) || isPrimitive(target))
  ) {
    
    
    warn(
      `Cannot delete reactive property on undefined, null, or primitive value: ${
      
      (target: any)}`
    );
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    
    
    target.splice(key, 1);
    return;
  }
  const ob = (target: any).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    
    
    process.env.NODE_ENV !== "production" &&
      warn(
        "Avoid deleting properties on a Vue instance or its root $data " +
          "- just set it to null."
      );
    return;
  }
  if (!hasOwn(target, key)) {
    
    
    return;
  }
  delete target[key];
  if (!ob) {
    
    
    return;
  }
  ob.dep.notify();
}

The internal principle of this method setis somewhat similar to the method, and different treatments are made according to different situations.

targetFirst judge that if the input does not exist or targetis an original value in a non-production environment , a warning will be thrown, as follows:

if (
  process.env.NODE_ENV !== "production" &&
  (isUndef(target) || isPrimitive(target))
) {
    
    
  warn(
    `Cannot set reactive property on undefined, null, or primitive value: ${
      
      (target: any)}`
  );
}

Then judge that if the input targetis an array and the input keyis a valid index, use the splicemethod of the array to keydelete the value corresponding to the index. Why use splicethe method is also explained above, because splicethe method of the array has been created by us The interceptor is rewritten, so using this method will automatically notify the relevant dependencies. as follows:

if (Array.isArray(target) && isValidArrayIndex(key)) {
    
    
  target.splice(key, 1);
  return;
}

If the input targetis not an array, it is treated as an object.

The attribute obtained next target, __ob__as we said, whether the attribute is truea sign of targetwhether it is a responsive object, and then judges if it tragteis Vuean instance, or Vuethe root data object of an instance, then throws a warning and exits the program, as follows:

const ob = (target: any).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
    
    
  process.env.NODE_ENV !== "production" &&
    warn(
      "Avoid adding reactive properties to a Vue instance or its root $data " +
        "at runtime - declare it upfront in the data option."
    );
  return val;
}

Then judge whether the input keyexists targetin , if keyit does not exist targetin , then you don’t need to delete it, just exit the program directly, as follows:

if (!hasOwn(target, key)) {
    
    
  return;
}

Finally, if targetit is an object, and the incoming one keyalso exists in targetit, then delete the attribute targetfrom it , and at the same time judge targetwhether the current one is a responsive object, if it is a responsive object, notify the dependency update; if not, delete it Then return directly without notification update, as follows:

delete target[key];
if (!ob) {
    
    
  return;
}
ob.dep.notify();

The above is deletethe internal principle of the method.

Guess you like

Origin blog.csdn.net/weixin_46862327/article/details/131407068