In the last two articles we explored abstract syntax trees and diff algorithms. Today we’ll talk about the principle of responsiveness.

Vue2. X – Principle of data response

Object.defineProperty()methods

  • Object.defineProperty()Method defines a property directly on an object, or modifies an existing property of an object and returns the object.
var obj = {}

Object.defineProperty(obj, 'a', { value: 3 })
Object.defineProperty(obj, 'b', { value: 5 })

console.log(obj)
console.log(obj.a, obj.b)
Copy the code
var obj = {}

Object.defineProperty(obj, 'a', {
  // getter
  get() {
    console.log(Visit a ' ')},// setter
  set() {
    console.log(Change 'a')}})Object.defineProperty(obj, 'b', { value: 5.enumerable: true })

console.log(obj)
console.log(obj.a++, obj.b)
Copy the code

defineReactivefunction

  • getter/setterVariable turnover is required to work
var obj = {}
var temp
Object.defineProperty(obj, 'a', {
  // getter
  get() {
    console.log(Visit a ' ')
    return temp
  },
  // setter
  set(newValue) {
    console.log(Change 'a')
    temp = newValue
  }
})
Copy the code
  • defineReactivefunction
function defineReactive(data, key, val) {
  // Closure environment
  Object.defineProperty(data, key, {
    / / can be enumerated
    enumerable: true.// Can be configured
    configurable: true.// getter
    get() {
      console.log('access' + key)
      return val
    },
    // setter
    set(newValue) {
      console.log('change' + key, newValue)
      if (val === newValue) return
      val = newValue
    }
  })
}

var obj = {}

defineReactive(obj, 'a'.10)
console.log(obj.a)
obj.a = 60
obj.a += 10
console.log(obj.a)
Copy the code

Recursively detects all attributes of an object

  • Observer: Converts a normal object into an object whose properties at each level are responsive (detectable)
  • index.js
import observe from './observe'
var obj = {
  a: {
    m: {
      n: 10}},b: 20
}

observe(obj)
obj.b++
console.log(obj.a.m.n)
Copy the code
  • observe.js
import Observer from './Observer'
// Create an Observer function
export default function (value) {
  // If value is not an object, do nothing
  if (typeofvalue ! = ='object') return
  / / define the ob
  var ob
  if (typeofvalue.__ob__ ! = ='undefined') {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}
Copy the code
  • Observer.js
import { def } from './utils'
import defineReactive from './defineReactive'
export default class Observer {
  constructor(value) {
    (this is not the class itself, but the instance)
    // Add an __ob__ attribute with the value being an instance of new this time
    def(value, '__ob__'.this.false)
    console.log('Observer constructor ', value)
    // The purpose of the Observer class is to convert a normal object into an object whose properties at each level are responsive (detectable)
    this.walk(value)
  }

  / / traverse
  walk(value) {
    for (const key in value) {
      defineReactive(value, key)
    }
  }
}
Copy the code
  • defineReactive.js
import observe from './observe'
export default function (data, key, val) {
  // Closure environment
  if (arguments.length === 2) {
    val = data[key]
  }
  // The child element has to observe, so this is a recursion. Instead of calling itself, multiple functions and classes are called in a loop
  let childOb = observe(val)

  Object.defineProperty(data, key, {
    / / can be enumerated
    enumerable: true.// Can be configured
    configurable: true.// getter
    get() {
      console.log('access' + key)
      return val
    },
    // setter
    set(newValue) {
      console.log('change' + key, newValue)
      if (val === newValue) return
      val = newValue
      // When a new value is set, the new value must also be observed
      childOb = observe(newValue)
    }
  })
}
Copy the code
  • utils.js
export const def = function (obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true.configurable: true})}Copy the code

Reactive processing of arrays

  • VueThe bottom layer has been rewrittenArray7 native methods:['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
  • These seven methods are inArray.prototype 上
  • index.js
import observe from './observe'
var obj = {
  a: {
    m: {
      n: 10}},b: 20.g: [22.33.55]
}

observe(obj)
obj.g.splice(2.1[Awesome!.888])
obj.g.push(66)
console.log(obj)
// obj.b++
// console.log(obj.a.m.n)
// defineReactive(obj, 'a')
// console.log(obj.a.m.n)
Copy the code
  • array.js
import { def } from './utils'
/ / get Array. The prototype
const arrayPrototype = Array.prototype
// Create an arrayMethods object with array. prototype
export const arrayMethods = Object.create(arrayPrototype)
// The 7 array methods to be overwritten
const methodsNeedChange = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

methodsNeedChange.forEach((methodName) = > {
  // Back up the original method. The functionality of the 7 methods cannot be stripped
  const original = arrayPrototype[methodName]
  __ob__ has been added. Why has __ob__ been added? Because arrays are definitely not the highest level
  For example, if obj is an array, obj cannot be an array. The first time we traverse the first layer of obj, we have already assigned g to it
  // Add an __ob__ attribute
  // Define a new method
  def(
    arrayMethods,
    methodName,
    function () {
      // Restore the original function
      const result = original.apply(this.arguments)
      const ob = this.__ob__
      // Change arguments to arrays
      const args = [...arguments]
      // There are three methods push/unshift/splice can insert a new item. Now you need to change the inserted item to observe
      let inserted
      switch (methodName) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          // Splice format is splice(subscript, quantity, new item inserted)
          inserted = args.slice(2)
          break
      }
      // Determine if there are any new items to insert, so that the new items also become responsive
      if (inserted) {
        ob.observeArray(inserted)
      }
      console.log('lalalala')
      return result
    },
    false)})Copy the code
  • Observer.js
import { def } from './utils'
import defineReactive from './defineReactive'
import { arrayMethods } from './array'
import observe from './observe'
export default class Observer {
  constructor(value) {
    (this is not the class itself, but the instance)
    // Add an __ob__ attribute with the value being an instance of new this time
    def(value, '__ob__'.this.false)
    console.log('Observer constructor ', value)
    // The purpose of the Observer class is to convert a normal object into an object whose properties at each level are responsive (detectable)
    // Check whether it is an array or an object
    if (Array.isArray(value)) {
      // If it is an array, point the prototype of the array to arrayMethods
      Object.setPrototypeOf(value, arrayMethods)
      // Make this array observe
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  / / traverse
  walk(value) {
    for (const key in value) {
      defineReactive(value, key)
    }
  }
  // Special traversal of the array
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      / / observe item by item
      observe(arr[i])
    }
  }
}
Copy the code

Depend on the collection

What is dependency?

  • Where data is needed, it is called a dependency
  • Vue1.x.fine-grainedDependent, using dataDOMIs dependent on
  • Vue2.x.Medium sizeDependent, using datacomponentIs dependent on
  • ingetterCollect dependencies insetterTrigger dependency in

DepClasses andWatcher 类

  • Encapsulate the code that depends on the collection into oneDepClass, which is dedicated to managing dependencies,eachObserverEach of the members has an instance ofDepAn instance of the
  • WactherIs a mediation through which data changesWactherRelay, notification component

  • Dependency is Watcher. Only getters triggered by Watcher collect dependencies, and whichever Wacther 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.

  • The code implementation is clever: Watcher sets itself to a specified location globally, then reads the data, and because it reads the data, it fires the getter for that data. In the getter, you get the Watcher that is currently reading, and you collect that Watcher into the Dep

code

  • index.js
import observe from './observe'
import Watcher from './Watcher'
var obj = {
  a: {
    m: {
      n: 10}},b: 20.g: [22.33.55]
}

observe(obj)
obj.a.m.n = 8
obj.g.splice(2.1[Awesome!.888])
obj.g.push(66)
console.log(obj)
new Watcher(obj, 'a.m.n'.(val) = > {
  console.log('* I'm Watcher and I'm monitoring A.M.N. ', val)
})
obj.a.m.n = 8888
Copy the code
  • Observer.js
import { def } from './utils'
import defineReactive from './defineReactive'
import { arrayMethods } from './array'
import observe from './observe'
import Dep from './Dep'
export default class Observer {
  constructor(value) {
    // On each Observer instance, there is a DEP
    this.dep = new Dep()
    (this is not the class itself, but the instance)
    // Add an __ob__ attribute with the value being an instance of new this time
    def(value, '__ob__'.this.false)
    console.log('Observer constructor ', value)
    // The purpose of the Observer class is to convert a normal object into an object whose properties at each level are responsive (detectable)
    // Check whether it is an array or an object
    if (Array.isArray(value)) {
      // If it is an array, point the prototype of the array to arrayMethods
      Object.setPrototypeOf(value, arrayMethods)
      // Make this array observe
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  / / traverse
  walk(value) {
    for (const key in value) {
      defineReactive(value, key)
    }
  }
  // Special traversal of the array
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      / / observe item by item
      observe(arr[i])
    }
  }
}
Copy the code
  • defineReactive.js
import observe from './observe'
import Dep from './Dep'
export default function (data, key, val) {
  const dep = new Dep()
  // Closure environment
  if (arguments.length === 2) {
    val = data[key]
  }
  // The child element has to observe, so this is a recursion. Instead of calling itself, multiple functions and classes are called in a loop
  let childOb = observe(val)

  Object.defineProperty(data, key, {
    / / can be enumerated
    enumerable: true.// Can be configured
    configurable: true.// getter
    get() {
      console.log('access' + key)
      // If you are in the dependency collection phase
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return val
    },
    // setter
    set(newValue) {
      console.log('change' + key, newValue)
      if (val === newValue) return
      val = newValue
      // When a new value is set, the new value must also be observed
      childOb = observe(newValue)
      // Publish and subscribe to notify deP
      dep.notify()
    }
  })
}
Copy the code
  • Dep.js
var uid = 0

export default class Dep {
  constructor() {
    console.log('Dep constructor ')
    this.id = uid++
    // Use an array to store your own subscribers. Subs stands for subscribes
    // This array contains instances of Watcher
    this.subs = []
  }
  // Add a subscription
  addSub(sub) {
    this.subs.push(sub)
  }
  // Add dependencies
  depend() {
    // dep. target is a global location that we specify ourselves
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  // Notification update
  notify() {
    console.log('I'm notify')
    // create a shallow clone
    const 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'

var uid = 0

function parsePath(str) {
  var segments = str.split('. ')
  return (obj) = > {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}

export default class Watcher {
  constructor(target, expression, callback) {
    console.log('Watcher constructor ')
    this.id = uid++
    this.target = target
    this.getter = parsePath(expression)
    this.callback = callback
    this.value = this.get()
  }
  update() {
    this.run()
  }
  run() {
    this.getAndInvoke(this.callback)
  }
  getAndInvoke(cb) {
    const value = this.get()
    if(value ! = =this.value || typeof value === 'object') {
      const oldValue = this.value
      this.value = value
      cb.call(this.target, value, oldValue)
    }
  }
  get() {
    // Set the global dep. target to Watcher itself, then you are in the dependency collection phase
    Dep.target = this
    const obj = this.target
    var value
    // Keep looking as long as you can
    try {
      value = this.getter(obj)
    } catch (error) {
      console.log(error)
    } finally {
      Dep.target = null
    }
    return value
  }
}
Copy the code

Finally, the source code address is attached

Gitee.com/yokeney/vue…