Vue2.0 source code analysis

If you think it is good, please send me a Star at GitHub

Vue2.0 source code analysis: responsive principle (on) Next: Vue2.0 source code analysis: componentization (on)

Deep into the responsivity principle

After introducing props, Data, Watch, and computed, we have a preliminary understanding of the responsive principle. In this chapter, we review the responsive principle again to explore its implementation principle.

In previous chapters, we have introduced: Vue. Js defines responsive objects using the object.defineProperty (obj, key, descriptor) method, which Can be found on Can I Use. IE8 does not support this method. This is the real reason vue.js does not support IE8 and below.

On the MDN website, we can see that this method supports a number of parameters, of which descriptor supports a number of optional properties. The most important for vue.js implementations of reactive objects are the get and set properties.

let val = 'msg'
const reactiveObj = {}
 Object.defineProperty(reactiveObj, msg, {
   get: function () {
     // called when reactiveobj.msg is accessed
     return val
   },
   set: function (newVal) {
     // this is called when reactiveobj.msg is set
     val = newVal
   }
 })
Copy the code

In Vue’s reactive object, it collects dependencies in getters and dispatches updates in setters. We’ll cover the collection of dependencies in getters and dispatches updates in setters in separate sections.

After introducing Object.defineProperty, let’s answer the question, what is a reactive Object? DefineProperty () is defined with both get and set options. We can call it a reactive Object.

When ue. Js is instantiated, props, data, and computed are turned into responsive objects. When we introduce responsive objects, we will focus on the processing of props and data, which occurs in initState(VM) in the this._init() method.

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

Let’s start by looking at how initProps handles the props logic:

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  constisRoot = ! vm.$parent// root instance props should be converted
  if(! isRoot) { toggleObserving(false)}for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if(process.env.NODE_ENV ! = ='production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () = > {
        if(! isRoot && ! isUpdatingChildComponent) { warn(`Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if(! (keyin vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)}Copy the code

In analyzing the overall flow of initProps, we know that initProps mainly does three things: props verification and evaluation, props responsiveness, and props proxy. This is very simple for the props agent and its main function is to allow us to evaluate.

The proxy agent

The proxy () method is defined in SRC/core/instance/state. Js file:

const sharedPropertyDefinition = {
  enumerable: true.configurable: true.get: noop,
  set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

Code analysis:

  • noop: it stands for empty function, and empty function stands for doing nothing.
  • target: it is the target proxy object inVue.jsIs in theVueInstance.
  • sourceKey: it is the source property inpropsWhat is passed in the proxy is_propsInternal private properties.
  • key: it is the property to be proxied inpropsIs the variety that we writepropsProperties.
  • sharedPropertyDefinition: it isObject.defineProperty(obj, key, descriptor)methodsdescriptorParameter, as you can see from the code above, inpropsIn the proxy it providesenumerable,configurable,getandsetThese are the choices.

Suppose we have the following Vue instance:

export default {
  props: ['msg'.'age']}Copy the code

MSG and this._props. Age instead of this._props. MSG and this._props.

/ / agent before
const msg = this._props.msg
console.log(msg)
// The value of props cannot be modified as long as the demo is performed
this._props.msg = Const MSG = this.msg console.log(MSG) // const MSG = this.msg console.log(MSG) //new msg'
Copy the code

The above is the analysis of the props agent process. It is the same for the data agent.

defineReactive

The defineReactive method is defined in the index.js entry file of its SRC /core/observer directory

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  const dep = new Dep()

  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)Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        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 } childOb = ! shallow && observe(newVal) dep.notify() } }) }Copy the code

Code analysis:

  • defineReactiveIn fact, yesObject.defineProperty()Method of wrapping a layer, mainly is treatedgetterandsetterRelevant logic.
  • defineReactiveFirst of all byObject.getOwnPropertyDescriptor()Method gets the currentobj.keyAttribute description, if its attributeconfigurableforfalse, cannot be defined as a responsive object, so forobj.keyComponent updates are not triggered by any assignment, such as:
export default {
  data () {
    return {
      obj: {}
    }
  },
  created () {
    const obj = {}
    Object.defineProperty(obj, 'msg', {
      configurable: false.value: 'msg'
    })
    this.obj = obj
    setTimeout(() = > {
      // this.obj.msg is not a responsive object and changes to it will not trigger component updates
      this.obj.msg = 'new msg'
    }, 3000)}}Copy the code

Observe and Observer

We can see the observe(val) code in defineReactive, so let’s introduce the observe() method and the Observer class. The observe() method definition is defined in the same file as the defineReactive() method, with the following code:

export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  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 ) { ob =new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Code analysis:

  • First of all on the transfervalueType check, not object orVNodeInstance when no operation is performedVNodeIs a class that will be used when generating the virtual DOM, which we’ll cover later,isObjectIs a definition insrc/shared/utils.jsTool methods in the file.
export function isObject (obj: mixed) :boolean {
  returnobj ! = =null && typeof obj === 'object'
}
Copy the code
  • Then thevalueusehasOwnDetermine if there is__ob__Properties and__ob__forObserverExample, this property is added to prevent repeated observations (to avoid redefining reactive types), i.e., return if the object is already reactive, otherwise proceed to the next step.hasOwnIs a definition insrc/shared/utils.jsTool methods in the file:
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string) :boolean {
  return hasOwnProperty.call(obj, key)
}
Copy the code
  • The lastvalueSome conditions are judged, among which the two most important conditions areArray.isArrayandisPlainObject, they judge separatelyvalueWhether it’s an array, whether it’s an ordinary object, the other boundary conditions are not going to be discussed. Among themisPlainObjectIs a definition insrc/shared/utils.jsTool methods in the file:
export function isPlainObject (obj: any) :boolean {
  return _toString.call(obj) === '[object Object]'
}
Copy the code

Next, we need to look at the implementation of the Observer class:

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
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } 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])
    }
  }

  /** * 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

Code analysis:

  • defAs defined in thesrc/core/utils/lang.jsA tool method in the file,defAnd that’s essentially trueObject.defineProperty()Method for a layer of wrapping, usingdefdefine__ob__The purpose of the__ob__Cannot be enumerated during object property traversal.
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
  • inVue.jsThe responsive handling of pure objects and arrays is different in the code firstvalueWhether it is an array. If it is not an array, thenwalk()Methods.walk()A method is essentially a recursive traversal of object properties and then a calldefineReactive()For example:
const nestedObj = {
  a: {
    b: {
      c: 'c'}}}// recursive call
defineReactive(nestedObj)
defineReactive(a)
defineReactive(b)
defineReactive(c)
Copy the code

If it is an array, the observeArray() method is called. ObserveArray is also a recursive call, except that the array is iterated instead of the object’s property keys. Then we also see that a hasProto judgment is made before the observeArray() method is called, and then different actions are taken based on that judgment. HasProto is a constant defined in SRC /core/util/env.js to determine whether the current browser supports the __proto__ attribute:

export const hasProto = '__proto__' in {}
Copy the code

As we all know, due to some limitations of the native API, vue.js provides variant method support for seven methods that can change their own arrays:

const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
Copy the code

The variation of the seven method processing logic in the SRC/core/ovserver/array. The js file:

import { def } from '.. /util/index'
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 */
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

Code analysis:

  • First of all toArray.prototypeThe prototype creates a new variable that will be displayed in theprotoAugmentorcopyAugmentMethod.
  • Then go through the seven methods and usedefTo redefine a wrap method. That is, when we call any of these seven methods, we call our wrapper method first and then our native array method in the wrapper method, so that we can do our own thing in the wrapper method, for examplenotify, this process can be described using the following pseudocode example:
// array.prototype. push method as an example
function mutatorFunc (value) {
  const result = Array.prototype.push(value)
  // do something
  return result
}
export default {
  data () {
    return {
      arr: []
    }
  },
  created () {
    this.arr.push('123')
    / / equivalent to
    mutatorFunc(123)}}Copy the code

Then we next look at the implementation of protoAugment and copyAugment, first the simplest protoAugment:

/ / define
const arr = []
export const arrayMethods = Object.create(arrayProto)
function protoAugment (target, src: Object) {
  target.__proto__ = src
}

/ / call
protoAugment(arr, arrayMethods)

/ / after the call
arr.__proto__ = {
  // omit others
  push: function () {},
  pop: function () {},
  shift: function () {},
  unshift: function () {},
  splice: function () {},
  sort: function () {},
  reverse: function () {}
}
arr.push()
arr.pop()
arr.shift()
arr.unshift()
arr.splice()
arr.sort()
arr.reverse()
Copy the code

Code analysis: When the browser supports the __proto__ attribute, direct __proto__ to the arrayMethods variable we created, which contains the seven variation methods we defined above.

When the __proto__ attribute is not supported by the browser, we call the copyAugment method:

/ / define
const arr = []
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export const arrayMethods = Object.create(arrayProto)
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])
  }
}

/ / call
copyAugment(value, arrayMethods, arrayKeys)

/ / after the call
arr = {
  // omit others
  push: function () {},
  pop: function () {},
  shift: function () {},
  unshift: function () {},
  splice: function () {},
  sort: function () {},
  reverse: function () {}
}
arr.push()
arr.pop()
arr.shift()
arr.unshift()
arr.splice()
arr.sort()
arr.reverse()
Copy the code

Code analysis: As we can see from the code, when the browser does not support __proto__, we will iterate over and assign all keys from the arrayMethods variable we created to the value array.

Depend on the collection

In this section, we introduce dependency collection, but before that we need to know what it is and what it is for.

Q: What is dependency collection? What is the purpose of dependency collection? A: Dependency collection is the Watcher collection of changes to subscription data. The goal is to know which subscribers should be notified to do the logical processing when reactive data changes and their setters are triggered. For example, when a reactive variable is used in the Template template, the Render Watcher dependency should be collected for the reactive variable at the first rendering of the component, and the Render Watcher should be notified to rerender the component when its data changes and the setter is triggered.

As we mentioned earlier, dependency collection takes place in the getter of Object.defineProperty(), so let’s review the defineReactive() code:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  // omit the code
  const dep = new Dep()
  // 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)Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    }
  })
}
Copy the code

As you can see from the code, when the getter is fired, it first checks whether dep.target exists, and if it does, it calls dep.depend(). Dep.depend () is where the dependencies are actually collected. After reading the code above, we might have a few questions:

  • DepWhat is?
  • Dep.targetWhat is?
  • dep.dependHow is dependency collection done? How is dependency removal done?

Dep

The Dep class is a class defined in the Dep. Js file in the Observer directory. The observer directory structure is as follows:

|-- observer       
|   |-- array.js
|   |-- dep.js
|   |-- index.js
|   |-- scheduler.js
|   |-- traverse.js
|   |-- watcher.js
Copy the code

Then, let’s look at the definition of the Dep class:

let uid = 0
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()
    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)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Copy the code

Code analysis:

  • DepClass first defines a static propertytarget, it isDep.targetWe’ll talk about it later. Then we define two instance properties,idisDepThe primary key of thesubsIs a store of variousWatcherThe array. For example,render watcher,user watcherandcomputed watcherAnd so on.
  • addSubandremoveSubThat corresponds to PIsubsArray to add and remove variousWatcher.
  • dependIs the dependency collection process.
  • notifyTriggered when data changessetterThere is a code like this:dep.notify()Its purpose is to notify when the responsive data changessubsAll the different things insidewatcherAnd then execute itupdate()Methods. This is part of the process of distributing updates, which we will cover in a later section.

With these properties and methods covered, we have a concrete idea of what A Dep is and what it does.

Dep. Target and Watcher

Let’s move on to the second question, what is dep.Target? Dep.target is an example of various Watcher types, as illustrated by the following code:

<tempalte>
  <div>{{msg}}</div>
</template>

<script>
export default {
  data () {
    return {
      msg: 'Hello, Vue.js'
    }
  }
}
</script>
Copy the code

When the component first renders, it takes the MSG value and executes pushTarget(this), which represents the current Watcher instance. PushTarget () is a method defined in the dep.js file, along with a method called popTarget. Their code looks like this:

Dep.target = null
const targetStack = []

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

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]}Copy the code

In pushTarget, we pass the target argument as the Watcher instance, and when pushTarget executes, it dynamically sets the Dep static property dep.target. After examining the code for pushTarget, we can see why Dep.target is an instance of Watcher.

Then, we have a new problem: How is the Watcher class defined? This is a class defined in the watcher.js file with the following key code:

let uid = 0

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop process.env.NODE_ENV ! = ='production' && warn(
          `Failed watching path: "${expOrFn}"` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  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)
      }
    }
  }
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

Copy the code

When we look at the Watcher class from a dependency collection perspective, we need to pay attention to the following four properties in its constructor:

this.deps = []             // Old DEP list
this.newDeps = []          // New DEP list
this.depIds = new Set(a)// Old DEP ID collection
this.newDepIds = new Set(a)// New deP ID collection
Copy the code

The use of these four attributes will be described in more detail in the addDep and cleanupDeps sections. In this section, we will focus on the constructor of Watcher and the implementation of the Get () method.

In the constructor of the Watcher class, when instantiated, the deps and newDeps arrays and the depIds and newDepIds collections are initialized to empty arrays and empty collections, respectively. At the end of the constructor, it is determined that, if computed Watcher (note: If only the lazy attribute is true for computed watcher), the this.get() function is called immediately to evaluate.

Next, let’s look at the implementation of this.get() and a scenario where pushTarget and popTarget are used together.

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}
Copy the code

The get() method starts with a call to pushTarget(this), which pushes the current Watcher instance into the target array. Then set dep. target to the current Watcher instance.

Dep.target = null
const targetStack = []

export function pushTarget (target: ? Watcher) {
  targetStack.push(target)
  Dep.target = target
}
Copy the code

This. Getter is then called to evaluate, using the following example of a calculated property:

export default {
  data () {
    return {
      age: 23}},computed: {
    newAge () {
      return this.age + 1
    }
  }
}

value = this.getter.call(vm, vm)
/ / equivalent to
value = newAge()
Copy the code

For computed Watcher, its getter property is the computed property method we wrote, and the procedure for calling this.getter is the procedure for performing the computed property method we wrote.

At the end of the this.get() method, popTarget() is called, which removes the last of the current target stack array and sets dep.target to the next-to-last.

Dep.target = null
const targetStack = []

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]}Copy the code

After analyzing pushTarget and popTarget, one question might be why does pushTarget/popTarget exist, and what purpose does it serve?

This is done because components can be nested, and the purpose of using stack arrays to push/unload components is to maintain proper dependencies during component rendering, as shown in the following code:

// child component
export default {
  name: 'ChildComponent'.template: '<div>{{childMsg}}</div>',
  data () {
    return {
      childMsg: 'child msg'}}}export default {
  name: 'ParentComponent'.template: `
      
{{parentMsg}}
`
.components: { ChildComponent } data () { return { parentMsg: 'parent msg'}}}Copy the code

As we all know, when a component is rendering, when the parent component has a child component, the child component will be rendered first, and the parent component will not be rendered until all the child components are rendered. Therefore, the execution sequence of the component rendering hook functions is as follows:

parent beforeMount()
child beforeMount()
child mounted()
parent mounted()
Copy the code

According to the above rendering steps, when parent beforeMount() starts, parent Render Watcher is instantiated and this.get() is called, the dep.target dependency is parent Render Watcher, The target stack array is:

// This is an example of Watcher
const targetStack = ['parent render watcher']
Copy the code

When child beforeMount is executed, child Render Watcher is instantiated and this.get() is called. Dep.target is dependent on Child Render Watcher and the target stack array is:

// This is an example of Watcher
const targetStack = ['parent render watcher'.'child render watcher']
Copy the code

When child Mounted () is mounted, this. Getter () is called, and popTarget() is called.

// This is an example of Watcher
const targetStack = ['parent render watcher']
Dep.target = 'parent render watcher'
Copy the code

Parent Mounted () = this.getter(); popTarget() = popTarget();

// This is an example of Watcher
const targetStack = []
Dep.target = undefined
Copy the code

From the above example analysis, we can understand why there is a dependent push/push step and the purpose of doing so. Next, let’s examine the logic of addDep and cleanupDeps during dependency collection.

AddDep and cleanupDeps

addDep

In the depend() method of the Dep class, we introduced the code implementation, which calls addDep(Dep) :

export default Dep {
  // Omit other code
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}}Copy the code

Dep.target.adddep (this) is equivalent to:

const watcher = new Watcher()
watcher.addDep(this)
Copy the code

Next, let’s look at the implementation logic of the addDep method in the Watcher class:

export default Watcher {
  // Simplify the code
  constructor () {
    this.deps = []              // Old DEP list
    this.newDeps = []           // New DEP list
    this.depIds = new Set(a)// Old DEP ID collection
    this.newDepIds = new Set(a)// New deP ID 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)}}}}Copy the code

Dep = deP = deP = deP = deP = deP = deP = deP = deP = deP = deP Adds the current Watcher instance to the SUBs array of the DEP instance.

Rigid analysis of the source code is not very convenient for us to understand addDep code logic, we use the following code example:

{{MSG}}</p> <script> export default {name: {{MSG}}</p> </template> 'App', data () { return { msg: 'msg' } } } </script>Copy the code

Process analysis:

  • Instantiation occurs when the component is first renderedrender watcherFor the time of,Dep.targetforrender watcher:
const updateComponent = () = > {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
  before () {
    if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code
  • First compile readmsgWhen a reactive variable is triggeredgetterfordep.depend()Rely on the collection, and then calladdDep()Method, becausedeps,newDeps,depIdsandnewDepIdsInitialize to an empty array or an empty set, so theta at this pointdepIs added to thenewDepIds,newDepsAnd will be executeddep.addSub(this), can be represented by the following code:
// instantiate Dep
const dep = {
  id: 1.subs: []}// Add to newDepIds, newDeps
this.newDepIds.push(1)
this.newDeps.push(dep)

/ / call addSub
dep.addSub(this)
console.log(dep) // { id: 1, subs: [new Watcher()] }
Copy the code
  • When the second compilation readsmsgWhen a reactive variable is triggeredgetterfordep.dependRely on collection becausedepisdefineReactiveFunction in the closure variable, so twice triggeredgetterIs the samedepInstance. When callingaddDepJudge at this timenewDepIdsIn the collectiondep.idfor1It already exists, so skip it.

You may notice that when parsing the code in the getter, we deliberately omit the following code:

if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}
Copy the code

You may be wondering: What is this code for? What does it do? So now, for example:

<template>
  <p>{{obj.msg}}</p>
</template>
<script>
export default {
  name: 'App',
  data () {
    return {
      obj: {
        msg: 'msg'
      }
    }
  }
}
</script>
Copy the code

Process analysis:

  • When it’s called the first timedefineReactiveWhen, at this timedefineReactiveThe first parameterobjandkeyAre:
obj = {
  obj: {
    msg: 'msg'
  }
}

key = 'obj'
Copy the code

At the beginning of defineReactive, a closure DEP instance is instantiated and we assume that the instantiated DEP looks like this:

const dep = new Dep()
console.log(dep) // { id: 1, subs: [] }
Copy the code

Observe (val); observe(val); Observe (val); Observe (val); Observe (val); observe(val); observe(val);

this.dep = new Dep()
Copy the code

It instantiates another DEp and assigns the instantiated DEp to this.dep. Let’s assume that the instantiated DEP looks like this:

const dep = new Dep()
console.log(dep) // { id: 2, subs: [] }
Copy the code

Because obj = {MSG: ‘MSG’} is an object, this.walk() iterates through the obj object’s properties and then calls defineReactive again to instantiate a closure DEP instance. We assume that the instantiated DEP looks like this:

const dep = new Dep()
console.log(dep) // { id: 3, subs: [] }
Copy the code

We now have three instances of DEP, two of which are the closure instance DEP in the defineReactive function and one of which is the attribute DEP of childOb(Observer instance).

  • When the component starts rendering, the reactive principle adds us intemplateReads theobj.msgVariable, so it fires firstobjThe object’sgetterAt this time,depforid=1The closure variable ofdep. At this timeDep.targetforrender watcherAnd then proceeddep.depend()Rely on the collection when going toaddDepMethod, because the four properties we care about are all empty arrays or empty collections, we will change the value ofdepI’m going to add to thatdepIt is expressed as follows:
const dep = {
  id: 1.subs: [new Watcher()]
}
Copy the code
  • indep.depend()Dependencies are determined after collection is completechildObBecause thechildObforObserverIs called, so the condition is judged to be truechildOb.dep.depend(). When performing theaddDep()At this timedepforid=2theObserverInstance attributesdepAnd do notnewDepIdsanddepIds, so it will be added to, at this pointdepIt is expressed as follows:
const dep = {
  id: 2.subs: [new Watcher()]
}
Copy the code
  • When the response variableobjthegetterWhen the trigger is complete, it will triggerobj.msgthegetterFor the time of,depforid=3The closure variable ofdep. At this timeDep.targetIs stillrender watcherAnd then proceeddep.depend()Depend on the collection, this process andobjthegetterThe process for doing dependency collection is basically the same whenaddDep()Method is executed at this timeDep ‘is denoted as follows:
const dep = {
  id: 3.subs: [new Watcher()]
}
Copy the code

The only difference is that childOb is undefined, and childob.dep.depend () will not be called for dependency collection of child attributes.

After analyzing the above code, it’s easy to answer the question: q: what does childob.dep.depend () do? What does it do? A: Childob.dep.depend () does dependency collection for child attributes so that when an object or one of its attributes changes, its dependencies can be notified to act accordingly.

<template> <p>{{obj. MSG}}</p> <button @click="change"> </button> <button @click="add"> </button> </template> <script> import Vue from 'vue' export default { name: 'App', data () { return { obj: { msg: 'msg' } } }, methods: { change () { this.obj.msg = 'new msg' }, add () { this.$set(this.obj, 'age', 23) } }, watch: { obj: { handler () { console.log(this.obj) }, deep: true } } } </script>Copy the code

Take the example above:

  • When there ischildOb.dep.depend()Collect subproperty dependencies when we modify them either waymsgThe value of is addedageNew properties, all fireuser watcher, that is, printingthis.objThe value of the.
  • When there is nochildOb.dep.depend()When collecting subproperty dependencies, we modify themmsgThe value of, although will be notifiedrender watcherComponent rerendering is performed without notificationuser watcherprintthis.objThe value of the.

cleanupDeps

In this section, our goal is to figure out why and how dependency cleanup is done.

Let’s look at the implementation of cleanupDeps in the Watcher class:

export default Watcher {
  // Simplify the code
  constructor () {
    this.deps = []              // Old DEP list
    this.newDeps = []           // New DEP list
    this.depIds = new Set(a)// Old DEP ID collection
    this.newDepIds = new Set(a)// New deP ID collection
  }
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0}}Copy the code

As an example, suppose we have the following components:

<template>
  <p v-if="count < 1">{{msg}}</p>
  <p v-else>{{age}}</p>
  <button @click="change">Add</button>
</template>
<script>
import Vue from 'vue'
export default {
  name: 'App',
  data () {
    return {
      count: 0,
      msg: 'msg',
      age: 23
    }
  },
  methods: {
    change () {
      this.count++
    }
  }
}
</script>
Copy the code

Process analysis:

  • When the component is rendered for the first time,render watcherThe instancenewDepsThere are two arraysdepExample, one of which is incountResponsive variablegetterThe other one was collected when triggeredmsgResponsive variablegetterCollected when triggered (agebecausev-if/v-elseThe cause of the command is not triggered when the component is first renderedagethegetter), we use the following code to represent:
this.deps = []
this.newDeps = [
  { id: 1.subs: [new Watcher()] },
  { id: 2.subs: [new Watcher()] }
]
Copy the code
  • When we click the button to proceedthis.count++Is triggered to re-update the component becausecount < 1The condition is false, so it also fires during component re-renderingageOf reactive variablesgetterDo dependency collection. When performing theaddDepAfter this timenewDepsHave changed:
this.deps = [
  { id: 1.subs: [new Watcher()] },
  { id: 2.subs: [new Watcher()] }
]
this.newDeps = [
  { id: 1.subs: [new Watcher()] },
  { id: 3.subs: [new Watcher()] }
]
this.depIds = new Set([1.2])
this.newDepIds = new Set([1.3])
Copy the code

On the last call to this.get(), the this.cleanupdeps () method is called, which first iterates through the old dependency list DEps, and if one of the dePs is not found in the new dependency ID collection newDepIds, Call dep.removesub (this) to remove the dependency. In the component rendering process, this stands for Render Watcher. When we call this method and change the MSG variable value, the component will not be rerendered. After iterating through the DEPS array, the values of DEps and newDeps, depIds and newDepIds are swapped, and newDeps and newDepIds are emptied.

After analyzing the examples above, we can see why dependency cleanup is necessary: to avoid repeating rendering of components with unrelated dependencies.

Distributed update

Following the introduction of dependency collection, we will examine the distribution of updates. In this section, our goal is to figure out what updates do and how they are implemented.

Let’s answer the first question: Q: What do you do when you distribute updates? A: Distributive update means notifying all Watcher(Dep dependent) subscribers to update when responsive data changes. In the case of The Render Watcher, update triggers the component to re-render; For computed Watcher, update means reevaluating a computed property; In the case of user Watcher custom watcher, update means invoking the user-provided callback function.

scenario

Most people analyze the update scenario and only indicate that the setter in object.defineProperty () method will issue the update when it is triggered. In fact, there are four places to issue the update. The other three are:

  • Vue.jsWhen the seven array variation methods are called, an update is distributed.
const methodsToPatch = ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse']

methodsToPatch.forEach(function (method) {
  def(arrayMethods, method, function mutator (. args) {
    // Simplify the code
    ob.dep.notify()
    return result
  })
})
Copy the code
  • Vue.setorthis.$set, will be distributed updates.
export function set (target: Array<any> | Object, key: any, val: any) :any {
  // Simplify the code
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
Copy the code
  • Vue.deleteorthis.$delete, will be distributed updates.
export function del (target: Array<any> | Object, key: any) {
  // Simplify the code
  delete target[key]
  if(! ob) {return
  }
  ob.dep.notify()
}
Copy the code

The above three dispatched updates are slightly different from those dispatched when setters are triggered in the Object.defineProperty() method, where the deP is a closure variable defined in the defineReactive method, Means it can only serve the defineReactive method. The dep of the former is taken from the this.__ob__ object. The this.__ob__ attribute is defined when the Observer is instantiated and refers to the Observer instance, as we have described earlier. This unique processing mode facilitates us to read deP dependencies in the above three scenarios, and then distribute updates of dependencies.

process

In the code above, we learned about the various times when dep.notify() is called. In this section we need to look at the process of sending out updates.

When dep.notify() is called, it executes the code in notify(). Let’s look at the implementation of this method in the DEP class:

notify () {
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
Copy the code

As you can see, the main thing notify does is iterate through the subs array and call the Update method. Next, let’s look at the code implementation of the Update method in the Watcher class:

import { queueWatcher } from './scheduler'
update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)}}Copy the code

When the update method is executed, two attributes, this.lazy and this.sync, are evaluated first, where this.lazy is a sign of computed properties in a computed watcher. This.sync is not the focus of this section on distributing updates, so I won’t cover it too much.

Let’s focus on queueWatcher, which is a method written in an observer/scheduler.js file:

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    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--
      }
      queue.splice(i + 1.0, watcher)
    }
    // queue the flush
    if(! waiting) { waiting =true

      if(process.env.NODE_ENV ! = ='production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy the code

There are several variables defined at the top of the code. Some of the most important variables are as follows:

  • queue: all kinds ofWatcherExecute the queue, eitherrender watcher,user watcherorcomputed watcher“As long as it’s not repetitiveWatcherWill eventually be pushed intoqueueQueue array.
  • has: Prevents repeated additionsWatcherFlag object of:
// indicates that the Watcher instance with id 1,2 has been added to queue
// The same Watcher instance will not be added to the queue repeatedly
const has = {
  1: true.2: true
}
Copy the code
  • index: Current traversalWatcherThe instance index, which isflushSchedulerQueueMethod.forTo iterate overqueueQueue arrayindex.

With the above important variables covered, let’s examine queueWatcher’s process:

  • The code starts by getting the currentWatcherSince it increasesid, judge in the mark objecthasExists in, if not, for thisidMark it and assign it totrue.
  • And then determine whether it isflushingState, if not, means we can normally put the currentWatcherPush toqueueQueue array.
  • And then I decided whether it waswaitingStatus, if not, it indicates that the command can be executedqueueQueue array, and then setwaitingfortrue, and finally callnextTick(flushSchedulerQueue).nextTickMethod isVue.jsA utility function that handles asynchronous logic, as long as we know:nextTickThe function argument in thetickThe execution.

FlushSchedulerQueue = flushSchedulerQueue = flushSchedulerQueue

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

A cursory look at flushSchedulerQueue shows that it does several things: restoring flushing, sorting queues, traversing queues, restoring states, and firing component hook functions. Let’s follow these steps and explain them respectively:

  • Reduction flushing stateIn:flushSchedulerQueueFirst of all,flushingThe restore is done so that it does not affect the executionqueueIn the queue, there areWatcherPush toqueueIn the queue.
  • Sort queue: uses arrayssortThe method,queueIn the queueWatcherAccording to the increasingidIs sorted from smallest to largest in order to ensure the following three scenarios:
  1. As we all know, component updates start with the parent component and then go to the child component. When a component is rendered, it will start rendering from the parent component, which is when the parent component is createdrender watcher, suppose that at this pointparent render watcherSince the increaseidfor1, and then render the child component, instantiating the child component’srender watcher, suppose that at this pointchild render watcherSince the increaseidfor2. forqueue.sort()After sorting,idThe values are sorted to the front of the array, so that inqueueSo when you iterate, you’re guaranteed to do it firstparent render watcherAnd then deal with itchild render watcher
  2. Because it’s user definedWatcherCan be created prior to component rendering, therefore for user customizationWatcherIn terms of, need takes precedence overrender watcherThe execution.
<template> <p>{{msg}}</p> <button @click="change">Add</button> </template> <script> export default { data () { return { count: 0, msg: 'msg', age: $this.$watch(' MSG ', () => {console.log(this.msg)})}, methods: { change () { this.msg = Math.random() } } } </script>Copy the code
  1. If a child component executes in the parent componentqueueWatcherThe process is destroyed, then all of the sub-componentsWatcherExecution should be skipped.
  • Through the queue: in the use offorWhen we iterate, we need to pay attention to the iterate condition, which is true firstqueueThe length is evaluated, and then the loop condition is judged, because in traversalqueueIn the array,queueElements in the array may change. During traversal, the current is first releasedWatcherinhasFlags the state in the object, and then callswatcher.run()Methods.runIs defined in theWatcherA method in the class:
export default class Watcher {
  // Simplify the code
  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.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        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)
        }
      }
    }
  }
}
Copy the code

Run method code is not very complex, it is to different treatment of different Watcher, if is to render the Watcher, in the execution of the enclosing the get () will perform this. In the process of the getter, enclosing the getter method corresponding to the following:

updateComponent = () = > {
  // Component rendering methods
  vm._update(vm._render(), hydrating)
}
Copy the code

If user watcher is true, this.cb.call() will be called, in which case this.cb will be the user callback written by the user:

export default {
  data () {
    return {
      msg: 'msg'
    }
  },
  created () {
    // user callback
    // this.cb = userCallback
    const userCallback = () = > {
      console.log(this.msg)
    }
    this.$watch(this.msg, userCallback)
  }
}
Copy the code

If it is computed watcher and its this.user value is false, this.cb.call() is called, and this.cb is the method we provide for calculating attributes:

export default {
  data () {
    return {
      msg: 'msg'}},computed: {
    // this.cb = newMsg () {}
    newMsg () {
      return this.msg + '!!!!!! '}}}Copy the code
  • Reducing state: callresetSchedulerStateThe purpose of the function is whenqueueWhen all queues are executed, restore all relevant states to their initial state, which includesqueue,hasandindexSuch as:
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if(process.env.NODE_ENV ! = ='production') {
    circular = {}
  }
  waiting = flushing = false
}
Copy the code
  • Triggers the component hook function: callcallActivatedHooksandcallUpdatedHooksThe difference is to trigger the componentactivatedandupdatedThe hook function, whereactivatedIs with thekeep-aliveAssociated hook functions.

Infinite loop

When developing with vue.js, we sometimes accidentally write code with an infinite loop, for example:

<template>
  <p>{{msg}}</p>
  <button @click="change">Add</button>
</template>
<script>
export default {
  data () {
    return {
      msg: 'msg'
    }
  },
  methods: {
    change () {
      this.msg = Math.random()
    }
  },
  watch: {
    msg () {
      this.msg = Math.random()
    }
  }
}
</script>
Copy the code

When we click the button and call the change method to modify the value of this. MSG, because we used watch to monitor the update of MSG value, the watch listener function will be executed. However, we changed the value of this. MSG in the watch listener function, which will cause the listener function we wrote to be called all the time. There is an endless loop. In vue.js, it does something special to avoid an infinite loop that crashes the browser.

In queueWatcher, we didn’t parse the following else code:

export const MAX_UPDATE_COUNT = 100
let circular: { [key: number]: number } = {}

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--
  }
  queue.splice(i + 1.0, watcher)
}
Copy the code

Let’s examine the following code from the above example:

  • When we click the button to modifythis.msgWhen the value is triggeredmsgthesetterAnd then proceeddep.notifySend out the update, then callqueueWatcherAt this time,msgThere are twoDepDependency, one isrender watcherAnd the other one isuser watcher, sothis.subsPhi is a length of phi2theWatcherThe array. At the beginning timequeueWatcherThe time,flushingThe status offalseBecause theuser watcherthanrender watcherCreate it first, so at this pointuser watcherI’m going to push it in firstqueueThe queue, and then the queuerender watcher:
// Display the use of the actual Watcher instance
const queue = ['user watcher'.'render watcher']
Copy the code
  • And then it will executewatchListen on the function, execute againqueueWatcherAt this timeflushingforfalseGo,elseBranching logic,whileThe main function of the loop is to find out what should bequeueWhere is the new array insertedwatcher, such as:
const queue = [
  { id: 1.type: 'user watcher' },
  { id: 2.type: 'render watcher'},]// When the watch listener is executed, the watcher should be inserted into the second item in the array
const queue = [
  { id: 1.type: 'user watcher' },
  { id: 1.type: 'user watcher' },
  { id: 2.type: 'render watcher'},]Copy the code

Because of the special example we wrote, the queue array pushes the user watcher continuously, and vue.js terminates this behavior prematurely when the number in the queue exceeds the limit (some watcher is traversed more than 100 times). Vue.js uses circular token objects to count, It marks the number of times each Watcher is traversed, for example:

// Watcher with id 1 is traversed 101 times
// Watcher with id 2 is traversed once
const circular = {
  1: 101.2: 1
}
Copy the code

Circular counts updates and terminations in the flushSchedulerQueue function:

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

So for the example above, vue.js prints an error message on the console:

// You may have an infinite update loop in watcher with expression "msg"
Copy the code

Overall flow chart

After analyzing the above process of distributing updates, we can get the following flow chart.

Implementation principle of nextTick

When developing with vue.js, if we want to manipulate the correct DOM based on data state, we must have dealt with the nextTick() method, which is one of the core methods in vue.js. In this section, we introduce how nextTick is implemented in vue.js.

Asynchronous knowledge

Because nextTick involves a lot of asynchrony, we’ll introduce asynchrony to make it easier to learn.

Event Loop

We all know that JavaScript is single-threaded and is executed based on an Event Loop that follows certain rules: All synchronous tasks are executed in the main thread, forming an execution stack. All asynchronous tasks are temporarily put into a task queue. When all synchronous tasks are completed, the task queue is read and put into the execution stack to start execution. The above is a single execution mechanism. The main thread repeats this process over and over again to form an Event Loop.

The above is a general introduction to Event Loop, but there are still some details we need to master when executing Event Loop.

We mentioned tick in the update section, so what is tick? The tick is a single execution of the main thread. All asynchronous tasks are scheduled by task queue, which stores tasks. According to the specification, these tasks are divided into Macro task and micro task. There is a subtle relationship between macro tasks and Micro Tasks: After each Macro task is executed, all micro Tasks are cleared.

Macro Task and Micro Task correspond as follows in the browser environment:

  • macro taskMacro task:MessageChannel,postMessage,setImmediateandsetTimeout.
  • micro taskMicro tasks:Promise.thenandMutationObsever.

MutationObserver

In the MDN documentation, we can see the detailed use of MutationObserver, which is not very complicated. It is used to create and return a new Instance of MutationObserver, which is called when the specified DOM changes.

Let’s write an example according to the documentation:

const callback = () = > {
  console.log('text node data change')}const observer = new MutationObserver(callback)
let count = 1
const textNode = document.createTextNode(count)
observer.observe(textNode, {
  characterData: true
})

function func () {
  count++
  textNode.data = count
}
func() // text node data change
Copy the code

Code analysis:

  • First of all, we definecallbackThe callback function andMutationObserverObject, where the constructor passes arguments that are ourscallback.
  • It then creates a text node and passes in the initial text of the text node, followed by the callMutationObserverThe instanceobserveMethod, passing in the text node we created and aconfigObserve the configuration object, wherecharacterData:trueWe have to observetextNodeThe text of the node changes.configThere are other option properties that you can use in theMDNYou can view it in the document.
  • And then, let’s define onefuncFunction, the main thing that this function does is modifytextNodeThe text content in the text node, when the text content changes,callbackIs automatically called, so the outputtext node data change.

Now that we know how to use MutationObserver, let’s take a look at how the nextTick method uses MutationObserver:

import { isIE, isNative } from './env'

// omit the code
else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () = > {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
}
Copy the code

As you can see, nextTick determines that a non-INTERNET Explorer browser is used only when MutationObserver is available and is a native MutationObserver. For determining the non-ie situation, you can look at issue#6466 of vue.js to see why.

SetImmediate and setTimeout

SetTimeout is a very common timer method for most people, so we won’t cover it too much.

In the nextTick method implementation, it uses setImmediate, an API method that is only available in advanced Internet Explorer and low Edge browsers, as noted on Can I Use.

Then why is this method used? It is because of the issue we mentioned before: MutationObserver is not reliable in Internet Explorer, so in Internet Explorer you level down to using setImmediate, which we can think of as similar to setTimeout.

setImmediate(() = > {
  console.log('setImmediate')},0)
/ / is approximately equal to
setTimeout(() = > {
  console.log('setTimeout')},0)
Copy the code

NextTick implementation

After introducing the knowledge related to nextTick and asynchrony, let’s analyze the implementation of nextTick method. The first thing to say is: asynchronous degradation.

let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () = > {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}Copy the code

We introduced the Event Loop in the previous section. Due to the special execution mechanism of Macro Task and Micro Task, we first determine whether the current browser supports promises. If not, we then demoted to determine whether MutationObserver is supported. It continues to demote to determining whether or not setImmediate is supported, and finally to using setTimeout.

After introducing asynchronous degradation, let’s look at the nextTick implementation code:

const callbacks = []
let pending = false
export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    timerFunc()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

The real code for nextTick is not complicated. It collects incoming CB’s and then executes the timerFunc method when pending is false, where timeFunc is defined during asynchronous demotion. The nextTick method also makes a final judgment that if no CB is passed in and a Promise is supported, it will return a Promise, so we can use nextTick in two ways:

const callback = () = > {
  console.log('nextTick callback')}/ / way
this.$nextTick(callback)

2 / / way
this.$nextTick().then(() = > {
  callback()
})
Copy the code

Finally, we’ll look at an implementation of the flushCallbacks method that wasn’t mentioned before:

const callbacks = []
let pending = false
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
Copy the code

The flushCallbacks method returns the pending state to false and executes the methods in the callbacks array.

Note for change detection

Although the Object.defineProperty() method works well, there are exceptions where changes to these exceptions do not trigger setters. In this case, we classify objects and arrays.

object

Suppose we have the following example:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      }
    }
  },
  created () {
    // 1. Add attribute b, attribute B is not reactive, does not trigger setter for obj
    this.obj.b = 'b'
    // 2.delete delete existing property, cannot trigger setter for obj
    delete this.obj.a
  }
}
Copy the code

From the above examples, we can see:

  • When a new property is added to a responsive object, the new property is not reactive and cannot be triggered by any subsequent changes to the new propertysetter. To solve this problem,Vue.jsProvides a globalVue.set()Methods and Examplesvm.$set()Method, they’re all really the samesetMethod, which we will cover globally in relation to responsiveness in a later sectionAPIThe implementation of the.
  • This is not triggered when a reactive object deletes an existing propertysetter. To solve this problem,Vue.jsProvides a globalvue.delete()Methods and Examplesvm.$delete()Method, they’re all really the samedelMethod, which we will cover globally in relation to responsiveness in a later sectionAPIThe implementation of the.

An array of

Suppose we have the following example:

export default {
  data () {
    return {
      arr: [1.2.3]
    }
  },
  created () {
    // 1. Cannot capture array changes through index changes.
    this.arr[0] = 11
    // 2. Cannot capture array changes by changing the array length.
    this.arr.length = 0}}Copy the code

From the above examples, we can see:

  • Modifying an array directly through an index does not capture changes to the array.
  • Changes to the array cannot be caught by changing the array length.

For the first case, we can use the aforementioned vue.set or vm.$set, and for the second, we can use the array splice() method.

In the latest version of Vue3.0, Proxy is used to replace Object.defineProperty() to achieve responsiveness. All the above problems can be solved after Proxy is used. However, Proxy belongs to ES6, so it has certain requirements for browser compatibility.

Change detection API implementation

In the previous section, we looked at some of the problems with change detection. In this section, we’ll look at how vue.js implements the API to solve these problems.

Vue. Set to achieve

Vue. Set and vm.$set refer to a set method defined in observer/index.js:

export function set (target: Array<any> | Object, key: any, val: any) :any {
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key intarget && ! (keyin Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  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
  }
  if(! ob) { target[key] = valreturn val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
Copy the code

Before analyzing the code, let’s review the use of vue. set or vm.$set:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      },
      arr: []
    }
  },
  created () {
    // Add a new attribute to the object
    this.$set(this.obj, 'b'.'b')
    console.log(this.obj.b) // b

    // Add a new element to the array
    this.$set(this.arr, 0.'AAA')
    console.log(this.arr[0]) // AAA

    // Modify array elements by index
    this.$set(this.arr, 0.'BBB')
    console.log(this.arr[0]) // BBB}}Copy the code

Code analysis:

  • setMethod first on the incomingtargetParameters are verified, whereisUndefDetermine whetherundefined.isPrimitiveDetermine whetherJavaScriptRaw value, an error message is displayed in the development environment if one of the conditions is met.
export default {
  created () {
    // Error message
    this.$set(undefined.'a'.'a')
    this.$set(1.'a'.'a')
    this.$set('1'.'a'.'a')
    this.$set(true.'a'.'a')}}Copy the code
  • IsArray () is used to check whether target is an Array. If it is a valid Array index, isValidArrayIndex is used to check whether target is a valid Array index. If it is a valid Array index, splice is used to set the value to the specified position in the Array. We also reset the length property of the array because the index we passed might be larger than the length of the existing array.

  • Then determine if it is an object and if the current key is already on that object. If it is, then we just need to copy it again.

  • Finally, add a property to the reactive object using the defineReactive method, which was introduced earlier and won’t be covered here. When defineReactive is executed, an update is sent immediately to inform the dependencies of reactive data to be updated immediately. The following two pieces of code are the core of the set method:

defineReactive(ob.value, key, val)
ob.dep.notify()
Copy the code

Vue. Delete

Delete and vm.$delete use the same delete method as defined in the observer/index.js file: Vue.

export function del (target: Array<any> | Object, key: any) {
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  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(! hasOwn(target, key)) {return
  }
  delete target[key]
  if(! ob) {return
  }
  ob.dep.notify()
}
Copy the code

Before analyzing the code, let’s review the following use of vue. delete or vm.$delete:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      },
      arr: [1.2.3]
    }
  },
  created () {
    // Delete object properties
    this.$delete(this.obj, 'a')
    console.log(this.obj.a) // undefined
    // Delete an array element
    this.$delete(this.arr, 1)
    console.log(this.arr)   / / [1, 3]}}Copy the code

Code analysis:

  • The object to be deleted is determined firsttargetCan’t forundefinedOr a raw value, if so, an error is displayed in the development environment.
export default {
  created () {
    // Error message
    this.$delete(undefined.'a')
    this.$delete(1.'a')
    this.$delete('1'.'a')
    this.$delete(true.'a')}}Copy the code
  • Then through theArray.isArray()Method to determinetargetWhether it is an array, and if so, pass againisValidArrayIndexCheck if it is a valid array index. If it is, variation is usedspliceMethod to remove the element at the specified location.
  • Then determine whether the current attribute to be deleted is intargetObject, if it’s not there, it just returns, doing nothing.
  • Finally, throughdeleteThe operator deletes an attribute on the object, and thenob.dep.notify()Notifies dependencies on responsive objects to be updated.

Vue. Observables

Vue.observable is a global method available in Vue2.6+ that makes an object responsive:

const obj = {
  a: 1.b: 2
}
const observeObj = Vue.observable(obj)
console.log(observeObj.a) / / triggers the getter

observeObj.b = 22 / / triggers the setter
Copy the code

This global method is defined in the initGlobalAPI, which we’ve already covered, not covered here:

export default function initGlobalAPI (Vue) {
  // ...
  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T= > {
    observe(obj)
    return obj
  }
  // ...
}
Copy the code

Observable implementation is very simple, just calling the observe method inside the method and returning the obj. The code implementation of Observe has been covered in the previous section, so there is no further explanation here:

export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  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 ) { ob =new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Vue2.0 source code analysis: responsive principle (on) Next: Vue2.0 source code analysis: componentization (on)