Before introducing the reactive principle, let’s look at how to detect Object changes. There are currently two ways to detect Object changes: Object.defineProperty and ES6 Proxy. In the Vue2.0 stage, the browser support for Proxy was not ideal, so 2.0 was implemented based on object.defineproperty. This article also introduces how to implement responsivity based on Object.defineProperty. In the next article, we will also introduce how to implement responsivity based on Proxy.

Basic knowledge of

In the process of parsing the source code, the Object. DefineProperty, observer mode and pointcut are used to parse how vUE implements bidirectional binding, and data changes drive view updates.

Object.defineProperty

Object.defineproperty is a new Object method added in ES5 that directly defines a new property on an Object or modifies an existing property of an Object and returns the Object.

ECMAScript has two types of properties: data properties and accessor properties

  • Data attributes include [[Video]], [[Enumerable]], [[Writable]], [[Value]];
  • Accessor properties contain a pair of set and GET functions. When a accessor property is read, a getter function is called, which returns a valid value. When a accessor property is written, a setter function is called and a new value is passed in. This function is responsible for deciding how to handle the accessor properties, which contain [[Video]], [[Enumerable]], [[Get]], [[Set]].
var obj = {};
var a;
Object.defineProperty(obj, 'a', {
  get: function() {
    console.log('get val');  return a;
  },
  set: function(newVal) {
    console.log('set val:'+ newVal); a = newVal; }}); obj.a;// get val 
obj.a = '111'; // set val: 111
Copy the code

Object. DefineProperty in the example code converts the A property of OBj into getter and setter, which can realize the data monitoring of OBJ. Vue formally implements responsiveness based on this feature. Vue iterates through all of the Object’s properties and converts them into getters/setters using Object.defineProperty.

Observer model

Vue is based on the observer mode to implement data updates and then trigger a series of dependencies to automatically update the view. The observer mode is an object that maintains a series of dependent objects and automatically notifies them of state changes. The basic elements of the observer model

  • Subject
  • An Observer

{% img /images/vue/observer. PNG “click to view the cheat sheet :vi/vim-cheat sheet” %}

Define a container to collect all dependencies

// Target class
class Subject {
  constructor() {
    this.observers = []
  }
  / / add
  add(observer) {
    this.observers.push(observer)
  }
  / / delete
  remove(observer) {
    let idx = this.observers.find(observer)
    idx > - 1 && this.observers.splice(idx,1)}/ / notice
  notify() {
    for(let oberver of this.observers) {
      observer.update()
    }
  }
}

// Observer class
class Observer{
  constructor(name) {
    this.name = name
  }
  update() {
    console.log('Target notified me of the update. I amThe ${this.name}`)}}Copy the code

The source code parsing

Overall overview

Let’s enter the vUE source code to start parsing how vUE is responsive.

Vue does a series of init operations during initialization, and we’ll focus on converting data into responsive data. Parsing the source step by step, in the init.js file, Observe observe(data, true /* asRootData */). Observe observe(data, true /* asRootData */).

Object.defineproperty () cannot detect array changes due to javascript limitations. Vue implements arrays and objects in two different ways. For Object types, it hijacks getters and setters to detect changes. In the case of arrays, intercepting array-related apis (push, POP, Shift, unshift…) via interceptors To monitor change.

// instance/observer
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'? getData(data, vm) : data || {} ... Omit some code// proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  constmethods = vm.$options.methods ... Omit some code// observe data
  observe(data, true /* asRootData */)}Copy the code
// observe/index.js
export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) { // If it is a basic type or virtual node
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__'.this)
    // score groups and objects separately
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods) // Add interceptor
      } else {
        copyAugment(value, arrayMethods, arrayKeys) // Add interceptor
      }
      this.observeArray(value) // Convert the array to responsivity
    } else {
      this.walk(value) // Convert the object to reactive form}}}Copy the code

Data is of type Object

{% img/images/vue/observer1 PNG “click to view a larger version: vi/vim – cheat – sheet” %}

// observe/index.js
// Loop over each key, hijacking add getter setter
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}

export function defineReactive (obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean) {
  const dep = new Dep() // Dep corresponds to Subject in observer mode, where the user collects user dependencies and sends notifications.// Omit some code
  letchildOb = ! shallow && observe(val)// Recursively each can convert data to observe
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.// Add accessor property get to the data
    // When to trigger get? When the page component is in the "mount" stage, the render page will be called. During rendering, data will be obtained and reactiveGetter will be automatically triggered.
    // Dep.target is what? By looking at lifecycle. Js in the mountComponent phase will new the Watcher and point the global dep. target to that Watcher
    // dep.denpend() does what? The Watcher is added to the SUBs queue of the DEP
    get: function reactiveGetter () { 
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() 
        if (childOb) {
          childOb.dep.depend() // Collect dependencies
          if (Array.isArray(value)) {
            dependArray(value) // Collect dependencies}}}return value
    },
    // Add accessor attribute set to data
    // When to trigger set? When the value corresponding to the key changes, the reactiveSetter call is automatically triggered and the notify notification is executed
    // What does notify do? Iterate through subs (Watcher), perform update in Watcher, and add Watcher to the queue to be updated
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      ... // Omit some code
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify()// Call deP notification to store all dependencies after data update}})}Copy the code

Dep (Target: Subject) defineReactive uses an important object Dep, so what does Dep do? Dep is a target object that manages Watcher (adding Watcher, deleting Watcher, adding itself to Watcher’s DEPS queue, notifying every Watcher it manages to update)

export default class Dep {
  statictarget: ? Watcher; id: number; subs:Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    ... // Omit some code
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Copy the code

Watcher is a mediator role that notifies it when data changes, and then notifies the rest of the world. He does all the dirty work

  • 1. Collect dependencies
  • 2. Responsible for executing CB to update all dependencies
// Watcher.js
export default class Watcher {...// Omit some code
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function
  ) {
    this.vm = vm
    ... // Omit some code
    this.cb = cb
    ... // Omit some code
    this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      ... // Omit some code
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /** * Evaluate the getter, and re-collect dependencies. */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    ... // Omit some code
    return value
  }

  /** * Add a dependency to this directive. */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  run () {
    if (this.active) {
      const value = this.get()
      if( value ! = =this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue) // Perform specific updates
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
          this.cb.call(this.vm, value, oldValue) // Perform specific updates
        }
      }
    }
  }

  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
Copy the code

Data is an Array type

{% img /images/vue/array. PNG 440 320″ click to view the big picture :vi/vim-cheat-sheet” %} Below will be a step by step to sort out the data structure in the data is array type, Vue source code is how to intercept and convert to response type

// Take the data structure as the column
data: {
  array: [1.2.3]}Copy the code
// instance/state.js
/ / the entry. observe(data,true /* asRootData */)...Copy the code
// observer/index.js
export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Convert data to observer, execute Walk (value)

import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

export class Observer {... constructor (value: any) {this.value = value
    this.dep = new Dep() // important ! The DEP here is actually used for data collection dependencies that are of type array
    this.vmCount = 0
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {
      if (hasProto) { // Check whether the browser supports __proto__
        protoAugment(value, arrayMethods) // Use __proto__ to override methods in interceptors directly
      } else {
        copyAugment(value, arrayMethods, arrayKeys) // Mount methods from interceptors to value via copy
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
}

// iterate over each key
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}


export function defineReactive (obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean) {
  const dep = new Dep() // Dep corresponds to Subject in observer mode, where the user collects user dependencies and sends notifications.// Omit some code
  letchildOb = ! shallow && observe(val)// This is an important step to recursively convert an array to an observer
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.// reactiveGetter is triggered when the page retrieves data in the mount phase, adding dependencies to the array
    get: function reactiveGetter () { 
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() 
        if (childOb) {
          childOb.dep.depend() // Array adds collection dependency
          if (Array.isArray(value)) {
            dependArray(value) // Collect dependencies}}}return value
    },
    // Array changes do not trigger the set callback here, but actually execute __obj__.dep.notify() in the interceptor (see array.js).
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      ... // Omit some code
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify()// Call deP notification to store all dependencies after data update}})}Copy the code

When accessing a method in an array, because of the interceptor added, when accessing a method in an array, a forged method is accessed.

// Interceptor method
// observer/array.js
const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (. args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify() // When the array changes, the DEP target is called to inform all dependencies to update
    return result
  })
})
Copy the code
// observer/index.js


// Override the prototype with __proto__
function protoAugment (target, src: Object) {
  target.__proto__ = src
}

// Mount methods from interceptors to value via copy
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
Copy the code

conclusion

How is VUE responsive? The implementation of objects and arrays is slightly different:

  • 1. Objects: In the create stage, the data in data will be recursively added get and set accessor attributes. In the mount stage, the page will create a global Watcher, and render will be performed in the mount stage, and the corresponding GET function of the page data will be called. Each data key has a corresponding DEP dependency, which will be added to the subs queue of the current Watcher when dep.depend() is executed. When the page data is updated, the set function is called to perform the notification.
  • 2. Array: In the create phase, if it is an array type, add an interceptor to the method that performs the array change, and add get and set accessors to the data. The set function is not triggered when the array changes, and the page executes render in the mount phase, calling the corresponding GET function of the data. Call childobj.dep.depend () to collect watcher,(childobj.dep is what? When initializing data, array is recursively converted to an observer, so childobj.dep refers to array dependencies. After an array data update, the __obj__.dep.notify() execution in the interceptor is notified, and the set is not triggered.

How does the page update render after notification? When the notification is sent, Watcher will be added to the queue and vUE will uniformly schedule the update. Later, VUE will perform patch, compare with virtual DOM, and make an overall update at the level of the current page component.