Interpretation of Vue source code -- responsive principle

Posted by mr_armageddon on Wed, 23 Feb 2022 04:06:59 +0100

When learning becomes a habit, knowledge becomes common sense. Thank you for your likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

preface

Last article Interpretation of Vue source code (2) -- Vue initialization process Explain the initialization process of Vue in detail and understand what new Vue(options) does. The implementation of data response is briefly introduced in one sentence, and this article will explain the implementation principle of Vue data response in detail.

target

  • Deeply understand the principle of Vue data response.
  • What is the difference between methods, computed and watch?

Source code interpretation

After learning from the previous article, I believe we all know the entry position of the source code reading of the principle of responsiveness, which is the step of processing data responsiveness in the initialization process, that is, calling the initState method in / SRC / core / instance / init JS file.

initState

/src/core/instance/state.js

/**
 * Two things:
 *   Data responsive entry: process props, methods, data, computed, and watch respectively
 *   Priority: the attributes in props, methods, data and computed objects cannot be duplicated, and the priority is consistent with the listing order
 *         The key in computed cannot be the same as the key in props and data, and the methods do not affect it
 */
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // Process props object, set response formula for each property of props object, and proxy it to vm instance
  if (opts.props) initProps(vm, opts.props)
  // Process the methods object, check whether the value of each attribute is a function, compare it with the props attribute, and judge it again. Finally, vm[key] = methods[key]
  if (opts.methods) initMethods(vm, opts.methods)
  /**
   * Did three things
   *   1,The attributes on the data object cannot be the same as those on the props and methods objects
   *   2,Proxy attributes on data objects to vm instances
   *   3,Set the response expression for the data on the data object 
   */
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  /**
   * Three things:
   *   1,Create a watcher instance for computed[key]. The default is lazy execution
   *   2,Proxy computed[key] to vm instance
   *   3,The key in calculated cannot duplicate the attribute in data and props
   */
  if (opts.computed) initComputed(vm, opts.computed)
  /**
   * Three things:
   *   1,Processing the watch object
   *   2,For each watch Key creates a watcher instance. The relationship between key and watcher instance may be one to many
   *   3,If immediate is set, the callback function is executed immediately
   */
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
    
  /**
   * In fact, it can also be seen here that there is no difference between calculated and watch in essence. They are both responsive through the watcher
   * If there is any difference, it is only the difference in the way of use. To put it simply:
   *   1,watch: It is applicable to asynchronous or expensive operations when data changes, that is, operations that need to wait for a long time can be put in the watch
   *   2,computed: Asynchronous methods can be used, but they don't make any sense. Therefore, computed is more suitable for some synchronous calculations
   */
}

initProps

src/core/instance/state.js

// Process props object, set response formula for each property of props object, and proxy it to vm instance
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // Cache each key of props and optimize performance
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  // Traversal props object
  for (const key in propsOptions) {
    // Cache key
    keys.push(key)
    // Get the default value of props[key]
    const value = validateProp(key, propsOptions, propsData, vm)
    // Set the data response 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 (!(key in vm)) {
      // Proxy key to vm object
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

proxy

/src/core/instance/state.js

// Set the proxy and proxy the key to the target
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)
}

initMethods

/src/core/instance/state.js

/**
 * Did the following three things, in fact, the most critical thing is the third thing
 *   1,Verify that the metadata [key] must be a function
 *   2,Weight judgment
 *         methods The key in cannot be the same as the key in props
 *         methos The key in overlaps with the existing methods on the Vue instance. It is generally some built-in methods, such as $and_ Starting method
 *   3,Put methods[key] on the vm instance and get vm[key] = methods[key]
 */
function initMethods (vm: Component, methods: Object) {
  // Get props configuration item
  const props = vm.$options.props
  // Traversal methods object
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${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] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

initData

src/core/instance/state.js

/**
 * Did three things
 *   1,The attributes on the data object cannot be the same as those on the props and methods objects
 *   2,Proxy attributes on data objects to vm instances
 *   3,Set the response expression for the data on the data object 
 */
function initData (vm: Component) {
  // Get data object
  let data = vm.$options.data
  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
    )
  }
  /**
   * Two things
   *   1,The attributes on the data object cannot be the same as those on the props and methods objects
   *   2,Proxy attributes on data objects to vm instances
   */
  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(vm, `_data`, key)
    }
  }
  // Set the response expression for the data on the data object
  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()
  }
}

initComputed

/src/core/instance/state.js

const computedWatcherOptions = { lazy: true }

/**
 * Three things:
 *   1,Create a watcher instance for computed[key]. The default is lazy execution
 *   2,Proxy computed[key] to vm instance
 *   3,The key in calculated cannot duplicate the attribute in data and props
 * @param {*} computed = {
 *   key1: function() { return xx },
 *   key2: {
 *     get: function() { return xx },
 *     set: function(val) {}
 *   }
 * }
 */
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()

  // Traversing the computed object
  for (const key in computed) {
    // Get the value corresponding to key, that is, getter function
    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 a watcher instance for the computed property
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        // Configuration item. computed is lazy by default
        computedWatcherOptions
      )
    }

    if (!(key in vm)) {
      // Proxy the properties in the computed object to the vm instance
      // This allows you to use VM Computedkey accesses the calculation property
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // The non production environment has a re judgment process. The attributes in the calculated object cannot be the same as those in data and props
      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)
      }
    }
  }
}

/**
 * Proxy the key in the computed object to the target (vm)
 */
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  // Construct attribute descriptors (get, set)
  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
      )
    }
  }
  // Intercept target Key access and settings
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

/**
 * @returns Returns a function that accesses VM Computedproperty is executed and the execution result is returned
 */
function createComputedGetter (key) {
  // The principle that the calculated attribute value will be cached is also combined with the watcher dirty,watcher.evalaute,watcher.update implementation
  return function computedGetter () {
    // Get the watcher corresponding to the current key
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // Calculate the value corresponding to the key by executing calculated Key callback function
      // watcher. The dirty attribute is the principle that the computed calculation result will be cached
      // <template>
      //   <div>{{ computedProperty }}</div>
      //   <div>{{ computedProperty }}</div>
      // </template>
      // In this case, in one rendering of the page, there is only the first computedProperty in the two DOMS
      // Will execute computed The callback function of computedproperty calculates the actual value,
      // Execute the watcher Evalaute, and the second one doesn't go through the calculation process,
      // Because the last time I executed watcher When evaluating, put the watcher Set dirty to false,
      // After the page is updated, wathcer The update method will put the watcher Dirty reset to true,
      // For recalculating the calculated. When the next page is updated Key results
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

/**
 * Same function as getecomputer
 */
function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

initWatch

/src/core/instance/state.js

/**
 * Two things are done to handle the entry of the watch object:
 *   1,Traversing the watch object
 *   2,Call the createWatcher function
 * @param {*} watch = {
 *   'key1': function(val, oldVal) {},
 *   'key2': 'this.methodName',
 *   'key3': {
 *     handler: function(val, oldVal) {},
 *     deep: true
 *   },
 *   'key4': [
 *     'this.methodNanme',
 *     function handler1() {},
 *     {
 *       handler: function() {},
 *       immediate: true
 *     }
 *   ],
 *   'key.key5' { ... }
 * }
 */
function initWatch (vm: Component, watch: Object) {
  // Traversing the watch object
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      // handler is an array, traversing the array, getting every item in it, and then calling createWatcher.
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

/**
 * Two things:
 *   1,Compatibility processing to ensure that the handler must be a function
 *   2,Call $watch 
 * @returns 
 */
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // If the handler is an object, get the value of the handler option
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // If the handler is a string, it means a methods method to get vm[handler]
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

/**
 * Create a watcher and return to unwatch. Complete the following five things:
 *   1,Compatibility processing to ensure that cb in the last new Watcher is a function
 *   2,Mark user watcher
 *   3,Create a watcher instance
 *   4,If immediate is set, cb is executed immediately
 *   5,Return to unwatch
 * @param {*} expOrFn key
 * @param {*} cb Callback function
 * @param {*} options Configuration item, the user directly calls this$ A configuration item may be passed when watching
 * @returns Returns the unwatch function, which is used to cancel watch listening
 */
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  // Compatibility processing because the user calls VM$ The cb set during watch may be an object
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  // options.user refers to the user watcher, as well as the render watcher, that is, the watcher 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) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // Returns an unwatch function, which is used to cancel listening
  return function unwatchFn () {
    watcher.teardown()
  }
}

observe

/src/core/observer/index.js

/**
 * The real entrance to responsive processing
 * Create an observer instance for the object. If the object has been observed, return the existing observer instance. Otherwise, create a new observer instance
 * @param {*} value Object = > {}
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // Non object and VNode instances are not processed in response
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // If the value object exists__ ob__  Property, it indicates that observation has been made, and it is returned directly__ ob__  attribute
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // Create observer instance
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

/src/core/observer/index.js

/**
 * The Observer class will be attached to each observed object, value__ ob__ =  this
 * The properties of the object are converted into getters / setters, and dependency and notification updates are collected
 */
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
    // A dep instance
    this.dep = new Dep()
    this.vmCount = 0
    // Set on value object__ ob__  attribute
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      /**
       * value As array
       * hasProto = '__proto__' in {}
       * Used to determine whether the object exists__ proto__  Property through obj__ proto__  Prototype chain of objects that can be accessed
       * But because__ proto__  It is not a standard attribute, so some browsers do not support it, such as IE6-10 and Opera 10 one
       * Why should we judge? It's because we have to pass it later__ proto__  Prototype chain of operational data
       * Override the default seven prototype methods of the array to realize the array response
       */
      if (hasProto) {
        // Yes__ proto__
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // value is the object and sets the response expression for each attribute of the object (including nested objects)
      this.walk(value)
    }
  }

  /**
   * Traverse each key on the object and set the response formula for each key
   * This is only possible if the value is an object
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Traverse the array, set observation for each item of the array, and handle the case that the array element is an object
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive

/src/core/observer/index.js

/**
 * Intercept reading and setting operations of obj[key]:
 *   1,Collect dependencies during the first reading. For example, there will be a read operation when the render function is executed to generate a virtual DOM
 *   2,Set new values when updating and notify dependent updates
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // Instantiate DEP, one key and one dep
  const dep = new Dep()

  // Get the attribute descriptor of obj[key] and return it directly if it is found to be a non configurable object
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

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

  // Recursive call to handle the case where the value of val, i.e. obj[key], is an object, so as to ensure that all keys in the object are observed
  let childOb = !shallow && observe(val)
  // Responsive core
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // get intercepts the reading operation of obj[key]
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      /**
       * Dep.target It is a static attribute of Dep class with the value of Watcher, which will be set when instantiating Watcher
       * When instantiating watcher, the callback function passed by new Watcher will be executed (except computed, because it is lazy to execute)
       * If there is VM in the callback function The read behavior of key will trigger the read interception here for dependency collection
       * After the callback function is executed, it will set Dep.target to null to avoid repeated collection of dependencies here
       */
      if (Dep.target) {
        // Depending on the collection, add a watcher in dep and DEP in the watcher
        dep.depend()
        // childOb refers to the observer object of the nested object in the object. If it exists, it is also subject to dependency collection
        if (childOb) {
          // This is this key. The reason why the responsive update can be triggered when the chidlkey is updated
          childOb.dep.depend()
          // If obj[key] is an array, the array response is triggered
          if (Array.isArray(value)) {
            // Add dependencies to the items of an array object
            dependArray(value)
          }
        }
      }
      return value
    },
    // set intercepts the setting of obj[key]
    set: function reactiveSetter (newVal) {
      // Old obj[key]
      const value = getter ? getter.call(obj) : val
      // If the new and old values are the same, return directly. If they are not the same as the new values, the reactive update process will not be triggered
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // The absence of setter indicates that this property is read-only. return directly
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      // Set new value
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // Observe the new value so that the new value is also responsive
      childOb = !shallow && observe(newVal)
      // Dependency notification update
      dep.notify()
    }
  })
}

dependArray

/src/core/observer/index.js

/**
 * Traverse each array element, recursively handle the case where the array item is an object, and add dependencies for it
 * Because the previous recursion phase cannot add dependencies to the object elements in the array
 */
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)
    }
  }
}

Array response

src/core/observer/array.js

/**
 * Defines the arrayMethods object, which is used to enhance array prototype
 * When accessing the seven methods on the arrayMethods object, it will be intercepted to implement the array response
 */
import { def } from '../util/index'

// Backup array prototype object
const arrayProto = Array.prototype
// Create new arrayMethods by inheritance
export const arrayMethods = Object.create(arrayProto)

// There are seven methods to manipulate the array, which can change the array itself
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutation methods and trigger events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  // Cache native methods, such as push
  const original = arrayProto[method]
  // def is object Defineproperty, intercepting arraymethods Method access
  def(arrayMethods, method, function mutator (...args) {
    // Execute native methods first, such as push apply(this, args)
    const result = original.apply(this, args)
    const ob = this.__ob__
    // If the method is one of the following three, the element is newly inserted
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // Respond to newly inserted elements
    if (inserted) ob.observeArray(inserted)
    // Notification update
    ob.dep.notify()
    return result
  })
})

def

/src/core/util/lang.js

/**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

protoAugment

/src/core/observer/index.js

/**
 * Set target__ proto__  The prototype object of is src
 * For example, array objects, arr__ proto__ =  arrayMethods
 */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

copyAugment

/src/core/observer/index.js

/**
 * Defines the specified attribute on the target object
 * For example, array: define the seven methods for the array object
 */
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])
  }
}

Dep

/src/core/observer/dep.js

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
 * A dep corresponds to an obj key
 * When reading responsive data, it is responsible for collecting dependencies. What are the watcher s that each dep (or obj.key) depends on
 * When the responsive data is updated, it is responsible for notifying those watcher s in the dep to execute the update method
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

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

  // Add a watcher in dep
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

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

  // Like adding dep to watcher
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  /**
   * Notify all watchers in dep and execute watcher Update() method
   */
  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)
    }
    // Traverse the watcher stored in dep and execute the watcher update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

/**
 * For the currently executing watcher, only one Watcher will be executing at the same time
 * Dep.target = Currently executing watcher
 * Complete the assignment by calling pushTarget method and reset by calling popTarget method (null)
 */
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
}

// Dependency collection end call, set Dep.target = null
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher

/src/core/observer/watcher.js

/**
 * A component a watcher (render watcher) or an expression a watcher (user watcher)
 * When the data is updated, the watcher will be triggered to access this The watcher will also be triggered when 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.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.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()
    this.newDepIds = new Set()
    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 }
      // In this Execute this in get Dependency collection will be triggered when getter
      // To be followed up When XX is updated, a response will be triggered
      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()
  }

  /**
   * Execute this Getter and collect dependencies again
   * this.getter It is the second parameter passed when instantiating the watcher, a function or string, such as the read this returned by updateComponent or parsePath Function of XX attribute value
   * Why collect dependencies again?
   *   Because triggering the update indicates that the responsive data has been updated, but the updated data has been observe d, but there is no dependency collection,
   *   Therefore, when updating the page, the render function will be executed again, and the read operation will be triggered during execution. At this time, dependency collection will be carried out
   */
  get () {
    // Open Dep.target, Dep.target = this
    pushTarget(this)
    // value is the result of the callback function execution
    let value
    const vm = this.vm
    try {
      // Execute the callback function, such as updateComponent, and enter the patch phase
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // Close Dep.target, Dep.target = null
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   * Two things:
   *   1,Add dep to yourself (watcher)
   *   2,Add a watcher to dep
   */
  addDep (dep: Dep) {
    // If dep already exists, it will not be added repeatedly
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // Cache dep.id for duplicate judgment
      this.newDepIds.add(id)
      // Add dep
      this.newDeps.push(dep)
      // Avoid adding watcher, this. Repeatedly in dep Depids is set in the cleanupDeps method
      if (!this.depIds.has(id)) {
        // Add watcher to dep
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  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
  }

  /**
   * According to the watcher configuration item, decide how to go next, usually queueWatcher
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // Go here when lazy execution, such as computed

      // Setting dirty to true allows the computedGetter to recalculate the execution result of the computed callback function when executing
      this.dirty = true
    } else if (this.sync) {
      // Synchronous execution, using VM$ You can pass a sync option when using the watch or watch option,
      // When true, the watcher will not go to the asynchronous update queue and directly execute this run 
      // Method
      // This attribute does not appear in the official document
      this.run()
    } else {
      // When updating, it is usually here to put the watcher into the watcher queue
      queueWatcher(this)
    }
  }

  /**
   * The refresh queue function flushSchedulerQueue is called to complete the following:
   *   1,Execute the second parameter passed by instantiating the watcher, updateComponent, or get this A function of XX (the function returned by parsePath)
   *   2,Update old value to new value
   *   3,Execute the third parameter passed when instantiating the watcher, such as the callback function of the user watcher
   */
  run () {
    if (this.active) {
      // Call this Get method
      const value = this.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
      ) {
        // Update old value to new value
        const oldValue = this.value
        this.value = value

        if (this.user) {
          // If it is the user watcher, the callback function, the third parameter passed by the user, will be executed. The parameters are val and oldVal
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          // Render watcher, this CB = NOOP, an empty function
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * The lazy watcher will call this method
   *   For example: computed, getting VM This method is called when the value of computedproperty is
   * Then execute this Get, the callback function of the watcher, gets the return value
   * this.dirty It is set to false, so that the page will only be computed once in this rendering Callback function of key,
   *   This is also one of the differences between computed and methods. It is the principle that computed has a cache
   * The page will update this Dirty will be reset to true. This step is in this Completed in the update method
   */
  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
    }
  }
}

summary

The interviewer asked: how is the Vue responsive principle implemented?

Answer:

  • The core of the response is through object Defineproperty intercepts access to and setting of data
  • There are two types of responsive data:

    • Object, loop through all the attributes of the object, and set getters and setters for each attribute to intercept access and set. If the attribute value is still the object, recursively set getters and setters for each key on the attribute value

      • When accessing data (obj.key), collect dependencies and store relevant watcher s in dep
      • When setting data, dep notifies the relevant watcher to update
    • Array, the seven enhanced arrays can change their own prototype methods, and then intercept the operations on these methods

      • When new data is added, it is processed in a responsive manner, and then dep notifies the watcher to update it
      • When deleting data, dep should also notify the watcher to update

The interviewer asked: what is the difference between methods, calculated and watch?

Answer:

<!DOCTYPE html>
<html lang="en">

<head>
  <title>methods,computed,watch What's the difference?</title>
</head>

<body>
  <div id="app">
    <!-- methods -->
    <div>{{ returnMsg() }}</div>
    <div>{{ returnMsg() }}</div>
    <!-- computed -->
    <div>{{ getMsg }}</div>
    <div>{{ getMsg }}</div>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    new Vue({
    el: '#app',
    data: {
      msg: 'test'
    },
    mounted() {
      setTimeout(() => {
        this.msg = 'msg is changed'
      }, 1000)
    },
    methods: {
      returnMsg() {
        console.log('methods: returnMsg')
        return this.msg
      }
    },
    computed: {
      getMsg() {
        console.log('computed: getMsg')
        return this.msg + ' hello computed'
      }
    },
    watch: {
      msg: function(val, oldVal) {
        console.log('watch: msg')
        new Promise(resolve => {
          setTimeout(() => {
            this.msg = 'msg is changed by watch'
          }, 1000)
        })
      }
    }
  })
  </script>
</body>

</html>

Click to view the dynamic diagram demonstration , dynamic diagram address: https://p6-juejin.byteimg.com...

The example is actually the answer

  • Usage scenario

    • methods are generally used to encapsulate some complex processing logic (synchronous and asynchronous)
    • computed is generally used to encapsulate some simple synchronization logic, return the processed data, and then display it in the template to reduce the weight of the template
    • watch is generally used to perform asynchronous or expensive operations when data changes
  • difference

    • methods VS computed

      Through the example, we can find that if the same methods or computed attribute is used in multiple places in a rendering, the methods will be executed multiple times, while the computed callback function will only be executed once.

      By reading the source code, we know that in a rendering, if you access the computed property multiple times, you will only execute the callback function of the computed property for the first time. For other subsequent accesses, you will directly use the first execution result (watcher.value), and the implementation principle of all this is through The control of dirty attribute is implemented. methods, each access is a simple method call (this.xxMethods).

    • computed VS watch

      Through reading the source code, we know that the essence of computed and watch is the same. They are implemented internally through Watcher. In fact, there is no difference. There are only two points to make the difference: 1. The difference in use scenarios; 2. Computed is lazy by default and cannot be changed.

    • methods VS watch

      In fact, there is nothing comparable between methods and watch. They are completely two things. However, in use, some logic in watch can be drawn into methods to improve the readability of the code.

link

Thank you for your likes, collections and comments. I'll see you next time.

When learning becomes a habit, knowledge becomes common sense. Thank you for your likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

Topics: Javascript Front-end ECMAScript TypeScript Vue.js