Writing in the front

The article for reading notes to clone down Vue source code and debugging take ~~

Vue’s response:

new Vue({
  el: '#app'.data: {
    msg: 'Hello'
  },
  methods: {
    changeMsg() {
      this.msg = 'World'; }}});Copy the code

Responsive object

initState

The initState method is called in the initializer _init method to initialize the props, data, and other properties into a responsive object of the Vue. The initState call follows the merge configuration. It internally integrates initialization for props, methods, data, computed, and Watch:

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

initProps

It does two things: it passes through the props and calls defineReactive to make its properties reactive. The attributes in the props can be accessed through the defined VM. _props. This. Props is the same as this.

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') {
      // Convert key to lowercase
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        // A warning is reported if attributes are reserved
        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

initData

It starts by calling the data function to get the data object, and then it does two things: it traverses the key and calls the proxy to vm.data; Observe subscribe to the whole data change:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    // Data returns an object when data is called as function
    // return data.call(vm, vm)
    ? 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)) {
        // The data key is defined in methods
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      // props and define the data key valueprocess.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)) {// Unreserved attribute name, proxy to vm.data
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)}Copy the code

The previous two inits turned both props and data into reactive objects, so let’s take a look at some of the functions they touch.

Proxy

In the previous example, this. MSG = ‘XXX’ is used to change the data in the data, and this. MSG is actually delegated to this._data. MSG, which is what the proxy function does. It proxies the access path for instance attributes using Object.defineProperty:

const sharedPropertyDefinition = {
  enumerable: true./ / can be enumerated
  configurable: true./ / can traverse
  get: noop,
  set: noop
}

// proxy(vm, `_props`, key)
// proxy(vm, `_data`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    // proxy access to this.key to this._xxx.key
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    / / set in the same way
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

observe

It is used to listen for changes in data, add an Observer to non-VNode object type data, return an Observer if it has already been added, otherwise instantiate an Observer if certain conditions are met:

// observe(vm._data = {}, true /* asRootData */)
// observe(data, true /* asRootData */)
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

Observer

It is a class that, when instantiated, defines some properties for the responsive service, instantiates the Dep class, and calls the def method to define an __ob__ property for value(where value is data), as you can see in the print, Call observe for each child element if value is an array, and defineReactive for each attribute if it is a normal data object:

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
    // Define an unenumerable __ob__ attribute for value(data),
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // Value (data) is an array
      this.observeArray(value)
    } else {
      // Call defineReactive for each value(data) attribute to make it reactive
      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

The dep function adds an attribute to the Object using object.defineProperty:

/** * Define a property. */
export function def (obj: Object, key: string, val: any, enumerable? : boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable:!!!!! enumerable,writable: true.configurable: true})}Copy the code

defineReactive

It starts by instantiating the Dep, gets the instance object, gets the property descriptor obj(in this case data), and then recursively calls the observe method for each property in data (when the property is an object). This ensures that each property has an __ob__ property and becomes responsive. This allows us to access or modify a deeply nested property in OBj and also trigger getters and setters. Finally, we use Object.defineProperty to add getters and setters to obj’s key property values. All they do is rely on collecting and sending updates:

// defineReactive(obj /* data */, keys[i] /* data key */)
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  const dep = new Dep()

  // Get the attribute description object of obj
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // Objects that cannot be configured are returned directly
  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) {
    // In the Observer, data passes only two arguments
    // The value of each attribute in data
    val = obj[key]
  }
  
  // Add an __ob__ object recursively for each attribute in data
  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

As you can see, we initialize props and data by adding getters and setters to the data using Object.defineProperty to intercept reads and writes of the Object, and recursively giving __ob__ objects to track changes to the data.


Depend on the collection

After adding a getter and setter to a reactive object, the getter is read on the object. The getter goes through a wave of dependency collection and returns the value accessed:

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

The specific process

When mountComponent executes, a render Watcher is defined and the updateComponent function is passed in as the getter for the Watcher:

updateComponent = () = > {
    // _update() generates vNodes as actual DOM elements
    // _render() generates vnodes
    vm._update(vm._render(), hydrating /* false */)}new Watcher(vm, updateComponent, noop /* Empty function */, {
    before () {
        if(vm._isMounted && ! vm._isDestroyed) {// Components are mounted and not destroyed, execute beforeUpdate life cycle
            callHook(vm, 'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code

In the Watcher class, we define dependency arrays, assign getters to the incoming updateComponent, and finally execute the get method, which implements pushTarget and getter methods:

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)
    / /...
    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()
  }

  /** * Evaluate the getter, and re-collect dependencies. */
  get () {
    debugger
    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)
      }
      // This is executed after the getter is done
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
	/ /...
}

Copy the code

The pushTarget method stores the current render Watcher to dep. target and adds it to the targetStack:

Dep.target = null
const targetStack = []

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

Then Watch executes its getter, which is the updateComponent passed in from the outside, and executes _render() to render, which accesses properties in data. The getter for the reactive object is triggered to start relying on the collection process:

vnode = render.call(vm._renderProxy, vm.$createElement)
Copy the code

DefineReactive defines an instance object of the Dep class. The Dep instance is first collected using the depend method of the current Dep instance:

// This dep is the deP in __ob__ of data
const dep = new Dep()

if (Dep.target) {
    dep.depend()
    if (childOb) {
        // DEP is defined in an Observer
        childOb.dep.depend()
        if (Array.isArray(value)) {
            dependArray(value)
        }
    }
Copy the code

The Depend method actually executes the addDep method of the current render Watcher to collect dependencies:

depend () {
    if (Dep.target) {
        // Dep.target is the current render Watcherdep
        // This is the dep for each attribute __ob__ in data
        Dep.target.addDep(this)}}Copy the code

The addDep method collects the current DEP into a dependency dependent array using some judgments:

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

Finally, execute the addSub method of the current DEP to subscribe the current render Watcher to the subs array of the deP. This purpose is to prepare which subs can be notified when the data changes:

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

At this point, you have completed a dependency collection process. The finally block is then executed in the Watcher get method:

finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
        traverse(value)
    }
    // This is executed after the getter is done
    popTarget()
    this.cleanupDeps()
}
Copy the code

PopTarget returns the current render Watcher to its previous state once the dependencies of the current VM are collected, because the mountComponent method is recursively executed if there are nested components:

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

Temporarily to skipcleanupDepsThe summary of the

As you can see, during the dependency collection process, the current render Watcher collects the current DEP, and the current DEP collects the current render Watcher, thus creating a bridge between the two. In other words, Watcher as an observer will collect the current DEP as a subscriber of the current Watcher, and deP will subscribe to the current Watcher in its subs, so that when the data corresponding to the DEP changes, The deP’s notify subscribed Watcher is called and the Update method on the Watcher is executed to tell the Watcher that it is ready to start updating the data. This is a typical implementation of the observer pattern.


Distributed update

After completing the dependency collection of data, click the button to perform the assignment operation and enter the setter of the responsive object, which carries out the process of distributing updates. The collection of dependencies is to distribute updates to the related dependencies when modifying data:

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)// Data [val] was updated before notify
    dep.notify()
}
Copy the code

As you can see, setters get the old value that they currently want to operate on, compare the old value to the new value, and then change the current value to the new value. If the new value is an object, they call the Observe method to change it to a responsive object. Finally, they call the notify method of the current DEP to notify Watcher to start sending out updates. The notify method iterates over the Watcher subscribed to dep.subs before calling their update method:

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

In the current process, update executes the queueWatcher method to perform a queue operation on the Watcher under DEP.subs:

update () {
    /* istanbul ignore else */
    if (this.lazy) {
        this.dirty = true
    } else if (this.sync) {
        this.run()
    } else {
        queueWatcher(this)}}Copy the code

In the current example, there is only one render Watcher, so the queue. Push branch is used to push the current Watcher to the queue, and the nextTick method is called to execute the flushSchedulerQueue. This is also an optimization point for Vue when it does a dispatch update. Instead of triggering a watcher callback every time the data changes, it adds the watcher to a queue, executes flushSchedulerQueue after nextTick, and verifies this later when it manually adds a watch:

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

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // Ensure that the same Watcher is pushed only once
  if (has[id] == null) {
    // Store the current watcher.id
    has[id] = true 
    if(! flushing) {// Insert the current Watcher 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.
      // When the second Watcher comes in
      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) {// Make sure the following logic is executed only once
      waiting = true

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

When there are multiple assignment statements in changeMsg, they are merged. Because they belong to the same render Watcher. When the first assignment is executed, the logic for has[id] == null is entered. When the second assignment is made, the render Watcher already exists in HAS, so it is not entered. Because in the setter for these attributes, the new value has already been assigned, they just need to wait for nextTick(flushSchedulerQueue) to execute:

this.msg = 'World';
this.msg2 = 'World2';
Copy the code

FlushSchedulerQueue sorts the Watcher in a flushSchedulerQueue from smaller to larger based on the incremented ID. The parent Watcher is created before the child Watcher, and the manually written watch is created before the render Watcher. So to execute the Watcher created first, consider the concept of one-way data flow to understand this:

queue.sort((a, b) = > a.id - b.id)
Copy the code

The queue is then iterated by executing the watcher.before method, which is defined at the time of the rendering watcher execution, which internally calls the beforeUpdate lifecycle function, then nullifying has[id] and executing watcher.run() :

// queue.length is not cached because a new watcher might be inserted during watcher.run()
// Refer to 'boundary case' below
for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // mountComponent passes before
      // The before method performs beforeUpdate hook
      watcher.before()
    }
    id = watcher.id
    // Clear the stored 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

The watcher. Run method is the DOM re-rendering method, which internally calls the Watcher. Get method, and executes the Watcher. Go through the getter process to update the DOM, and finally get the old and new values to execute the CB callback, which corresponds to the user’s handwritten Watch. In rendering Watcher, the callback is a NOOP:

run () {
    if (this.active) {
      // The getter is called again
      // Set changes val to the new value, and here again calls the call that goes into updateComponent
      // _render() re-reads the changed val to render the page
      debugger
      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

Once DOM is updated, look at the flushSchedulerQueue termination logic:

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

ResetSchedulerState is called to reset some of the global variables above:

function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if(process.env.NODE_ENV ! = ='production') {
    circular = {}
  }
  waiting = flushing = false
}
Copy the code

Check that the vm instance of Watcher is activated and updated when two call hooks are called:

callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
Copy the code

Edge cases

If you assign the listening value in manually add Watch:

watch: {
    msg() {
        this.msg = Math.random(); }},Copy the code

So when it pushes into the queue in queueWatcher, it goes into the flushing = true branch, because when it does the watcher.cb callback, it goes into the setter of the response object, There is no resetSchedulerState process to reset variables after watcher.run. At this point, a new Watcher is inserted into the queue after the current Watcher, which can be regarded as a copy of the Watcher written manually by the user:

if(! flushing) {// Insert the current Watcher 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)
}
Copy the code

Since a new Watcher is added each time a run is performed, an endless loop of adding Watcher is created, which ends with a warning after the watch.run() method indicating that it is currently in an infinite update loop:

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

Pay attention to

When the subs[I].update() method is looped, the current Watcher of the loop is pushed to the queue, but when the nextTick function is encountered, the loop is returned, and the next Watcher is pushed, and nextTick is a key step in the dispatch of the update.

conclusion

1. Update the old and new data and call notify to start the queue operation;

Update to add watcher to queue;

3. After adding the queue, Watcher in the nextTick cycle queue executes watcher.run to update DOM;


nextTick

NextTick is a core implementation of a setter, which is executed after the DOM completes rendering. In Vue 2.5 and up, the nextTick function uses Promise to implement microtasks in the current mainstream Web environment. Its entire implementation is in a next-tick.js.

First, it defines some global variables that control the Promise queue:

// Callback function queue
const callbacks = []
// promise pending
let pending = false
// microtask entry function
let timerFunc
Copy the code

In case the current environment supports promises, use promise.resove () to get a microtask and assign timerFunc as an entry function:

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

In the microtask, the fulshCallbacks function is executed, which iterates through the callbacks queue:

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

Now that the microtask has been created, let’s look at how the nextTick function uses the microtask to perform it. It receives a CB callback, inserts the CB callback into the Callbacks callback queue as a callback, and then starts executing the timerFunc function. Finally, if no CB is passed in, a Promise is returned, which means nextTick supports the Promise call:

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) {
      This.nexttick ().then(() => {}) '
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    timerFunc()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

TimerFunc will use p.chen (flushCallbacks) to execute all incoming callbacks into a microtask. When the macro task is completed, timerFunc will press start to read the results in the microtask:

timerFunc = () = > {
    p.then(flushCallbacks)
    // ...
}
Copy the code

According to the concept of ASYNCHRONOUS JS, synchronous code, such as render, patch and other processes, will be executed first. After the completion of synchronous code execution, the result of asynchronous code execution will be read, that is, the callback passed in by the user will be called. NextTick (flushSchedulerQueue) returns to the dep.subs[I]. Update loop. The flushSchedulerQueue is in an asynchronous queue. The component completes the patch process, which is why nextTick gets the rendered DOM, and the flushSchedulerQueue executes sequentially in an asynchronous queue (because it is called by the flushCallbacks loop), ensuring that the parent component finishes updating before the child. Vue makes clever use of event loops in nextTick.

In addition, when initGlobalAPI and Vue are initialized, the nextTick and $nextTick functions are mounted to Vue, which actually call the nextTick here. It can be called with vue. nextTick, vm.$nextTick:

// initGlobalAPI
Vue.nextTick = nextTick

// renderMixin
Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)}Copy the code

Precautions for detecting changes

Properties that are not defined in data have no getters and setters, so accessing a value that does not exist in an object is not reactive. In general, you can use vue. set or vm.$set to make a property responsive. Sets can add responsive properties to objects and arrays:

var vm = new Vue({
  data: {a:1.someObj: {}}})// vm.b and vm. somobj. a are non-responsive
vm.b = 2
vm.someObj.a = 1

// make it responsive
Vue.set(vm.someObj, 'a'.1);
Copy the code

Set and $set are defined when initializing an instance, respectively:

// initGlobalAPI
Vue.set = set

// stateMixin
Vue.prototype.$set = set

// set
export function set (target: Array<any> | Object, key: any, val: any) :any {
  debugger
  // target cannot be undefined, null, or a common value
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}// Array case
  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)) {
    // Key is stored in target, and val is assigned to target directly
    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

Object properties

Getters and setters have been set in the set before initializing the reactive object, and target[value] is returned:

if (key intarget && ! (keyin Object.prototype)) {
    // Key is stored in target, and val is assigned to target directly
    target[key] = val
    return val
}
Copy the code

It then calls defineReactive to set the getter and setter for Val, going through the reactive object setup process. After that, ob.dep.notify is manually called to manually distribute updates. When the page does not use this new value, dep.subs does not subscribe to any Watcher and returns val; When the new value is used on the page, the update will be sent from notify, and the new value will be rendered to the page by executing the updateComponent method again using watcher.run:

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

Array element

In the case of arrays, calling the splice method directly makes val responsive:

if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
}
Copy the code

In fact, Vue rewrote splice here. In most modern browser environments, it goes to the protoAugment method, directing target’s __proto__ to arrayMethods:

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
    // Define an unenumerable __ob__ attribute for value(data),
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // Value (data) is an array
      this.observeArray(value)
    } else {
      // Call defineReactive for each value(data) attribute to make it reactive
      this.walk(value)
    }
  }

function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
Copy the code

ArrayMethods actually refers to array. prototype and then iterates over all of the Array’s native methods. This overwrite is actually a layer of interception on the execution of the native method:

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

As you can see, it performs the native array methods, converts the values that need to be inserted into the array to an array, calls the observeArray loop to call the Observe method to make it a responsive object, and finally calls dep.notify to notify Watcher of the update.

conclusion

Set is to add getters and setters to a property that does not exist in a reactive object/array by finally calling defineReactive as an object or array. Observe > Observer (Object, Array)> defineReactive.


Computed attribute computed

The compute property can be used for complex computations of data and caches the computed results:

const vm = new Vue({
  el: '#app'.data: {
    firstName: 'Foo'.lastName: 'Bar',},methods: 
    changeName() {
      this.firstName = 'Coven';
      // this.firstName = 'Foo';}},computed: {
    fullName() {
      return `The ${this.firstName} The ${this.lastName}`; }}});Copy the code

Error correction

After a painful debugging and reasoning process, I discovered an error in my previous definition of reactive objects. When setting getters and setters for attributes in data defineReactive, there is a const dep = new dep (). This dep is not the deP under the current target object __ob__ as I understood it at the time. It is the DEP held by each property under the current target object. Adding a DEP for each attribute makes each attribute subscribe to the current Watcher and notify Watcher when the current attribute is updated:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  // This deP is created for each attribute in data
  const dep = new Dep()
  
  / /...
  
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true./* Rely on collection */
    get: function reactiveGetter () {
      if (Dep.target) {
        // Subscribe to Watcher for the current property
        dep.depend()
        // ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // The current property has changed, notify Watcher to start updating
      dep.notify()
    }
  })
}
Copy the code

Initialize the

First look at the procedure for initializing initComputed. Firstly, the user defined computed object is traversed to check the validity of the definition. Then, a computedWatcher is defined for each computed attribute method and stored in vm._computedWatchers. Then, whether the computed attribute name already exists under the VM is determined. Otherwise perform defineComputed for each calculated property method:

function initComputed (vm: Component, computed: Object) {
  // debugger
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    // Computed supports object writing with set and GET
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if(process.env.NODE_ENV ! = ='production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}". `,
        vm
      )
    }

    if(! isSSR) {// create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if(! (keyin vm)) {
      defineComputed(vm, key, userDef)
    } else if(process.env.NODE_ENV ! = ='production') {
      // The key is defined in the current VM instance, data/props
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
Copy the code

DefineComputed core is add getter and setter for calculating attribute function, is the most commonly used in the development of compute the attributes in complex computation, so it focus on the getter, it is createComputedGetter:

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  constshouldCache = ! isServerRendering()// true
  if (typeof userDef === 'function') {
    // Add getter and setter to the current userDes function
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else{ sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if(process.env.NODE_ENV ! = ='production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`.this)}}Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

CreateComputedGetter returns the actual getter, and its core is to extract a previously cached computed Watcher based on the computed property key, and then execute the Getter of the Watcher, the computed property method:

function createComputedGetter (key) {
  return function computedGetter () {
    debugger
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
Copy the code

access

As you know, attributes defined in the template are accessed during _render(), and the getters for calculated properties are triggered when the calculated properties are accessed. When calculating attribute initialization, you mentioned that a computed watcher is defined for calculating attributes:

watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    // const computedWatcherOptions = { lazy: true }
    computedWatcherOptions
)
Copy the code

Here we extract some differences between computed Watcher definition and rendered Watcher. The key for computed Watcher is this.lazy and this.dirty:

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    // ...
    // options
    if (options) {
      // ...
      this.lazy = !! options.lazythis.sync = !! options.sync }else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.dirty = this.lazy // for lazy watchers
    // ...
    The lazy representation is a calculated property, and unlike the render Watcher, it is not evaluated on new Watcher()
    this.value = this.lazy
      ? undefined
      : this.get()
  }
Copy the code

As you can see at the end, instead of performing the get evaluation immediately, the evaluation is performed when the property getter is evaluated. Return to calculate properties getter, dirty at this time is true, will perform watcher. The evaluate methods, the evaluate is performed watcher. Get:

if (watcher.dirty) {
    watcher.evaluate()
}
Copy the code

Before get is executed, dep. target at this point is rendering Watcher. The evaluate method performs Watcher’s get, which is the evaluate attribute function, and ultimately returns the value of the evaluate attribute, in this case return ${this.firstname} ${this.lastname}. When you start performing get, now that emp. target is currently computed watcher, this. FirstName and this.lastName are accessed and their getters are triggered when you perform a computed attribute function, Then the DEP objects to which they belong do a dependency collection on the current computed Watcher and subscribe to dep.subs:

get () {
    // debugger
    // Computed Watcher takes over the current process
    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)
      }
      // This is executed after the getter is done
      // Computed watcher accesses the this. XXX property, so the getter for this. XXX is triggered
      // Final value gets the return value of userDef in computed
      // Take the current computed Watcher out of the stack, and return dep. target to the previous watcher
      // targetStack is lifO
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
Copy the code

At this point, you have the results of the calculated properties, which will be displayed on the page when the rendering process is complete. Next, let’s look at how it evaluates by changing the value of the computed attribute dependency:

changeName() {
    this.firstName = 'Coven';
},
Copy the code

It fires the setter for firstName, performs dep.notify, subscribes to render Watcher and computed Watcher in dep.subs, and loops them through update. Rendering Watcher follows normal queueWatcher flow, and computed Watcher only sets dirty to true:

update () {
    /* istanbul ignore else */
    if (this.lazy) {
        // If computed Watcher is used, do not start the update distribution process
        this.dirty = true
    } else if (this.sync) {
        this.run()
    } else {
        queueWatcher(this)}}Copy the code

When dep.notify is complete, the update is executed, and _render() is executed, and the getter for the calculated property is executed:

function createComputedGetter (key) {
  return function computedGetter () {
    debugger
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
Copy the code

After the update process, where dirty is true, the process of calculating property access begins. In the setter for firstName, val = newVal has been performed to update its value, so the getter for this computed watcher gets the latest firstName value and renders it to the page.

Combined with the above process debugging to the end, I found that recalculation occurs only when the value of the dependency in the calculated property (subscribed to computed watcher in the DEP) changes and the _render() process accesses the calculated property again.


Listening property Watch

In a responsive system, users can manually define a watch to monitor changes in data, which is a user Watcher. Let’s look at its implementation principle:

watch: {
    msg() {
        // this.msg = Math.random();
        console.log('msg changed.'); }},Copy the code

Initialize the

In initState initialization, if the user defines watch, initWatch is executed to initialize the user Watcher. It iterates through the Watch property, gets each function/object in the watch, and calls the createWatcher method on the handler in the function or object:

function initWatch (vm: Component, watch: Object) {
  debugger
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
Copy the code

CreateWatcher calls the $watch method defined in initializing stateMixin. In fact, the user defined watch in the option also calls the $watch API:

function createWatcher (
  vm: Component,
  expOrFn: string | Function, handler: any, options? :Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
Copy the code

$watch instantiates a Watcher. Note that options.user = true indicates that this is a user-defined Watcher, which is subscribed to before rendering Watcher:

Vue.prototype.$watch = function (
    expOrFn: string | Function, cb: any, options? :Object
  ) :Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}return function unwatchFn () {
      watcher.teardown()
    }
  }
Copy the code

In this case, the expOrFn is the name of the watch function imported by the user, cb is the watch function itself, user is true, note that the expOrFn is a string, ParsePath (expOrFn) is called to expOrFn (expOrFn), which is converted to a function that assigns values to the getter, and finally the get method is called to evaluate:

get () {
    // debugger
    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)
        }
        // This is executed after the getter is done
        // Computed watcher accesses the this. XXX property, so the getter for this. XXX is triggered
        // Final value gets the return value of userDef in computed
        // Take the current computed Watcher out of the stack, and return dep. target to the previous watcher
        // targetStack is lifO
        popTarget()
        this.cleanupDeps()
    }
    return value
}
Copy the code

This.getter = parsePath(expOrFn) is the most important step in the instantiation process. ParsePath verifies that the function name is a valid one. Split into an array of strings, because you can define watch’s function name using someobj. foo to listen for a property in the object, and finally it returns a function that is Watcher’s getter.

Segments [segments[I]] = obj (segments[I]]) {MSG (n, o) {}; ‘someobj.msg ‘(n, o) {} This is a very clever way:

const bailRE = new RegExp(` [^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string) :any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('. ')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code

Going back to the get function, when the getter is executed it actually accesses the data in the data, fires the getter for the data, and then the deP object of the data subscribes to the current user Watcher and is notified to distribute updates when they are updated.

Perform watch

When you look at the dispatch update, you know that the data update triggers the setter for the data to dispatch the update process. When watch listens to an attribute, the dep.subs of this attribute will subscribe to the user watcher when initializing watch, so when the user watch executes run, It goes to the logic this.user = true and executes cb, the user-defined watch, returning the old and new values. This was sketched out when we looked at the distribution update:

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

deep watch

When defining a watch, you can set a deep: true property to listen for all property changes in an object or array:

watch: {
    someObj: {
        deep: true.handler(){}}}Copy the code

In the finally block at the end of user Watcher’s initial call to the getter, this piece of logic is executed, which is the implementation of deep: true:

if (this.deep) {
    traverse(value)
}
Copy the code

Traverse actually calls _traverse, which checks if the current user watch is not listening on an object or array, or returns if it is not. It iterates through all the properties in value and recursively calls itself, which actually fires the getter for the property, subscribing to the current user Watcher for each property’s DEP.subs, so that the watch callback can be triggered when the properties inside value change. There is also a minor optimization in this function implementation. The traversal records child responsive objects with their DEP IDS to seenObjects to avoid repeated access later.

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if((! isA && ! isObject(val)) ||Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
Copy the code

Summary computed and Watch

Computed and Watch are actually instance objects of the Watcher class. So far, I have been exposed to three different uses of Watcher: rendered Watcher, computed Watcher, and User Watcher. In both cases, the _render() method triggers the getter dependency collection of the data, and the assignment triggers the setter of the data to issue updates. In terms of application scenarios, computed attributes are suitable for template rendering, where a value is computed depending on other responsive objects or even computed attributes. The listening attribute is suitable for observing changes in a value to complete a complex piece of business logic.