When we use Vue, we just need to modify the data, the view will automatically update, this is the data response today to learn Vue implementation of data response principle ~

Source link gitee.com/ykang2020/v…

Object attribute – Attribute Type – Data attribute – Accessor attribute -Object.defineProperty() method -get method -set method

With getter and setter methods you can listen for data, access and Settings are captured by the listener and the getter is triggered when you read the data, and the setter is triggered when you modify the data

1. Define defineReactive

1.1 Why (temporary variable)

When we want to do data hijacking, the first thing to think about is adding getters and setters to properties in Object.defineProperty(), but there are problems with that

The defineProperty() method requires temporary global variable turnaround getters and setters

Let’s look at the following example

let obj = {};
let temp;

Object.defineProperty(obj, "a", {
  get() {
    console.log("Getter attempts to access a property of OBj.");
    return temp;
  },
  set(newValue) {
    console.log("Setter is trying to change the a property of obj.", newValue); temp = newValue; }});console.log(obj.a); 
obj.a = 5
console.log(obj.a);
Copy the code

1.2 How (Closure)

So we define a function to encapsulate defineProperty to achieve data hijacking

You don’t need to set temporary variables to use defineReactive, you use closures instead

function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    // Enumeration can be for-in
    enumerable: true.// Can be configured, such as delete
    configurable: true.// getter
    get() {
      console.log(Getter attempts to access obj's${key}Attribute `);
      return value;
    },
    // setter
    set(newValue) {
      console.log(The setter is trying to change obj's${key}Attribute `, newValue);
      if (value === newValue) return; value = newValue; }}); }let obj = {};
/ / initialization
defineReactive(obj, "a".10);
console.log(obj.a);

obj.a = 5;
console.log(obj.a);
Copy the code

2. Responsive processing of objects — Recursively detect all properties of objects

2.1 Why (nested)

The defineProperty() function defined above cannot listen for nested objects

That is, objects nested objects

function defineReactive(data, key, value) {
  if (arguments.length === 2) {
    value = data[key];
  }
  Object.defineProperty(data, key, {
    // Enumeration can be for-in
    enumerable: true.// Can be configured, such as delete
    configurable: true.// getter
    get() {
      console.log(Getter attempts to access obj's${key}Attribute `);
      return value;
    },
    // setter
    set(newValue) {
      console.log(The setter is trying to change obj's${key}Attribute `, newValue);
      if (value === newValue) return; value = newValue; }}); }let obj = {
  b: {
    c: {
      d: 4,}}};/ / initialization
defineReactive(obj, "b");
console.log(obj.b.c.d);
Copy the code

This shows no listening inside (obj.b.c.D)

2.2 How (Recursion)

So we create an Observer class — > to convert a normal object into an object whose properties at each level are responsive (detectable)

Traverse object

observe.js

If the value is already responsive data, you do not need to create an Observer instance. Instead, you can directly return the created Observer instance to avoid repeated value detection

import Observer from "./Observer";
/** * listen for value *@param {*} value 
 * @returns * /
export default function observe(value) {
  // If value is not an object, do nothing
  if (typeofvalue ! ="object") return;
  let ob;
  if (typeofvalue.__ob__ ! = ="undefined") {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
}
Copy the code

Observer.js

Convert a normal object to an object whose properties at each level are responsive (detectable)

The Observer class is attached to each detected object. Once attached, the Observer converts all properties of the object into getters/setters

The function of __ob__ can be used to indicate whether the current value has been converted to responsive data by the Observer. An instance of the Observer can also be accessed via value.__ob__

import def from "./def";
import defineReactive from "./defineReactive";
/** * Convert a normal object to an object whose attributes at each level are responsive (detectable)
export default class Observer {
  / / the constructor
  constructor(value) {
    // Add an __ob__ attribute to the instance. The value is an instance of the current Observer, which is not enumerable
    def(value, "__ob__".this.false);
    
    console.log("Observer constructor", value);
    
    // Convert a normal object to an object whose properties at each level are responsive (detectable)
    this.walk(value);
  }
  // Iterate over each key of value
  walk(value) {
    for (let key invalue) { defineReactive(value, key); }}}Copy the code

def.js

The utility method defines an object property

/** * defines an object attribute *@param {*} obj 
 * @param {*} key 
 * @param {*} value 
 * @param {*} enumerable 
 */
export default function def(obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true.configurable: true}); }Copy the code

defineReactive.js

Define a listener for the key property of the object data

import observe from "./observe";

/** * define a listener for the key of the object data@param {*} Data Incoming data *@param {*} Key Listens to the property *@param {*} The turnover variable */ provided by the value closure environment
export default function defineReactive(data, key, value) {
  console.log('defineReactive()', data,key,value)
  if (arguments.length === 2) {
    value = data[key];
  }
  
  // The child element must observe to recurse
  let childOb = observe(value)
  
  Object.defineProperty(data, key, {
    // Enumeration can be for-in
    enumerable: true.// Can be configured, such as delete
    configurable: true.// getter
    get() {
      console.log('Getter attempts to access${key}Attribute `);
      return value;
    },
    // setter
    set(newValue) {
      console.log(The setter is trying to change${key}Attribute `, newValue);
      if (value === newValue) return;
      value = newValue;
      
      // When a new value is set, the new value must also be observed
      childOb = observe(newValue)
    },
  });
}

Copy the code

Dependency structure between filesThe three functions call each other to form recursion

So, what if we nested an array, and we’re going to look at arrays

2.3 test

index.js

import observe from "./observe";

let obj = {
  a: 1.b: {
    c: {
      d: 4,}}}; observe(obj); obj.a =5;
obj.b.c.d = 10;
Copy the code

3. Reactive processing of arrays

Because we can change the contents of an Array using the methods on the Array prototype, ojbect’s getter/setter approach doesn’t work.

ES6 did not previously provide the ability to intercept prototype methods, so we could override native prototype methods with custom methods.

Vue is responsive processing of an array by overwriting its seven methods (methods that change the contents of the array itself)

These methods are:push,pop,shift,unshift,splice,sort,reverse

These seven methods are all defined in Array.prototype to preserve method functionality while adding data hijacking code

The idea is to prototype array. prototype, create a new object arrayMthods and then define (rewrite) these methods on the new object arrayMthods that define the Array prototype pointing to arrayMthods

This is equivalent to overwriting array. prototype with an interceptor. Whenever a method on the Array prototype is used to manipulate an Array, the method provided in the interceptor is actually executed. Use the prototypical methods of native arrays in interceptors to manipulate arrays.

3.1 Prior knowledge

Object.setPrototypeOf()Modify object prototype

The object.setProtoTypeof () method sets the Prototype of a specified Object (that is, the internal [[Prototype]] property) to another Object or null

Object.setPrototypeOf(obj, prototype)
Copy the code

Prototype Specifies the new prototype of the object (an object or null).

Object.create()Create a new object

The object.create () method creates a new Object, using an existing Object to provide the __proto__ of the newly created Object

ObjectThe create (proto, [propertiesObject])Copy the code

Proto The prototype object of the newly created object. PropertiesObject optional. You need to pass in an Object whose property type refers to the second parameter of Object.defineProperties(). If this parameter is specified and not undefined, the incoming object’s own enumerable properties (that is, properties defined by itself, rather than enumerated properties in its stereotype chain) will add the specified property value and corresponding property descriptor to the newly created object.

Returns a new object with the specified prototype object and properties.

3.2 implementation

array.js

import def from "./def";

const arrayPrototype = Array.prototype;

// Create an arrayMethod based on array. prototype
export const arrayMethods = Object.create(arrayPrototype);

// The 7 array methods to be overwritten
const methodsNeedChange = [
  "push"."pop"."shift"."unshift"."splice"."sort"."reverse",];// Execute these methods in batches
methodsNeedChange.forEach((methodName) = > {
  // Back up the original method
  const original = arrayPrototype[methodName];

  // Define a new method
  def(
    arrayMethods,
    methodName,
    function () {
      console.log("Array data has been hijacked.");

      // Restore the original function (array method)
      const result = original.apply(this.arguments);
      // Turn an array-like object into an array
      const args = [...arguments];

      // Remove __ob__ from this array
      // Get the Observer instance in the interceptor
      const ob = this.__ob__;

      // There are three methods: push, unshift, splice to insert new items, to hijack (detect) these data (insert new items)
      let inserted = [];
      switch (methodName) {
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }

      // Check to see if there is a new inserted item
      if (inserted) {
        ob.observeArray(inserted);
      }

      return result;
    },
    false
  );
});
Copy the code

Observer.js

The function of __ob__ can be used to indicate whether the current value has been converted to responsive data by the Observer and to access an Observer instance

import def from "./def";
import defineReactive from "./defineReactive";
import observe from "./observe";
import {arrayMethods} from './array'
/** * An Object * Observer class that converts a normal object to one whose properties at each level are responsive is attached to each detected object. The Observer collects property dependencies by converting all properties of an object into getters/setters * and notifies those dependencies when properties change */
export default class Observer {
  / / the constructor
  constructor(value) {
  
    // Add an __ob__ attribute to the instance. The value is an instance of the current Observer, which is not enumerable
    def(value, "__ob__".this.false);
    // The function of __ob__ can be used to indicate whether the current value has been converted to responsive data by the Observer; An instance of the Observer can also be accessed via value.__ob__
    
    // console.log("Observer constructor ", value);
    // Check whether it is an array or an object
    if (Array.isArray(value)) {
      // arrayMethods is an array, and the prototype of that array points to arrayMethods
      Object.setPrototypeOf(value, arrayMethods);
      // The early implementation looked like this
      // value.__proto__ = arrayMethods;
      
      / / observe the array
      this.observeArray(value);
    } else {
      this.walk(value); }}// The object traverses each key of the value
  walk(value) {
    for (let key invalue) { defineReactive(value, key); }}// Array traversal
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      // Observe item by itemobserve(arr[i]); }}}Copy the code

4. Collect dependencies

4.1 Why collect dependencies

The purpose of hijacking data is to notify those where it was used when its properties changed.

Collect dependencies first, collect the data used in the place, such as property changes, in the previous collection of dependencies trigger a cycle again

So the summary is that objects collect dependencies in the getter, trigger dependencies in the setter and arrays collect dependencies in the getter, trigger dependencies in the interceptor

4.2 How Do I Collect Dependencies

Where does the dependency collection go? Dep

The goal is clear, we’re going to collect dependencies in the getter, so where do we collect dependencies?

Encapsulate dependency collection into a class called Dep. This class helps us manage dependencies by collecting dependencies, removing dependencies, and sending notifications to dependencies

What is dependency? Watcher

Where data is needed, called a dependency, is Watcher

Only getters triggered by the Watcher collect dependencies, and whichever Watcher fires the getter is collected into the Dep

Dep uses a publisk-subscribe model, which loops through the list to notify all watchers when data changes.

Watcher is a mediator role that notifies it when data changes, and then notifies the rest of the world

4.3 implementation

The nifty part of the code implementation: Watcher sets itself to a specified location globally and then reads the data. Because the data is read, the getter for that data is triggered. In the getter, you get the Watcher that is currently reading, and you collect that Watcher into the Dep.

The watcher instance needs to subscribe to a dependency (data), that is, get a dependency or collect a dependency on watcher. When a dependency on Watcher occurs, watcher’s callback function is triggered, that is, to distribute updates

Dep.js

Dep uses a publisk-subscribe model, which loops through the list to notify all watchers when data changes.

The Dep class helps us manage dependencies by collecting them, removing them, sending notifications to them, and so on

let uid = 0;
/** * The Dep class helps us manage dependencies by collecting dependencies, deleting dependencies, sending notifications to dependencies, etc. */
export default class Dep {
  constructor() {
    console.log("Dep");
    this.id = uid++;
    // Use an array to store its own subscribers, and place an instance of Watcher
    this.subs = [];
  }

  // Add a subscription
  addSub(sub) {
    this.subs.push(sub);
  }

  // Delete the subscription
  removeSub(sub) {
    remove(this.subs, sub);
  }

  // Add dependencies
  depend() {
    // dep. target is a global location that we specify. Window. target is ok, as long as it is globally unique and unambiguous
    if (Dep.target) {
      this.addSub(Dep.target); }}// Notification update
  notify() {
    console.log("notify");
    // Make a shallow copy
    const subs = this.subs.slice();
    / / traverse
    for (let i = 0, l = subs.length; i < l; i++) { subs[i].update(); }}}/** * Removes item * from arR array@param {*} arr 
 * @param {*} item
 * @returns * /
function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1); }}}Copy the code

Watcher.js

import Dep from "./Dep";

let uid = 0;
/** * Watcher is a mediator role that notifies it when data changes and then notifies other places */
export default class Watcher {
  constructor(target, expression, callback) {
    console.log("Watcher");
    this.id = uid++;
    this.target = target;
    // To read the contents of data.b.c, execute this.getter()
    this.getter = parsePath(expression);
    this.callback = callback;
    this.value = this.get();
  }

  get() {
    // Enter the dependency collection phase.
    // Set the global dep. target to Watcher itself
    Dep.target = this;
    const obj = this.target;
    var value;
    // Keep looking as long as you can
    try {
      value = this.getter(obj);
    } finally {
      Dep.target = null;
    }
    return value;
  }

  update() {
    this.run();
  } 

  run() {
    this.getAndInvoke(this.callback);
  }
  getAndInvoke(callback) {
    const value = this.get();
    if(value ! = =this.value || typeof value === "object") {
      const oldValue = this.value;
      this.value = value;
      callback.call(this.target, value, oldValue); }}}/** * use STR. Break it into array segments and loop through the array, reading the data layer by layer until obj is the data that STR wants to read *@param {*} str
 * @returns* /
function parsePath(str) {
  let segments = str.split(".");
  return function (obj) {
    for (let key of segments) {
      if(! obj)return;
      obj = obj[key];
    }
    return obj;
  };
}

Copy the code

defineReactive.js

Objects collect dependencies in getters, arrays collect dependencies in setters, and interceptors fire dependencies

import Dep from "./Dep";
import observe from "./observe";

/** * define a listener for the key of the object data@param {*} Data Incoming data *@param {*} Key Listens to the property *@param {*} The turnover variable */ provided by the value closure environment
export default function defineReactive(data, key, value) {
  
  // Each data item maintains its own array to store its dependent watcher
  const dep = new Dep();

  // console.log('defineReactive()', data,key,value)
  if (arguments.length === 2) {
    value = data[key];
  }

  // The child element must observe to recurse
  let childOb = observe(value);

  Object.defineProperty(data, key, {
    // Enumeration can be for-in
    enumerable: true.// Can be configured, such as delete
    configurable: true.// Getter collects dependencies
    get() {
      console.log('Getter attempts to access${key}Attribute `);

      // Collect dependencies
      if (Dep.target) {
        dep.depend();
        // Determine if there are any children
        if (childOb) {
          // Array collection dependencieschildOb.dep.depend(); }}return value;
    },
    // setters trigger dependencies
    set(newValue) {
      console.log(The setter is trying to change${key}Attribute `, newValue);

      if (value === newValue) return;
      value = newValue;

      // When a new value is set, the new value must also be observed
      childOb = observe(newValue);

      // Trigger dependencies
      // Publish subscription mode, notify DEPdep.notify(); }}); }Copy the code

array.js

Arrays collect dependencies in getters and trigger dependencies in interceptors

import def from "./def";

const arrayPrototype = Array.prototype;

// Create an arrayMethod based on array. prototype
export const arrayMethods = Object.create(arrayPrototype);

// The 7 array methods to be overwritten
const methodsNeedChange = [
  "push"."pop"."shift"."unshift"."splice"."sort"."reverse",];// Execute these methods in batches
methodsNeedChange.forEach((methodName) = > {
  // Back up the original method
  const original = arrayPrototype[methodName];

  // Define a new method
  def(
    arrayMethods,
    methodName,
    function () {
      console.log("Array data has been hijacked.");

      // Restore the original function (array method)
      const result = original.apply(this.arguments);
      // Turn an array-like object into an array
      const args = [...arguments];

      // Remove __ob__ from this array
      // Get the Observer instance in the interceptor
      const ob = this.__ob__;

      // There are three methods: push, unshift, splice to insert new items, to hijack (detect) these data (insert new items)
      let inserted = [];
      switch (methodName) {
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }

      // Check to see if there is a new inserted item
      if (inserted) {
        ob.observeArray(inserted);
      }

      // Publish subscription mode, notify DEP
      // Send messages to dependencies
      ob.dep.notify();

      return result;
    },
    false
  );
});
Copy the code

5. Test detection

let obj = {
  a: 1.b: {
    c: {
      d: 4,}},e: [22.33.44.55]};Copy the code
console.log(obj);
Copy the code

observe(obj);
Copy the code

observe(obj);
console.log(obj);
Copy the code

observe(obj);
obj.a = 5;
Copy the code

observe(obj);
obj.a++
Copy the code

observe(obj);
console.log(obj.b.c)
Copy the code

observe(obj);
obj.b.c.d = 10;
Copy the code

observe(obj);
obj.e.push(66.77.88);
console.log(obj.e);
Copy the code

observe(obj);
obj.e.splice(2.1[13.14]);
console.log(obj.e);
Copy the code

observe(obj);
new Watcher(obj, "b.c.d".(val) = > {
  console.log("Watcher listening", val);
});
Copy the code

Source linkGitee.com/ykang2020/v…