Preface:

This article is purely a personal study of reading notes, there are inappropriate places please comment guidance ~

  • Learn Array change detection
  • How does Array track changes
  • In Array interceptors are used as follows
  • Collect rely on
  • Gain Observer power
  • Detects Array additions and element changes

1. Detection of Array changes

For example:

this.list.push(1)
Copy the code

Detection in objects is done through getters/setters, but this example uses the push method to change the array and does not trigger the getter/setter method.

Because you can get throughArrayMethod on the prototype to change the contents of the array, soObjectThat bygetter/setterThe implementation of the.

2. How does Array track changes

Note: Prior to ES6, JavaScript did not provide metaprogramming capabilities, that is, the ability to intercept prototype methods. But we can override the native prototype method with custom methods.


Example: Override Array.prototype with an interceptor. Any subsequent manipulation of an Array using the methods on the Array prototype executes the methods provided in the interceptor. This allows us to track Array changes through the interceptor.

Use interceptors to override native methods.

3. Interceptors

How is this interceptor implemented? The interceptor is an Object with the same properties as array. prototype, except that some of the methods in this Object can change the contents of the Array itself.

In JavaScript, there are seven methods on the Array prototype that can change the contents of the Array itself: push, POP, Shift, unshift, splice, sort, and reverse.

The code to implement the interceptor is as follows:

const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) ; [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']. ForEach ((method)=>{const original = arrayProto[method] object.defineProperty (arrayMethods,method,{  value:function mutator(... args){ return original.apply(this,args) }, enumerable:false, writable:true, configurable:true }) })Copy the code

ArrayMethods inherits array. prototype and has all of its functionality. ArrayMethods to override ‘array. prototype.

The Object.defineProperty ‘method on arrayMethods encapsulates methods that can change the contents of the original array itself.

When push is used, arrayMethods.push is called, which executes the mutator function.

Implement the methods on Original (original Array. Prototype) in mutator to do what it should do.

So, we can do other things in the mutator function, such as adding notification to send changes, etc.

4. Override the Array prototype with interceptors

To make an interceptor work, you need to override array. prototype with it. But you can’t override it directly, because that would contaminate the global Array.

In practice, you want the interceptor to work only on the data that has been detected for change, in other words, you want the interceptor to cover only the prototype of the reactive array (for example, seven methods that can change the original array).

To make data responsive, you need to go through the Observer, so just use interceptors in the Observer to override prototypes that will be converted to responsive Array data.

Such as:

export class Observer{ constructor(value){ this.value = value; If (array.isarray (value)){arrayMethods}else{this.walk(value)}}Copy the code

\

Value.__proto__ = arrayMethods assigns an interceptor (arrayMethods that can intercept after processing) to value.__proto__ can override the value prototype by using __proto__.

__proto__ is an early implementation of Object.getPrototypeOf and Object.setPrototypeOf, so using object. setPrototypeOf in ES6 instead of __proto__ can achieve exactly the same effect.

\

Mount interceptor methods to array properties

__proto__ accesses stereotypes in a way that is not supported by all browsers, so we need to deal with situations where __proto__ cannot be used.


Vue is simple: if __proto__ is not available, then arrayMethods will be used on the detected array:

Import {arrayMethods} from './array' // check whether __proto__ is available const hasProto = '__proto__' in {} const arrayKeys = Object.getOwnPropertyNames(arrayMethods) export class Observer{ constructor(value){ this.value = value; If (array.isarray (value)){// Add the Array const augment = hasPro? protoAugment : copyAugment augment(value,arrayMethods,arrayKey) }else{ this.walk(value) } } ... } function protoAugment(target,src,keys){ target.__proto__ = src } function copyAugment(target,src,keys){ for(let i=0,l=keys.length; i<l; i++){ const key = keys[i] def(target,key,src[key]) } }Copy the code

HasProto determines whether the browser supports __proto__ : if it does, use protoAugment to override the prototype; If not, the copyAugment function is called to attach the interceptor to the value.

When the method to access an object is you, go back to its prototype and find that method only if it doesn’t exist.

6. How to collect dependencies

Interceptors are essentially created to have the ability to be notified when the contents of an array change.

Object, is indefineReactiveIn thegetterCollected using Dep, eachkeyThere will always be a correspondingDepList to store dependencies.


Dependencies are collected in the getter and stored in the Dep

And where do arrays collect dependencies?

  • Arrays are also in the getter
  • whileArrayThe dependency andObjectSame, also indefineReactiveIn the collection:
  • ArrayingetterTo collect dependencies and trigger dependencies in interceptors
function defineReactive(data,key,val){ if(typeof val === 'object') new Observer(val) let dep = new Dep(); Object.defineProperty(data,key,{ enumerable:true, configurable:true, get:function(){ dep.depend(); // Collect the Array dependency return val; }, set:function(newVal){ if(val === newVal){ return } dep.notify() val = newVal } }) }Copy the code

7. Where do dependency lists exist

Vue.js stores Array dependencies in an Observer:

Export class Observer{constructor(value){this.value = value this.dep = new dep (); // New dep if(array.isarray (value)){ const augment = hasProto ? protoAugment:copyAugment augment(value,arrayMethods,arrayKey) }else{ this.walk(value) } } ...  }Copy the code

Why is the DEP (dependency) of an array stored on an Observer instance?

  • becausegetterCan be accessed fromObserveThe instance
  • At the same timeArrayIt can also be accessed in interceptorsObserveThe instance

Collect dependencies

Function defineReactive(data,key,val){let childOb = observe(val) // let dep = new dep (); Object.defineProperty(data,key,{ enumerable:true, configurale:true, Get :function(){dep.depend() // Add if(childOb){childob.dep.depend (); } return val; } set:function(newVal){ if(val === newVal) return dep.notify() val = newVal } }) }Copy the code
// Try to create an Observer instance for value // If successful, return the newly created Observer instance // If value already has an Observer instance, Export function observe(value,asRootData){if(! isObject(value)) return let ob if(hasOwn(value,'__ob__') && value.__ob__ instanceof Observer){ ob = value.__ob__ }else{ ob = new Observer(value) } return ob; }Copy the code

Add the function observe:

  • observeThe function attempts to create oneObserverThe instance
  • ifvalueIf the data is already responsive, you do not need to create it againObserverThe instance
  • Returns the created one directlyObserverInstance to avoid repeated detectionvalueThe problem of change.

In this way you can collect dependencies for arrays.

Get an Observer instance in the interceptor

How do I access an Observer instance in an interceptor?

  • becauseArrayThe interceptor is a wrapper around the prototype, so it can be accessed in the interceptorthis(Array currently being manipulated)
  • depStored in theObserverIn, so need inthisRead onObserverAn instance of the
Function def(obj,key,val,enumerable){Object. DefineProperty (obj,key,{value:val, enumerable:!! enumerable, writable:true, configurable:true, }) } export class Observer{ constructor(value){ this.value = value; this.dep = new Dep(); def(value,'__ob__',this) if(Array.isArray(value)){ const augment = hasProto ? protoAugment:copyAugment; augment(value,arrayMethods,arrayKey) }else { this.walk(value) } } ... }Copy the code

A new code has been added to the Observer to add an unenumerable attribute __ob__ to value, whose value is the current Observer instance.

You can then retrieve the Observer instance via the __ob__ attribute of the array data, and then retrieve the DEP on __ob__.

__ob__ can also be used to indicate whether the current value has been converted to responsive data by the Observer.

In other words:

  • There will be one on any data that has been detected for a change__ob__Property to indicate that they are responsive.
  • through__ob__judge
    • If value is reactive, return directly__ob__
    • If it is not reactive, usenew ObserverTo convert the data into responsive data.
  • When Value is tagged__ob__After that, you can passvalue.__ob__To access theObserverThe instance
  • If it’s an Array interceptor, because interceptors are prototype methods
    • Straight throughthis.__ob__To access theObserverInstance.
; [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']. ForEach ((method)=>{const original = arrayProto[method] object.defineProperty (arrayMethods,method,{  value:function mutator(... Const ob = this.__ob__ // Add return original. Apply (this,args)}, Enumerable :false, writable:true, configurable:true }) })Copy the code

We get an Observer instance in the mutator function through this.ob.

Send notifications to array dependencies

When an array change is detected, a notification is sent to the dependency.

  • The first thing is to have access to dependencies.
  • Access in interceptorsObserverThe instance
  • Only in theObserverGet in the exampledepattribute
  • Finally, just send a notice
; [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] forEach ((method) = > {/ / cache the original method is const what = arrayProto. [method] dep (arrayMethods, method, function mutator(... Args){const result = original.apply(this,args) const ob = this.__ob__ ob.dep.notify() // Send a message to the dependent return result; })})Copy the code

In the above code, we call ob.dep.notify() to notify the dependency that the data has changed.

Detect changes in array elements

All children of reactive data are detected, whether in Object or Array.

How do I detect changes in all subsets of data?

Add some processing in the Observer to make arrays responsive as well:

export class Observer{ constructor(value){ this.value = value; Def (value,'__ob__',this) if(array.isarray (value)){this.observerArray(value)}else{this.walk(value)}}... ObserverArray (items){for(let I =0,l=items.length; i<l; i++){ observe(items[i]) } ... }Copy the code

I’ve added the observeArray method,

  • Its function is to cycleArrayEach of these terms,
  • performobserveFunction to detect changes.

The observe function, which performs a New Observer for each element in the array, is clearly a recursive process.

12. Detect changes to new elements

12.1 Obtaining New Elements

In the above code, we check the method by switch, and if the method is a push, unshift, splice, or other method that can add an array element, we pull the new element out of the ARgs and temporarily store the inserted element.

; [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] forEach ((method) = > {/ / cache the original method is const what = arrayProto. [method] def (arrayMethods, method, function mutator(... args){ const result = original.apply(this,args) const ob = this.__ob__ let inserted switch(method){ case 'push': case 'unshift': inserted = args; break; case 'splice': inserted = args.slice(2) break; } ob.dep.notify() // Send a message to a dependency return result; })})Copy the code

Next, we use an Observer to convert the inserted element into a response.

12.2 Using the Observer to Detect New Elements

We can access __ob__ through this in the interceptor, and then call the observeArray method on __ob__ :

; [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] forEach ((method) = > {/ / cache the original method is const what = arrayProto. [method] def (arrayMethods, method, function mutator(... args){ const result = original.apply(this,args) 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) // Add ob.dep.notify() // Send a message to a dependency return result; })})Copy the code

Ob.observearray is used to detect new elements that have been added to an Observer instance from this.__ob__.

13. Questions about Array

Because Vue detects Array changes by intercepting archetypes, some Array operations cannot be intercepted by Vue. Js, for example:

this.list[0] = 2
Copy the code

Changing the value of the first element in the array does not detect the array change, so re-render or watch is not triggered

Such as:

this.list.length = 0;
Copy the code

This empty-array operation also does not detect array changes, so it does not trigger re-render or watch, etc.

Because vue.js is implemented in such a way that it cannot intercept the two examples above, there is no way to respond.

Prior to ES6, there was no way to emulate the native behavior of arrays, so there was no way to intercept them.

ES6 provides the ability of metaprogramming, so it has the ability to intercept. The current Vue3 uses the Proxy provided by ES6 to achieve this function, so as to solve this problem.

14,

1. Array tracks changes differently than Object.

  • Because it changes content through methods, we keep track of the changes by creating interceptors that override the array prototype.

2. To avoid contaminating global array. prototype, we use __proto__ in the Observer to override prototype methods only for arrays that need to detect changes

  • but__proto__ It was not a standard property prior to ES6 and was not supported by all browsers.
  • Against unsupported __proto__ Property, we loop through the interceptor directly, setting methods in the interceptor directly to the array to interceptArray.prototypeNative methods on.

3. Array collection relies on the same getter as Object collection.

  • Depending on where the dependency is used, the array sends a message to the dependency in the interceptor
  • So dependence can’t be likeObjectThat’s kept in thedefineReactiveIn the
  • It’s storing dependencies inObserverInstance.

4. In An Observer, we stamp __ob__ on each data that detects changes and store this (Observer instance) on __ob__.

Main functions:

  • To flag whether the data has been detected for changes (ensure that the same data is detected only once)
  • It can be easily accessed by data __ob__In order to getObserverDependencies saved on the instance.
  • Sends notifications to dependencies when an intercepted array changes.

5. In addition to detecting changes in the array itself, it also detects changes in the elements of the array.

  • callobserveArrayMethod converts each element in the array to reactive and detects changes.

6. In addition to detecting existing data, when users add data to the array using methods such as push, change detection is also performed on the new data.

  • If it isPush, unshift and splicemethods
    • Extract the new data from the parameters
    • Then use theobserveArrayFor the new dataChange detection.

7. Since JavaScript did not provide metaprogramming capabilities prior to ES6, some syntax cannot track changes to array type data.

  • Only methods on the prototype can be intercepted
  • Cannot intercept array-specific syntax
  • For example, usinglengthEmptying an array cannot be intercepted.