vue2 responsive principle level

Keywords: Front-end Vue.js

Interview question: please explain the principle of vue2 response

vue official statement: cn.vuejs.org/v2/guide/re...

The ultimate goal of responsive data is to run some functions when the object itself or object properties change, the most common being the render function.

In the specific implementation, vue uses several core components:

  1. Observer:
  2. Dep
  3. Watcher
  4. Scheduler

Observer

The goal of Observer is very simple, that is, to convert an ordinary object into a responsive object

In order to achieve this, Observer converts each property of the object into a property with getter and setter through Object.defineProperty. In this way, vue has the opportunity to do something else when accessing or setting the property.

Code implementation response

/**
 * Define a reactive property on an Object.
 * Define a responsive data
 */
export function defineReactive (
  obj: Object, // Incoming object
  key: string, // Object property name
  val: any,    // The value of the object property
  customSetter?: ?Function, // Custom setter
  shallow?: boolean // No depth response
) {
  // Create a dependent instance object
  const dep = new Dep()
  //  Gets the current property descriptor
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // If the object cannot be configured, return directly
  if (property && property.configurable === false) {
    return
  }

  // Cat for pre-defined getter / setters meets the predefined getter/setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // In case of deep response, call the observe method
  let childOb = !shallow && observe(val);
  // Use Object.defineProperty to set and getter, so that you can
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Perform dependency collection
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
    // When the data changes, set a new value
      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
      }
      childOb = !shallow && observe(newVal)
      // Notify of data changes
      dep.notify()
    }
  })
}

Copy code

The above list is the response of a single object. In fact, if it is a nested object in the object, it is necessary to recursively traverse all the attributes of the object to complete the deep attribute conversion

Observer is the internal constructor of Vue. After Vue 2.6, we can use this function indirectly through the static method Vue. Observable (object) provided by Vue. Implementation of api

// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
  // The data is returned directly after the data is expressed in a response. Since the object is passed by reference, there will be the following code
  observe(obj)
  return obj
}
Copy code

Concrete implementation of observe

/**
 * Try to create an observer instance for the value. If the observation is successful, a new observer is returned. If the value already has an observer, an existing observer is returned.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // If the incoming data is not an object or a vue virtual node, it is returned directly
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  
  let ob: Observer | void
  // Judge whether the incoming data has__ ob__ And the prototype of value is Observer
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
  // Create a value instance object for value. The Observer converts the attribute key of the target object into a getter/setter, which is used to collect dependencies and send updates
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy code

In the component life cycle, this happens after beforeCreate and before created.

Because only the current attributes of the object can be traversed during traversal, it is impossible to monitor the dynamically added or deleted attributes in the future. Therefore, vue provides two instance methods $set and $delete to allow developers to add or delete attributes to existing responsive objects through these two instance methods$ Implementation of set

/**
 * Set the properties of the object. Add a new property and trigger a change notification when the property does not exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
// Judge target object
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // If the object is an array and the subscript is a valid index (number) of an array
  if (Array.isArray(target) && isValidArrayIndex(key)) {
   // Expand array length
    target.length = Math.max(target.length, key)
    // Put data 
    target.splice(key, 1, val)
    return val
  }
  // If the attribute exists in the target Object but does not exist on the prototype Object of the superclass Object
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__;
  // The object cannot be a Vue instance or the root data object of a Vue instance
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // ob does not exist. Add attributes directly to the object
  if (!ob) {
    target[key] = val
    return val
  }
  // Change the attribute to a responsive attribute
  defineReactive(ob.value, key, val)
  // The dependency notification uses the of the object for render updates
  ob.dep.notify()
  return val
}
Copy code

Implementation of $del

/**
 * Delete properties and trigger updates if necessary.
 */
export function del (target: Array<any> | Object, key: any) {
// Like set, determine whether the target is a reference value
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // Determine whether the target is an array and whether the key is a number
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__;
  // The object cannot be a Vue instance or the root data object of a Vue instance
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  // If the key does not exist in the target, it is returned directly
  if (!hasOwn(target, key)) {
    return
  }
  // Exist for deletion
  delete target[key]
  if (!ob) {
    return
  }
  // Notify and render
  ob.dep.notify()
}
Copy code

✨✨✨ Note: through $set and $del, we will find that although these two methods are officially provided, they should be used as little as possible. After all, we need to make a lot of judgment and then notify render.

For an array, vue changes its implicit prototype because vue needs to listen for methods that may change the contents of the array

Key code to turn an array into a response

// Determine whether there is an object prototype
if (hasProto) {
  // By using__ proto__ Intercept the prototype chain to expand the target array
  protoAugment(value, arrayMethods) 
  The above sentence is equal to  value.__proto__ = arrayMethods
} else {
// Expand the target object or array by defining hidden properties.
  copyAugment(value, arrayMethods, arrayKeys)
}
// Turn an array into a response
this.observeArray(value)
Copy code

In short, the goal of Observer is to make an object, its property reading, assignment and internal array changes can be Vue perceived. Enables Vue to do something when data changes..

Dep

There are two unsolved problems: what to do when reading attributes and what to do when attributes change. This problem needs to be solved by Dep.

Dep means Dependency, which means Dependency.

Vue will create a Dep instance for each attribute, object itself and array itself in the responsive object. Each Dep instance has the ability to do the following two things:

  • Record dependencies: when a property of a responsive object is read, it collects dependencies
  • Distribute updates: when a property is changed, it will distribute updates

Create Dep core code for responsive objects

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
    // Create a dep instance, and each response object will have a dep instance
    this.dep = new Dep()
    this.vmCount = 0
   // ... turn data into responsive data
  }
Copy code

Dep is originally a publish subscribe mode

/**
 * dep Is an observable object, and multiple instructions can subscribe to it
 */
export default class Dep {
// Objective of observation
  static target: ?Watcher;
  id: number;
  // Collection of currently observed target objects
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
// Add a subscriber
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
 // Remove subscriber
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
 // Collection dependency
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
// notice
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
     // Let update update update the dependency
      subs[i].update()
    }
  }
}
Copy code

Watcher

There is another problem here, that is, how does Dep know who is using me?

To solve this problem, we need to rely on another thing, Watcher.

When responsive data is used in the execution of a function, it is impossible to know which function is using its own. vue gives the function to a thing called a watcher for execution. The watcher is an object. When each such function is executed, a watcher should be created and executed through the watcher. watch simplified version

The observer parses the expression, collects dependencies, and triggers a callback when the expression value changes. This is used for the $watch () api and instructions

export default class Watcher {
    constructor(
        vm: Component,
        expOrFn: string | Function,   // The name of the property to watch
        cb: Function,    // Callback function
        options?: ?Object,   // configuration parameter
        isRenderWatcher?: boolean  // Whether it is a render function observer. When Vue is initialized, this parameter is set to true
    ) {
        // Omit part of the code... The function of the code here is to initialize some variables
        // expOrFn can be a string or a function
        // When will it be a string? For example, when we use it normally, watch: {X: FN}, Vue will convert the key 'x' into a string
        // When will it be a function? In fact, when Vue is initialized, it is the passed in rendering function new Watcher(vm, updateComponent,...);
        
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn
        } else {
        
            // When expOrFn is not a function, it may be described in this way: Watch: {'a.x' () {/ / do}}, which is specific to the properties of an object
            // At this time, you need to pass the parsePath method, which returns a function
            // The function will get the value of the attribute 'a.x'
            
            this.getter = parsePath(expOrFn)
            
            // Omit some code
        }
        
        // This.get is called here, which means that this.get will be called when new Watcher
        // this.lazy is a modifier, which is false unless passed in by the user. You can ignore it first
        
        this.value = this.lazy? undefined: this.get()
    }
    
    get () {
        // Assign the current watcher instance to the Dep.target static attribute
        // That is, after this line of code is executed, the value of Dep.target is the current watcher instance
        // And put Dep.target on the stack and store it in the targetStack array
        
        pushTarget(this)
        
        // Omit some code
        
        try {
        
            // this.getter is executed to get the initial value of the attribute
            // If the updateComponent function is passed in during initialization, udnefound will be returned at this time
            
            value = this.getter.call(vm, vm)
            
        } catch (e) {
            // Omit some code
        } finally {
            // Omit some code
            
            // Out of stack
            popTarget()
            
            // Omit some code
        }
        
        // Returns the value of the property
        return value
    }
    
    // Here's another review
    // Dep.dependent method will execute Dep.target.addDep(dep), which is actually watcher.addDep(dep)
    // watcher.addDep(dep) will execute dep.addSub(watcher)
    // Add the current watcher instance to the sub array of dep, that is, collect dependencies
    // dep.depend and this addDep method have several this, which may be a little windy.
    
    addDep (dep: Dep) {
        const id = dep.id
        
        // The following two if conditions are the effect of de duplication, and we can ignore them for the time being
        // 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 the sub array of dep
                
                dep.addSub(this)
            }
        }
    }
    
    // Distribute updates
    
    update () {
    
        // If the user defines lazy and this.lazy is a descriptor, we can ignore it here first
        
        if (this.lazy) {
            this.dirty = true
            
        // this.sync indicates whether the callback is triggered immediately after the value is changed. If the user definition is true, this.run is executed immediately
        } else if (this.sync) {
        
            this.run()
            
        // Internally, queueWatcher also executes the run method of the watcher instance, but internally calls nextTick for performance optimization.
        // It puts the current watcher instance into a queue, traverses the queue and executes the run() method of each watcher instance at the next event loop
        
        } else {
            queueWatcher(this)
        }
    }
    
    run () {
        if (this.active) {
            // Get new property value
            const value = this.get()
            if (
                // If the new value is not equal to the old value
                
                value !== this.value ||
                
                // If the new value is a reference type, be sure to trigger the callback
                // For example, if the old value is an object,
                // In the new value, we only change the value of a certain attribute in the object, and the new value is equal to the old value itself
                // That is, if this.get returns a reference type, the callback must be triggered
                
                isObject(value) ||
                
                // Depth watch 
                this.deep
            ) {
                // set new value
                const oldValue = this.value
                this.value = value
                // this.user is a flag. If the developer adds the watch option, this value defaults to true
                // If the watch is added by the user, add a try catch. It is convenient for users to debug. Otherwise, the callback is executed directly.
                
                if (this.user) {
                    try {
                    
                        // Triggers a callback with the new and old values as parameters
                        // This is why when we write watch, we can write: function (newval, oldval) {/ / do}
                        
                        this.cb.call(this.vm, value, oldValue)
                        
                    } catch (e) {
                        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
                    }
                } else {
                    this.cb.call(this.vm, value, oldValue)
                }
            }
        }
    }
    
    // Omit some code
    
}
Copy code

The Watcher will set a global variable to make the global variable record the watcher currently responsible for executing equal to itself, and then execute the function. During the execution of the function, if there is a dependency record Dep.dependent (), Dep will record the global variable. When Dep sends updates, it will notify all previous recorded watchers to update, Execute the run function

  • Each vue component instance corresponds to at least one watcher, in which the render function of the component is recorded.

  • The Watcher will first run the render function once to collect dependencies, so the responsive data used in render will record the watcher.

  • When the data changes, dep will notify the watcher, and the Watcher will re run the render function to re render the interface and re record the current dependencies.

Scheduler

The last problem that remains is that after Dep notifies the watcher, if the watcher reruns the corresponding function, it may cause the function to run frequently, resulting in low efficiency

Imagine that if attributes a, b, c and d are used in a function given to the watcher, the dependencies of attributes a, b, c and d will be recorded, so the following code will trigger four updates:

state.a = "new data";
state.b = "new data";
state.c = "new data";
state.d = "new data";
Copy code

This is obviously inappropriate. Therefore, after receiving the notification of sending updates, the watcher actually does not immediately execute the corresponding function, but gives itself to something called the scheduler

Scheduler core code

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    // Judge a limit situation and whether you are joining the team
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      // Out of line
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush queue is refreshing
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      // Put it into nextick for micro queue execution
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy code

The scheduler maintains an execution queue. The same watcher in the queue only exists once. The watchers in the queue are not executed immediately. It will put these watchers to be executed into the micro queue of the event cycle through a tool method called nextTick. The specific method of nextTick is completed through Promise

nextTick is exposed to developers through this.$nextTick

For the specific processing method of nextTick, see: cn.vuejs.org/v2/guide/re...

That is, when the responsive data changes, the render function is executed asynchronously and in the micro queue

nextick core method

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    // There are two kinds of maintenance functions in the callbacks stack
    if (cb) {
      try {
      // Change the context of cb
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // When not waiting
  if (!pending) {
    pending = true
    // Execute the timerFuc function, and the implementation of this function will be determined according to the current environment.
    // Vue internally attempts to use native for asynchronous queues  ` Promise.then`,`MutationObserver`   and  ` setImmediate`,
    // If the execution environment does not support it, the  ` setTimeout(fn, 0)`   Replace.
    
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
Copy code

Overall process

Posted by crunchyfroggie on Wed, 24 Nov 2021 11:42:23 -0800