preface

Before reading the responsive principle, we need to comb through the overall process. We need to comb through the simple process of subscription publishing model, so that we can have a deeper understanding of the subsequent reading

First of all, what does new Vue() do, starting here

initState

An entrance to the responsive principle

vue-dev/src/core/instance/state.js

  1. Initialize the props
  2. Initialize the methods
  3. Initialize the data
  4. Initialize the computed
  5. Initialize the watch
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // Initialize props
  if (opts.methods) initMethods(vm, opts.methods) // Initialize methods
  if (opts.data) {
    initData(vm) // Initialize data
  } else {
    observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)// Initialize computed
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch)// Initialize watch}}Copy the code

initProps

  1. Cache all keys in props with an array
  2. Traverse through props to obtain the default value
  3. Set data responsiveness
  4. Proxies the current key to the VM
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // Cache the propskey so that future props updates can iterate with the array
  const keys = vm.$options._propKeys = []
  constisRoot = ! vm.$parent// If it is the root instance, props should be converted
  if(! isRoot) { toggleObserving(false)}/ / traverse the props
  for (const key in propsOptions) {
    // Add the current key to the array cache
    keys.push(key)
    PropsData [key]
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if(process.env.NODE_ENV ! = ='production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () = > {
        if(! isRoot && ! isUpdatingChildComponent) { warn(`Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      // Set data responsiveness
      defineReactive(props, key, value)
    }
    // Delegate the current key to the VM
    if(! (keyin vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)}Copy the code

initMethods

  1. Traversing methods verifies that all keys must be arrays
  2. Verify that the current key does not overlap with existing methods on props or Vue instances
  3. Mount the current method to the VM instance
function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if(process.env.NODE_ENV ! = ='production') {
      // check methoss[key], must be a function
      if (typeofmethods[key] ! = ='function') {
        warn(
          `Method "${key}" has type "The ${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly? `,
          vm
        )
      }
      // Verify that the key in methods cannot be the same as that in props
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      // Verify that the key in Methos overlaps with an existing method on the Vue instance
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`)}}Vm [key] = methods[key]
    vm[key] = typeofmethods[key] ! = ='function' ? noop : bind(methods[key], vm)
  }
}
Copy the code

initData

  1. Get data
  2. Verify that data returns an object
  3. Check whether the key in data is the same as that in methods and props
  4. Make data response to data
function initData (vm: Component) {
  let data = vm.$options.data
  // Get data. Data is a function that returns an object
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // The data function should return 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
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if(process.env.NODE_ENV ! = ='production') {
      // Verify that the current key is not equal to the key in methods
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    // Verify that the current key cannot be equal to the key in props
    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)) {// Delegate the current key to the VM
      proxy(vm, `_data`, key)
    }
  }
  // Set data responsiveness
  observe(data, true /* asRootData */)}Copy the code

observe

  1. Check whether an incoming value is in object format instead of a direct return
  2. Determine if the data is already responsive, if so, return the instance directly
  3. Execute new Observer(Value) to perform data responsive processing
/** * Attempts to create an observer instance for a value, * returns a new observer if it succeeds, and * returns an existing observer if it already exists. * /
export function observe (value: any, asRootData: ? boolean) :Observer | void {
  // Return if value is not an object
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // If the prototype has '__ob__', it is already responsive
    // If there is an observer instance, return
    ob = value.__ob__
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) {// If the data is not responsive
    // Add a new observer instance
    // ⚠️ Note that value is only processed data responsively if the type is an object
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Observer

  1. Define a DEP butler
  2. Add to incoming data__ob__
  3. In the case of arrays, the enhanced array modifies the seven methods of the array itself to make it interceptable
  4. If value is an array, traverse the array, and if there are objects in the array, make them observe()
  5. Execute the walk() method, iterate over all key values, and execute the defineReactive() method for reactive processing
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
    // instantiate a DEP steward
    this.dep = new Dep()
    this.vmCount = 0
    // Define an attribute. Add __ob__ to value
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {// If value is an array
      // If __proto__ is present, browser compatibility issues, some older browsers do not have __proto__ attributes
      if (hasProto) {
        // Overwrite the array prototype
        protoAugment(value, arrayMethods)
      } else {
        // Define seven methods for arrays
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // Set array responsiveness if the items in the array are objects
      this.observeArray(value)
    } else {
      // Handle reactive
      this.walk(value)
    }
  }

 	/** * if value type is Object * iterates through all properties and converts them to * getter/setter. * /
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /** * View the Array entry list. * /
  observeArray (items: Array<any>) {
    // Traverse the array, passing each key in the observe method. If there is an item in the array that is the object's data
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
Copy the code

defineReactive

  1. Create a DEP small manager, one key for each DEP, to collect the dependencies for that field
  2. Determine that a property cannot be processed responsively if it cannot be modified or deleted
  3. Record getters and setters, get val
  4. Perform observe recursively so that the underlying element is also data responsive
  5. Responsive core content: GET and set
    1. Get intercepts the read operation on the key
      1. Execute the getter or return val
      2. Dep.target is a static property of the class (Watcher)
      3. Dep.depend () for dependency collection, add watcher to DEP and deP to watcher
      4. If there are nested objects (also observer objects), do dependency collection as well
      5. If it is an array, the array response is triggered
      6. Adds a dependency to the item of the object for the array item
    2. Set intercepts writes to keys
      1. If the new value is the same as the old value, return is not required
      2. If you have a getter but no setter, it’s a read-only property and returns directly
      3. Execute the setter to set the new value
      4. Respond to new values
      5. Perform notification updates in deP
// Define responsiveness properties on the object.
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  // A key corresponds to a DEP
  const dep = new Dep()
	// Get the corresponding attribute description
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // If the property is unchangeable or cannot be deleted, return
  if (property && property.configurable === false) {
    return
  }

  // Record getters and setters to get val
  const getter = property && property.get
  const setter = property && property.set
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key]
  }

  letchildOb = ! shallow && observe(val)// recursive call
  // Define reactive data
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      // Execute if there is a getter, otherwise return val
      const value = getter ? getter.call(obj) : val
      // dep. target is a class static property (Watcher) static target:? Watcher;
      if (Dep.target) {// If there is Watcher
        // This is Watcher's Depend
        // Add deP to Watcher
        dep.depend()
        // There exists child ob
        // Add child OB to Watcher as well
        if (childOb) {
          childOb.dep.depend()
          // If it is an array
          if (Array.isArray(value)) {
            // The array option is an object, which is dependent collection, because none of the previous methods can rely on the collection of elements whose array items are objects
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // Get the old val
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // If the new val is the same as the old val, return directly
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      // Read-only property, return directly, does not trigger responsivity
      if(getter && ! setter)return
      
      if (setter) {
        setter.call(obj, newVal)/ / set the value
      } else {
        val = newVal
      }
      // Make the new value also responsivechildOb = ! shallow && observe(newVal)// Notifications depend on updates
      dep.notify()
    }
  })
}


/** * Collect array element dependencies when an array is accessed, because * cannot intercept array element access like the property getter. * /
function dependArray (value: Array<any>) {
  for ( let i = 0; i < value.length; i++) {
    let e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
Copy the code

Array response

  1. Caching native methods
  2. Execute def() to intercept access to arrayMethods.Method
  3. Implementing native methods
  4. The new value element sets the data responsiveness
/* * Defines an arrayMethods object that enhances the Array prototype * dynamic access to the Array prototype */

import { def } from '.. /util/index'

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

const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

/** * intercepts the above seven methods and triggers the event */
methodsToPatch.forEach(function (method) {
  // Cache native methods
  const original = arrayProto[method]
  // def is object.defineProperty, which intercepts arrayMethods.method access
  def(arrayMethods, method, function mutator (. args) {
    // Implement native methods
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    // The 'push', 'unshift', and 'splice' methods add elements
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // The new value element sets the data responsiveness
    if (inserted) ob.observeArray(inserted)
    // Notification update
    ob.dep.notify()
    return result
  })
})

Copy the code

rotoAugment

Set target.proto_ = SRC

Set the array object prototype to arrayMethods

/** * add target object or array */ by intercepting
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = srcSRC is an enhanced prototype approach that supports data responsiveness
  /* eslint-enable no-proto */
}
Copy the code

opyAugment

/** * Add target object or array by definition * hide attribute. * The prototype chain uses __proto__ (nonstandard attribute) * because some browser objects do not have __proto__, compatibility issues */
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

ef

The enhanced seven array methods are overwritten on the array prototype, thus intercepting the original array methods and enhancing responsiveness

The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.

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

Dep

/* * One deP for one obj. Key * Collects dependencies in a responsive get, and the watcher for each DEP will notify the Watcher in the DEP in a responsive set to perform the update method */
export default class Dep {
  statictarget: ? Watcher; id: number; subs:Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
	// Add watcher to deP
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
// Delete watcher from dep
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
	// Add deP to watcher
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}// Notify updates, iterate over each watcher update
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if(process.env.NODE_ENV ! = ='production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) = > a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// Called when dependency collection is needed, set dep.target = watcher
export function pushTarget (target: ? Watcher) {
  targetStack.push(target)
  Dep.target = target
}
// Depending on the collection end call, set dep. target = null
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]}Copy the code

Watcher


/** * a component a watcher or an expression a watcher * will be triggered when data is updated, or watcher will be triggered when accessing this.putedProperty
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.// This is updateComponent
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    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 // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn // Assign updateComponent to the getter
    } 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
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /** * Evaluate getters and re-collect dependencies. * When the update is triggered, the render function is executed and dependency collection is done again */
  get () {
    // Enable dependency collection and determine the target in the Dep during dependency collection
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // Execute the updateComponent method here
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // Remove target from Dep to disable dependency collection
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /** * Add a dependency to this directive. */
  addDep (dep: Dep) {
    const id = dep.id
    // If there is a deP, do not add it again
    if (!this.newDepIds.has(id)) {
      // Save the id first
      this.newDepIds.add(id)
      / / add the dep
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // Also add watcher itself to dep
        dep.addSub(this)}}}/** * Clean up dependencies */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}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
  }

  /** * Will be executed when a dependency changes. dep.notify() */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // Computed has a fixed parameter lazy, which goes here
      this.dirty = true // Here is the cache switch that ensures that functions in computed are executed only once per update
    } else if (this.sync) {
      // Here is a synchronous update...
      this.run()
    } else {
      // Here is the usual update logic, let watcher queue, execute through Nexttick
      queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
  run () {
    if (this.active) {
      const value = this.get()/ / call the get
      if( value ! = =this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          // If it is watcher, it will execute the third argument passed and call the function
          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)
        }
      }
    }
  }

  /** * This only applies to lazy computations. * What dirty does is cause computed to run only once in a second cycle, and each page update is set to true */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /** * Depend on all deps collected by this watcher. */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /** * Remove self from all dependencies' subscriber list. */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)}let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)}this.active = false}}}Copy the code

initComputed

The key point here is that const computedWatcherOptions = {lazy: true}

Watcher is lazy and executed only once per page update. Methods are executed several times when accessed several times in page update

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
	// Iterate over computed options
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if(process.env.NODE_ENV ! = ='production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}". `,
        vm
      )
    }

    if(! isSSR) {// Create Watcher for computed attributes.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions // Default to lazy execution unchangeable {lazy: true})}This.putedkey = this.putedKey; this.putedKey = this.putedKey; this.putedKey = this.putedKey; this.putedKey
    if(! (keyin vm)) {
      defineComputed(vm, key, userDef)
    } else if(process.env.NODE_ENV ! = ='production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
Copy the code

initWatch

function initWatch (vm: Component, watch: Object) {
  // Iterate over the watch option
  / / call createWatcher
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      // If it is an array, iterate over the array
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function, handler: any, options? :Object
) {
    // Handler gets the value of the option if it is an object
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
    // Handler is a string that is a method of methods
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}


Vue.prototype.$watch = function (
    expOrFn: string | Function, cb: any, options? :Object
  ) :Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
  	/ / create a Watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // If the immediate option is set, the callback function is executed immediately
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}//unwatchFn Is used to unlisten
    return function unwatchFn () {
      watcher.teardown()
    }
  }
Copy the code

proxy

Proxy, which is used to set each key proxy in props, data, and computed to a VM instance

Allows this. Key to access parameters and methods in a configuration item

For example, when we use this.dataKey to access vm. Data. dataKey it is because of the proxy

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
  }
  // Set the key proxy to target
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code