Firstly, the data response diagram is drawn in the form of graph

You can see from the flowchart that there are several different methods and classes

  1. Array processing by the Observe and Observer classes that observe data
  2. DefineReactive to observe the data
  3. Dep for data dependency
  4. Add a handler Watch to the dependency

A diagram of several files

  • index.js
import defineReactive from './data/defineReactive.js'
import {observe} from './data/observer'
import Watcher from './data/watcher.js'
var obj = {
  'a': {
    "b": {'n': 5}},'b': 6.'g': ['90'.'76'.'766'.'56']
}
observe(obj)
new Watcher (obj, 'a.b.n'.(val) = > {
  console.log('* * * * * * * *',val)
})
obj.a.b.n = 890
Copy the code
  • observer.js
import {def} from './utils.js'
import defineReactive from './defineReactive.js'
import {arrayMethods} from './array.js'
import Dep from './dep.js'
export default class Observer {
  constructor (value) {
    this.dep = new Dep()
    // A non-enumerable attribute this refers not to the class itself, but to the instance
    // Add an __ob__ attribute to value via def and point to the instance itself
    def(value, '__ob__'.this.false)
    // The ultimate goal of the Observer is to convert properties at each level in OBJ to reactive
    if (Array.isArray(value)) {
      // If it is an array, force the prototype to point to the new address
      Object.setPrototypeOf(value, arrayMethods)
      // Change this array to observe
      this.observeArray(value)
    }else{
      this.walk(value)
    }
  }
  walk (value) {
    // Iterate over each KEY value to make each level of attributes responsive
    for(let key in value) {
      defineReactive(value,key)
    }
  }
  observeArray (arr) {
    for (let i=0, l= arr.length; i<l; i++) {
      observe(arr[i])
    }
  }
}
export function observe (value) {
  // Check if it is an object
  if (typeofvalue ! = ='object') return;
  var ob;
  // Check if __ob__ is bound to the object
  if (typeofvalue.__ob__! = ='undefined') {
    ob = value.__ob__
  }else {
    ob = new Observer(value)
  }
  return ob
}
Copy the code
  • utils.js
// Def uses defineProperty to add attributes and attribute values to an object
export function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value : val,
    enumerable, // class enumeration
    writable: true.// Is it writable
    configurable: true // It is not set by class})}// Returns a higher-order function, used in Watcher, to get the value of an object
export function parsePath (str) {
  let segments = str.split('. ')
  return (obj) = > {
    for (let i =0; i< segments.length; i++) {
      if(! obj)return;
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code
  • array.js
// Here are some functions in the array, using decorator mode to add some new functions-trigger data update

import {def} from './utils.js'
// Find the prototype object of Array
const arrayPrototype = Array.prototype
// Create an arrayMethods prototype with array.prototype so that arrayMethods have an array method
export let arrayMethods = Object.create(arrayPrototype)
let methodNeedChange = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

methodNeedChange.forEach((methodName) = > {
  // Back up the original method
  const original = arrayMethods[methodName]
  // Redefine the function that needs to be changed
  def(arrayMethods,methodName,function() {
   // Execute the old method first, using apply,
    const result = original.apply(this.arguments)
    // The __ob__ attribute is bound to each value of this object
    let ob = this.__ob__;
    let args = [...arguments] // Change the class array to an array
    // There are three ways to add content to the array. Change the added content page to observe
    // push unshift splice

    let inserted = []
    switch (methodName) {
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
      // splice(start subscript, amount, value to add)
        inserted = args.slice(2);
        break;
    }

    if (inserted) {
      // Change the new entry to observe
      ob.observeArray(inserted)
    }
    console.log('La la la la')
    ob.dep.notify()
   
    return result
  },false)})Copy the code
  • defineReactive.js

import {observe} from './observer'
import Dep from './dep.js'
export default function defineReactive (data,key,val) {
  const dep = new Dep()
  if (arguments.length == 2) {
    val = data[key]
  }
  let childOb = observe(val)
  Object.defineProperty(data,key,{
    enumerable: true.// can be enumerated
    configurable: true.// Can be set, such as delete
    get () {
      // If you are in a dependency collection phase
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      console.log('You tried to access obj's'+key+'properties' + val)
      return val
    },
    set (newVal) {
      console.log('You've got it'+key+'value' + newVal)
      if (val===newVal) {
        return
      }
      val = newVal
      // When a new value is set, continue to add observations
      childOb = observe(val)
      dep.notify()
    }
  })
}

Copy the code
  • dep.js

let uid =0;
export default class Dep {
  constructor() {
    console.log('I'm the constructor of the DEP class')
    this.id = uid++
    // Publish subscriber pattern creates an array of subscribers to place watcher instances in
    this.subs = []
  }
  addSub (sub) {
    this.subs.push(sub)
  }
  // Add dependencies
  depend () {
    // dep. target is a global location specified
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  notify () {
    console.log('I'm notify')
    // create a shallow clone
    let subs = this.subs.slice()
    / / traverse
    for (let i=0,l=subs.length; i< l; i++) {
      subs[i].update()
    }
  }
}
Copy the code
  • watcher.js
import Dep from './dep.js';
import {parsePath} from './utils.js'
let uid = 0
export default class Watcher{ 
  constructor(target, expression, callBack) {
    this.id = uid++
    this.target = target;
    // parsePath is a higher-order function for parsing expressions
    this.getter = parsePath(expression)
    this.callBack = callBack;
    this.value = this.get()
    console.log('I'm watcher's builder.')
  }
  update () {
    this.run()
  }
  get () {
    // Enter the dependency collection phase with the global dep. target set to watcher itself
    Dep.target = this;
    const obj = this.target;
    // Keep looking as long as you can
    var value
    try {
      value = this.getter(obj)
    } finally {
      Dep.target = null
    }
    return value
  }
  // Get and evoke
  run () {
    this.getAndInvoke(this.callBack)
  }
  getAndInvoke (cb) {
    const value = this.get();
    if(value ! = =this.value || typeof value == 'object') {
      const oldVal = this.value
      this.value = value
      // Here are the parameters in watcher, new values and old values
      cb.call(this.target,value,oldVal)
    }
  }
} 
Copy the code

Finally, to sum up:

  1. In Observe, add an __ob__ attribute to objects and arrays by traversing them. The value of the attribute is the Observer instance itself. There is also a DEP bound to each instance
  2. Each value is rendered responsive by object traversal
  3. Add the object and its properties to be reactive in defineReactive and invoke Observe on the value to make each layer reactive
  4. When you call new Watcher, you will first call the get method in Watcher, set the object itself to the static property value of dep. target, and then get the current object, the corresponding property value. This triggers get in defineProperty to collect the dependencies, essentially collecting the Watcher instance itself.
  5. Changing another value in an object in index.js triggers set in defineProperty, triggers notify, and triggers dependencies collected when viewing the object in DEP. Update = newVal; update = newVal; update = newVal; update = newVal