Object. DefinePrototype sets getters and setters for object properties to listen for data changes. Today we will explore the responsivity principle of vue.

object.definePrototype

Syntax: Object.defineProperty(obj, prop, Descriptor)

The base application

/* * Params obj listener object prop Listener attribute Descriptor */
let _obj = {
      name: 'Rookie'
    }, 
    tag = 'Old dog';

Object.defineProperty(_obj, 'name', {
  get: () = > {
      console.log('trigger getter');
      return tag; // Return the tag variable
  },
  set: newVal= > {
      console.log("Trigger setter", newVal);
      returnnewVal; }})console.log(_obj.name); // Trigger the getter

_obj.name = 'Touch the fish' // Trigger the setter to touch the fish
Copy the code

From the above code, you can see that we use object.defineProperty to listen on the name attribute of _obj, trigger GET, return the tag variable (old grease), trigger SET, return newVal (touch the fish). Vue2. X uses object.deineProperty API to intercept get and set of objects to realize object data responsiveness.

The Vue2. X response is done in the Observer. Let’s explore the Observer

The Observer responsive

Responsive process

  1. From new Vue() we know that the _init() method executes the initState() method;
  2. The initData() method is executed in the initState() method;
  3. Call the observe method in initData;
  4. Observe instantiate new Observer in observe;
  5. Observer instantiates a reactive object that calls the defineReactive method;
// 1, initState * SRC \core\instance\state
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // initProps
  if (opts.methods) initMethods(vm, opts.methods) // initMethods
  if (opts.data) { // The component has data
    initData(vm)  // initData
  } else {
    observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch)// initWatch
  }
  // Here we can see the props, methods, data, and watch priorities
}

// 2, initData * SRC \core\instance\state.js

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // *** *
  observe(data, true /* asRootData */) // observe data
}

// 3, observe * SRC \core\observer\index.js
export function observe (value: any, asRootData: ? boolean) :Observer | void {
  // Return if it is not an object or virtual DOM
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  // Check if there is __ob__ if there is, the data has been observed and assigned to the return value
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve && // can be observed! isServerRendering() &&// Not server rendering
    (Array.isArray(value) || isPlainObject(value)) && // Is an array or object
    Object.isExtensible(value) && // Is an extensible property! value._isVue// Not a VUE instance
  ) {
    ob = new Observer(value)  // Instance observer
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
// 4, Observer * SRC \core\ Observer \index.js
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 // The value to be observed
    this.dep = new Dep() // Create a dependency collector
    this.vmCount = 0
    // Assign the Observer instance to the __ob__ property of value.
    def(value, '__ob__'.this)
    // Determine the data type
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value) // Array observation method
    } else {
      this.walk(value) // The object performs the walk method}}/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value  type is Object. */
  walk (obj: Object) { // Get an array of the object's key values to traverse the monitor key
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])  // Responsive object core}}/** * Observe a list of Array items. */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

DefineReactive * SRC \core\observer\index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  const dep = new Dep()
  
  // Get the object property descriptor
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // The property descriptor exists and the 64x property cannot be configured
  if (property && property.configurable === false) { 
    return
  }

  // Property descriptors exist to get getters, setters
  const getter = property && property.get
  const setter = property && property.set
  
  // Return obj[key] with no getter, setter and length of 2
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key]
  }
  Shallow indicates whether the value is deeply processed.
  // this means that if obj's key, corresponding to val, is still an object, it becomes an observable
  letchildOb = ! shallow && observe(val)// Listen to the key of the object
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Dependencies are collected at specific times. By Dep. Target
      if (Dep.target) {
        dep.depend() // Rely on collection
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      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()
      }
      // #7981: for accessor properties without setter
      if(getter && ! setter)return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // Determine whether the set value is an object recursive observationchildOb = ! shallow && observe(newVal) dep.notify()// Trigger the update}})}Copy the code

The paper process

The initState() method is executed in the _init() method;
initState()

// the initState() method executes the initData() method;
function initState(vm){
  let data = vn.$options.data;
  initData(data)
}

// Call observe in initData
function initData(data) {
  observe(data, true)}// Observe instantiates the Observer
function observe() {
  new Observer(value)
}

// An Observer object executes the walk method
class Observer{
  this.walk(obj);
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

The defineReactive method is used to set the property descriptor of the object
function defineReactive(obj, key) {
  const dep = new Dep()
  // Omit some code
  Object.defineProperty(obj, key, {
    // Attribute descriptor
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      
      if (Dep.target) {
        dep.depend() // Collect dependencies
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      dep.notify()  // Trigger the update}})}// Object type data response process
// Finally, by triggering the object data get collection dependency (dep.depend), triggering the set call dep.notify() to achieve data view responsiveness
Copy the code

At this point, reactive binding of objects is implemented

Dep dependency management

I believe that many people do not understand the concept of Dep. What is Dep used for? Now that we’ve implemented responsive binding of objects, we can listen for changes in the data, so how do we update the view, and where exactly?

A Dep can be thought of as a container for collecting the specifics of where to update to, collecting dependencies (watcher) when data gets triggered, setting when data changes, and updating views.

// *src\core\observer\dep.js
// Used as a Dep identifier
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>;
 
    // Define a subs array to store the watcher instance
    constructor () {
        this.id = uid++
        this.subs = []
    }
    
    // Add the watcher instance to subs
    addSub (sub: Watcher) {
        this.subs.push(sub)
    }
 
    // Remove the corresponding watcher instance from subs.
    removeSub (sub: Watcher) {
        remove(this.subs, sub)
    }
    
    // Dependency collection, which is the dep.dpend method seen above
    depend () {
        // dep. target is the watcher instance
        if (Dep.target) {
            // Call the addDep method of the watcher instance with the current DEP instance
            Dep.target.addDep(this)}}// Dispatch updates, which is the dep.notify method we saw earlier
    notify () {
        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)
        }
        
        // Iterate through the subs array, triggering the Update of the Watcher instance in turn
        for (let i = 0, l = subs.length; i < l; i++) {
            // Call the watcher instance update method
            subs[i].update()
        }
    }
}
 
// Attach a static attribute target to the Dep,
// The dep. target value is assigned to the current Watcher instance object when pushTarget and popTarget are called.
Dep.target = null
// Maintain a stack structure for storing and deleting dep.target
const targetStack = []
 
// pushTarget will be called on new Watcher
export function pushTarget (_target: ? Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
}
 
// popTarget is also called on new Watcher
export function popTarget () {
    Dep.target = targetStack.pop()
}
Copy the code

Dep is a class that relies on collecting and distributing updates, that is, storing the Watcher instance and triggering the Update method on the Watcher instance.

Reactive summary

At this point initData is done. The getter and setter Settings for Define Active above are not triggered at the beginning because dependencies have not been collected yet. Here we just define the rules and use them in conjunction with the template. Next, let’s look at where the template is mounted.

// post the _init method to consolidate the initial procedure * SRC \core\instance\init.js
export function initMixin  (Vue: Class<Component>) {
  Vue.prototype._init = function (options? :Object) {
    const vm: Component = this
    // Omit some code
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) 
    initState(vm) // *** completes the responsivity to the data but at this point no dependencies have been collected
    initProvide(vm)
    callHook(vm, 'created')

    // Omit some code
    // Mount phase
    if (vm.$options.el) {
      vm.$mount(vm.$options.el) // *** The dependencies are actually collected during the mount phase}}}Copy the code

$mount

The Vue prototype defines $mount in two places, but ultimately calls the mountComponent method defined in the first place

// the first definition
// src\platforms\web\runtime\index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
Copy the code
// The second definition
// src\platforms\web\entry-runtime-with-compiler.js

var mount = Vue.prototype.$mount; // The $mount method defined in the first section is saved
  Vue.prototype.$mount = function (el, hydrating) {
    el = el && query(el); // Query is used to find dom elements

    // el cannot be body, HTML
    if (el === document.body || el === document.documentElement) {
      warn(
        "Do not mount Vue to <html> or <body> - mount to normal elements instead."
      );
      return this
    }

    var options = this.$options;
    // We need to check whether the render function is initialized
    // The render function returns vNode
    if(! options.render) {var template = options.template;
      // There is no template
      if (template) {
        if (typeof template === 'string') {
          if (template.charAt(0) = = =The '#') {
            template = idToTemplate(template);
            /* istanbul ignore if */
            if(! template) { warn( ("Template element not found or is empty: " + (options.template)),
                this); }}}else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          {
            warn('invalid template option:' + template, this);
          }
          return this}}else if (el) {
        // 有el
        template = getOuterHTML(el);
      }
      
      // Generate the render function
      if (template) {
        /* istanbul ignore if */
        if (config.performance && mark) {
          mark('compile');
        }
        
        var ref = compileToFunctions(template, {
          outputSourceRange: "development"! = ='production'.shouldDecodeNewlines: shouldDecodeNewlines,
          shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        }, this);
        var render = ref.render;
        var staticRenderFns = ref.staticRenderFns;
        options.render = render;
        options.staticRenderFns = staticRenderFns;

        /* istanbul ignore if */
        if (config.performance && mark) {
          mark('compile end');
          measure(("vue " + (this._name) + " compile"), 'compile'.'compile end'); }}}/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
    // Call the variable mount (actually calling the first defined $mount method to call the mountComponent method)
    return mount.call(this, el, hydrating)
  };
Copy the code

mountComponent

// src\core\instance\lifecycle.js
export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
  vm.$el = el
  
  // Omit some code
  
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
    updateComponent = () = > {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () = > {
      vm._update(vm._render(), hydrating) // *** Updates the view core ***
      // updateComponent calls vm._render()
      Vnode = render. Call (vm, vm.$createElement)
      // _update is responsible for rendering VNode into a real DOM and rendering it
      // The render function takes the value of vm.a. Or read the value of vm.b.
      // When vm. A or B is read, the getter for the corresponding property is triggered
      // The current Watcher is then added to the deP corresponding to the property.
      // When the setter for the property is triggered, dep.notify() is executed to update each watcher collected by the DEP
      // update calls the run method.
      // The run method triggers the current get method, and the interface is updated when getter.call is executed.}}// The core new Watcher is finally instantiated
  new Watcher(vm, updateComponent, noop, { // The updateComponent method is the focus
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')}return vm
}
Copy the code

After all the hardships, FINALLY waiting for you, fortunately I did not give up

Watcher

Watcher is also a class that initializes Watcher instances of data. It has an update method on its prototype to distribute updates.

SRC \core\observer\ Watcher.js SRC \core\observer\ Watcher

export default class Watcher {
  constructor(
  vm: Component,
   expOrFn: string | Function,
   cb: Function.// The callback function
   options?: ?Object, isRenderWatcher? : booleanWhen Vue is initialized, this parameter is set to true
  ) {
    
    // omit some code... What this code does is initialize some variables
    
    
    // expOrFn can be a string or a function
    // When is the key 'x' converted to a string, such as watch: {x: fn}
    // New Watcher(vm, updateComponent, noop...); // New Watcher(vm, updateComponent, noop...); // New Watcher(vm, updateComponent, noop...); ;
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // When the watch form 'a.b' is used, the parsePath method returns a function
      // The function internally retrieves the value of the 'a.b' attribute
      this.getter = parsePath(expOrFn)
      
      // omit some code...
    }
   
    // If it is an attribute, return undefined, otherwise get value
    // this.get() will be called on new Watcher
    this.value = this.lazy
      ? undefined
    : this.get()
  }
  
  get () {
    // Assign the current watcher instance to the dep. target static property
    // If this line of code is executed, the dep. target is the current watcher instance
    // Add dep. target to the targetStack array
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // *** This. Getter executes the updateComponent method
      // Let's go back to the comments for the updateComponent method
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      if (this.deep) { 
        traverse(value)
      }
      popTarget() / / Dep. Target out of the stack
      this.cleanupDeps()
    }
    return value
  }
  
  // Here is a review
  AddDep (dep) // Dep. depend, which executes dep.target.adddep (dep)
  // addDep(dep) will execute dep.addSub(watcher)
  // Add the current Watcher instance to the SUBs array of the DEP, that is, collect dependencies
  // Dep. depend and the addDep method, with several this, may be a bit convoluted.
  addDep (dep: Dep) {
    const id = dep.id
    // The following two if conditions are both deduplicated, so we can ignore them for the moment
    // Just know that this method executes dep.addSub(this).
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // Add the current watcher instance to deP's subs array
        dep.addSub(this)}}}// Send updates
  update () {
    if (this.lazy) {
      this.dirty = true
      // if this.sync is true, this.run is immediately executed
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  
  run () {
    if (this.active) {
      const value = this.get() // Execute the get method
      if( value ! = =this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "The ${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  
  // Omit some code
}
Copy the code

conclusion

Object. DefinePrototype intercepts get of object attributes to add dependencies, and triggers set of attributes to update dependencies to trigger view updates. At the heart of the update view is vm._update(vm._render(), hydrating). In the next article we’ll look at vm._update and vm._render principles.

If there are any mistakes please point out, must be the first time to learn and update.

So far we still leave a problem, that is the array data response formula, here is not repeated, hope interested JYM can go to understand.