One feature of Vue is its responsiveness to data, where changes to data are applied to the view without DOM manipulation. In principle, the Object. DefifineProperty () is used, and the setter method for Object attributes is defined to intercept the changes of Object attributes, so the changes in attribute values are converted to changes in the view. When Vue initializes, initState is called, which initializes props, Methods, data, computed, watch, and so on.

Responsive object

initState

// src/core/instance/state.js
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) } }Copy the code

initProps

// src/core/instance/state.js
function initProps (vm: Component, propsOptions: Object{
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // Cache each key of props, performance optimization
  const keys = vm.$options._propKeys = []
  constisRoot = ! vm.$parent// root instance props should be converted
  // The case of non-root instances
  if(! isRoot) {// Reactive optimization mainly optimizes the recursive process of reactive processing
    toggleObserving(false)}for (const key in propsOptions) {
    / / the cache key
    keys.push(key)
    // Check whether the data passed conforms to the specification defined in prop
    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
        )
      }
      // Set responsiveness for each key of props
      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 responsiveness for each key of props
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if(! (keyin vm)) {
      proxy(vm, `_props`, key)
    }
  }
  // Reactive optimization mainly optimizes the recursive process of reactive processing
  toggleObserving(true)}Copy the code

The initialization of the props is to traverse it, and the traverse does two things:

  • calldefineReactiveReactive processing is done for each value.
  • throughproxyvm._props.xxxAccess proxy tovm.xxxOn.

initData

// src/core/instance/state.js
function initData (vm: Component{
  let data = vm.$options.data
  // Determine whether data is a function or an object
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  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 to the VM instance.
  // The properties on data cannot be the same as those on props and methods.
  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') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    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)) {// Proxy operation
      proxy(vm, `_data`, key)
    }
  }
  // Reactive operations
  observe(data, true /* asRootData */)}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

Initialization of data is similar to props, and there are three main things that are done here:

  • checkdataThe attributes of thepropsmethodsHave the same properties on.
  • throughproxyvm._data.xxxAccess proxy tovm.xxxOn.
  • callobservedataThe data on the.

proxy

// 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

The purpose of the proxy is to delegate properties on props and data to the VM instance, which is why we defined props. XXX, which can be accessed through this. XXX.

observe

// src/core/observer/index.js
export function observe (value: any, asRootData: ? boolean) :Observer | void {
  // Non-objects and VNode instances do not respond
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // If an __ob__ attribute exists on the value object, then the observation has been made and the __ob__ attribute is returned
    ob = value.__ob__
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) {// Create an observer instance
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Observe: Create an Observer instance Observer for a non-VNode object. If the Observer is observed successfully, return the existing Observer. Otherwise, create a new instance.

Observer

// src/core/observer/index.js
/** * Observer class that is attached to each observed * nce attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */
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
    // Why declare Dep in Observer?
    this.dep = new Dep()
    this.vmCount = 0
    // Set the __ob__ attribute on the value object, referring to the current Observer instance
    def(value, '__ob__'.this)
    // Determine the type
    if (Array.isArray(value)) {
      // Override the default seven prototype methods for arrays to implement array responsiveness
      // hasProto = '__proto__' in {}
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  /** * iterates over each key on the pair, setting the response * only goes here if type Object */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  /** * if the value in the array is still an object, then you need to do a response */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
Copy the code

The Observer class is attached to the object being observed, that is, every responsive object has an __ob__; Then a judgment is made on the data type; __proto__ is not a standard attribute, so some browsers do not support it, such as IE6-10. Opera10.1.

Why declare Dep in the Observer? For those of you who are familiar with reactive behavior, we should have a DEP for each key to manage dependencies and trigger setters to notify updates when the key value changes. Dep here is mainly used to add and delete Object properties, Array change methods. {a: {b: 1}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, c: 2}} $set {a: {b: 1, C: 2}} What changes need to be truly updated, hand over to Brother Diff. If you don’t know the tao here, you can review the whole response.

defineReactive

// src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean{
  // instantiate dep, one key at a time
  const dep = new Dep()
  // Get obj[key] property descriptor, find it is not configurable object directly return
  const property = Object.getOwnPropertyDescriptor(obj, key)
  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]
  }
  // A recursive call to handle the case where val is an object
  letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
    enumerabletrue.configurabletrue.// Hijack the read operation
    getfunction reactiveGetter ({
      const value = getter ? getter.call(obj) : val
      // Dep.target is a static attribute of Dep that holds the current Watcher instance.
      // When the new Watcher is instantiated (except for computed, because it executes lazily), read artifacts are triggered and hijacked to run the get function for dependency collection.
      // At the end of instantiating Watcher, dep. target is set to null to avoid double collection.
      if (Dep.target) {
        // Depending on the collection, add watcher to the DEP and add deP to the watcher
        dep.depend()
        // childOb indicates that val is still a complex type, object, or array.
        if (childOb) {
          // This DEP was created in the Observer, as mentioned earlier.
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // The array is still an object
            dependArray(value)
          }
        }
      }
      return value
    },
    // Hijack the modify operation
    setfunction reactiveSetter (newVal{
      [key] / / the old obj
      const value = getter ? getter.call(obj) : val
      // If the old and new values are the same, return without updating
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      // If the setter does not exist, the property is read-only and returns directly
      if(getter && ! setter)return
      // Set the new value
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // Observe the new value so that it is also responsivechildOb = ! shallow && observe(newVal)// Rely on notification updates
      dep.notify()
    }
  })
}
Copy the code
// src/core/observer/index.js
/** * iterates through each element of the array, recursively handling the case that the array element is an object, adding dependencies to it
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

DefineReactive is used to hijack reading and writing of data using Object.defineProperty, adding getters and setters to the property key for dependency collection and notification of updates. If the value passed in is still an object, recursively call the Observe method to ensure that all the child properties become responsive.

Depend on the collection

Dep

// src/core/observer/dep.js
/* @flow */
import type Watcher from './watcher'
import { remove } from '.. /util/index'
import config from '.. /config'
let uid = 0
/** * A dep is an observable that can have multiple * directives subscribing to it. */
export default class Dep {
  statictarget: ? Watcher; id: number; subs:Array<Watcher>;
  constructor() {
    this.id = uid++
    this.subs = []
  }
  // Add a subscription and save the Watcher instance to subs
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  // Remove the subscription to remove the Watcher instance from subs
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  // Add deP to Watcher
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}// Notification 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)
    }
    // Go through the watcher stored in the DEP and execute watcher.update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
/** * Only one watcher is executing at a time * dep. target = the current executing watcher, and the assignment is completed by calling pushTarget. Call the popTarget method to complete the reset
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

Watcher

// src/core/observer/watcher.js
/* @flow */
import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError,
  invokeWithErrorHandling,
  noop
} from '.. /util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '.. /util/index'
let uid = 0
/** * One watcher per component (render watcher) or one watcher per expression (user watcher) * Watcher will be triggered when data is updated, Watcher */ will also be triggered when visiting this.computedProperty
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
    }
    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
    } else {
      // this.getter = function() { return this.xx }
      // Dependency collection is triggered when this.getter is executed in this.get
      // Reactive will be triggered when this.xx is updated later
      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()
  }
  Getter is the second argument passed when watcher is instantiated, a function or string, such as: The function returned by updateComponent or parsePath that reads the value of this.xx * why collect dependencies again? * Dependencies are not collected */ because the observe-observed dependencies are not collected */ because the render function is re-executed when the page is updated and the read operation is triggered
  get () {
    // called when dependency collection is needed
    // Set targetstack.push (target) and dep.target = watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // Execute the callback function, such as updateComponent, to enter the patch phase
      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)
      }
      // Rely on the collection to end the call
      // Set targetstack.pop () and targetStack[targetstack.length-1]
      popTarget()
      // Clear dependencies
      this.cleanupDeps()
    }
    return value
  }
  /** * add dep to watcher */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // Save the ID for deduplicating
      this.newDepIds.add(id)
      // Add dep to current watcher
      this.newDeps.push(dep)
      // Avoid adding watcher repeatedly in deP
      if (!this.depIds.has(id)) {
        // Add the current watcher to dep
        dep.addSub(this)}}}/** * Clear dependencies, each data change is rerendered, * then the vm._render() method is executed again, and the data getters are triggered again, So Watcher initializes two arrays of Dep instances in the constructor: * this.deps represents the last array of Dep instances, and this.newDeps represents the newly added array of Dep instances. * /
  cleanupDeps () {
    let i = this.deps.length
    // Iterate through deps to remove the subscription to the Wathcer in dep.subs array
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}// newDepIds swaps with depIds and empties newDepIds
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    // newDeps swaps with deps, then empties newDeps
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  
  / /... Here are a few more ways to look at it
}
Copy the code

Dependency Collection process

Recall that there is logic in the mountComponent for performing a mount.

// src/core/instance/lifecycle.js
  updateComponent = () = > {
    vm._update(vm._render(), hydrating)
  }
  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code

Collection process:

  1. When instantiatedWatcherIs executedWatcherConstructorthis.get().
  2. getIs called first inpushTarget(this)In fact, it isDep.target = The Watcher currently being executedAnd push it onto the stack.
  3. Then performvalue = this.getter.call(vm, vm)
  4. this.getterAnd that corresponds toupdateComponentWhat is actually executed isvm._update(vm._render(), hydrating); It calls firstvm._render().
  5. vm._render()That generates a VNode, which triggers access to the data, which triggers the getter.
  6. Per objectkeyThere will always be a correspondingdepThat’s called in the getterdep.depend(), will callDep.target.addDep(this); Because at step 2Dep.target = The Watcher currently being executed, so the call should beThe currently executing watcher.adddep (this).
  7. addDepIn the case of not repeated calldep.addSub(this), will be executedthis.subs.push(sub), that is, theWatcherSave the instance todepsubsIn the.
  8. invm._render()This triggers the getter for all the data, which actually completes the dependency collection process, butWatcherConstructorthis.get()There are some more operations to follow.
  9. if (this.deep) { traverse(value) }We’re going to recursevalueThat triggers all of its childrengetter
  10. Then performpopTarget(), dependency collection ends, resetDep.target.
  11. And then finally, callthis.cleanupDeps()To clear dependencies.

Array response

// src/core/observer/index.js
export class Observer {
  // ...
  constructor(value: any) {
    // ...
    if (Array.isArray(value)) {
      // Override the default seven prototype methods for arrays to implement array responsiveness
      // hasProto = '__proto__' in {}
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // ...}}}Copy the code

Remember the code mentioned above in the Observer? Special treatment is made for arrays of type. Determine that __proto__ is a compatible notation because some browsers do not support it.

protoAugment

// src/core/observer/index.js
/** * Set the target.__proto__ prototype object to SRC * such as the array object, arr.__proto__ = arrayMethods */
function protoAugment (target, src: Object{
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
Copy the code

copyAugment

// src/core/observer/index.js
/** * uses def, object.defineProperty, to define its own property values * such as array: define the seven methods */ for array objects
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

def

// src/core/util/lang.js
export function def (obj: Object, key: string, val: any, enumerable? : boolean{
  Object.defineProperty(obj, key, {
    value: val,
    enumerable:!!!!! enumerable,writabletrue.configurabletrue})}Copy the code

Seven methods

// src/core/observer/array.js
Prototype * When accessing the seven methods on the arrayMethods object, it will be hijacked to implement array-responsive */
import { def } from '.. /util/index'
// Back up the array prototype object
const arrayProto = Array.prototype
// Create a new arrayMethods by inheritance
export const arrayMethods = Object.create(arrayProto)
// Operate on an array of seven methods
const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
/** * intercepts the method and fires the event */
methodsToPatch.forEach(function (method{
  // Cache native methods such as push
  const original = arrayProto[method]
  // def is object.defineProperty, which hijacks access to ArrayMethods. method
  def(arrayMethods, method, function mutator (. args{
    // Implement native methods like push.apply(this, args)
    const result = original.apply(this, args)
    const ob = this.__ob__
    // If method is one of the following, a new value is inserted
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // Respond to the newly inserted value
    if (inserted) ob.observeArray(inserted)
    // Notification update
    ob.dep.notify()
    return result
  })
})
Copy the code

The array method rewrite does a few things:

  • arrayMethodsinheritedArray.
  • Seven methods in an array that can change the array itself are hijacked and overwritten so that when they are called, the overwritten method is called.
  • The rewritten method first invokes the logic from the original prototype.
  • Identify three ways you can add valuespush,unshift,splice, gets the newly inserted value for response processing.
  • The last callob.dep.notify()Notification update.

$set and $delete

In the application, the data is set to be responsive during initialization. But the data for the response is defined and declared at initialization. If you add attributes to data, it does not exist at initialization. For example: {a: {b: 1}} adds an attribute c -> {a: {b: 1, c: 2}}, attribute C does not exist in the initialization phase, so how does it respond? Vue provides the global API Vue. Set, Vue. Delete and instance methods vm.$set, vm.$delete to handle the addition and removal of object attributes, ensuring that updated views are triggered. Let’s look at some definitions of this method.

// src/core/global-api/index.js
import { set, del } from '.. /observer/index'
export function initGlobalAPI (Vue: GlobalAPI{
  // ...
  Vue.set = set
  Vue.delete = del
}
Copy the code
// src/core/instance/state.js
import {
  set,
  del,
  observe,
  defineReactive,
  toggleObserving
} from '.. /observer/index'
export function stateMixin (Vue: Class<Component>{
  // ...
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
}
Copy the code

Set and vue. delete are the same as the instance methods vm.$set and vm.$delete.

set

// src/core/observer/index.js
Val * If target is an object and the key does not already exist, set the response for the new key, and then execute dependency notification */
export function set (target: Array<any> | Object, key: any, val: any) :any {
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}Vue. Set (array, IDx, val)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  Vue. Set (obj, key, val)
  if (key intarget && ! (keyin Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // Can't add dynamic add response attributes to Vue instances or $data, one of vmCount's uses,
  // Ob. vmCount = 1 for this.$data
  if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // Target is not a reactive object. The new property will be set, but it will not be reactive
  if(! ob) { target[key] = valreturn val
  }
  // Define new attributes for the object, set responsiveness via defineReactive method, and trigger dependency updates
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
Copy the code

del

// src/core/observer/index.js
/** * deletes the specified key * array of the target object via vue. delete or vm.$delete using the splice method. The object deletes the specified key using the delete operator and performs dependency notification */
export function del (target: Array<any> | Object, key: any{
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)}// If target is an array, use the splice method to delete the element with the specified subscript
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  // Avoid deleting Vue instance attributes or $data
  if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  // If the attribute does not exist, end it
  if(! hasOwn(target, key)) {return
  }
  // Delete the attributes of an object with the delete operator
  delete target[key]
  if(! ob) {return
  }
  // Perform dependency notification
  ob.dep.notify()
}
Copy the code

methods

initMethods

// src/core/instance/state.js
function initMethods (vm: Component, methods: Object{
  // Get the props configuration item
  const props = vm.$options.props
  // Iterate over the methods object
  for (const key in methods) {
    if(process.env.NODE_ENV ! = ='production') {
      if (typeofmethods[key] ! = ='function') {
        warn(
          `Method "${key}" has type "The ${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly? `,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      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] = typeofmethods[key] ! = ='function' ? noop : bind(methods[key], vm)
  }
}
Copy the code

Iterating through a Methods object and processing each one does several things:

  • checkmethoss[key]It has to be a function.
  • checkmethoss[key]Can’t withpropsIn the same.
  • checkmethoss[key]Cannot be the same as the method on the instance, usually some built-in method, such as to$ 和  _The way to start.
  • willmethods[key]In thevmInstance.

Compute properties vs. listen properties

computed

// src/core/instance/state.js
const computedWatcherOptions = { lazytrue }
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 a computed object
  for (const key in computed) {
    /** * computed = { * key1: function() { return xx }, * } */
    const userDef = computed[key]
    // getter = function() { return xx }
    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 an instance of Watcher for the computed property, which is computed Watcher differently than rendered Watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        // Config item, computed is lazy by default
        computedWatcherOptions
      )
    }
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if(! (keyin vm)) {
      // Proxy attributes in computed objects to VM instances
      defineComputed(vm, key, userDef)
    } else if(process.env.NODE_ENV ! = ='production') {
      // Attributes of computed cannot be the same as those of data, props, and methods.
      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)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}
/** * Proxies key in computed objects to target (VM) */
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
{
  constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else{ sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if(process.env.NODE_ENV ! = ='production' &&
    sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function ({
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`.this)}}// The proxy accesses computed[key] and sets get and set for computed[key]
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
/** * returns a function as the getter for computed[key] */
function createComputedGetter (key{
  // The principle that computed attribute values are cached is also implemented here in combination with watcher.dirty, watcher.evalaute, and watcher.update
  return function computedGetter ({
    // Get computed Watcher for computed[key]
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      / / cache
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // Add a subscription
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
/** * functions as createComputedGetter */
function createGetterInvoker (fn{
  return function computedGetter ({
    return fn.call(this.this)}}Copy the code

As you can see from the above code, the initialization of the computed property does several things:

  • rightcomputed[key]createWatcherInstance.
  • checkcomputedThe property of thedata,props,methodsProperty with the same name.
  • The agentcomputed[key]vmIs on the instance, and sets the getter and setter.

Process analysis

As mentioned earlier, during the initialization of computed properties, an instance of Watcher is created for computed[key], which is not quite the same as rendered Watcher. In Watcher’s constructor there is this logic:

// src/core/observer/watcher.js
constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    if (options) {
      this.lazy = !! options.lazy }this.dirty = this.lazy // for lazy watchers
    this.value = this.lazy
      ? undefined
      : this.get()
  }
Copy the code

Default lazy execution, not immediately evaluated. When the render function executes a call to the calculated property, it fires the getter for the calculated property, also known as the computedGetter:

// src/core/instance/state.js
  return function computedGetter ({
    // Get computed Watcher for computed[key]
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      / / cache
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // Add a subscription
      if (Dep.target) { // This time the render watcher, depend call will change
        watcher.depend()
      }
      return watcher.value
    }
  }
Copy the code

Get computed[Key] corresponding to computed Watcher; Execute watcher.evaluate() if watcher.dirty is true, and then execute watcher.depend().

// src/core/observer/watcher.js
 evaluate () {
    this.value = this.get()
    this.dirty = false
  }
 get () {
    // called when dependency collection is needed
    // Set targetstack.push (target) and dep.target = watcher
    pushTarget(this)
    let value
    try {
      // This triggers a dependency property read
      value = this.getter.call(vm, vm)
    } catch (e) {
     / /...
    } finally {
        // Rely on the collection to end the call
        // Set targetstack.pop () and targetStack[targetstack.length-1]
      popTarget()
    }
    return value
  }
Copy the code

Evaluate is implemented by evaluating this.get(); Then set this.dirty to false. During evaluation, because the values used for evaluation are also reactive, their getters are also fired. As described earlier, they collect the current WATcher, which in this case DP. Target is computed Watcher, as a dependency in their Dep. This is equivalent to adding computed Watcher to the attribute-dependent DEP; It also adds its OWN DEP to Computed Watcher. Then proceed:

// src/core/observer/watcher.js
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend() // Actually call dep's Depend}}// src/core/observer/dep.js
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}Copy the code

Watcher.depend () executes the above code, and watcher.evaluate() resets dep.target to render watcher. This.deps [I] is the DEP in computed Watcher, that is, attribute-dependent DEP. So this.deps[I].depend() is equivalent to adding the render Watcher to the attribute-dependent DEP. That is, rendered and computed Watcher are collected in the DEP corresponding to the dependency attribute.

When the dependent property of the calculated property changes, the setter is triggered to notify Watcher of the update, calling the watcher.update method.

// src/core/observer/watcher.js
export default class Watcher {
  // When set is triggered by responsive data in computed
  update () { 
    if (this.lazy) {
      // Notification computed needs to be recalculated
      this.dirty = true}}}Copy the code

First, notify computed Watcher that it needs to be recalculated, then notify the view to perform rendering, which accesses computed values, and finally render to the page.

watch

// src/core/instance/state.js
function initWatch (vm: Component, watch: Object{
  // Iterate over the watch object
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      // If handler is an array, each item is iterated
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
Copy the code
// src/core/instance/state.js
function createWatcher (
  vm: Component,
  expOrFn: string | Function, handler: any, options? :Object
{
  // If handler is an object, get the value of the handler option in it
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // If hander is a string, it is a method to obtain vm[handler].
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
Copy the code

$watch = vm.$watch = vm.$watch Vm.$watch is defined in stateMixin:

// src/core/instance/state.js
export function stateMixin (Vue: Class<Component>{
   // ...
  Vue.prototype.$watch = function (
    expOrFn: string | Function, cb: any, options? :Object
  ) :Function {
    const vm: Component = this
    // Processing cb may be an object
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    // options.user stands for user watcher, as well as render watcher, which is instantiated in the updateComponent method
    options = options || {}
    options.user = true
    / / create a Watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // If the user sets immediate to true, the callback function is executed immediately
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    // Returns a function to remove the watcher
    return function unwatchFn ({
      watcher.teardown()
    }
  }
}
Copy the code

Vm.$watch does a few things:

  • To deal withcbIs the case of the object.
  • throughoptions.user = truelogouser watcher.
  • createWatcherInstance.
  • If the user sets itimmediate 为 true, the callback function is executed immediately.
  • Returns a function to remove thiswatcher.

This is where we go when we set deep: True with watch.

// src/core/observer/watcher.js
export default class Watcher {
  get () {
     / /...
     if (this.deep) {
        traverse(value)
      }
      // ...}}Copy the code

Remember, this is going to recursively access the value, trigger the getters for all of its children, and you get deep listening.

Through the analysis of the implementation of computed and watch attributes, computed is mainly used for applications that depend on the calculation of other attributes, and watch is used for operations that we need to do when a certain attribute changes. Computed and Watch are essentially the same things that Watcher did. One is computed Watcher and the other is User Watcher.

A link to the

Vue (V2.6.14) source code detoxification (pre) : handwritten a simple version of Vue

Vue (V2.6.14) source code detoxification (a) : preparation

Vue (V2.6.14) source code detoxification (two) : initialization and mount

Vue (V2.6.14) source code detoxification (three) : response type principle

Vue (V2.6.14) source code detoxification (four) : update strategy (to be continued)

Vue (v2.6.14) source code detoid (v) : Render and VNode (to be continued)

Vue (v2.6.14) source code: Update and patch (to be continued)

Vue (v2.6.14) source code detoxification (seven) : template compilation (to be continued)

If you think it’s ok, give it a thumbs up!! You can also visit my personal blog at www.mingme.net/