Project address: Gitee

Serial address:

1: Basic principles

3: Render Watcher

4: Final chapter

1. Why do arrays need special treatment

The last article covered the basics of vUE data responsiveness, and ended by saying that we need to do a separate processing of the array. Now, a lot of you might be wondering, why do you do special things to arrays? Isn’t an array an object whose key is a numeric value? Then we might as well give it a try

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log('get: ', val)
      return val
    },
    set(newVal) {
      console.log('set: ', newVal)
      newVal = val
    }
  })
}

const arr = [1.2.3]
arr.forEach((val, index, arr) = > {
  defineReactive(arr, index, val)
})
Copy the code

If we access and get arR values, getters and setters are also fired, isn’t that ok? But what if arr. Unshift (0)? Each element of the array is moved back one bit in turn, which triggers the getter and setter, causing the dependency to change. Since arrays are sequential, the key and value are not bound, so this nursing approach is problematic.

If you can’t listen on the index of an array, listen on the array itself. Vue overwrites 7 methods in JS that change arrays: push, pop, unshift, Shift, splice, reverse, sort. So, we need to modify the Observer:

class Observer {
  constructor(value) {
    this.value = value
    if (Array.isArray(value)) {
      // Proxy prototype...
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk(obj) {
    Object.keys(obj).forEach((key) = > defineReactive(obj, key, obj[key]))
  }
  // Continue to listen for elements in the array (if the array elements are objects)
  observeArray(arr) {
    arr.forEach((i) = > observe(i))
  }
}
Copy the code

The key processing is the proxy prototype

2. Agent prototype

When we call arr.push(), we’re actually calling array.prototype.push. We want to do something special with the push method. In addition to overriding the method, we can also set up a proxy prototype

We added a layer of proxies between Array instances and array. prototype to distribute updates (dependency collection will be described below). Arrays call the methods of the proxy prototype to distribute updates, and the proxy prototype calls the methods of the real prototype to implement the original functions:

// Observer.js
if (Array.isArray(value)) {
  Object.setPrototypeOf(value, proxyPrototype) // value.__proto__ === proxyPrototype
  this.observeArray(value)
}

// array.js
const arrayPrototype = Array.prototype // Cache the real prototype

// The method that needs to be handled
const reactiveMethods = [
  'push'.'pop'.'unshift'.'shift'.'splice'.'reverse'.'sort'
]

// Add proxyPrototype.__proto__ === arrayProrotype
const proxyPrototype = Object.create(arrayPrototype)

// Define reactive methods
reactiveMethods.forEach((method) = > {
  const originalMethod = arrayPrototype[method]
  // Define variation responsive methods on proxy prototypes
  Object.defineProperty(proxyPrototype, method, {
    value: function reactiveMethod(. args) {
      const result = originalMethod.apply(this, args) // Execute the default prototype method
      / /... Distribute updates...
      return result
    },
    enumerable: false.writable: true.configurable: true})})Copy the code

The question is, how do we distribute updates? For objects, we use dep.nofity to distribute updates. We get the DEP array because we use getters and setters to form closures that store the DEP array and ensure that each property has its own DEP. What about arrays? If you define a deP in array.js, all arrays will share that deP, which is obviously not possible. Therefore, vue adds a custom attribute to each object: The __ob__ attribute holds its own Observer instance, and then adds a dep attribute to the Observer.

3. __ob__attribute

Make one change to Observe:

// observe.js
function observe(value) {
  if (typeofvalue ! = ='object') return
  let ob
  // __ob__ can also be used to indicate whether the current object is being listened on
  if (value.__ob__ && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}
Copy the code

The Observer also needs to change:

constructor(value) {
  this.value = value
  this.dep = new Dep()
  // Define an __ob__ attribute on each object, pointing to an Observer instance of each object
  def(value, '__ob__'.this)
  if (Array.isArray(value)) {
    Object.setPrototypeOf(value, proxyPrototype)
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}

// The utility function def encapsulates object.defineProperty
function def(obj, key, value, enumerable = false) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true.configurable: true})}Copy the code

Thus, the object obj: {arr: [… } would become obj: {arr: [… __ob__ : {}], __ob__ : {}}

// array.js
reactiveMethods.forEach((method) = > {
  const originalMethod = arrayPrototype[method]
  Object.defineProperty(proxyPrototype, method, {
    value: function reactiveMethod(. args) {
      const result = originalMethod.apply(this, args)
      const ob = this.__ob__ / / new
      ob.dep.notify()        / / new
      return result
    },
    enumerable: false.writable: true.configurable: true})})Copy the code

Push, unshift, and splice may add elements to the array, and these additions should also be listened on:

Object.defineProperty(proxyPrototype, method, {
  value: function reactiveMethod(. args) {
    const result = originalMethod.apply(this, args)
    const ob = this.__ob__
    // Special handling of push, unshift, splice
    let inserted = null
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        // The third and subsequent argument to the splice method is the new element
        inserted = args.slice(2)}// If there are new elements, continue to align and listen
    if (inserted) ob.observeArray(inserted)
    ob.dep.notify()
    return result
  },
  enumerable: false.writable: true.configurable: true
})
Copy the code

At this point, we complete the distributive update of the array by adding an __ob__ attribute to the object, followed by dependency collection

4. Rely on collection

Before relying on the collection, let’s take a look at the __ob__ attribute

const obj = {
  arr: [{a: 1}}]Copy the code

After observing (obj), obj looks like the following

obj: {
  arr: [{a: 1.__ob__: {... }/ / add
    },
    __ob__: {... }/ / add].__ob__: {... }/ / add
}
Copy the code

Our defineReactive function called Observe (val) to recursively set the response for the data and now observe() returns ob, value.__ob__, so we might as well accept this return value

// defineReactive.js
let childOb = observe(val) / / modify

set: function reactiveSetter(newVal) {
  if (val === newVal) {
    return
  }
  val = newVal
  childOb = observe(newVal) / / modify
  dep.notify()
}
Copy the code

So what is this childOb? For example, look at obj.arr:

  1. performobserve(obj)Will triggernew Observer(obj)To set upobj.__ob__Property, and then iterateobjProperty, executedefineReactive(obj, arr, obj.arr)
  2. performdefineReactive(obj, arr, obj.arr)Is executedobserve(obj.arr), the return value isobj.arr.__ob__

That is, the getters and setters for each property (such as an ARR property) not only hold their own DEP via closures, but also hold their own Observer instance via __ob__, which in turn has a DEP property on it. If you add the following code:

// defineReactive.js
get: function reactiveGetter() {
  if (Dep.target) {
    dep.depend()
    childOb.dep.depend() / / new
  }
  return val
}
Copy the code

The following would happen: The getter and setter for each property stores the DEP through the closure. The DEP collects the dependent Watcher, and the closure stores the chilOb. The chilob. dep also stores the dependent Watcher, and the two properties store the same watcher. The distributional updates mentioned above can be implemented.

The childOb held in the obj.prop closure is obj.prop.__ob__, and the deP held in the closure is the same as the childob.dep

Obj [arr][0]. Obj [arr][0]. Obj [arr][obj]. But the DEP in the property closure can collect its own dependent Watcher. Therefore, the above code may report errors, so make the following changes

get: function reactiveGetter() {
  if (Dep.target) {
    dep.depend()
    if (childOb) {
      childOb.dep.depend() / / new}}return val
}
Copy the code

5. To rely on an array is to rely on all the elements in the array

Look at this example:

const obj = {
  arr: [{a: 1 }
  ]
}

observe(obj)

// Obj after data listening
obj: {
  arr: [{a: 1.__ob__: {...}
    },
    __ob__: {... }].__ob__: {...}
}
Copy the code

Create a watcher that depends on arR. Add an attribute to obj. Arr [0] :

Vue.set(obj.arr[0].'b'.2) // Vue. Set
Copy the code

This will not trigger the Watcher callback. Because our watcher relies on arR, the getter for obj.arr is triggered when evaluated, so the watcher is collected in childob.dep (arr.__ob__.dep). But watcher is not collected in obj.arr[0].__ob__, so setting a new value for it will not trigger an update. But Vue says that depending on an array is equivalent to relying on all the elements in the array, so we need to go further

// defineReactive.js
get: function reactiveGetter() {
  if (Dep.target) {
    dep.depend()
    if (childOb) {
      childOb.dep.depend()
      / / new
      if (Array.isArray(val)) {
        dependArray(val)
      }
    }
  }
  return val
}

function dependArray(array) {
  for (let e of array) {
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
Copy the code

When the dependency is an array, the new code iterates through the array and adds watcher to __ob__.dep for each element.

6. Summary

We do this by setting up the proxy stereotype, making arrays perform mutated methods for responsiveness, and setting an __ob__ attribute for each property so that we can access the DEP outside of the closure to distribute updates, including the vue.set method that takes advantage of this attribute.

In the case of arrays, dependency collection is still done in the getter, but with arr.__ob__, and dependencies need to be added to all items of the array, while update distribution is done in the mutation method, again with __ob__.

The next article will cover how to implement a rendering watcher, mainly to solve the problem of repeated dependencies, please pay attention to.

If you feel good, please point a thumbs-up!!