reference

This article is based on reference and writes in the most simple way: after the individual thinking about vue rely on collecting parts: ustbhuangyi. Making. IO/vue – analysi… Vue source code parsing: github.com/answershuto…

The source code to achieve

The implementation process of bidirectional binding is as follows

According to the figure above (see github.com/DMQ/mvvm), bidirectional binding must implement the following:

  • Implement a data listener Observer that listens for all attributes of the data
  • Implement a subscriber container Dep that collects subscribers and notifies them when data changes
  • Implement an instruction parser Compile, scan and parse the instruction of each element node, replace the data according to the instruction template, bind the corresponding update function, initialize the view
  • Implement an Observer Watcher that acts as a bridge between the Observer and Compile, subscribing to receive notification of each property change, executing the corresponding callback function of the instruction, and updating the view

Source code execution flowchart:

Listen for the implementation of the data

Data hijacking monitor source flow chart is as follows:

Initialize data initData

InitData initializes data, listens for data changes, and responds to data changes

  • Get data and judge
  • Gets Props, checks whether the properties in data are defined in Props, and raises a warning if they are defined
  • Proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy proxy
  • Observe Data. Bind data
function initData (vm: Component) {

  /* Get data */
  // If data in the configuration is a function, the function is executed; if data is an object, the function is fetched directly
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  /* Check if it is an object */
  // Issue a warning if data is not an object and is not in a formal environment
  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
  /* Iterate over the data object */
  const keys = Object.keys(data)
  const props = vm.$options.props
  let i = keys.length

  // Iterate over the data in data
  while (i--) {
    /* Ensure that the key in data does not duplicate the key in props, props takes precedence. If any conflict occurs, warning*/ will be generated
    if(props && hasOwn(props, keys[i])) { process.env.NODE_ENV ! = ='production' && warn(
        `The data property "${keys[i]}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if(! isReserved(keys[i])) {/* Check whether the field is reserved */

      /* Proxy the attributes on data to the VM instance, that is, assuming data has property A,this.data.a can be accessed through this.a */
      proxy(vm, `_data`, keys[i])
    }
  }
  // observe data
  /* Observe this step as the root data and recursively bind the underlying object. Observe this step as the root data. * /
  observe(data, true /* asRootData */)}Copy the code

Proxy (Proxy data)

When we access properties of data, props, computed, and methods in vUE, we use this.propertyName. When we initialize, we also distinguish between these properties. Given that data has property A, how do we access this.data.a from this.a? Proxies do just that, helping us proxy data to vm instances.

// Target is the instance of the proxy,proxyObjectName is the name of the proxied object, and proxyKey is the attribute of the proxied object
function proxy(target,proxyObjectName,proxyKey){
 Object.defineProperty(target,proxyKey,{
  enumerable:true.configurable:true.get:function proxyGetter(){
   // Notice that this points to target at runtime
   return this[proxyObjectName][proxyKey]
  },
  set:function proxySetter(newVal){
   this[proxyObjectName][proxyKey] = newVal
  }
 })
}
Copy the code

[proxyKey] [proxyKey] [proxyKey] [proxyKey] [proxyKey] [proxyKey] This is different from copying references directly

observe

The observe function attempts to create an Observer instance (OB) and returns a new Observer instance if it is successfully created or an existing Observer instance if it already exists. The Observer instance is placed on the current object

  • Determines whether the current data is an object
  • Check whether the __ob__ attribute exists, reference it if it does, and create an Observer instance if it does not
  • Count if it is root data
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */
 /* Attempt to create an Observer instance (__ob__) and return a new Observer instance if it is successfully created or an existing Observer instance if it already exists. * /
export function observe (value: any, asRootData: ? boolean) :Observer | void {
  /* Check if it is an object */
  if(! isObject(value)) {return
  }
  let ob: Observer | void

  /* The __ob__ attribute is used to determine whether an Observer instance exists. If no Observer instance exists, a new Observer instance is created and assigned to __ob__. If an Observer instance exists, the Observer instance is returned
 
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (

    /* This is done to ensure that value is a pure object, not a function or Regexp. * /observerState.shouldConvert && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
  }
  if (asRootData && ob) {

    Observeobserve's asRootData is not true*/
    ob.vmCount++
  }
  return ob
}
Copy the code

Vue’s reactive data is marked with an __ob__ attribute, which stores an Observer of that attribute, an instance of an Observer, to prevent repeated binding. So to determine whether the data is responsive, see if the current data contains an __ob__ attribute

Observer(Data listener)

An Observer instance exists in the __ob__ attribute of each responsive data, and the Observer constructor iterates through all attributes of the object, binding them bidirectionally to make the attributes responsive.

An Observer instance should have the following attributes:

  • Value :any Saves the value of the current object
  • Dep: DEP saves the dependency collection
  • VmCount :number Saves the number of times the vm instance uses the current object as root $data

There are the following methods:

  • Walk (obj:Object) binds data of the Object type
  • ObserveArray (array: array), bind an array member (call observe to the member)

The steps are as follows:

  • Bind the Observer instance to the __ob__ property of the target object
  • Checks whether the current target object is an array
  • Listen for array methods and try to build an Observer instance for each member of the array (call the observe function).
  • Is the property of the responder object
/** * Observer class that are attached to each observed * object. Once attached, the observer converts target * object's property keys into getter/setters that * collect dependencies and dispatches updates. */
 /* The Observer class is assigned to each responsive object. Once you have an Observer instance, Obsever translates getters /setters for the properties of the target object so that getters can collect dependencies and setters can publish updates */
 
export class  {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0

    /* Bind an Observer instance to data's __ob__ property. Observe will check if an __ob__ object already holds an Observer instance. Def method definition can refer to https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16 * /
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {
      // The response of the array is reflected when the method is called, so modifying the array members directly with the subscript cannot respond
      /* If it is an array, replace the original method in the prototype of the array with the modified array method that can intercept the response, so as to listen for the array data change response. Here, if the current browser supports the __proto__ attribute, the native array method on the current array object prototype is overridden directly, if not, the array object prototype is overridden directly. * /
      // Determine whether the __proto__ attribute is supported
      If supported, the array method on the current array object prototype is overridden directly
      // If not supported, override the target array's methods one by one
      const augment = hasProto 
        ? protoAugment  /* Override the prototype directly to modify the target object */
        : copyAugment   /* Defines (overrides) a method of the target object or array */
        
      augment(value, arrayMethods, arrayKeys)
      
      // Observe each member of the array
      /* If this is an array, observe*/ from each member of the group
      this.observeArray(value)
      
    } else {

      /* If it is an object, walk the binding */
      this.walk(value)
    }
  }

  /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */
  walk (obj: Object) {
    const keys = Object.keys(obj)

    /* The walk method walks through every attribute of the object to bind defineReactive */
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

  /** * Observe a list of Array items. */
  observeArray (items: Array<any>) {

    /* The array needs to traverse each member with observe*/
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}


Copy the code

Def function implementation

/** * 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})}Copy the code

Array response

If you modify a member of an array and that member is an object, you simply recursively bind the members of the array in both directions. But how do we listen for changes in array members when we pop, push, etc., and the objects we push are not bidirectionally bound? VUE provides methods to override the seven array methods push, POP, Shift, unshift, splice, sort, and reverse. The array is also of type Object, which means that the array is an object with specific implementation methods that we need to listen for and respond to

Overridden arrays (objects)

If the browser supports the __proto__ property, then the __proto__ property is directly changed to the array (object) overridden by VUE. If the __proto__ property is not supported, then each method in the current array needs to be overridden by the methods in the array (object) overridden by VUE.

/** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */
 /* Overrides the prototype directly to modify the target object or array */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

/** * Augment an target Object or Array by defining * hidden properties. */
/* istanbul ignore next */
/* Defines (overrides) a method of the target object or array */
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

VUE array (arrayMethods)

  • Get the prototype of the native array and create a new array object arrayMethods based on the prototype of the native array to prevent contamination of the native arrayMethods
  • Overwrite the push, pop, Shift, unshift, splice, sort, reverse methods of array objects

Steps to override methods:

  • Call the native array method
  • Get new data based on the method name, for example, if splice only takes data starting with the third argument
  • Observe the observeArray method. Bind the new array data (since the target array method will eventually inherit or be overridden by the arrayMethods method). So the object array attribute __ob__ can be called in the overridden method.
  • Notify all observers subscribed to the current data by calling notify() of the subscriber container Dep’s publish data update method in an Observer instance
/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype * /
 // Type checking is not used here because the Flow framework does not perform well with array prototype methods
 // Why is Vue3.0 developed in typeScript

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

/* Get the prototype of the native array */
const arrayProto = Array.prototype
/* Create a new array object and modify the array's seven methods to prevent contamination of the native array methods */
export const arrayMethods = Object.create(arrayProto)

/** * Intercept mutating methods and emit events */
 /* This overwrites the methods of the array without polluting the prototype of the native array, intercepts the changes in the members of the array, and notifies all associated observers to respond while performing the native array operation */
[
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
.forEach(function (method) {
  // cache original method
  /* Caches the array's native methods, followed by */
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /* Call the native array method */
    const result = original.apply(this, args)

    /* The newly inserted element in the array needs to be observed again to respond */
    const ob = this.__ob__
    // Record the newly inserted element
    let inserted
    / / if the splice (startIndex, removeNumber,... AddItems), then the new element starts with subscript 2
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // Bind the newly inserted element
    if (inserted) ob.observeArray(inserted)

    // notify change
    /* DEP notifies all registered observers for responsive processing */
    ob.dep.notify()
    return result
  })
})

Copy the code

If the current browser supports the __proto__ attribute, the entire array object can be overridden directly. If this attribute is not available, the current array object must be overridden by def methods, which is inefficient, so the first method is preferred.

As you can see from the above overridden array object, if you modify the array by subscript or setting length, you cannot listen, so you cannot bind the new element, but you can use vue. set or splice

Implementation compiler

Compile

Compile’s main tasks:

  • Parse template instructions, replace variables in the template with data, and generate an abstract syntax tree
  • Initialize the render view
  • Bind the update function to the Node Node corresponding to each instruction and add Watcher as the subscriber to the data
  • Watcher receives notification of data changes and calls update view

In the process of traversing parsing, dom nodes are operated for several times. In order to improve performance and efficiency, the EL of the root node of vue instance is first converted into document fragment for parsing and compilation. After parsing, the fragment is added back to the original real DOM node

Main steps:

  • Convert DOM nodes to fragment nodes
  • Traversal builds all nodes, distinguishing element nodes from text nodes
  • If it is an element node, traverse the instruction attributes in the element node such as V-text and V-ON
  • Compile instructions according to their type (common instructions such as V-text, V-if, etc., event instructions such as V-on :click)
  • Ordinary directives call methods on compileUtil, get their view update methods, and generate observer Wathcer(which is associated with nodes that need to be updated via the closure that instantiates Wathcher callback). Bind the view update method to an instance of Watcher.
function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        / / generated fragments
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}

Compile.prototype = {
  node2Fragment: function(el) {
      var fragment = document.createDocumentFragment(), child;
      // Copy the native node to the fragment
      while (child = el.firstChild) {
          fragment.appendChild(child);
      }
      return fragment;
  },
	init: function() { 
      this.compileElement(this.$fragment); 
  },
  compileElement: function(el) {
      // Iterate over the current node and all its children
      var childNodes = el.childNodes, me = this;
      [].slice.call(childNodes).forEach(function(node) {
          var text = node.textContent;
          var reg = / \ {\ {(. *) \} \} /;	// Expression text
          if (me.isElementNode(node)) {
              // Compile as element node
              me.compile(node);
          } else if (me.isTextNode(node) && reg.test(text)) {
              // Text node compiles
              me.compileText(node, RegExp. $1);
          }
          // Iterate over compiled child nodes
          if(node.childNodes && node.childNodes.length) { me.compileElement(node); }}); },compile: function(node) {
      // Iterate over the attributes of the current node
      var nodeAttrs = node.attributes, me = this;
      [].slice.call(nodeAttrs).forEach(function(attr) {
          // Directive named after V-xxx
           the command is V-text
          var attrName = attr.name;	// v-text
          // Determine whether the attributes beginning with v- are satisfied
          if (me.isDirective(attrName)) {
              var exp = attr.value; // content
              var dir = attrName.substring(2);	// Get the instruction text
              if (me.isEventDirective(dir)) {
                // Compile event directives, such as V-on :click
                  compileUtil.eventHandler(node, me.$vm, exp, dir);
              } else {
                // Compile common instructions, such as V-textcompileUtil[dir] && compileUtil[dir](node, me.$vm, exp); }}}); }};Copy the code

compileUtil

var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    bind: function(node, vm, exp, dir) {
        // Initialize the view and bind the view update function to the Watcher instance
        var updaterFn = updater[dir + 'Updater'];
        // Initialize the view for the first time
        updaterFn && updaterFn(node, vm[exp]);
        // Instantiate the observer, which adds the subscriber watcher to the corresponding attribute message subscriber
        new Watcher(vm, exp, function(value, oldValue) {
          // If the property value changes, it will be notified to execute this update function to update the view
          // The closure holds node and is associated with the current Watcher
            updaterFn && updaterFn(node, value, oldValue);
        });
    },
    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },

    model: function(node, vm, exp) {
        this.bind(node, vm, exp, 'model');

        var me = this,
            val = this._getVMVal(vm, exp);
	    
        node.addEventListener('input'.function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },
    // Event processing
    eventHandler: function(node, vm, exp, dir) {
        var eventType = dir.split(':') [1],
            fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false); }},_getVMVal: function(vm, exp) {
        var val = vm;
        exp = exp.split('. ');
        exp.forEach(function(k) {
            val = val[k];
        });
        return val;
    },
    _setVMVal: function(vm, exp, value) {
        var val = vm;
        exp = exp.split('. ');
        exp.forEach(function(k, i) {
            // Not the last key, update the value of val
            if (i < exp.length - 1) {
                val = val[k];
            } else{ val[k] = value; }}); }};Copy the code

Updater (update function corresponding to instruction)

// Update function
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? ' ' : value;
    },

    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == 'undefined' ? ' ' : value;
    },

    classUpdater: function(node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, ' ').replace(/\s$/.' ');

        var space = className && String(value) ? ' ' : ' ';

        node.className = className + space + value;
    },

    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? ' ': value; }};Copy the code

Observer Watcher

Watcher is a bridge that binds the view update function to any monitored data and calls the view update callback when the monitored data is updated

Note the difference between these two DEPs:

  • The Deps for the property (stored in the property’s __ob__, which is an Observer instance) collects the Watcher instance in the getter, and when the data is updated, Deps informs all Watcher instances subscribed to the current data to update their views
  • Watcher’s Deps is used to hold the current observer dependent data in the subscriber center (there may be multiple, for example, V-text =” a&B “, when Watcher is instantiated, subscriptions will be added to Deps in a and B, so we need to obtain depIDS of A and B to prevent repeated subscriptions). Prevent the repeated addition of Watcher to data-dependent subscriber centers

Another point worth noting:

  • The Watcher class is used not only for instruction binding in views, but also for Watch (listening for something) and computed(relying on the value returned by listening for something)

Watcher main function

  • Get (): Gets the value of the current expression/function, triggering the dependency collection (in this case the Depend method of Dep calls the current dep.target.adddep method)
  • AddDep (): subscribing the current Watcher to the subs of the deP that holds the data (this is a true collection dependency)
  • Update (): scheduler interface that calls the Dep, either directly or asynchronously, depending on whether lazy is used
  • Run (): Call get() to get the latest data and call the callback interface for view updates
  • AddDep (): Adds a Dep to the current Dep container
  • CleanupDeps (): Cleans up the dependency collection by calling removeSub in the Dep of all dependencies on the current Watcher to cleanup the current Watcher instance
  • Teardown (): Removes itself from all data-dependent DEPs

Watcher class source code

export default class Watcher {
  vm: Component;      // Store the VM instance
  expression: string;
  cb: Function;       // View update callback function
  id: number;       
  deep: boolean;      // Whether to use deep listening (for deep parameter in watch)
  user: boolean;      // Whether it is a user behavior monitor (watch, computed) to determine which queue to put into (there are two asynchronous queues) and whether to prompt a warning
  lazy: boolean;      //true Retrieves the expOrFn current value next time triggered; False Retrieves the current value immediately
  sync: boolean;      // Whether to perform the callback for synchronization
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: ISet;
  newDepIds: ISet;
  getter: Function;
  value: any;

  constructor( vm: Component, expOrFn: string | Function, cb: Function, options? : Object ) {this.vm = vm
    /*_watchers store subscriber instance */
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.sync }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
    ExpOrFn = expOrFn; /* expOrFn = expOrFn; /* expOrFn = expOrFn
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {} 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 the getter, and re-collect dependencies. */
   /* Get the getter value and re-collect the dependency */
  get () {
    /* Set its own Watcher observer instance to dep. target to rely on the collection. * /
    pushTarget(this)
    let value
    const vm = this.vm

    /* Perform the getter, which looks like a render operation, but actually performs a dependency collection. After setting dep. target to its own observer instance, perform the getter operation. For example, there may be a, B, and C data in the current data, and the getter rendering needs to rely on A and C, so the getter function of a and C data will be triggered when the getter is executed. In the getter function, the existence of dep. target can be determined and the dependency collection can be completed. Put the observer object into the subs of the Dep in the closure. * /
    // If the user is listening, issue a warning
    // Call the expression, where the getter refers to the current watcher expression, but the expression fires the data-dependent getter
    if (this.user) {
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}}else {
      value = this.getter.call(vm, vm)
    }
    // I'm in touch with you
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    /* If deep is present, the dependency of each deep object is triggered to track its changes */
    if (this.deep) {
      /* Recurse to each object or array, triggering its getter so that each member of the object or array is collected by dependency, forming a "deep" dependency */
      traverse(value)
    }

    /* Remove the observer instance from the target stack and set it to dep.target */
    popTarget()
    this.cleanupDeps()
    return value
  }

  /** * Add a dependency to this directive. */
   /* Add a dependency to the Deps collection */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)}}}/** * Clean up for dependency collection. */
   /* Clean up the dependency collection */
  cleanupDeps () {
    /* Remove all observer objects */
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      // Remove the old Dep's binding to the current Watcher that does not exist in the new Dep
      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
  }

  /** * Subscriber interface. * Will be called when a dependency changes. */
   /* Scheduler interface to call back when a dependency changes. * /
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /* Execute run to render the view directly
      this.run()
    } else {
      /* Asynchronously pushed to observer queue, called by scheduler. * /
      queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
   /* Dispatcher work interface that will be called back by the dispatcher. * /
  run () {
    if (this.active) {
      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.
        /* Even if the values are the same, observers with the Deep attribute and observers on the object/array should be triggered to update because their values may change. * /
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        /* Sets the new value */
        this.value = value

        /* Triggers a callback to render view */
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */
   /* Get the observer value, for computed Watchers */ only
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

  /** * Depend on all deps collected by this watcher. */
   /* Collect all DEPS dependencies for this watcher, only for Computed watcher */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /** * Remove self from all dependencies' subscriber list. */
   /* Remove itself from all dependent collection subscriptions */
  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.
      /* Remove itself from the observer list of the VM instance, skip this step if the VM instance is being destroyed because this operation is expensive. * /
      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

PushTarget and popTarget

export function pushTarget (_target: Watcher) {
  // Put the previous Watcher on the stack
  if (Dep.target) targetStack.push(Dep.target)
  // Set the current watcher to target
  Dep.target = _target
}

export function popTarget (){
 // Restore dep. target to the previous state
  Dep.target = targetStack.pop()
}
Copy the code

Dependency collection process

  • Collect dependencies through the constructor call get()
  • Get calls pushTarget to put the current Watcher instance into dep. target, which triggers the current expression (that is, the getter that triggers the dependent data within the expression or function).
  • Dep.depend calls dep.target.adddep (this) in the getter of the dependent data by calling dep.depend.
  • Dep.target.adddep (this), aka watcher.adddep (Dep :Dep), puts the current data-dependent Dep into the Watcher instance and calls dep.addSub(this).
  • Dep.addsub (this) puts the current Watcher instance into the SUBs array of the DEP, which collects Watcher as a subscriber
  • In the case of deep Watch, after the deep walk is complete and the getter is triggered,popTarget restores the dep. target to its previous state and calls cleanupDeps to clear the dependencies

Rely on empty

NewDeps and this.newDeps represent an array of Dep instances held by the Watcher instance. And this.depIds and this.newDepIds represent the id sets of this.deps and this.newDeps, respectively

Vue mounts via the mountComponent function, which has an important piece of logic that goes something like this:

updateComponent = (a)= > {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code

When you instantiate Watcher here, you call the get() method, which is called the updateComponent method, which calls the Render () method, which generates the render VNode and in the process accesses the data on the VM, This triggers the getter for the data object. Given that Vue is data-driven and every change of data is rerendered, the vm._render() method is executed again, triggering getters of the data, so Wathcer initializes an array of two Dep instances in the constructor. NewDeps represents the newly added array of Dep instances, while DEps represents the last added array of Dep instances.

So why do you need to remove the DEPS subscription? During the dePS subscription process, it is already possible to eliminate the duplicate subscription by id.

Considering one scenario, our template will render different sub-templates A and B according to v-if. When we render A when certain conditions are met, the data in A will be accessed. At this time, we add getter for the data used by A and perform dependent collection. These subscribers should be notified. So if we change the conditions to render template B, and then add getter for the data used by TEMPLATE B, if we do not have the dependency removal process, then I modify the data of template A, it will notify the callback of the subscription of data A, which is obviously wasteful.

So Vue designed after add each new subscription, will remove the old subscription, thus guarantee the in which we have just scenario, if the rendering b template to modify a template data, a data subscription callback has been removed, so there is no waste, is really very impressive Vue processing in some details.

Dep

The Dep is the subscriber center, the array member is the Watcher, and the Watcher is collected in the getter of the property. Instead of calling addSub directly to the Depend method addDep in the Watcher instance,addDep puts the DEP into the Watcher’S DEP array and then calls the addSub method of the DEP to collect the dependencies

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

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 = []
  }

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

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

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

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

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []

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

export function popTarget () {
  Dep.target = targetStack.pop()
}

Copy the code

The realization of the defineReactive

/** * Define a reactive property on an Object. */
export function defineReactive (obj: Object, key: string, val: any, customSetter? : Function) {
  /* Define a dep object in the closure */
  const dep = new Dep()

  // Note here that if the attribute is not configurable, the binding will be unbound
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  /* If the object already has a getter and setter function, it is taken out. The getter/setter is executed in the new definition, ensuring that the getter/setter is not overwritten. * /
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  /* The child of the object recursively observes and returns the Observer of the child node */
  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {

      */ if the original object has a getter method
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {

        /* Do dependency collection */
        dep.depend()
        if (childOb) {

          /* Child dependencies are collected by placing the same Watcher observer instance into two Depends: the depend in the closure itself and the Depend */ of the child element
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {

          /* An array requires a dependency collection on each member. If the members of an array are still arrays, recurse. * /
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {

      /* Compares the current value with the new value through the getter method. If the value is consistent, the following operations are not required */
      const value = getter ? getter.call(obj) : val
      /* 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()
      }
      if (setter) {

        /* Execute setter*/ if the original object has setter methods
        setter.call(obj, newVal)
      } else {
        val = newVal
      }

      /* The new value must be observed again to ensure that the data is responsive */
      childOb = observe(newVal)

      /* The dep object notifies all observers */
      dep.notify()
    }
  })
}


Copy the code

Two important points to note:

  • If the property is configured without any additional information, that is, the bidirectional binding is cancelled if the property is set to false
  • If the object contains child objects, it is deeply bidirectional bound, and the Watcher observer instance is recursively placed into the deP of the child element, so that changes to the element cause the view to be updated depending on the current object

conclusion

Now go back to the execution flow and the idea becomes clear

From the process of learning bidirectional binding source code, you can learn the following

  • The properties of data and props cannot duplicate, because the props has a higher priority
  • Look at __ob__ to see if the data is being listened on: To check whether the data is being listened on, see if the current data has the __ob__ attribute, which houses the Obeserver instance. The process of instantiating the Observer is the process of data listening
  • There are only seven ways to set up a listener for an array: through the process of data listening, you can know that the listener for an array can only use push, POP, shift, unshift, reverse, sort, splice, etc., to modify the element. It’s not gonna be monitored.
  • DefineReactive disables the binding of data without any additional information. The process of making the data responsiblable through defineReactive works without any additional information, because the Object. DefineProperty is invalid. The Object. Freeze allows you to set the data freely to false if the bidirectional binding works without any additional information, which instantiates the Observer and disables the bidirectional binding.
  • Changes to subattributes can also cause view updates: By defineReactive, you can see that Watcher is bound to the current data-dependent Dep and to the data-dependent Dep, meaning that changes to subattributes also cause view updates
  • After the initialization phase of new child attributes cannot be listening: due to the initialization phase of new child property does not exist, so cannot be listening, can only wait until the next render () function is executed, can be monitored, this is also our set v – model to a new object properties, will find it a two-way binding fails, repeatedly UI again after the operation, It works again as a result of binding again after the render() function is executed. So we’ll use the official Vue.$set() to add new listener properties to the object
  • The attribute of WatcherThe :Watcher serves as a bridge between the binding view update function and the dependent data, and its properties determine its usefulness.
    • The deep property determines that the Watcher instance is a deep-listening observer, and recursively triggers the getter for the child property, so that the child property is collected by the Watcher Dep.
    • Sync decides whether to call the view update callback immediately or asynchronously push it to the observer queue queueWatcher() when data is updated
    • The user attribute flags whether a user is a Watcher of the user’s behavior, and if so, warns of errors and determines which asynchronous queue to put in

The object.freeze () method freezes an Object (an array is also an Object). A frozen object can no longer be modified. If you freeze an object, you cannot add new attributes to the object, delete existing attributes, modify the enumerability, configurability, writability of existing attributes of the object, or modify the value of existing attributes. In addition, the stereotype of an object cannot be modified after it is frozen. To make an object immutable, you need to recursively freeze each property of type object (deep freeze):

// Deep freeze function
function deepFreeze(obj){
   var propNames = Object.getOwnPropertyNames(obj)
   
   // Freeze attributes before freezing themselves
   propNames.forEach((name) = >{
   	let prop = obj[name]
	// If prop is an object, freeze it
	if(typeof prop === 'object'&& prop ! = =null){
	   deepFreeze(prop)
	}
   })
   // Freeze the current object
   return Object.freeze(obj)

}
Copy the code