The data was hijacked

In VUE, the pattern of publishing subscriptions listens for changes in data state and notifies the view of updates. So, when to subscribe, when to publish, this is the use of data hijacking.

Vue uses Object.defineProperty() for data hijacking.

let msg = "hello" const data = {}; Object.defineProperty(data, 'msg', { enumerable: true, configurable: Console. log('get MSG ') return MSG; }, set(newVal) {console.log('set MSG ') MSG = newVal; }}); data.msg //'get msg' data.msg = 'hi' //'set msg'Copy the code

Attribute defined with Object.defineProperty can add custom logic to its get and set methods when evaluating and assigning values. When the value of data. MSG is updated, every value of data. MSG needs to be updated as well, which can be treated as a subscription to data. MSG, so add watcher to get method. When data. MSG is reassigned, all watchers are notified of the corresponding update, so notify all watchers in the set method.

In VUE, the data defined in data is reactive because VUE hijacks all the attributes in data.

function initData (vm) { var data = vm.$options.data; observe(data, true); } function observe (value, asRootData) { var ob = new Observer(value); Return ob} //Observer is used to hijack data, Var Observer = function Observer (value) {if (array.isarray (value)) { If (hasProto) {protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else {// When the data is an Object, the recursive Object uses object.defineProperty for each layer of the Object, such as {a: {b: {c: 1}} this.walk(value); }};Copy the code

When using VUE, data usually has arrays. Unlike objects, data hijacking cannot be implemented via Object.defineProperty.

object

Data hijacking of an Object begins by traversing all attributes of the Object, using Object.defineProperty hijacking for each attribute, recursively when the attribute’s value is also an Object.

Function observeObject(obj){if(! obj || typeof obj ! ForEach ((key) => {let value = obj[key] // Data hijacking observeObject(value) let DefineProperty (obj,key,{enumerable: true, signals: True, get(){dep.addSub(watcher) // pseudo-code, add watcher return value}, set(newVal){value = newVal //obj attribute reassignment is also data hijack, / ** let a = {b: 1} a.b = {c: 1} **/ observeObject(value) dep.notify() //Copy the code

An array of

There are two main types of array state changes: one is the change of the item of the array, the other is the change of the array length. So array data hijacking also considers these two aspects.

  • Array item hijacking:
function observeArr(arr){ for(let i=0; i<arr.length; I ++){observe(arr[I]) //Copy the code

Vue does not hijack items that are simple data types, which also causes a problem with vUE arrays. When items are simple data types, the view is not updated when the items are modified.

<div><span v-for="item in arr">{{item}}</span></div> <button @click="changeArr">change array</button> <! -- Clicking the button will not update the view to 523-->Copy the code
Data: {arr:} [1, 2, 3], the methods: {changeArr () {this. Arr [0] = 5}}Copy the code
  • Array length hijacking is done by rewriting seven methods that can change the length of the original array (push, pop, shift, unshift, splice, sort, reverse) come true.
let arrayProto = Array.prototype; let arrayMethods = Object.create(arrayProto); Prototype let methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; ForEach ((method) => {arrayMethods[method] = function(... Args) {let result = arrayProto[method]. Apply (this,args) // Call the original array method let inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); Break} if (inserted) {// Push, unshift, and splice may add new array elements, where the new elements are also hijacked observeArray(inserted); } dep.notify(); Return result}}) arr.__proto__ = arrayMethods // Arr is an array that needs to be hijacked to modify its original prototype chain methods.Copy the code

Implement a simple two-way data binding

  • The first step is initialization.
class Vue { constructor(options){ this.$data = options.data this.$getterFn = options.getterFn observe(this.$data) // // Options. getterFn is a value function, New Watcher(this.$data, this.$getterfn.bind (this), key => {console.log(key + "updated ")})}} new Watcher(this.$data, this.Copy the code
  • Step 2, implement the observe method. The main use of the above publish-subscribe model and data hijacking.
function observe(data){ if(! data || typeof data ! == 'object') return let ob; When an observer is created for data, it is added to the data attribute. If the data already has an observer, If (data.hasownProperty ('__ob__') && data.__ob__ instanceof observer) {ob = data.__ob__; if (data.hasownProperty ('__ob__') && data.__ob__ instanceof Observer) {ob = data.__ob__; }else{ ob = new Observer(data) } return ob } class Observer { constructor(data){ this.dep = new Dep() Object.defineproperty (data, '__ob__', {// Attach the observer to the desired data, Enumerable: False, Different: false, value: 64x, cis: 64X, cis: 64X, Cis: 64X, Cis: 64X, Cis: 64X, Cis: 64X, Cis: 64X This}) if(array.isarray (data)){if(array.isarray (data)){ __proto__ = arrayMethods this.observearray (data)}else{this.walk(data)}} walk(data){let keys = Object.keys(data) keys.forEach((key) => { defineReactive(data, Key)})} observeArray(data){data.foreach ((val) => {observeArray(val)})}} let arrayMethods = Object.create(arrayProto); Prototype let methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; methodsToPatch.forEach((method) => { arrayMethods[method] = function(... Args) {let result = arrayProto[method]. Apply (this,args) // Call the array method let inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); // Push, unshift, and splice may insert new array elements, where the new elements are also hijacked this.__ob__.observeArray(inserted); } this.__ob__.dep.notify('array') return result}}) function defineReactive(data,key){let defineReactive = New Dep() // Let value = data[key] // When value is an array, Dep is not added for each array, but for the entire array. // When the array executes the above 7 methods, __ob__.dep.notify('array') let childOb = observe(value) object.defineProperty (data,key,{ Enumerable: true, different: true, get(){// Add a different subscriber. Dep.target is a global object. It points to the current watcher dep.target && dep.addSub(dep.target) if(array.isarray (value)) {dep.target && childOb.dep.addSub(Dep.target) } return value }, set(newVal){ if(newVal === value) return value = newVal observe(value) dep.notify(key) } }) }Copy the code

When watcher is triggered is obvious. Adding Watcher is a little less obvious. Here are some changes to watcher’s constructor.

Dep.target = null
class Watcher{
    constructor(data,getterFn,cb){
        this.cb = cb
		
        Dep.target = this
        getterFn()
        Dep.target = null
    }

    update(key){
        this.cb && this.cb(key)
    }
}
Copy the code

The key is:

Dep.target = this
getterFn()
Dep.target = null
Copy the code

In new Watcher(), these three lines of code are executed. Dep.target = this Assigns the current watcher to the dep. target global variable. GetterFn () returns the value of vm.$data. So when it gets its value, it executes the get method for each attribute.

Get (){// dep. target points to the current watcher and adds the current watcher to the subscription array corresponding to this property. Dep.target && dep.addSub(Dep.target) if(Array.isArray(value)) { Dep.target && childOb.dep.addSub(Dep.target) // If the value of the property is an array, add the current watcher to the subscription array corresponding to that array. } return value },Copy the code

This completes the operation of adding watcher to the property that needs to be accessed, and then restoring dep.target to NULL.

{{msg.m}}, add watcher, complete the subscription. Let’s simulate this by simply accessing the values.

let vm = new Vue({
   el: '#root',
   data:{
       msg: {
           m: "hello world"
       },
       arr: [
          {a: 1},
          {a: 2}
       ]
   },
   getterFn(){
       console.log(this.$data.msg.m)
       this.$data.arr.forEach((item) => {
           console.log(item.a)
       })
   }
})
Copy the code

Effect:As you can see, the data accessed by getterFn triggers watcher’s callback function when it changes its value.

Vue several watcher

There are three main types of Watcher in VUE:

  • Render Watcher: Rerender the page when the data used for rendering changes
  • Computed Watcher: Updates the value of computed as data data changes
  • User Watcher: Executes the callbacks defined by Watch when you want watch’s data to change

Render the watcher

The render Watcher is created when the vm.$mount() method is executed.

Vue.prototype.$mount = function () { var updateComponent = function () { vm._update(vm._render(), hydrating); }; // New Watcher(VM, updateComponent, noop, options,true); // New Watcher(vm, updateComponent, noop, options,true); };Copy the code

Watcher’s constructor:

var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher) { this.vm = vm; if (options) { ... this.lazy = !! options.lazy; // Mainly used for computed watcher} else {this.deep = this.user = this.lazy = this.sync = false; } this.cb = cb; if (typeof expOrFn === 'function') { this.getter = expOrFn; } else {this.getter = parsePath(expOrFn); } // If this.lazy is false, this.get() is executed immediately. undefined: this.get(); }; Watcher.prototype.get = function get () { pushTarget(this); // Dep.target = this var value; var vm = this.vm; value = this.getter.call(vm, vm); // Execute the value function to complete the watcher subscription popTarget(); // dep. target = null return value};Copy the code

As soon as the rendering Watcher is created, the value function is executed to complete the dependency collection of reactive data. As you can see, the data defined in data has the same watcher, which was created when the vm.$mount() method was executed. Watcher update method:

Watcher.prototype.update = function update () { if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); // Render Watcher will follow this logic and will eventually execute this.run(), except that it is optimized with queues}}; Watcher.prototype.run = function run () { var value = this.get(); // The updateComponent method is executed again}Copy the code

For data defined in data, they all have the same watcher, and watcher.update() is executed every time data in data is updated. Update () rendering watcher will eventually execute the updateComponent method. If you modify N data attributes at once, such as change in the example below, you will theoretically execute updateComponent() N times, which is obviously not scientific.

As an optimization, maintain a watcher queue and try to add watcher(queueWatcher(this)) to the queue every time watcher.update() is executed. If the watcher is already in the queue, no more watcher will be added. Finally, execute these Watcher run methods once in nextTick.

This way, if you modify N data attributes at once, you actually only perform updateComponent() once

data:{
    msg: "hello",
    msg2: "ni hao"
}, 
methods:{
    change(){
        this.msg = "hi"
        this.msg2 = "hi"
  }
},
Copy the code

computed watcher

data:{
    msg: "hello"
},
computed: {
    newMsg(){
        return this.msg + ' computed'
    }
},
Copy the code
<div>{{newMsg}}</div>
Copy the code

When MSG is updated, newMsg is updated. Because computed subscribes to accessed data (MSG in this case).

function initComputed (vm, computed) { var watchers = vm._computedWatchers = Object.create(null); for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; Which [key] = new Watcher (/ / Watcher of value function is the function we defined in the computed vm, getter | | it, it, computedWatcherOptions // { lazy: true } ); if (! (key in vm)) { defineComputed(vm, key, userDef); }}}Copy the code

With initComputed, watcher is created, which has an attribute lazy: ture. The constructor of the watcher is lazy: ture. The constructor of the watcher is lazy: ture.

this.value = this.lazy? undefined: this.get();  
Copy the code

Watcher joins MSG’s subscription array only if the page values {{newMsg}} on computed. Here’s a look at the defineComputed method, whose general logic is as follows:

Function defineComputed (target,key,userDef) {// target: vm, key: newMsg Object.defineProperty(target, key, { enumerable: true, configurable: true, get: Function computedGetter () {var watcher = this._computedWatchers && this._computedWatchers[key]; Watcher () {if (watcher) {if (watcher) {if (watcher).dirty = watcher. Lazy: evaluate(); Watcher.evaluate ()} if (dep.target) {watcher.depend(); } return watcher.value } }, set: userDef.set || noop }); } Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); // Execute the value function of watcher, return the result of the value function, and add watcher to the MSG subscription array this.dirty = false; //this.dirty is set to false for caching. };Copy the code

Computed Watcher has an attribute, dirty, that marks whether a value function is performed.

1, Initialize watcher with watcher. Dirty = watcher. Lazy, set to true. The first time the page accesses newMsg it executes watcher.evaluate()

2. After the value is selected, watcher.dirty = false. The next time the page is evaluated, it returns the calculated value watcher.value.

If the MSG subscribed by watcher changes, watcher update() will be notified. A watcher with lazy true executes the update method watcher.dirty = true, so that the page value newMsg is reexecuted, returning the new value. This enables computed caching.

Watcher.prototype.update = function update () { if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); }};Copy the code

user watcher

watch:{
   msg(newValue,oldValue){
      console.log(newValue,oldValue)
   }       
},
Copy the code

Or this:

mounted(){
   this.$watch('msg',function(newValue,oldValue){
       console.log(newValue,oldValue)
   })
}
Copy the code

The core method of user Watcher is vm.$watch:

$ue.prototype.$watch = function (expOrFn,cb,options) {//expOrFn --> MSG --> url --> url --> url --> url --> url --> url --> url --> url --> url --> url --> url --> url --> url; function(oldValue,newValue){console.log(oldValue,newValue)} var watcher = new Watcher(vm, expOrFn, cb, options); }; }Copy the code

ExpOrFn is an expression, unlike expOrFn for rendered watcher or computed watcher.

// Check whether (typeof expOrFn === 'function') {this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); }Copy the code

When the user Watcher is created, it is evaluated based on this expression, adding the watcher to the subscription array.

expOrFn: 'msg'   -----> vm.msg
expOrFn: 'obj.a'  -----> vm.obj ----->vm.obj.a
Copy the code

When deep:true, the value of the current attribute is recursively iterated, adding watcher to all attributes, and executing watcher.update() each time an attribute is modified.

Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; value = this.getter.call(vm, vm); if (this.deep) { traverse(value); // Recursively iterate over the value, adding the watcher to the subscription array of the value property each time the value is added. } popTarget(); return value };Copy the code

Vue source code series article:

The responsive principle of VUe2.0

Vue compilation process analysis

Vuex principle from shallow to deep handwriting vuex

The VUE component goes from building a VNode to generating a real tree of nodes