Attribute descriptor

Object. DefineProperty (attribute descriptor) is a core API in Vue, which implements property proxying and data hijacking to make data responsive.

Object. DefineProperty syntax

Object.defineProperty( object, prop, descriptor )

  • Object: required, target object
  • Prop: Required, property name that needs to be defined or modified
  • descriptor: Required, property of the target property
    • Value: Indicates the value of the target attribute. The default value is undefined
    • Writable: indicates whether the target attribute can be overwrited. true: indicates that the attribute can be overwrited. false: indicates that the attribute cannot be overwrited. the default value is false
    • enumberable: Whether the target attribute is enumerable (yesfor... in.Object.keys()), true: enumerable false: not enumerable The default value is false
    • The writable, Enumberable, and 64x property of the target can be deleted and reset without any additional control system. The target property cannot be deleted or the property property cannot be reset Default is false
    • get:function(){… } : When this property is accessed, the get method is triggered. The get method returns the value as the value of the property
    • set:function(value){… } : When the value of this property is modified, the set method is fired. The new value of the property is passed in as an argument to the function

Note: When using the get and set methods, writable and value are not allowed

const person = {}
let initName = 'brother'
ObjectDefineProperty (obj,"name", {get() {
    return initName  
  },
  set(newVal) {
    initName = newVal
  }
})
console.log(person.name)   // 'brother'
person.name = 'brother'
console.log(person.name,initName)   // 'brother' 'brother'
Copy the code

Implementing the data broker

In Vue, we can access all the properties of Data through the instance because the Vue instance proxies the data property.

class Vue {
  constructor(options) {
    // Attach the configuration item to the Vue instance
    this.$options = options
    this._data = options.data
    // Initialize data with initData (mount data attribute to Vue instance)
    this.initData()
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    // Traversing data will block all data fields to the Vue instance
    for (let i= 0; i<keys.length; i++) {
      Object.defineProperty(this,key[i],{
        enumberable: true.configurable: true.set: function proxySetter(newVal) {
          data[keys[i]] = newVal
        },
        get: function prosyGetter() {
          return data[keys[i]]
        }
      })
    }
  }
}
const VM = new Vue({data: {name:"Zhang"}})
console.log(VM)
VM.name = 'bill'       // Change the value of the data attribute name
console.log(VM.name)   // 'I'
Copy the code

Implementing data hijacking

In the set and GET properties of Vue instance properties, you can hijack the read and write operations of each property, and then do some special logic to achieve responsiveness.

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    // Data broker.// Data hijacking
    // Note: The data hijacking operation will be reused later, so it is implemented separately, not with the data broker for loop
    for (let i= 0; i<keys.length; i++) {
      let value = data[keys[i]]
      Object.defineProperty(this,key[i],{
        enumberable: true.configurable: true.set: function reactiveSetter(newVal) {
          if(newVal===data[keys[i]]) return // Terminate the logic with the same value as before
          console.log(`${keys[i]}It's given a new value${newVal}`)
          value = newVal
        },
        get: function reactiveGetter() {
          console.log(` access${keys[i]}The value of the `)
          return data[keys[i]]
        }
      })
    }
  }
}
const VM = new Vue({data: {name:"Zhang"}})
console.log(VM)
VM.name = 'bill'       // name is given a new value -- li Si
Copy the code

Recursive deep data hijacking

The above operation only realizes the hijacking of one-dimensional data. If a data is a complex data type, the read and write of the internal attributes of the data cannot be hijacked. So we need to recursively hijack the data. That is:

const VM = new Vue({
  data: {person: {name:"Zhang"}}})console.log(VM)
VM.person = 'bill'       // person is given a new value -- Li Si
VM.person.name = 'bill'  // The setter for name cannot hijack data
Copy the code
class Vue {...initData(){...// Data hijacking
    observe(data)
  }
}
// 1. Observe the data type and create an Observer instance
function observe(data) {
  // 1.1 Returns if the observed data is of a basic type
  const type = Object.prototype.toString.call(data)
  if(type ! = ='[object Object]'|| (type ! = ='[object Array]')) return
  // 1.2 Observation Data involves some complex logic to encapsulate this process as an Observer class
  new Observer(data)
}
// 2, Observer class: used to observe data, generate complex logic responsible for handling dependencies Dep instances, etc
class Observer {
  constructor(data) {
    this.walk(data)
  }
  walk(data) {
    let keys = Object.keys(data)
    // 2.1 Put data hijacking operations in initData here
    // Iterate over data, hijacking all attributes passed to data
    for (let i= 0; i<keys.length; i++) {
      defineReactive(data,keys[i],data[keys[i]])
    }
  }
}
// defineReactive utility function: used to recursively hijack data and turn it into responsive data
defineReactive(object,key,value){
  // 3.1 recursively call defineReactive to recursively hijack deep data defineReactive--observe--Observer--defineReactive
  observe(obj[key])
  // 3.2 data hijacking
  Object.defineProperty(object,key,{
    enumberable: true.configurable: true.// Note: Do not access the object property through object in the setter/getter of the object property or you will get stuck in an infinite loop
    set: function reactiveSetter(newVal) {
      if(newVal===value) return // Terminate the logic with the same value as before
      console.log(`${keys[i]}It's given a new value${newVal}`)
      // Update the view
      value = newVal
    },
    get: function reactiveGetter() {
      console.log(` access${keys[i]}The value of the `)
      return value
    }
  })
}
Copy the code

Logical combing

letOption = {person: {name:"Zhang"}}let vm = new Vue(option)
Copy the code

1. New Vue instance

At this stage, option instances, option.data, etc., are mounted to the Vue sample, and methods such as initData are called to complete the initialization of the relevant data

2. Data Initialization

This stage is implemented in initData method, which will complete data initialization operations such as 1. Traverse option.data, mount data data such as Person to Vue instance 2

3, Determine the data type, new Observer instance

In this stage, observe the parameter data through the observe method. If the data type is complex, observe the data through the new Observer instance. The argument passed in the initial call is option.data

4. Hijacking data

In this phase of the Observe class instantiation, we first call the instance Walk method to iterate over the incoming data and call defineReactive to add reactive logic to each data

5. Responsiveness of data

In the defineReactive function at this stage, observe is called to pass in data first to realize the recursive hijacking of deep-seated data and the responsive logic of adding data in set/ GET

The realization of the Watcher

Watcher can listen for changes to data and trigger various callbacks, such as template update callbacks, calculated property update callbacks, and so on. Since template updates involve template compilation and VDOM, the Watcher implementation is tested with the Watch option and $watch

1. Add Watcher with option

var vm = new Vue({
  data: {msg: 1
  },
  watch: {// When the data. MSG value changes, the callback is executed with both the changed value and the previous value of data. MSG.
    msg(val, oldVal){... }}})Copy the code

2. Call the instance method to add Watcher

var callback = function(val, oldVal) {... }var unwatch = vm.$watch('msg',callback)
// Remove the listener
unwatch('msg',callback)
Copy the code

Implementation approach

Add an event center for each reactive data through which the Watcher is collected. When responsive data changes, it notifies Watcher of updates through the event center. In Vue, the function of the event center is implemented using the abstract class Dep.

Dep class

Add a Dep instance for each responsive Data and save, collect, and distribute watcher through the Dep.

// defineReactive utility function: used to recursively hijack data and turn it into responsive data
function defineReactive(obj, key, value) {
  // Recursively call defineReactive to recursively hijack deep data defineReactive--observe--Observer--defineReactive
  observe(obj[key])
  // create a new Dep instance for each data and maintain it through closures
  let dep = new Dep()
  // Data hijacking
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4 Dep assigns dependency updates
      dep.notify(newVal,value)
      value = newVal
    },
    get: function reactiveGetter() {
      // 4.5. Dep collects dependencies
      dep.depend()
      return value
    }
  })
}
// 4, Dep abstract class: responsible for collecting dependencies, notification of dependency updates, etc
class Dep {
// 4.1 subs is used to save all subscribers
  constructor(option) { this.subs = []}
  The Depend method is used to collect subscriber dependencies
  depend() { this.subs.push(/** New callback */)}
  // the notify method is used to send subscribers updates
  notify(newVal,value) {
    this.subs.forEach(watcher= > watcher.update(newVal,value))
  }
}
Copy the code

Watcher class

The logic involved in each subscriber callback is complex, so it is isolated into the Watcher class.

// Class Dep: collects dependencies, notifydependency updates, etc
class Dep {
  constructor(option) {
    // 4.1 subs is used to save all subscribers
    this.subs = []
  }
  The Depend method is used to collect subscriber dependencies
  depend() {
    // 5.5 if the Watcher instance is initialized
    if (Dep.target) {
      // 5.6, set dep. target and trigger the getter for each data Watcher instance to complete the dependency collection
      this.subs.push(Dep.target)
    }
  }
  // the notify method is used to send subscribers updates
  notify(newVal,value) {
    Execute Watcher's run method for each subscriber to complete the update
    this.subs.forEach(watcher= > watcher.run(newVal,value))
  }
}
// The Watcher class triggers dependency collection and handles update callbacks
class Watcher {
  constructor(vm, exp, cb) {
    Mount Vue instance, data attribute name, and update callback to Watcher instance
    this.vm = vm
    this.exp = exp
    this.cb = cb
    // 5.2, trigger the getter for data to complete the dependency collection
    this.get()
  }
  get() {
    // set the Watcher instance as the target object for Dep dependency collection
    Dep.target = this
    Trigger data getter interceptor
    this.vm[this.exp]
    // Clear the dependent target object
    Dep.target = null
  }
  run(newVal,value) {
    this.cb.call(this.vm,newVal,value)
  }
}
Copy the code

Implement vm. $watch

Subscription callbacks are added dynamically in the Vue class via the $watch instance method

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
  }
  initData() {....}  
  // add subscription callback dynamically
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
}
/ / test
let vm = new Vue({ data: {message: 111} })
vm.$watch("message".function (val, oldVal) {
   console.log("Message value changed", val, oldVal);
})
vm.message = 22  // Print message value changed by 22 111
Copy the code

Implementation option. Watch

Subscription callbacks are added dynamically in the Vue class through the initWatch instance method

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    this.initWatch()
  }
  initData() {....}  
  initWatch() {
    const watches = this.$options.watch
    // The watch option exists
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  // add subscription callback dynamically
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
}
/ / test
let vm = new Vue({
  data: {
    name: "Ha ha ha.".age: 13
  },
  watch: {
    age(newVal, val) {console.log("age", newVal, val); },name(newVal, val) {console.log("name", newVal, val); } } }) vm.age =22
 vm.name = "Test"
Copy the code

Asynchronous Watcher

The Watcher callback is executed asynchronously in Vue source code.

Why is the Watcher callback set to execute asynchronously?

  • A watcher executed asynchronously avoids the watch callback being executed first
// When watcher calls back to sync
// Mounted If the value of this.age is modified, the age watch callback will be triggered, and the value of this.name will be modified
let vm = new Vue({
  data: {
    name: "Ha ha ha.".age: 13
  },
  watch: {
    age(newVal, val) {
       this.name = 'bill'}},mounted() {
     this.age = 20 
     console.log(this.name)  // 'I'}})Copy the code
  • 2. Avoid triggering the watch callback multiple times, which is conducive to performance optimization
// When the watcher callback is synchronized, the data monitored by watch is frequently modified. The watch callback will be triggered multiple times. Performance is wasted.
let vm = new Vue({
  data: {
    name: "Ha ha ha.".age: 13
  },
  watch: {
    age(newVal, val) {
       this.name = 'bill'}},mounted() {
     this.age = 20 
     this.age = 21 
     this.age = 22
     this.age = 23}})Copy the code

Implementation approach

let watcherId = 0
// Watcher task queue
let watcherQueue = []
// The Watcher class triggers dependency collection and handles update callbacks
class Watcher {
  constructor(vm, exp, cb) {
    Mount Vue instance, data attribute name, and update callback to Watcher instance
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 5.2, trigger the getter for data to complete the dependency collection
    this.get()
  }
  get() {
    // set the Watcher instance as the target object for Dep dependency collection
    Dep.target = this
    Trigger data getter interceptor
    this.vm[this.exp]
    // Clear the dependent target object
    Dep.target = this
  }
  run(newVal, value) {
    // 5.8 If the task already exists in the task queue, the task is terminated
    if (watcherQueue.indexOf(this.id)! = = -1) return
    // 5.9 Add the current watcher to the queue
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() = > {
      this.cb.call(this.vm, newVal, value)
      // 5.10 The task is deleted from the task queue
      watcherQueue.splice(index, 1)}}}Copy the code

The realization of $set

What does $set do, and what problems does it solve?

<body>
  <script crossorigin="anonymous"
    integrity="sha512-pSyYzOKCLD2xoGM1GwkeHbdXgMRVsSqQaaUoHskx/HF09POwvow2VfVEdARIYwdeFLbu+2FCOTRYuiyeGxXkEg=="
    src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
  <script>
    let VM = new Vue({
      data: {
        person: {
          age: 10}},watch: {
        person() {
          console.log("Person has changed"); }}})// Pass the object. Property does not turn the property into responsive data, nor does it trigger the callback in Watch that listens on Person
    // vm.person. name = 'haha'
    // Through the $set of the Vue instance, you can add responsive attributes to the object and trigger a callback in watch that listens on Person
    // VM.$set(VM. Person, 'name', 'haha ')
  </script>
</body>
Copy the code

Implementation approach

Printing Vue instances we can see that each data has an __ob__ attribute. This __ob__ is the Observer instance, which we configured as a closure for each data and collected and distributed dependencies through the Dep instance. Here you can mount an Observer instance (__ob__) for each data data, modify the data data with $set, and issue data dependent callbacks.

So the implementation of $set is basically as follows:

  • In the generatedObserverInstance, also create a new oneDepInstance (event center), hanging inObserverInstance. thenObserverThe instance is mounted to the data data.
  • The triggergetter, not only willWatcherIn the closureDepCollect a copy of the instance, also in__ob__theDepA copy is also collected in the instance.
  • use$setIs triggered manually__ob__.Dep.notify()Distribute dependency updates.
  • Before calling notify, you need to call notifydefinedReactiveChange the new attribute to reactive.

Full source code as of this stage:

// 6.X is the implementation of $set related source code!!
class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    this.initWatch()
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    // Data broker
    for (let i = 0; i < keys.length; i++) {
      Object.defineProperty(this, keys[i], {
        enumerable: true.configurable: true.set: function proxySetter(newVal) {
          data[keys[i]] = newVal
        },
        get: function proxyGetter() {
          return data[keys[i]]
        },
      })
    }
    // Data hijacking
    observe(data)
  }
  initWatch() {
    const watches = this.$options.watch
    // The watch option exists
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
  // 6.6 __ob__ mount, dependency collection is complete
  $set(targt,key,value) {
   constoldValue = {... targt}// 6.7 Makes the new attribute passed in also responsive
    defineReactive(targt,key,value)
    // 6.8 Manually sending dependency updates
    targt.__ob__.dep.notify(oldValue,targt)
  }
}
// 1. Observe the data type and create an Observer instance
function observe(data) {
  const type = Object.prototype.toString.call(data)
  // 1.1 Returns if the observed data is of a basic type
  if(type ! = ='[object Object]'&& (type ! = ='[object Array]')) return
  // 1.2 Observation Data involves some complex logic to encapsulate this process as an Observer class
  / / 1.2 new Observer (data)
  // 6.3 Return the Observer instance and receive it in defineReactive.
  if(data.__ob__) return  data.__ob__
  return new Observer(data)
}
// 2, Observer class: Observer/listener, used to observe data, generate complex logic responsible for handling dependencies Dep instances, etc
class Observer {
  constructor(data) {
    // 6.1 Mount a Dep instance for an Observer instance (event center)
    this.dep = new Dep()
    // 2.1 Change all attributes of data to responsive
    
    this.walk(data)
    // 6.2 Attach an Observer instance to the non-enumerable attribute __ob__ for external $set use
    Object.defineProperty(data, "__ob__", {
      value: this.enumerable: false.configurable: true.writable: true})}walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      // console.log("definedBeforer",keys[i]);
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
}
// defineReactive utility function: used to recursively hijack data and turn it into responsive data
function defineReactive(obj, key, value) {
  // 3.1 Recursively call defineReactive to recursively hijack deep data defineReactive--observe--Observer--defineReactive
  [key] / / 3.1 observe (obj)
  // 6.4 Receive Observer instances to collect dependencies on Watcher for the attribute Dep
  let childOb = observe(obj[key])
  // create a new Dep instance for each data and maintain it through closures
  let dep = new Dep()
  // 3.2 Data hijacking the key of the current data object
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4 Dep assigns dependency updates
      dep.notify(newVal, value)
      value = newVal
    },
    get: function reactiveGetter() {
      Closure Dep collection relies on Watcher
      dep.depend()
      The observe function does not return an Observer instance if the data is of a simple type. If yes, collect a dependency for the Dep of the Observer instance
      if(childOb) childOb.dep.depend()
      return value
    }
  })
}
// Dep class: event center, responsible for collecting dependencies, notifying dependency updates, etc
class Dep {
  constructor(option) {
    // 4.1 subs is used to save all subscribers
    this.subs = []
  }
  The Depend method is used to collect subscriber dependencies
  depend() {
    // 5.5 if the Watcher instance is initialized
    if (Dep.target) {   
      // 5.6, set dep. target and trigger the getter for each data Watcher instance to complete the dependency collection
      this.subs.push(Dep.target)
    }
  }
  // the notify method is used to send subscribers updates
  notify(newVal, value) {
    Execute Watcher's run method for each subscriber to complete the update
    this.subs.forEach(watcher= > watcher.run(newVal, value))
  }
}
let watcherId = 0
// Watcher task queue
let watcherQueue = []
// 5, Watcher class: subscriber that triggers dependency collection and handles update callbacks
class Watcher {
  constructor(vm, exp, cb) {
    Mount Vue instance, data attribute name, and update callback to Watcher instance
    this.vm = vm
    this.exp = exp
    this.cb = cb
    // console.log("watchID",watcherId);
    this.id = ++watcherId
    // 5.2, trigger the getter for data to complete the dependency collection
    this.get()
  }
  get() {
    // set the Watcher instance as the target object for Dep dependency collection
    Dep.target = this
    Trigger data getter interceptor
    this.vm[this.exp]
    // Clear the dependent target object
    Dep.target = null
  }
  run(newVal, value) {
    // 5.8 If the task already exists in the task queue, the task is terminated
    if (watcherQueue.indexOf(this.id)! = = -1) return
    // 5.9 Add the current watcher to the queue
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() = > {
      
      this.cb.call(this.vm, newVal, value)
      // 5.10 The task is deleted from the task queue
      watcherQueue.splice(index, 1)}}}Copy the code

Implementation of the array processing

Vue2.X has some flaws in its handling of arrays

Possible problems with hijacking an array by array subscript:

Listening subscript callback disorder: using array subscripts to hijack an array may cause listening subscript callback disorder when the order of array elements changes.

let arr = [
  {name:"0"},
  {name:"1"},
  {name:"2"}]// It is possible to hijack each element by array subscript during initialization, but inserting a new element before each subscript 1 causes listening subscript callbacks to be corrupted.
// The callback that arR [1] used to handle element {name:2} becomes the callback that handles the insertion of new elements.
Copy the code

Wasteful performance: An arraylist may contain dozens, hundreds, or even thousands of items when the user is actually manipulating only a dozen items of data. This is obviously out of proportion to our efforts. As Yudhoyono has mentioned, the cost of hijacking all the data in an array is far out of proportion to our benefits. So Vue 2.x abandoned this approach.)

Source code for the array processing

In the source code, there is no use of array subscript to hijack the array, the array to do the following processing 1, array dependency callback collection is also through __ob__.dep implementation. __ob__.dep.notify is manually triggered when the array calls methods like push, pop, etc. 2. On the array prototype object, we insert a new object as the middle layer. When a user calls a method on an array prototype object, it goes through the middle layer first. We can intercept it by executing the original method and dispatching the dependency update after triggering __ob__.dep.notify(). Change each element of an array to a responsive transform array method:

// 7.x is the new code to implement the transform array
class Observer {
  constructor(data) {
    // 6.1 Mount a Dep instance for an Observer instance (event center)
    this.dep = new Dep()
    // The 7.5 array cannot call walk because walk hijacks subscripts via defineProperty, resulting in dependency callback errors, etc
    if(Array.isArray(data)) {
      // 7.6 Overwrite the prototype object with our modified array prototype
      data.__proto__ = ArrayMethods
    }else {
      // 2.1 Change all attributes of data to responsive
      this.walk(data)
    }
    // 6.2 Attach an Observer instance to the non-enumerable attribute __ob__ for external $set use
    Object.defineProperty(data, "__ob__", {
      value: this.enumerable: false.configurable: true.writable: true})}walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      // console.log("definedBeforer",keys[i]);
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
}
// 7.0 gets the array prototype object
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// Declare an array method that needs to be modified
const methods = ['push'.'pop']
// 7.2 Modify the array method
methods.forEach(method= >{
  ArrayMethods[method] = function (. args) {
    const oldValue = [...this]  
    // 7.3 Pass parameters to execute the original method
    const result = Array.prototype[method].apply(this,args)
    // 7.4 Sending dependency updates
    this.__ob__.dep.notify(oldValue,this)
    return result
  }
})
Copy the code

Change array elements and inserted new data to reactive:

class Observer {
  constructor(data) {
    // 6.1 Mount a Dep instance for an Observer instance (event center)
    this.dep = new Dep()
    // The 7.5 array cannot call walk because walk hijacks subscripts via defineProperty, resulting in dependency callback errors, etc
    if(Array.isArray(data)) {
      // 7.6 Overwrite the prototype object with our modified array prototype
      data.__proto__ = ArrayMethods
      // 7.7 Make all the children of the array responsive
      this.observeArray(data)
    }else {
      // 2.1 Change all attributes of data to responsive
      this.walk(data)
    }
    // 6.2 Attach an Observer instance to the non-enumerable attribute __ob__ for external $set use
    Object.defineProperty(data, "__ob__", {
      value: this.enumerable: false.configurable: true.writable: true})}walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      // console.log("definedBeforer",keys[i]);
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
  // 7.8 Makes all children of the passed array responsive
  observeArray(arr) {
   for (let i = 0; i < arr.length; i++) {
    observe(arr[i])
   }
  }
}
// 7.0 gets the array prototype object
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// Declare an array method that needs to be modified
const methods = ['push'.'pop']
// 7.2 Modify the array method
methods.forEach(method= >{
  ArrayMethods[method] = function (. args) {
    const oldValue = [...this] 
    // 7.9 Change the newly inserted data to responsive
     if(method==='push') {this.__ob__.observeArray(args)
     }
    // 7.3 Pass parameters to execute the original method
    const result = Array.prototype[method].apply(this,args)
    // 7.4 Sending dependency updates
    this.__ob__.dep.notify(oldValue,this)
    return result
  }
})
Copy the code

Implementing computed properties

The characteristics of

1, it is a function, its value is the result of the operation of the function. 2, the change of any data used to calculate the property will cause the change of the calculated property. 3, the calculated property does not exist in the data, need to be initialized separately. 4. The calculated property is read-only and cannot be modified. It doesn’t have setters. 5. The calculated property is lazy, and it doesn’t recalculate immediately when the data it depends on changes, only when you retrieve the calculated property. 6. The calculated attributes are cached. When the data on which they depend has not changed, the results of the calculated attributes will not be retrieved, but the previous calculation results will be used.

Implementation approach

The calculated property itself is also a Watcher callback, except that it may depend on multiple properties. When Watcher is initialized, the second parameter is passed in the key name representing the properties that Watcher depends on. We can pass in and execute a function that evaluates a property, which triggers getters for multiple dependent properties in the evaluated property and collects watcher callbacks for the evaluated property.

Preliminary implementation (8.0-8.8):

// This phase code is 8.0 -- 8.8
class Vue {
  constructor(options){...// 8.4 Both the initialization of computed properties and the initialization of data must be placed before the initialization of Watch, because watch can detect them only after the initialization of computed properties and data is completed.
    this.initComputed()
    this.initWatch()
  }
  ...
  // 8.3 Initializes the calculated properties separately
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        // 8.5 The second argument is passed to the computed attribute function
        const watcher = new Watcher(this,  computeds[keys[index]],function() {})// 8.6 mount the Watcher to the Vue instance
        Object.defineProperty(this,keys[index],{
          enumerable: true.configurable: true.// 8.7 Does not allow users to modify computing attributes
          set:function computedSetter() {
            console.warn("Please do not modify the calculated properties")},// 8.8 evaluates using watcher's get method and returns the result of the evaluation
          get:function computedGetter() {
            watcher.get()
            returnwatcher.value } }) } } } ... }...let watcherId = 0
// Watcher task queue
let watcherQueue = []
// 5, Watcher class: subscriber that triggers dependency collection and handles update callbacks
class Watcher {
  constructor(vm, exp, cb) {
    Mount Vue instance, data attribute name, and update callback to Watcher instance
    this.vm = vm
    this.exp = exp
    this.cb = cb
    // console.log("watchID",watcherId);
    this.id = ++watcherId
    // 5.2, trigger the getter for data to complete the dependency collection
    this.get()
  }
  get() {
    // set the Watcher instance as the target object for Dep dependency collection
    Dep.target = this
    Check whether a function is passed when evaluating attributes before collecting dependencies
    if(typeof this.exp === 'function') {// 8.2 Execute the function and evaluate
      this.value = this.exp.call(this.vm)
    }else {
      Trigger data getter interceptor
      this.value = this.vm[this.exp]
    }
    // Clear the dependent target object
    Dep.target = null
  }
  run(newVal, value) {
    // 5.8 If the task already exists in the task queue, the task is terminated
    if (watcherQueue.indexOf(this.id)! = = -1) return
    // 5.9 Add the current watcher to the queue
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() = > {
      this.cb.call(this.vm, newVal, value)
      // 5.10 The task is deleted from the task queue
      watcherQueue.splice(index, 1)})}}...Copy the code

Caching and lazy processing of calculated attributes (8.9-8.15):

Lazy = true we make an identifier for the watcher that evaluates the property this.lazy = true, which means that the watcher is lazy. We also add a this.dirty identifier to indicate that the dependency of the calculated property has changed and that the dirty value of the calculated property must be reevaluated and cannot be used in the last calculation.

class Vue {
  constructor(options){...// 8.4 Both the initialization of computed properties and the initialization of data must be placed before the initialization of Watch, because watch can detect them only after the initialization of computed properties and data is completed.
    this.initComputed()
   ...
  }
  // 8.3 Initializes the calculated properties separately
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        // 8.5 The second argument is passed to the computed attribute function
        // 8.15 The watcher that calculates attribute initialization needs to be marked lazy
        const watcher = new Watcher(this,  computeds[keys[index]],function() { },{lazy:true})
        // 8.6 mount the Watcher to the Vue instance
        Object.defineProperty(this,keys[index],{
          enumerable: true.configurable: true.// 8.7 Does not allow users to modify computing attributes
          set:function computedSetter() {
            console.warn("Please do not modify the calculated properties")},// 8.8 evaluates using watcher's get method and returns the result of the evaluation
          get:function computedGetter() {
            // 8.9 Re-evaluate if only watcher is dirty
            if(watcher.dirty) {
              watcher.get()
              // 8.10 Update the dirty status
              watcher.dirty = false
            }
            return watcher.value
          }
        })
      }
    }
  }
}
// Dep class: event center, responsible for collecting dependencies, notifying dependency updates, etc
class Dep {
  constructor(option) {
    // 4.1 subs is used to save all subscribers
    this.subs = []
  }
  The Depend method is used to collect subscriber dependencies
  depend() {
    // 5.5 if the Watcher instance is initialized
    if (Dep.target) {
      // 5.6, set dep. target and trigger the getter for each data Watcher instance to complete the dependency collection
      this.subs.push(Dep.target)
    }
  }
  // the notify method is used to send subscribers updates
  notify(newVal, value) {
    Execute Watcher's run method for each subscriber to complete the update
    // 8.12 Depend on update Before sending an update, check whether the update is required
    this.subs.forEach(watcher= > watcher.update(newVal, value))
  }
}
let watcherId = 0
// Watcher task queue
let watcherQueue = []
// 5, Watcher class: subscriber that triggers dependency collection and handles update callbacks
class Watcher {
  constructor(vm, exp, cb,option = {}) {
    // 8.13 watcher Added the new parameter option to set watcher by default
    this.lazy = this.dirty = !! option.lazyMount Vue instance, data attribute name, and update callback to Watcher instance
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 8.14 Lazy Watcher initialization does not require collecting dependencies
    if(! option.lazy) {// 5.2, trigger the getter for data to complete the dependency collection
      this.get()
    }
  }
  get() {
    // set the Watcher instance as the target object for Dep dependency collection
    Dep.target = this
    Check whether a function is passed when evaluating attributes before collecting dependencies
    if(typeof this.exp === 'function') {// 8.2 Execute the function and evaluate
      this.value = this.exp.call(this.vm)
    }else {
      Trigger data getter interceptor
      this.value = this.vm[this.exp]
    }
    // Clear the dependent target object
    Dep.target = null
  }
  8.11 Call update before run to determine whether to run directly
  update(newVal, value) {
    Do not run when the current watcher is lazy. Instead, it marks Watcher as dirty data and waits for the user to fetch the results before running
    if(this.lazy) {
      this.dirty = true
    }else {
      thiss.run(newVal, value)
    }
  }
  run(newVal, value) {
    // 5.8 If the task already exists in the task queue, the task is terminated
    if (watcherQueue.indexOf(this.id)! = = -1) return
    // 5.9 Add the current watcher to the queue
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() = > {
      this.cb.call(this.vm, newVal, value)
      // 5.10 The task is deleted from the task queue
      watcherQueue.splice(index, 1)}}}Copy the code

The test was fine and did not recalculate without changing the data on which the calculated properties depended.

Current bug in calculating properties:

Problem description

When we change the dependency (person) on the evaluated property (x), neither the evaluated property nor the Watch callback that listens on the evaluated property is fired.

  • Our code
  <script src="./index.js"></script>
  <script>
    let vm = new Vue({
      data: {
        person: {
          name: "Zhang"}},watch: {
        x(oldValue, newValue) {
          console.log("X listening trigger"); }},computed: {
        x() {
          console.log("X calculation trigger");
          return JSON.stringify(this.person)
        }
      }
    })
  </script>
Copy the code

` `

  • In Vue, there are no problems
  <script crossorigin="anonymous"
    integrity="sha512-pSyYzOKCLD2xoGM1GwkeHbdXgMRVsSqQaaUoHskx/HF09POwvow2VfVEdARIYwdeFLbu+2FCOTRYuiyeGxXkEg=="
    src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
  <script>
    let vm = new Vue({
      data: {
        person: {
          name: "Zhang"}},watch: {
        x(oldValue, newValue) {
          console.log("X listening trigger"); }},computed: {
        x() {
          console.log("X calculation trigger");
          return JSON.stringify(this.person)
        }
      }
    })
  </script>
Copy the code

Problem a

It is convenient to explain here that we name the calculated attribute and watcher in Watch as Watcher 1 and Watcher 2 respectively.

The callback to Watcher 1 has two prerequisites: the data it depends on changes to dirty data and the watcher is evaluated. We changed the dependency but did not evaluate it. Watcher 2 listens on Watcher 1. When watcher 2 is initialized, watcher 1 is evaluated first.

When the Vue is initialized, Person collects a copy of the # 1 Wtacher in Person.dep. When the value of person changes to notify, watcher should be evaluated once.

// 9.0 calls watcher's get method and evaluates it
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    // 8.13 watcher Added the new parameter option to set watcher by default
    this.lazy = this.dirty = !! option.lazyMount Vue instance, data attribute name, and update callback to Watcher instance
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 8.14 Lazy Watcher initialization does not require collecting dependencies
    if(! option.lazy) {// 5.2, trigger the getter for data to complete the dependency collection
      this.get()
    }
  }
  get() {
    // set the Watcher instance as the target object for Dep dependency collection
    Dep.target = this
    Check whether a function is passed when evaluating attributes before collecting dependencies
    if (typeof this.exp === 'function') {
      // 8.2 Execute the function and evaluate
      this.value = this.exp.call(this.vm)
    } else {
      Trigger the data getter interceptor to evaluate it
      this.value = this.vm[this.exp]
    }
    // Clear the dependent target object
    Dep.target = null
  }
  8.11 Call update before run to determine whether to run directly
  update(newVal, value) {
    Do not run when the current watcher is lazy. Instead, it marks Watcher as dirty data and waits for the user to fetch the results before running
    if (this.lazy) {
      this.dirty = true
    } else {
      thiss.run(newVal, value)
    }
  }
  run(newVal, value) {
    // 5.8 If the task already exists in the task queue, the task is terminated
    if (watcherQueue.indexOf(this.id) ! = = -1) return
    // 5.9 Add the current watcher to the queue
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() = > {
      // 9.0 relies on updates to evaluate the watcher property to solve the problem of not triggering the calculation attribute watcher
      this.get()
      this.cb.call(this.vm, newVal, value)
      // 5.10 The task is deleted from the task queue
      watcherQueue.splice(index, 1)}}}Copy the code

Question 2

When we change the dependent person value for the computed property X, we find that the watch callbacks for the computed property and the watch callbacks for the computed property are still not fired. Let’s print the Person in Vue and the person in our code respectively to check whether there is any problem with the phone they depend on

The normal process

  • The VM is initialized, and the DEP of vm. Person collects dependencies, one watcher and two watcher in turn.
  • Vm. Person = {name:’ person ‘}, vm. Person = {name:’ person ‘}, vm. The DEP of vm.person updates all watcher with notify
  • Because the evaluation property watcher 1 is lazy, calling the get method of watcher 1 does not evaluate. Just mark dirty on watcher 1 as true.
  • When watcher 2 is updated, get (evaluate watcher 1) is called first, and the dirty of Watcher 1 is true. Wathcer (calculate attribute x) is executed first to calculate the result, and then the callback of Watcher 2 (x callback in watch) is executed.

Our PERSON DEP only collects watcher number one (calculated property), not Watcher number two. This is also why the watch callbacks that evaluate properties and listen to evaluate properties are not fired

Question 3

Why didn’t we collect watcher Number two?

  • Look at our code, in initComputed, to generate watcher number one for the computed property X. Watcher initials the call to watcher.get(), mounts watcher 1 to dep.target in the get method, and executes vm.x.
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    this.lazy = this.dirty = !! option.lazythis.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    if(! option.lazy) {this.get()
    }
  }
  get() {
    Dep.target = this
    Check whether a function is passed when evaluating attributes before collecting dependencies
    if (typeof this.exp === 'function') {
      // 8.2 Execute the function and evaluate
      this.value = this.exp.call(this.vm)
    } else {
      Trigger the data getter interceptor to evaluate it
      this.value = this.vm[this.exp]
    }
    // Clear the dependent target object
    Dep.target = null}}Copy the code
  • In vm.x,this.person triggers the getter for Person, and Person. dep collects the number one watcher
function defineReactive(obj, key, value) {
  let childOb = observe(obj[key])
  let dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4 Dep assigns dependency updates
      dep.notify(newVal, value)
      value = newVal
    },
    get: function reactiveGetter() {
      Closure Dep collection relies on Watcher
      dep.depend()
      The observe function does not return an Observer instance if the data is of a simple type. If yes, collect a dependency for the Dep of the Observer instance
      if (childOb) childOb.dep.depend()
      return value
    }
  })
}
class Dep {
  constructor(option) {
    this.subs = []
  }
  The Depend method is used to collect subscriber dependencies
  depend() {
    // 5.5 if the Watcher instance is initialized
    if (Dep.target) {
      // 5.6, set dep. target and trigger the getter for each data Watcher instance to complete the dependency collection
      this.subs.push(Dep.target)
    }
  }
   ...
}
Copy the code
  • Execute initWatch after initComputed, and in initWatch new number two watcher. When initialized, watcher # 2 is first mounted to dep. target, and get is called to trigger the getter of vm.x.
class Vue {.../ / 1.0
  initWatch() {
    const watches = this.$options.watch
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        / / 1.1
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        const watcher = new Watcher(this, computeds[keys[index]], function () {}, {lazy: true })
        Object.defineProperty(this, keys[index], {
          ...
          Watcher 2's get method fires the computedGetter for Watcher 1
          get: function computedGetter() {
            if (watcher.dirty) {
              watcher.get()
              watcher.dirty = false
            }
            return watcher.value
          }
        })
      }
    }
  }
...
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    this.lazy = this.dirty = !! option.lazythis.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    if(! option.lazy) {// 2 2 watcher initializes the get method
      this.get()
    }
  }
  get() {
    // 1.3 The second watcher is mounted to dep.target
    Dep.target = this
    if (typeof this.exp === 'function') {
      this.value = this.exp.call(this.vm)
    } else {
      // 1.4 Watcher Number two triggers the getter for Watcher number one
      this.value = this.vm[this.exp]
    }
    Dep.target = null}... }Copy the code
  • When watcher 1 calls get, dep.targety is overridden by watcehr 1 and watcher 2. As a result, the DEP of Person failed to collect the number two Watcher

solution

  • When the watcher is initially mounted to dep. target, we should use a stack to save the Watcher. When a new watcher is generated and collected, pop the new watcher from the stack and mount the previous watcher to dep.target.
  • When DEP collects Watcher, Watcher collects DEP. When calculating the propertiesgetterWhen finished, check to see if dep. target has any uncollected Watcher. If there are uncollected watchers, the deP collected by the calculated property Watcher is notified to continue collecting the DEP.target

Complete code (9.0-9.12 code for this stage)

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    // 8.4 Both the initialization of computed properties and the initialization of data must be placed before the initialization of Watch, because watch can detect them only after the initialization of computed properties and data is completed.
    this.initComputed()
    this.initWatch()
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      Object.defineProperty(this, keys[i], {
        enumerable: true.configurable: true.set: function proxySetter(newVal) {
          data[keys[i]] = newVal
        },
        get: function proxyGetter() {
          return data[keys[i]]
        },
      })
    }
    observe(data)
  }
  initWatch() {
    const watches = this.$options.watch
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  // 8.3 Initializes the calculated properties separately
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        // 8.5 The second argument is passed to the computed attribute function
        // 8.15 The watcher that calculates attribute initialization needs to be marked lazy
        const watcher = new Watcher(this, computeds[keys[index]], function () {}, {lazy: true })
        // 8.6 mount the Watcher to the Vue instance
        Object.defineProperty(this, keys[index], {
          enumerable: true.configurable: true.// 8.7 Does not allow users to modify computing attributes
          set: function computedSetter() {
            console.warn("Please do not modify the calculated properties")},// 8.8 evaluates using watcher's get method and returns the result of the evaluation
          get: function computedGetter() {
            // 8.9 Re-evaluate if only watcher is dirty
            if (watcher.dirty) {
              watcher.get()
              // 8.10 Update the dirty status
              watcher.dirty = false
            }
            // 9.12 Determine in the getter of the calculated property whether there are more Watcher to collect
            if(Dep.target) {
              for (let i = 0; i < watcher.deps.length; i++) {
                // 9.13 Remove the DEP from watcher and continue collecting the remaining watcher
                watcher.deps[i].depend()
              }
            }
            return watcher.value
          }
        })
      }
    }
  }
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
  // 6.6 __ob__ mount, dependency collection is complete
  $set(targt, key, value) {
    constoldValue = { ... targt }// 6.7 Makes the new attribute passed in also responsive
    defineReactive(targt, key, value)
    // 6.8 Manually sending dependency updates
    targt.__ob__.dep.notify(oldValue, targt)
  }
}
function observe(data) {
  const type = Object.prototype.toString.call(data)
  if(type ! = ='[object Object]'&& (type ! = ='[object Array]')) return
  // 6.3 Return the Observer instance and receive it in defineReactive.
  if (data.__ob__) return data.__ob__
  return new Observer(data)
}
// 2, Observer class: Observer/listener, used to observe data, generate complex logic responsible for handling dependencies Dep instances, etc
class Observer {
  constructor(data) {
    // 6.1 Mount a Dep instance for an Observer instance (event center)
    this.dep = new Dep()
    // The 7.5 array cannot call walk because walk hijacks subscripts via defineProperty, resulting in dependency callback errors, etc
    if (Array.isArray(data)) {
      // 7.6 Overwrite the prototype object with our modified array prototype
      data.__proto__ = ArrayMethods
      // 7.7 Make all the children of the array responsive
      this.observeArray(data)
    } else {
      // 2.1 Change all attributes of data to responsive
      this.walk(data)
    }
    // 6.2 Attach an Observer instance to the non-enumerable attribute __ob__ for external $set use
    Object.defineProperty(data, "__ob__", {
      value: this.enumerable: false.configurable: true.writable: true})}walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      // console.log("definedBeforer",keys[i]);
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
  // 7.8 Makes all children of the passed array responsive
  observeArray(arr) {
    for (let i = 0; i < arr.length; i++) {
      observe(arr[i])
    }
  }
}
// defineReactive utility function: used to recursively hijack data and turn it into responsive data
function defineReactive(obj, key, value) {
  // 6.4 Receive Observer instances to collect dependencies on Watcher for the attribute Dep
  let childOb = observe(obj[key])
  // create a new Dep instance for each data and maintain it through closures
  let dep = new Dep()
  // 3.2 Data hijacking the key of the current data object
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4 Dep assigns dependency updates
      dep.notify(newVal, value)
      value = newVal
    },
    get: function reactiveGetter() {
      Closure Dep collection relies on Watcher
      dep.depend()
      The observe function does not return an Observer instance if the data is of a simple type. If yes, collect a dependency for the Dep of the Observer instance
      if (childOb) childOb.dep.depend()
      return value
    }
  })
}
9.1 Adding a stack to store depTarget
let targetStack = []
// Dep class: event center, responsible for collecting dependencies, notifying dependency updates, etc
class Dep {
  constructor(option) {
    // 4.1 subs is used to save all subscribers
    this.subs = []
  }
  // 9.7 After collecting the DEP, call dep.addSub to collect the Watcher
  addSub(watcher) {
    this.subs.push(watcher)
  }
  The Depend method is used to collect subscriber dependencies
  depend() {
    // 5.5 if the Watcher instance is initialized
    if (Dep.target) {
      // 5.6 For each data Watcher instance, the dep. target is set first and the getter for data is triggered to complete the dependency collection
      // this.subs.push(Dep.target)
      // 9.6 Watcher collects DEP
      Dep.target.addDep(this)}}// the notify method is used to send subscribers updates
  notify(newVal, value) {
    Execute Watcher's run method for each subscriber to complete the update
    // 8.12 Depend on update Before sending an update, check whether the update is required
    this.subs.forEach(watcher= > watcher.update(newVal, value))
  }
}
let watcherId = 0
// Watcher task queue
let watcherQueue = []
// 5, Watcher class: subscriber, trigger dependency collection, processing callback
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    // 8.13 watcher Added the new parameter option to set watcher by default
    this.lazy = this.dirty = !! option.lazyMount Vue instance, data attribute name, and processing callback to Watcher instance
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 9.8 watcher is used to save collected DEPs
    this.deps = []
    // 8.14 Lazy Watcher initialization does not require collecting dependencies
    if(! option.lazy) {// 5.2, trigger the getter for data to complete the dependency collection
      this.get()
    }
  }
  addDep(dep) {
    // 9.9 Since watcher may collect dePs multiple times per 9.0 evaluation, it terminates if it has already collected dePs
    if (this.deps.indexOf(dep) ! = = -1) return
    // 9.10 Collecting DEPs
    this.deps.push(dep)
    // 9.11 let DEP collect watcher
    dep.addSub(this)}get() {
    // 9.2 If deP collection depends on watcehr, add it to the stack first
    targetStack.push(this)
    // set the Watcher instance as the target object for Dep dependency collection
    Dep.target = this
    Check whether a function is passed when evaluating attributes before collecting dependencies
    if (typeof this.exp === 'function') {
      // 8.2 Execute the function and evaluate
      this.value = this.exp.call(this.vm)
    } else {
      Trigger the data getter interceptor to evaluate it
      this.value = this.vm[this.exp]
    }
    // 9.3 Let Watcher off the stack after evaluating and collecting dependencies
    targetStack.pop()
    // 9.4 Check whether there are uncollected watcher in the stack
    if (targetStack.length) {
      // 9.5 Get watcher at the top of the stack
      Dep.target = targetStack[targetStack.length - 1]}else {
      // Clear the dependent target object
      Dep.target = null}}8.11 Call update before run to determine whether to run directly
  update(newVal, value) {
    Do not run when the current watcher is lazy. Instead, it marks Watcher as dirty data and waits for the user to fetch the results before running
    if (this.lazy) {
      this.dirty = true
    } else {
      this.run(newVal, value)
    }
  }
  run(newVal, value) {
    // 5.8 If the task already exists in the task queue, the task is terminated
    if (watcherQueue.indexOf(this.id) ! = = -1) return
    // 5.9 Add the current watcher to the queue
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() = > {
      // 9.0 relies on updates to evaluate the watcher property to solve the problem of not triggering the calculation attribute watcher
      this.get()
      this.cb.call(this.vm, newVal, value)
      // 5.10 The task is deleted from the task queue
      watcherQueue.splice(index, 1)}}}// 7.0 gets the array prototype object
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// Declare an array method that needs to be modified
const methods = ['push'.'pop']
// 7.2 Modify the array method
methods.forEach(method= > {
  ArrayMethods[method] = function (. args) {
    const oldValue = [...this]
    // 7.9 Change the newly inserted data to responsive
    if (method === 'push') {
      this.__ob__.observeArray(args)
    }
    // 7.3 Pass parameters to execute the original method
    const result = Array.prototype[method].apply(this, args)
    // 7.4 Sending dependency updates
    this.__ob__.dep.notify(oldValue, this)
    return result
  }
})
Copy the code

There was no problem with the test.

Implement the compilation of templates

Having implemented Vue’s response test system, we now need to complete the response to the UI DOM when the data changes.

Let’s start with a little chestnut:

  • In the Vue constructor, we create a new watcher, and in the watcher, we manipulate the DOM binding vm.name
  • Since the second parameter of watcehr is an evaluation function, Watcher will execute the evaluation function first. In the evaluation function, the getter of vm.name is triggered, and the DEP of vm.name will collect the watcher. When vm.name is changed, vm.name.dep notifts Watcher that the update will execute the evaluation function again
class Vue {
  constructor(options){...this.initWatch()
    // Simple example: Vue initializes a new watcher to update Html via watcher
    new Watcher(this.() = > {
      document.querySelector("#app").innerHTML = `<p>The ${this.name}</p>`
    }, () = >{})}... }Copy the code

Testing:

The NAME attribute is modified. DOM is updated successfully.

Compilation of templates in Vue

In Vue, the watcher responsible for updating the DOM is called the Render Watcher, and its evaluation function is far more complex than ours

We have problems

  • The user can use template syntax, Vue instructions, etc., in the template, I need to process the template first and finally convert it into a function to update the DOM
  • It is expensive to update the DOM directly, so we need to update the DOM on demand

Virtual DOM

The virtual DOM is an abstraction layer of the real DOM, which can reduce unnecessary DOM manipulation and achieve cross-platform features. Vue introduces the Virtual DOM(VDOM). What is the Virtual DOM? In a nutshell, it’s a JS object that describes what the CURRENT DOM looks like. Each Vue instance has a render function vm.$options. Render, which the instance can use to generate the VDOM. When a Vue instance is passed DOM or template, it first converts the template string into a rendering function, a process known as compilation.

Parser

The Vue compilation process is roughly divided into three steps

  • The first step is to convert the template string toElement ASTsThe process is throughThe parserThe implementation.
  • The second stepElement ASTsMarking static nodes is mainly used to optimize virtual DOM rendering. This process passesThe optimizerimplementation
  • The third step will beElement ASTsconvertrenderFunction body, the process is throughCode generatorimplementation

AST

AST: Abstract syntax tree, which is a description of source code as it is converted from one type of code to another.

The Vue AST

Type indicates the type of the node. When type is 2, it indicates that the node uses variables. Expression records what variables are used

{
  children: [{...}],parent: {},
  tag: "div".type: 1.//1- element node 2- variable text node 3- plain text node,
  expression:'_s(name)'.//type if 2, return _s(variable)
  text:'{{name}}' // The string before the text node is compiled
}
Copy the code

There are two stages to generate an AST: lexical analysis and grammatical analysis

const a = 1

Lexical analysis:

Parse keywords such as const, a, =, 1 in your code and convert them to tokens.

Grammatical analysis:

After the token is processed and combined, the AST is generated in Vue. After the token is parsed, it is processed immediately.

Parse element nodes

This section uses simple HTML templates as an example and does not deal with complex cases such as V-if, V-show, V-for, single tag, and comment. Parsing element nodes, we can identify them with <, which can represent either a start tag or an end tag. If the start tag is, we add a layer to the ast tree hierarchy. If it is the end tag, it falls back to the last level in the AST tree hierarchy. Each layer also records its parent element. You also need a stack to keep track of the current level of the element, push the element onto the stack if it has a start tag, and push the element off the stack if it has an end tag. When it is a text node, the stack is not processed.

The sample

ElementASTs (ElementASTs)
{/ * * * * children: [{...}], * parent: {}, * tag: "div", * type: 1, //1- element node 2- text node with variable 3- plain text node, * expression:'_s(name)', //type if 2 returns _s(variable) * text:'{{name}}' // Text node before compilation string *} */
function parser(html) {
  // Stack: records the level of the current element
  let stack = []
  // The root element node
  let root = null
  // The parent node of the current element
  let currentParent = null
  // 1.0 continuously parses the template string
  while (html) {
    let index = html.indexOf("<")
    / / 2.1 if the element has a text node example: before the HTML = "{{name}} < div > 1 < / div > < / root >"
    if (index > 0) {
      // 2.2 Intercept the text before the label
      let text = html.slice(0, index)
      2.3 Push the literal node into the children of the parent element
      currentParent.children.push(element)
      // 2.4 Truncate the part that has been processed
      html = html.slice(index)
      / / 1.0 if to start tag Example: the HTML = "< root > {{name}} < div > 1 < / div > < / root >"
    } else if (html[index + 1]! = ='/') {
      // 1.1 Get the element type
      let gtIndex = html.indexOf(">")
      let eleType = html.slice(index + 1, gtIndex).trim()
      EleType = 'div id="app"' after processing: eleType = 'div'
      let emptyIndex = eleType.indexOf("")
      let attrs = {}
      if(emptyIndex ! = = -1) {
        // 1.3 Get the element tag attribute
        attrs = parseAttr(eleType.slice(emptyIndex + 1))
        eleType = eleType.slice(0, emptyIndex)
      }
      1.4 Creating an AST Node
      const element = {
        children: [],
        attrs,
        parent: currentParent,
        tag: eleType,
        type: 1
      }
      // 1.5 has no root element node
      if(! root) { root = element }else {
        // 1.6 Pushes the current element node into children of the parent element
        currentParent.children.push(element)
      }
      // 1.7 Parsing to the start of the element tag pushes the element up the stack
      stack.push(element)
      // 1.8 Updates the current parent element
      currentParent = element
      // 1.9 truncates the part that has been processed
      html = html.slice(gtIndex + 1)
      HTML = ""
    } else {
      let gtIndex = html.indexOf(">")
      // 3.1 Parse to the element's end tag hierarchy and unstack one
      stack.pop()
      // 3.2 Update the current parent element
      currentParent = stack[stack.length - 1]
      // 3.3 Cut off the part that has been processed
      html = html.slice(gtIndex + 1)}}return root
}
// Parse the tag attributes
function parseAttr(eleAttrs) {
  let attrs = {}
  attrString = eleAttrs.split("")
  attrString.forEach(e= > {
    if (e && e.indexOf("=")! = = -1) {
      const attrsArr = e.split("=")
      attrs[attrsArr[0]] = attrsArr[1]}else {
      attrs[e] = true}});return attrs
}
Copy the code

Test: No problem

<body>
  <div id="app">{{name}}<p>The first P tag</p>
    <p>The second P tag<i>The label I</i></p>
  </div>
  <script src="./parse.js"></script>
  <script>
    const ast = parser(document.getElementById("app").outerHTML)
    console.log(ast);
  </script>
</body>
Copy the code

Parse text nodes

In the previous parse of the element node, the literal node was completely pulled out without parse. So let’s do that separately. Here we use {{and}} as identifiers to convert the difference expression in the text to the form _S (name)

function parser(html) {...while (html) {
    ...
    if (index > 0) {
      // 2.2 Intercept the text before the label
      let text = html.slice(0, index)
      5.4 Call the parseText utility function to parse the text
      let element = parseText(text)
      // 5.5 Add parent attributes to text nodes
      element.parent = currentParent
      2.3 Push the literal node into the children of the parent element
      currentParent.children.push(element)
      ...
    } else if (html[index + 1]! = ='/') {... }}return root
}
// Parse text nodes
function parseText(text) {
  Unparsed text
  let originText = text
  // May be plain text or variable text default: plain text
  let type = 3
  // The text node of the element node may be composed of multiple segments
  / / example: < p > I {{name}}, I {{age}} < / p > token = [' my '{{name}},' my ', {{age}}]
  let token = []
  while (text) {
    let start = text.indexOf({{" ")
    let end = text.indexOf("}}")
    //4.0 If interpolation exists
    if(start ! = = -1&& end ! = = -1) {
      // 4.1 Marks the text node type as text with variables
      type = 2
      // 4.2 There is plain text before interpolation
      if (start > 0) {
        // 4.3 Advance token before interpolation in plain text
        token.push(JSON.stringify(text.slice(0, start)))
      }
      // 4.4 Get the expression in the interpolation
      let exp = text.slice(start + 2, end)
      // 4.5 Parse expressions and advance tokens
      token.push(`_s(${exp}) `)
      // 4.6 Cut off the part that has been processed
      text = text.slice(end + 2)
      // 5.0 There is no interpolation
    } else {
      // 5.1 Terminates text to push tokens directly
      token.push(JSON.stringify(text))
      text = ' '}}let element = {
    text: originText,
    type
  }
  // 5.3 If type is 2, the variable text node requires expression
  if (type === 2) {
    element.expression = token.join("+")}return element
}
Copy the code

test

<body>
  <div id="app">My {{name}}, MY year {{age}}<p>The first P tag</p>
    <p>The second P tag<i>The label I</i></p>
  </div>
  <script src="./parse.js"></script>
  <script>
    const ast = parser(document.getElementById("app").outerHTML)
    console.log(ast);
  </script>
</body>
Copy the code

Code generator

Previously we passed the template string (

) convert to the abstract syntax tree ASTs. Now we need to convert the abstract syntax tree to the render function using the code generator codeGen

View rendering functions in Vue source code

<body>
  <div>123
    <p>{{name}}</p>
  </div>
  <script crossorigin="anonymous"
    integrity="sha512-pSyYzOKCLD2xoGM1GwkeHbdXgMRVsSqQaaUoHskx/HF09POwvow2VfVEdARIYwdeFLbu+2FCOTRYuiyeGxXkEg=="
    src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
  <script>
    let vm = new Vue({
      el: "div".data: { name: "Zhang"}})console.log("Vue", vm.$options.render)
  </script>
</body>
Copy the code

function anonymous() {
   `with(this){ return _c('div',[_v("123\n "),_c('p',[_v(_s(name))])]) }`
}
// Insert this, change the execution of this in the function body, and any variables or functions in the function will be treated as properties or methods of this
// `with(this){
// return this._c('div',[this._v("123\n "),this._c('p',[this._v(this._s(this.name))])])
/ /} `

Copy the code

Render function body:

_c: Element node used to transform the virtual DOM

  • Parameter 1 (string) : The label name of the element node
  • Parameter 2 (array) : Optional, child node of element node

_v: used to convert the plain text node. _s: Used to get the value of a variable in the text

Convert AST to render function ideas

  • Recursive AST node that generates the following format string _C (tag name, tag attribute object, descendant array)
  • If you encounter a text node if it’s a plain text node it generates the string _v(text string)
  • _v(_s(variable name))
  • Create a with(this) method outside the function, passing in the context.

New codegen. Js

// Convert AST to render function body
/**{children: [{...}], parent: {}, tag: "div", type: 1, //1- element node 2- text node 3- text node, expression: '_s (name)', / / the type if is 2, it returns _s (variable) text: '{{name}} / / text string} * / node before compilation
function codegen(ast) {
  // The first layer of the 1.0 AST must be an element node
  let code = genElement(ast)
  return {
     // When the rendering function is executed, passing this to change the direction of this in the function body.
    render: `with(this){return ${code}} `}}// Convert the element node
function genElement(el) {
  // 2.1 Obtaining child nodes
  let children = genChildren(el)
  // 2.0 returns _c(tag name, tag attribute object, tag child node array)
   return `_c(The ${JSON.stringify(el.tag)}.The ${JSON.stringify(el.attrs)}.${children}) `
}
// Convert the text node
function genText(node) {
  // 5.0 Text nodes with variables
  if (node.type === 2) {
    // node.expression Any variable is evaluated by this.[node.expression]!!!!
    return `_v(${node.expression}) `
  }
  5.1 Plain text node
  return `_v(The ${JSON.stringify(node.text)}) `
}
// Determine the node to which the type is transferred
function genNode(node) {
  // 4.0 Check the node type
  if (node.type === 1) {
    return genElement(node)
  } else {
    return genText(node)
  }
}
// Convert the child node
function genChildren(node) {
  // 3.0 Checks whether there are child nodes
  if (node.children && node.children.length > 0) {
    // 3.1 Convert all child nodes [child node 1, child node 2... , recursively convert all child nodes genNode--genElement--genChildren--genNode
    return ` [${node.children.map(node => genNode(node))}] `}}Copy the code

Test: Importing the parser we completed earlier, the parsed AST generates the body of the rendering function through the code generator

<body>
  <div>The label before<p>I {{name}}</p>After the tag</div><! --<div id="app">1{{name}} This is the other content {{age}}</div> -->
  <script src="./parse.js"></script>
  <script src="./codegen.js"></script>
  <script>
    const ast = parser(document.querySelector("div").outerHTML)
    const render = codegen(ast)
    console.log(render);
  </script>
</body>
</html>
Copy the code

Configure the parser and code generator in the source code

class Vue {
  constructor(options) {
    this.$options = options
    ......
    // Simple example: Vue initializes a new watcher to update Html via watcher
    // new Watcher(this, () => {
    // document.querySelector("#app").innerHTML = `

${this.name}

`
}, () => {}) // 10.0 uses parsers and code generators to generate rendering functions if (this.$options.el) { // 10.1 Obtaining a Template String let html = document.querySelector("div").outerHTML // 10.2 Generate an abstract syntax tree let ast = parser(html) // 10.3 Generate the body of the render function let funCode = codegen(ast).render // 10.4 Generate the render function and mount it to the Vue instance this.$options.render = new Function(funCode) } ...... } Copy the code

Create index.html and import parse.js, codegen.js, and index.js, respectively

<body>
  <div>The label before<p>I {{name}}</p>
  </div>
  <script src="./parse.js"></script>
  <script src="./codegen.js"></script>
  <script src="./index.js"></script>
  <script>
    let vm = new Vue({
      el: "div".data: {
        name: 'ha ha',}})console.log("vm", vm.$options.render);
  </script>
</body>
Copy the code

Implementing VDOM (Virtual DOM)

A VDOM is a JS object that describes the current DOM.

Such as:

<ul>
 <li>1</li>
 <li>2</li>
</ul>
// Corresponding VDOM
{
  tag:"ul".attrs: {},children:[
    {
      tag:"li".attrs: {},chilren:[
        {
          tag:null.attrs: {},children: [].text:"1"}]}, {tag:"li".attrs: {},chilren:[
        {
          tag:null.attrs: {},children: [].text:"2"}]}]}Copy the code

The role of VDOM

1. It provides better performance in most cases than brute force refreshing the entire DOM tree.

Manipulating JS objects is fast, but manipulating DOM elements is slow. If the data changes, we can’t just regenerate the DOM from the template string and stuff it into the page, which is obviously a waste of time. We can use VDOM to describe the view and re-create the VDOM tree when the data changes. Compare the two VDOM trees to find the updated DOM node specified by the changed element.

2. VDOM is naturally cross-platform and only needs to call the DOM API of the corresponding platform. You can generate and update views for the platform.

The VDOM is generated by the render function

We do this through the VNode abstract class

// Add _c, _v, _s etc. methods to Vue abstract class 10.x-13.x
class Vue {
 constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    // 8.4 Both the initialization of computed properties and the initialization of data must be placed before the initialization of Watch, because watch can detect them only after the initialization of computed properties and data is completed.
    this.initComputed()
    this.initWatch()
    // 10.0 uses parsers and code generators to generate rendering functions
    if (this.$options.el) {
      // 10.1 Obtaining a Template String
      let html = document.querySelector("div").outerHTML
      // 10.2 Generate an abstract syntax tree
      let ast = parser(html)
      // 10.3 Generate the body of the render function
      let funCode = codegen(ast).render
      // 10.4 Generate the render function and mount it to the Vue instance
      this.$options.render = new Function(funCode)
    }
  }
  // 11.0 Generate element node
  _c(tag, attrs, children, text) {
    return new VNode(tag, attrs, children, text)
  }
  12.0 Generate a plain text node
  _v(text) {
    return new VNode(null.null.null, text)
  }
  // 13.0 Get variable content
  _s(val) {
    console.log("_s", val);
    // 13.1 Returns an empty string if the value is null
    if (val === null || val === undefined) {
      return ' '
      // 13.2 If it is an object
    } else if (typeof val === 'object') {
      return JSON.stringify(val)
      // 13.3 If the value is a number or a string
    } else {
      return val
    }
  }
}
// Create a VNode abstract class
class VNode {
  constructor(tag, attrs, children, text) {
    this.tag = tag
    this.attrs = attrs
    this.children = children
    this.text = text
  }
}
Copy the code

Test: The virtual DOM was successfully obtained

VDOM diff and patch

VDOM is efficient because it can compare the difference between two VDOM nodes through diff algorithm, and then update the specified DOM node through patch. Before implementing diff and Patch, we need to implement a createEle method that converts the VDOM into the real DOM, which will be used later in DOM updates.

createEle

The createEle function converts the VNode and its children into a real DOM

// 15.0 Generate the real DOM
function createEle(vnode) {
  // 15.1 is a literal node
  if(! vnode.tag) {const el = document.createTextNode(vnode.text)
    // 15.2 Save the node
    vnode.ele = el
    return el
  }
  // 15.3 is an element node
  const el = document.createElement(vnode.tag)
  vnode.ele = el
  // 15.4 Convert the child node into a real DOM and insert it into the parent node
  vnode.children.map(createEle).forEach(e= > {
    el.appendChild(e)
  })
  return el
}
Copy the code

test

Update views in response

When Vue is initialized, the new Watcher collects the dependency (this.name) through the evaluation function of the Watcher. When the dependency changes, the evaluation function reevaluates and completes the view update.

// Example before 10.0: Vue initializes new a watcher to update Html via watcher
  new Watcher(this.() = > {
    document.querySelector("#app").innerHTML = `<p>The ${this.name}</p>`
  }, () = >{})Copy the code

When dependent data changes, the evaluation function is triggered to update the view immediately. This brute force update of the view is obviously a performance problem, and we need to introduce the virtual DOM to optimize performance.

Train of thought

  • First implement a$mountFunction, called when the real DOM is first mounted, in the originalrender watcehrThe logic of$mountIn the.
  • implementation_updateFunction that takes the new VDOM, compares the old and new vDOM, and updates the real DOM.render watcehrThe logic is no longer to violently update the view but to call_updatefunction
// 16. x-17.x: Vue initialization --$mount--new Watcher--_update--patch
class Vue {
  constructor(options){...this.initWatch()
    // 10.0 uses parsers and code generators to generate rendering functions
    if (this.$options.el) {
      // 10.1 Obtaining a Template String
      let html = document.querySelector("div").outerHTML
      // 10.2 Generate an abstract syntax tree
      let ast = parser(html)
      // 10.3 Generate the body of the render function
      let funCode = codegen(ast).render
      // 10.4 Generate the render function and mount it to the Vue instance
      this.$options.render = new Function(funCode)
      Call $mount to update the view
      this.$mount(this.$options.el)
    }
  }
  $mount(el) {
    16.1 Mount the container root node to the Vue instance
    this.$el = document.querySelector(el)
    // create new render Watcher
    this._watcher = new Watcher(this.() = > {
      16.3 Generating the Virtual DOM
      const vnode = this.$options.render.call(this)
      16.4 Call _update to update the view
      this._update(vnode)
    }, () = >{})}_update(vnode) {
    //17.0 has the last vnode
    if (this._vnode) {
      17.1 Call patch and pass in the last vnode and this vnode
      patch(this._vnode, vnode)
    } else {
      // 17.2 Passing the real DOM node when mounting the Vue instance for the first time
      patch(this.$el, vnode)
    }
    17.3 Save the vNode
    this._vnode = vnode
  }
  ......
 }
Copy the code

Realize the patch

Patch is the most core part of VDOM mechanism. The logic of VDOM to patch in Vue is realized by snabbDomJS library. The main implementation idea here does not exclude complex cases such as node attributes and keys.

Train of thought

  • The patch function receives two parameters: the old vDOM and the new vDOM
  • When mounted for the first time, patch old VDOM is passed in as real DOM, which needs to be handled separately
  • Subsequent updates will fall into these categories
    • If the new node does not exist, delete the corresponding DOM
    • Call if the new and old node tags have different types or different textcreateEleGenerate a new DOM and replace the old DOM
    • The old node does not exist. The new node exists. The callcreateEleGenerate a new DOM and add a new DOM after the original DOM node
    • Traversing the child nodes recursively performs the above logic
// This stage code: 18.x
// 18.6 Check whether the old and new nodes are changed
function changed(oldNode, newNode) {
  returnoldNode.tag ! == newNode.tag || oldNode.text ! == newNode.text }function patch(oldNode, newNode) {
  const isRealyElement = oldNode.nodeType
  // 18.0 When oldNode=this.$el is first mounted for the element node page
  if (isRealyElement) {
    let parent = oldNode.parentNode
    18.1 Replace the vUE container node with the new node generated by VDOM
    parent.replaceChild(createEle(newNode), oldNode)
    return
  }
  Last patch will mount ele on newNode
  let el = oldNode.ele
  // 18.3 New VDOM node exists Mount DOM to vdom.ele. Ele will be used in patch next time
  if (newNode) {
    newNode.ele = el
  }
  let parent = el.parentNode
  18.4 If the new VDOM node does not exist, delete the corresponding node in the DOM
  if(! newNode) { parent.removeChild(el)18.5 Label types or text of new and Old Nodes are Inconsistent
  } else if (changed(oldNode, newNode)) {
    18.7 Call createEle to generate a new DOM node to replace the old ONE
    parent.replaceChild(createEle(newNode), el)
    18.8 Comparing Child Nodes
  } else if (newNode.children) {
    let newLength = newNode.children.length
    let oldLength = oldNode.children.length
    18.9 Traverse all children of the old and new VDOM nodes
    for (let index = 0; index < newLength || index < oldLength; index++) {
      The old vDOM of the child node does not exist. Call createEle to generate a DOM and insert it into the parent node el
      if (index > oldLength) {
        el.appendChild(createEle(newNode.children[index]))
      } else {
        18.11 The comparison of other child nodes is achieved by calling patch
        patch(oldNode.children[index], newNode.children[index])
      }
    }
  }
}
Copy the code

test

We can see that when we change the view binding name value, the view is updated.

The data responsive system, virtual DOM and DOM directed update have been completed to this Vue. Due to the word limit of this article, I will be in another article “detailed explanation of Vue 2.X core source code, hand lu a simple version of the Vue framework (next chapter” process summary and all the code (with notes) display