Source code analysis vUE responsive principle

After a brief implementation of the vUE responsive principle (see this if you can’t open it), we have a brief understanding of how responsive is implemented through the publish-subscribe model, this article mainly from the source analysis of the VUE responsive principle implementation.

Find initData

In vue/ SRC /core/index.js, you can see import vue from ‘./instance/index’, importing vue.

In vue/SRC/core/instance/index, js,

import { initMixin } from './init'
/ /...

function Vue (options) {
  / /...
  this._init(options)
}

initMixin(Vue)
// ...

export default Vue

Copy the code

As you can see, Vue is a function method that calls an initialization method called _init and passes in the options argument. The file also executes the initMixin method.

In the vue/SRC/core/instance/init. Js,

// ...
import { initState } from './state'
import { extend, mergeOptions, formatComponentName } from '.. /util/index'

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options? :Object) {
    const vm: Component = this

    // ...

    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // ...
    initState(vm)

    // ...}}Copy the code

See that the _init method is defined in the initMixin method. In the _init method, the constant VM is declared and the current instance is assigned, the options are accepted and processed, and the initState method is called.

We won’t go into options in this article, but it is important to note that the object parameters passed in when instantiating Vue can be obtained here.

In the vue/SRC/core/instance/state. Js,

import {
  set,
  del,
  observe,
  defineReactive,
  toggleObserving
} from '.. /observer/index'

export function initState (vm: Component) {
  // ...
  const opts = vm.$options
  // ...
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)}// ...
}
Copy the code

When instantiating Vue, such as

new Vue({
    el: '#app'.data: {
        text: 'hello world',}});Copy the code

The data passed in is opts.data, and when it exists, call initData, otherwise call the observe method, and initialize the _data property as an empty object.

initData

We’ll talk about the observe method later, but we already found the initData method, so let’s see.

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
}
Copy the code

Declare the data object and assign it to vm.$options.data.

GetData (data, vm) is called when data is a function, and the result is assigned to data and vm._data. If yes, set vm._data to data. If no, set data and vm._data to empty objects.

export function getData (data: Function, vm: Component) :any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return{}}finally {
    popTarget()
  }
}
Copy the code

Vue/SRC/core/observer/dep. Js:

Dep.target = null
const targetStack = []

export function pushTarget (target: ? Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]}Copy the code

Call (vm, VM). When data is a function, this of the data method points to the current vue instance, calls the data method and passes in the vue instance.

Then down

function initData (vm: Component) {
  // ...
  // Determine whether data is an Object instance, if not, assign the value to an empty Object, and warn in non-production environments that the value returned by data must be an Object
  if(! isPlainObject(data)) { data = {} process.env.NODE_ENV ! = ='production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // ...
}
Copy the code

vue/src/shared/util.js

export function isPlainObject (obj: any) :boolean {
  return _toString.call(obj) === '[object Object]'
}
Copy the code

To continue down

function initData (vm: Component) {
  // ...
  // Get an array of property names
  const keys = Object.keys(data)
  / / get props
  const props = vm.$options.props
  / / get the methods
  const methods = vm.$options.methods
  // Iterate over attributes
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // Non-production environment, method name and attribute name same name warning
    if(process.env.NODE_ENV ! = ='production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    // For non-production environments, props and properties have the same name warning
    if(props && hasOwn(props, key)) { process.env.NODE_ENV ! = ='production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if(! isReserved(key)) {// Filter attributes beginning with $or _ and proxy the rest to the instance's _data attribute.
      proxy(vm, `_data`, key)
    }
  }
  // Call the observe method
  observe(data, true /* asRootData */)}Copy the code

vue/src/core/util/lang.js

export function isReserved (str: string) :boolean {
  const c = (str + ' ').charCodeAt(0)
  return c === 0x24 || c === 0x5F
}
Copy the code

vue/src/core/instance/state.js

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

observe

vue/src/core/observer/index.js

export let shouldObserve: boolean = true

export function observe (value: any, asRootData: ? boolean) :Observer | void {
  // Make sure that the value passed (data above) is an object
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  // Define an ob variable
  let ob: Observer | void
  // Determine whether the data object already has an __ob__ attribute and is an instance of Observer, and assign it to ob if it does
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } 
  /** * otherwise judge: * When shouldObserve is true (which defaults to true and needs to be changed by toggleObserving), * and is not a service rendering, * and data is an instance of an array or Object, * and the data Object is extensible, * And the data object is not an instance of Vue * * If the above conditions are met, generate an Observer instance from the data object and assign it to ob */
  else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
  }
  if (asRootData && ob) {
    // In the case of root data, the ob object's vmCount property counts +1
    ob.vmCount++
  }
  / / returns the ob
  return ob
}
Copy the code

vue/src/shared/util.js

export function isObject (obj: mixed) :boolean %checks {
  returnobj ! = =null && typeof obj === 'object'
}
Copy the code

Observer constructor

import { arrayMethods } from './array'
import Dep from './dep'

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

// Override the native array method on the prototype object with __proto__ as the vUE rewritten array method
function protoAugment (target, src: Object) {
  target.__proto__ = src
}

// The seven array methods overwritten by the variable are defined one by one on the data object to override the native array methods on the prototype chain
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])
  }
}

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // Count the current data as the vUE instance of root data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    Define an __ob__ attribute on data using the object.defineProperty method
    def(value, '__ob__'.this)
    // When data is an array
    if (Array.isArray(value)) {
      // Check whether there is __proto__
      if (hasProto) {
        // Override the native array method on the prototype object with __proto__ as the vUE rewritten array method
        protoAugment(value, arrayMethods)
      } else {
        // The seven array methods overwritten by the variable are defined one by one on the data object to override the native array methods on the prototype chain
        copyAugment(value, arrayMethods, arrayKeys)
      }
      Call the observeArray method
      this.observeArray(value)
    } else {
      // Call walk when data is an object
      this.walk(value)
    }
  }

  // Call defineReactive on each property of data
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  // Traverse the array and call the observe method one by one
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
Copy the code

vue/src/core/util/lang.js

export function def (obj: Object, key: string, val: any, enumerable? : boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable:!!!!! enumerable,writable: true.configurable: true})}Copy the code

vue/src/core/util/env.js

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

vue/src/core/observer/array.js

Vue overwrites seven prototype array methods ‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
// Rewrite the seven array methods above so that changing an array using these seven methods triggers notify to update the view
methodsToPatch.forEach(function (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)
    ob.dep.notify()
    return result
  })
})
Copy the code

defineReactive

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  // Declare a constant DEp as an instance of deP, where closure is formed
  const dep = new Dep()
  
  const property = Object.getOwnPropertyDescriptor(obj, key)
  If the property cannot be overridden, it is returned directly
  if (property && property.configurable === false) {
    return
  }

  // Cache getters and setters
  const getter = property && property.get
  const setter = property && property.set
  // If there is no getter but only a setter, and no val value is passed in, assign the value from data to the val variable
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key]
  }

  // Shallow is false, the observe recursive child is called, and the returned Observer is assigned to childOb
  letchildOb = ! shallow && observe(val)// Override attributes
  Object.defineProperty(obj, key, {
    enumerable: true./ / can be enumerated
    configurable: true./ / can be configured
    get: function reactiveGetter () {
      // Execute if the original getter exists
      const value = getter ? getter.call(obj) : val
      / / Dep. Target exists
      if (Dep.target) {
        // Call Depend to collect the observer
        dep.depend()
        // There are child objects that collect child object observers
        if (childOb) {
          childOb.dep.depend()
          // If the value is an array, the depth is traversed to collect observers
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // Execute if the original getter exists
      const value = getter ? getter.call(obj) : val
      // The value is changed or returned directly for NaN
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      // call the customSetter for non-production environments
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      // Returns with getter but no setter
      if(getter && ! setter)return
      // if the original setter exists
      if (setter) {
        setter.call(obj, newVal)
      } else { // There is no direct update value
        val = newVal
      }
      // Shallow is false, the observe recursive child is called, and the returned Observer is assigned to childObchildOb = ! shallow && observe(newVal)// Call notify on the DEP instance to publish notifications
      dep.notify()
    }
  })
}

// Deep traversal number group, have __ob__ need to collect observer
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
Copy the code

Dep constructor

Vue/SRC/core/observer/dep. Js:

export default class Dep {
  statictarget: ? Watcher;// dep. target is a static property of the constructor Dep, meaning there is only one target globally
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = [] // The subs array is used to store observer objects
  }

  // Add an observer
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  // Remove the observer
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // If dep. target exists, the observer instance's addDep method is called to add the Dep to Watcher's DEps array
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}// Issue a notification
  notify () {
    const subs = this.subs.slice()
    // If the Watcher instance runs asynchronously through the observer queue, the Watcher instance's run is sorted before the Watcher instance runs asynchronously through the observer queue. If the Watcher instance runs asynchronously through the update queue, the Watcher instance's run is sorted before the Watcher instance runs
    if(process.env.NODE_ENV ! = ='production' && !config.async) {
      // Since the id of the Watcher instance is an increasing number, it can be sorted directly by the array sort
      subs.sort((a, b) = > a.id - b.id)
    }
    // Call the observer update method one by one
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Copy the code

vue/src/shared/util.js

export function remove (arr: Array<any>, item: any) :Array<any> | void {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)}}}Copy the code

The Watcher constructor

vue/src/core/observer/watcher.js

A data object has an __ob__ attribute corresponding to an Observer instance. The Observer instance overrides each attribute on data and holds its own DEP array via a closure. Each DEP array, All Watcher observer instances of this property are collected, and each observer instance has its own DEPS dependency set, collecting the DEP of the closure in reverse.

So with that in mind, let’s take a look at Watcher a little bit

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    // _watcher holds the observer instance
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // Note that the id of the Watcher instance is incremental
    this.active = true
    this.dirty = this.lazy
    this.deps = [] // The added dependency array
    this.newDeps = [] // A cache array to hold dependencies to be added
    this.depIds = new Set(a)// Add an array of dependent ids
    this.newDepIds = new Set(a)// A cache array to hold the dependency ids to be added
    this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // expOrFn may be a function or a string representation of attributes on an object, such as "a.b", which is parsed by parsePath and returned as a function
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop process.env.NODE_ENV ! = ='production' && warn(
          `Failed watching path: "${expOrFn}"` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    // Lazy is false to perform get initialization
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  // Get the value of the listener attribute to collect DEP dependencies
  get () {
    // Change dep. target to point to the current Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // Execute the getter to get the listening data property and fire the deP-dependent depend() method corresponding to the property to call addDep
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      // If depth listening is set
      if (this.deep) {
        traverse(value) // Call traverse to recursively traverse arrays and object types, firing each getter
      }
      // Change dep. target to null
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  // Add dependencies, called in dep dependent depend(),
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) { 
      // Add the dependency to the cache
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // Call the DEP-dependent addSub to collect the current observer
      if (!this.depIds.has(id)) {
        dep.addSub(this)}}}// Clear cache dependencies
  cleanupDeps () {
    // Iterate over the dependent array
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      // DePs that are not in the cache need to call deP-dependent removeSub to remove the current observer
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}// Set newDepIds and newDeps to depIds and deps and clear the cache
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  / / update
  update () {
    // Set lazy to true and dirty to true
    if (this.lazy) {
      this.dirty = true
    } 
    // To synchronize, run is executed
    else if (this.sync) {
      this.run()
    }
    QueueWatcher is pushed asynchronously to the observer queue, which is eventually called to the run method via nextTick
    else {
      queueWatcher(this)}}// Update the value and perform the callback
  run () {
    // active Defaults to true
    if (this.active) {
      const value = this.get()
      // If the value is unequal, or if the value is an array or object, or if it is deep listening
      if( value ! = =this.value ||
        isObject(value) ||
        this.deep
      ) {
        // Assign the latest value to this.value
        const oldValue = this.value
        this.value = value
        // Execute Watcher's callback
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  // Trigger GET to set dirty to false
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  // Iterate through deps to execute each deP dependent depend method
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  // Delete the current Watcher instance
  teardown () {
    if (this.active) {
      Removes itself from the array of observer instances of the current Vue instance when _isBeingDestroyed is false
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)}// Execute removeSub for each DEP dependency to remove the current watch
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)}// Set active to false
      this.active = false}}}Copy the code

vue/src/core/util/lang.js

const bailRE = new RegExp(` [^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string) :any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('. ')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code

Vue/SRC/core/observer/traverse by js:

const seenObjects = new Set(a)export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if((! isA && ! isObject(val)) ||Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  // Avoid repeated traversal
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  // Deeply iterate over groups and objects
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
Copy the code

QueueWatcher is a queueWatcher constructor. QueueWatcher is a queueWatcher constructor. QueueWatcher is a queueWatcher constructor.