Force has its object

As you know, in Vue, modifying the value of an object property directly cannot trigger a reactive style. When you directly modify the value of an object property, you will find that only the data changes, but the page content does not.

What’s the reason?

The reason: Vue’s responsive system is based on a method called Object.defineProperty, which listens for an element in an Object to be retrieved or modified. However, this method has a major drawback. Adding or deleting attributes does not trigger listening, for example:

var vm = new Vue({
    data () {
        return {
            obj: {
                a: 1}}}})// 'vm.obj.a' is now responsive

vm.obj.b = 2
// 'vm.obj.b' is not responsive
Copy the code

The reason is that when Vue is initialized, Vue will deeply respond the return value of data method to make it responsive data. Therefore, VM.obj.a is responsive. However, the vm.obj.b set up later has not undergone the baptism of responsiveness at Vue initialization, so it should not be responsive.

So, can vm.obj. B become reactive? The vm.$set method is a perfect way to do this. I won’t go into details here, but I’ll probably write an article about the principles behind vm.

More dismal array

Having said all that, I haven’t mentioned the main character of this article, arrays. Now it’s time for the main character.

Arrays fared even worse than objects. Check out the official documentation:

Due to JavaScript limitations, Vue cannot detect the following altered arrays:

  1. When you set an item directly using an index, for example:vm.items[indexOfItem] = newValue
  2. When you modify the length of an array, for example:vm.items.length = newLength

It is possible that the official documentation is not very clear, so let’s continue with the following example:

var vm = new Vue({
    data () {
        return {
            items: ['a'.'b'.'c']
        }
    }
})
vm.items[1] = 'x' // Not responsive
vm.items.length = 2 // Not responsive
Copy the code

That is, arrays cannot even listen for changes to their own elements. The reason is that when Vue responds to elements in an object returned by the data method, if the elements are arrays, it only responds to the array itself, not the elements inside the array.

As a result, as the official documentation states, it is not possible to directly modify the elements inside the array to trigger the response.

So, is there a way to crack it?

Of course, there are seven array methods that can happily trigger array responses:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

We can see that these 7 array methods seem to be the original array methods. Why can these 7 array methods trigger the response, trigger the view update?

Are you thinking: array methods are great, array methods can do whatever they want?

These 7 array methods really can do whatever they want.

Because they’re mutated array methods.

Array variation

What is a variation array method?

The variable array method is to expand the function of the array method on the premise of keeping the original function unchanged. In Vue, the so-called function expansion is to add responsive functions.

There are two steps to turning a normal array into a mutated array:

  1. Expanded function
  2. An array of hijacked

Expanded function

Let’s start with a question:

There is a requirement that ‘HelloWorld’ be printed in the console every time the function is called without changing the function’s function or how it is called

In fact, the idea is very simple, divided into three steps:

  1. Use the new variable to cache the original function
  2. Redefine the original function
  3. Call the original function in the newly defined function

Take a look at the code implementation:

function A () {
    console.log('Called function A'.)}const nativeA = A
A = function () {
    console.log('HelloWorld')
    nativeA()
}
Copy the code

As you can see, in this way we can extend function A without changing its behavior.

Next, we use this method to extend the functionality of the original array method:

// Change the method name
const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

const arrayProto = Array.prototype
// Inherit the original array method
const arrayMethods = Object.create(arrayProto)

mutationMethods.forEach(method= > {
    // Cache the native array method
    const original = arrayProto[method]
    arrayMethods[method] = function (. args) {
        const result = original.apply(this, args)
        
        console.log('Perform responsive functions')
        
        return result
    }
})
Copy the code

As you can see from the code, we call the methods in the arrayMethods object in two ways:

  1. Call function expansion method: direct callarrayMethodsThe methods in
  2. Call native methods: In this case, the native methods defined in the array stereotype are found through the stereotype chain

With this approach, we have extended the functionality of the array native methods, but there is a huge problem: how do we make an array instance call the extended array methods?

The solution to this problem is array hijacking.

An array of hijacked

Array hijacking, as the name implies, is to replace the original array instance to inherit method with our extended method.

Think about it. We implemented an extended arrayMethods (arrayMethods), a custom array that inherits from an array object. We just need to concatenate it with an ordinary array instance and let it inherit from it.

And the way to do that is through the prototype chain.

The implementation method is shown in the following code:

let arr = []
// Inherit arrayMethods via implicit archetypes
arr.__proto__ = arrayMethods

// execute the mutated method
arr.push(1)
Copy the code

Through the function expansion and array hijacking, we finally realized the variable array, next let’s see how Vue source code is to achieve the variable array.

The source code parsing

We came to the SRC/core/observer/index, js in the observer in the class constructor function:

constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__'.this)
    // Check if it is an array
    if (Array.isArray(value)) {
        // Ability test
        const augment = hasProto
        ? protoAugment
        : copyAugment
        // Select different methods of array hijacking based on the result of capability detection
        augment(value, arrayMethods, arrayKeys)
        // Reactive processing of arrays
        this.observeArray(value)
    } else {
        this.walk(value)
    }
}
Copy the code

Observer class is the core component of Vue responsive system, and the most important function in the initialization stage is to responsitize the target object. Here, we’ll focus on its handling of arrays.

Its handling of arrays is mainly the following code

// Ability test
const augment = hasProto
? protoAugment
: copyAugment
// Select different methods of array hijacking based on the result of capability detection
augment(value, arrayMethods, arrayKeys)
// Array responsiveness is not relevant in this article
this.observeArray(value)
Copy the code

Firstly, augment constant is defined, the value of which is determined by hasProto.

Let’s look at hasProto:

export const hasProto = '__proto__' in {}
Copy the code

As you can see, hasProto is simply a Boolean constant that indicates whether the browser supports direct use of __proto__ (implicit stereotypes).

So, the first code is easy to understand: choose different array augmentation methods according to the ability detection results, if the browser supports implicit prototyping, then call protoAugment function as the array augmentation method, otherwise use copyAugment.

Different array hijacking methods

Now let’s look at protoAugment and copyAugment.

function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
Copy the code

It can be seen that the protoAugment function is extremely concise and consistent with the method mentioned in the array augment idea: connect the array instance directly with the array augment by implicit prototype, and inherit the methods in the array augment in this way.

Let’s look at copyAugment again:

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    // Encapsulate object.defineProperty
    def(target, key, src[key])
  }
}
Copy the code

Because browsers do not support direct use of implicit archetypes in this case, array hijacking is much more troublesome. We know that the first argument this function takes is an array instance and the second argument is a variation array, so what is the third argument?

// Get the attribute names of all their attributes in the variation array
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
Copy the code

ArrayKeys, defined at the beginning of the file, are the property names of all their own properties in the mutated array, which is an array.

Looking back at copyAugment function, it is very clear that all the methods in the variable array are directly defined in the array instance itself, which is equivalent to realizing the hijacking of the array in disguised form.

With array hijacking in place, let’s take a look at how Vue extends array functionality.

Expanded function

Array expanded function code is located in the SRC/core/observer/array. Js, HTML code is as follows:

import { def } from '.. /util/index'

// Cache array prototype
const arrayProto = Array.prototype
Arraymethods. __proto__ === array. prototype
export const arrayMethods = Object.create(arrayProto)

// A way to extend functionality is needed
const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function (method) {
  // cache original method
  // Cache the native array method
  const original = arrayProto[method]
  // Define extension methods in the array of variations
  def(arrayMethods, method, function mutator (. args) {
    // Execute and cache the results of the native array method
    const result = original.apply(this, args)
    // Reactive processing
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    // Returns the result of the execution of the native array method
    return result
  })
})
Copy the code

As you can see, the source code is implemented in the same way as I did in the array variation idea, but with the addition of reactive processing.

conclusion

The variation array of Vue is essentially a decorator mode. By learning its principle, we can easily deal with the need to expand its function under the premise of keeping the original function unchanged in practical work.