Vue source code analysis

1. Principle analysis of data response

1 Key Principles

Object.defineproperty, which adds listening dependencies from the GET method and triggers dependencies from the set method

2 combined with the source code, detailed analysis

  1. When vue is initialized, the initData function is triggered and the value of this.$option.data is not an object, but a function. Check whether the methods and props have declared the value of the data object, and then use the observe function to listen. observe(data, true /* asRootData */)

    // state.js
    function initData (vm: Component) {
      let data = vm.$options.data // Get vue data
      Data.call (vm,vm) returns an object
      // {name: 'xiaoming '}
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      if(! isPlainObject(data)) { data = {} process.env.NODE_ENV ! = ='production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // proxy data on instance
      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)
        }
      }
      // observe data Perform data monitoring
      observe(data, true /* asRootData */)}Copy the code
  2. Observe new observe (value) the observe function examines whether data has been observed.

    // observe/index.js
    export function observe (value: any, asRootData: ? boolean) :Observer | void {
      Return if data is not an object
      if(! isObject(value) || valueinstanceof VNode) {
        return
      }
      let ob: Observer | void
      // Whether the listener is already marked
      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 ) {// If there is no observation, the observation function is triggered
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        // count up
        ob.vmCount++
      }
      return ob
    }
    Copy the code
  3. Observe class, the instantiated registers Dep () as an example, Array. The isArray (value) to determine whether Array, is performing observeArray, or traverse Observe function, deep observation arrays. The object calls defineReactive(obj, key) directly, which redefines the object data

    // observe/index.js
    class Observer {
      value: any;
      dep: Dep;
      vmCount: number; // number of vms that have this object as root $data
    
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__'.this)
        // array. isArray(value) Define defineReactive(obj, key)
        if (Array.isArray(value)) {
          if (hasProto) {
            protoAugment(value, arrayMethods)
          } else {
            copyAugment(value, arrayMethods, arrayKeys)
          }
          this.observeArray(value) // Observe the array in depth
        } else {
          this.walk(value)
        }
      }
    
      /** * Walk through all properties 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)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i]) // Redefine the object type -> reactive data}}/** * Observe a list of Array items. */
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    Copy the code
  4. DefineReactive Redefines responsive data, defines Object.defineProperty, triggers dep.depend when data is retrieved, and triggers dep.notify() when data is modified.

    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function, shallow? : boolean) {
      const dep = new Dep() // instantiate a Dep, monitor
    
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
      if((! getter || setter) &&arguments.length === 2) {
        val = obj[key]
      }
    
      letchildOb = ! shallow && observe(val)// Recurse, if it is still an object, continue with observe
      Object.defineProperty(obj, key, {
        enumerable: true.configurable: true.get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend() // Collection depends on Watcher
            if (childOb) {
              childOb.dep.depend() // Collection depends on Watcher
              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 } childOb = ! shallow && observe(newVal) dep.notify()// Trigger the corresponding numeric update}})}Copy the code
  5. Solve the problem.

    Why is dep.target judged here?

    If (dep.target) {dep.depend() // Collect dependent watcher if (childOb) {childob.dep.depend () // Collect dependent watcher if (array.isarray (value)) {dependArray(value)}}} // The 'dep. target' is' true 'because' notify() 'called' pushTarget 'function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target }Copy the code

3 summary

How are 2 arrays monitored

1 Key Principles

  • Overrides the array method on the VUE prototype chainpush,popAnd so on in the call arrayAPIData monitoring will be performed to notify dependencies of updates. If the array contains a reference type, the reference type is monitored.

2 combined with the source code, detailed analysis

  1. Observe: If a new Observer(value) is used, value is evaluated. If an array is used, array operations are overwritten

    // observe/index.js
    // Observe in class
    if (Array.isArray(value)) {
          if (hasProto) {
            protoAugment(value, arrayMethods) 
          } else {
            copyAugment(value, arrayMethods, arrayKeys) // arrayMethods are methods for overriding arrays
          }
          this.observeArray(value) // Deeply traverse the array. If there are still objects in the array, go on
        } else {
          this.walk(value)
        }
    
    / / observeArray method
    observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    Copy the code
  2. Dep.notify () is triggered when the array method is called to update the view

    // observe/array.js
    const arrayProto = Array.prototype
    export const arrayMethods = Object.create(arrayProto)
    
    const methodsToPatch = [
      'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
    ]
    
    /** * Intercept mutating methods and emit events */
    // Rewrites the original array method, preserves the original method, expands the observation. Manually call dep.notify() to update the view
    methodsToPatch.forEach(function (method) {
      // cache original method
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (. args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        if (inserted) ob.observeArray(inserted)
        // notify change
        ob.dep.notify()
        return result
      })
    })
    Copy the code

Vue asynchronous rendering principle

Characteristics of 1.

Vue, without asynchronous updates, rerenders components every time data is updated, causing performance problems.

2. Combined with source code, detailed analysis

  1. Dep.notify (), which triggers data updates to notify Watcher

    / / dep. Js
    // Execute the update method in watcher
    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)
        }
        // Iterate through subs to trigger update in watcher.js
        // Subs contains multiple watchers, which are triggered simultaneously.
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    Copy the code
  2. The Update in the watcher class fires, putting the watch in the queueWatcher queue

    update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)}}Copy the code
  3. QueueWatcher is a filter watcher that distinguishes between push to queue and remove from queue based on the plushing state. NextTick (flushSchedulerQueue)

    function queueWatcher (watcher: Watcher) {
      const id = watcher.id // Filter the id of watcher, if it exists, do not process, if it does not exist, mark
      if (has[id] == null) {
        has[id] = true
        if(! flushing) {// If the state is! The flushing goes into the queue
          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--
          }
          queue.splice(i + 1.0, watcher) // Flushing is removed
        }
        // queue the flush
        if(! waiting) { waiting =true
    
          if(process.env.NODE_ENV ! = ='production' && !config.async) {
            flushSchedulerQueue()
            return
          }
          nextTick(flushSchedulerQueue)
        }
      }
    }
    Copy the code
  4. ‘nextTick(flushSchedulerQueue) executes watcher.run()

    function flushSchedulerQueue () {
      currentFlushTimestamp = getNow()
      flushing = true
      let watcher, id
    
      // Sort queue before flush.
      // This ensures that:
      // 1. Components are updated from parent to child. (because parent is always
      // created before the child)
      // 2. A component's user watchers are run before its render watcher (because
      // user watchers are created before the render watcher)
      // 3. If a component is destroyed during a parent component's watcher run,
      // its watchers can be skipped.
      queue.sort((a, b) = > a.id - b.id)
    
      // do not cache length because more watchers might be pushed
      // as we run existing watchers
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
          watcher.before()
        }
        id = watcher.id
        has[id] = null
        watcher.run()
        // in dev build, check and stop circular updates.
        if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
          circular[id] = (circular[id] || 0) + 1
          if (circular[id] > MAX_UPDATE_COUNT) {
            warn(
              'You may have an infinite update loop ' + (
                watcher.user
                  ? `in watcher with expression "${watcher.expression}"`
                  : `in a component render function.`
              ),
              watcher.vm
            )
            break}}}// keep copies of post queues before resetting state
      const activatedQueue = activatedChildren.slice()
      const updatedQueue = queue.slice()
    
      resetSchedulerState()
    
      // call component updated and activated hooks
      callActivatedHooks(activatedQueue)
      callUpdatedHooks(updatedQueue)
    
      // devtool hook
      /* istanbul ignore if */
      if (devtools && config.devtools) {
        devtools.emit('flush')}}Copy the code

3 summary

  • Call notify() to tell Watcher to update
  • Call watcher’s update method in turn
  • Deredo watcher (by ID) and put it on the queue
  • After the command is executed, the queue is asynchronously cleared and the nextTick(flushSchedulerQueue) queue is updated in batches