— Take your time. Everything will be faster

preface

Following the previous article “VUE 2.0 source code Reading Plan (1) – Tool functions”, we will explore the principle of responsiveness, which is the core of VUE. This article takes a modular reading approach link, be sure to read against the source code. For source code, focus on understanding ideas, do not have to die line by line code, to learn to learn. Thinking is the promotion of the realm, with the height of thinking, not afraid of not doing. This article is really not good to write, must be combined with their own code more thinking, welcome advice 😇😇😇.

primers

Vue2.0 source code uses static type checking for flow, which does not use typescript, so there is a comment at the beginning of the line. The purpose is to enable static type checking. Flow is written roughly the same as typescript. So don’t be too confused to read the source code.

var vm = new Vue({
  data: {
    a: 1}})Copy the code

This is how we initialize a Vue instance, and of course in a single-file component our data is defined as a function, because single-file components are components and components are reusable.

extension

Vue single-file components are parsed by vue-loader, and all vue-loader does is compile the template and style from the.vue file into the.js (compile into the render function) and mix it into the Object that you export in the.vue. The resulting JS simply exports an Object that conforms to the Component definition.

In Vue 2.0, all Vue components will eventually require the render method. Whether we develop components in a single.vue file, or write el or template attributes, we will eventually convert to render. This process is Vue’s template compilation process.

Template compilation will be covered in a later chapter, continuing with the text.

After initialization, vm.a is responsive. We’re going to ask what’s going on? So we need to see what the Vue constructor does. Let’s start by finding the Vue constructor link:

function Vue (options) {
  if(process.env.NODE_ENV ! = ='production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)
}
Copy the code

Options is the value we pass in for data, computed, methods… This. _init(options). This refers to the Vue instance, so we need to go to the Vue prototype to find the definition.

_init

_init is defined in init.js. Let’s look at it:

Vue.prototype._init = function (options? :Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

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

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if(process.env.NODE_ENV ! = ='production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
Copy the code

Vue initialization logic is written very clearly: merge configuration, initialize life cycle, initialize event center, initialize render, call beforeCreate hook function, initialize injection, initialize state, etc.

initState()

Because this article explores the reactive principle, we focus on the initState(VM) function as follows:

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

InitState initializes props, methods, data, computed, and watch.

initData()

We focus on initData:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if(! isPlainObject(data)) { data = {} process.env.NODE_ENV ! = ='production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if(process.env.NODE_ENV ! = ='production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if(props && hasOwn(props, key)) { process.env.NODE_ENV ! = ='production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if(! isReserved(key)) { proxy(vm,`_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)}Copy the code

InitData first evaluates the type of the data. If it is a function, it calls getData to get the return value. Then it calls isPlainObject to determine whether the data is a plain object. IsPlainObject inside the Object. The prototype. ToString. The judgment of the call (). The properties of data cannot be the same as those of methods and props, and then the vm._data can be used as a proxy on vm, which is why the properties are defined on data but can be obtained on this. Note that the proxy function is not a proxy in ES6. It is a custom function with a different beginning case and a similar name. The proxy function internally proxies through the object’s accessor property, and searches for itself.

ES5 object.defineProperty () applies only to objects and does not apply to arrays, so Object and Array change detection are different.

Object change detection

inobserverUnder the folder, there’s this6This is the core code of responsiveness.

Three key roles:

  • Observer: adds attributes to an objectgetterandsetterFor dependency collection and distribution of updates
  • Dep: Used to collect the dependencies of the current reactive object, one for each reactive object including its childrenDepInstance (insidesubsisWatcherInstance array), is passed when the data is changeddep.notify()Notice the variouswatcher.
  • Watcher: Observer object, instance divided into renderwatcher (render watcher), calculate the propertieswatcher (computed watcher), and the listenerwatcher(user watcher) three

observe()

In the index.js file, we find the observe function. Let’s look at this function:

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

The value passed in is checked first and returned if it is not an object or a VNode type. The __ob__ attribute is then used to determine whether the response has been added. If so, it is returned; otherwise, an Observer instance is instantiated and returned.

Question 1:valueWhy notVNodeType?

VNode is not allowed to be responsive. The createElement function that generates the VNode instance (the first parameter in the render function) determines that the second parameter data cannot be responsive. This is because data may be changed during VNode rendering. If it is a listening property, it will trigger monitoring and cause unexpected problems. Data is a property in the VNode class and is passed in as a second argument to new VNode(), so adding responsivity to vNodes is not allowed. Take a look at the implementation of createElement:

export function _createElement(
    context: Component,
    tag ? : string | Class < Component > | Function | Object,
    data ? : VNodeData,
    children ? : any,
    normalizationType ? : number
) :VNode | Array < VNode > {
if(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
    `Avoid using observed data object as vnode data:   The ${JSON.stringify(data)}n` +
    'Always create fresh vnode data objects in each render! ',
    context)
    
    return createEmptyVNode()
 }
...
}
Copy the code

Note:Object.isExtensible(value)That’s a useful judgment

Be sure to take advantage of this feature. I missed a useful example in the last article:

new Vue({
    data: {
        // Vue does not bind getters and setters for list objects
        list: Object.freeze([
            { value: 1 },
            { value: 2 }
        ])
    },
    mounted () {
        // The interface will not respond
        this.list[0].value = 100;

        // In either case, the interface responds
        this.list = [
            { value: 100 },
            { value: 200}];this.list = Object.freeze([
            { value: 100 },
            { value: 200}]); }})Copy the code

This is useful when you need to define a complex object without relying on its responsivity, such as when drawing Echarts. I have often seen people define such a complex option in data. Because VUE recursively binds getters and setters to all children of data, performance is bound to suffer when data is large.

Observer()

Continuing with the body, let’s move on to the Observer implementation:

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

Execute the constructor() function at new Observer(). Note that the constructor() function assigns a dep attribute to new dep () and then injects the __ob__ attribute to value as an instance of the current Observer to indicate that the response has been added. Then if determines that the value is an array, so in this section we’ll just look at the object, go to the else branch, and execute walk, which is pretty simple to iterate over all the enumerable properties of value and define ereActive (). The focus then falls on the defineReactive() function.

defineReactive()

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

DefineReactive does what it’s supposed to do: define a reactive object and dynamically add getters and setters to it. The new Dep() works without any additional information. If the new key is false, the new Dep() works without any additional information. If the new key is false, the accessor property cannot be defined again using the Object.defineProperty method. Get and set from accessor properties. If get does not exist, val is obj[key]. let childOb = ! Shallow && Observe (val) recurses the child property, making it also responsive, which is why we access or modify a deeply nested property in OBj that also fires getters and setters.

We define the getter, we look at the end, we get the value, we return it, we keep the default behavior of getting the data, we collect the dependency in the middle. A dependency is stored in dep.target, and the depend method is called to collect the dependency, which also contains the dependency collection of child attributes. Define setter, get value first, compare old value and new value, go ahead, skip custom setter, check if getter exists and setter doesn’t exist (that is, if you define accessor property you define get but not set), end, Otherwise, recursing down the child property of the new value will make it responsive as well, eventually triggering the dependency of the current property.

Note that anonymous functions are almost never written in the vue source code, as you can see from defining getters and setters, which is a good habit.

So far, we have a general idea of how vue works, but we only know that we collect dependencies through dep.depend() and trigger dependencies through dep.notify(), so the biggest question is:

  1. DepWhat do you do?
  2. What exactly is dependency?
  3. How exactly are dependencies collected or triggered?

Here is a breakdown.

Dep

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

We need to note that the target static property, which is specified as a Watcher type, is a globally unique Watcher, which is a very clever design because only one global Watcher can be evaluated at any one time, In addition, its own property subs is also Watcher’s array, subs is the place to store dependencies.

Question 2:WatcherIs it dependence?

Watcher is not a dependency, you can understand that Watcher is the proxy execution of dependencies, each dependency corresponds to a Watcher, collect the Watcher when the dependency is triggered, through the proxy execution of Watcher.

Question 3: What exactly is dependency?

It’s simple: anyone who uses data is a dependency, and we create an instance of Watcher for that person. The granularity of vue2.0 dependencies is medium component level, that is, a dependency bound to a state is a component. Therefore, it can be understood that dependency is the current component that uses a certain state. When the state changes, the component is notified by Watcher agent, and then the virtual DOM is used to patch the components inside the component, and then the re-rendering of the interface components is triggered.

Having said so much Watcher, what is it? Let’s take a look:

Watcher

class Watcher {...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.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()
  }

  /** * Evaluate the getter, and re-collect dependencies. */
  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

With some code omitted, let’s briefly examine what happens when Watcher is instantiated. _watcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher = renderWatcher RenderWatcher is instantiated in new Vue(). RenderWatcher is instantiated in new Vue(). RenderWatcher is instantiated in new Vue(). RenderWatcher is instantiated in new Vue().

new Watcher(vm, updateComponent, noop, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code

For those of you who have not seen the template compilation and patch source code, you can simply assume that updateComponent is a function that updates the component. Specifically, updateComponent is a function that generates the vNode of the component, performs patch and then renders. Of course, patch will not be done for the first rendering as there is no oldVNode to compare it to. Dep. Notify notifies it to execute.

ExpOrFn = exporu.fn, exporu.fn = updateComponent, exporU.fn = exporu.fn, exporu.fn = updateComponent, is a function type, which is assigned to this.getter, and finally called this.get().

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

PushTarget sets dep. target to an instance of the current Watcher, and then executes this.getter (updateComponent). During template compilation, the data property is accessed, triggering the getter to complete the dependency collection. At the end of this step, the dom structure of the current component has been updated, and the browser will re-render it based on the updated DOM. Finally, popTarget is used to set the dep. target to null.

Distributed update

Once the dependencies are collected, we’ll come back to distribute the updates. We update the state, trigger the setter, and execute dep.notify. The notify definition is as follows:

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

The main thing is to iterate over the subs (collected dependencies) and call the update method, which is defined as follows:

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

The lazy attribute corresponds to computed watcher, which is fetched from the cache; Sync corresponds to the user Watcher and does not put the updated watcher into the nextTick queue but performs the update immediately. Since we are analyzing render Watcher, the logic goes into else. I will write a separate article about the three types of Watcher later. Parsing queueWatcher:

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

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

This introduces the concept of a queue, which is one of the optimizations of Vue when distributing updates. Instead of triggering the watcher callback every time the data changes, the watcher is added to a queue first. FlushSchedulerQueue is then executed after nextTick.

First, the has object is used to ensure that the same Watcher is added only once. And then the flushing judgment; Finally, waiting is used to ensure that the call logic to nextTick(flushSchedulerQueue) is only once.

flushSchedulerQueue

Let’s look at the implementation of 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()
    
    ......
  }
  // 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)
}
Copy the code

The queue is sorted first, to ensure the following (translation of comments in the code block) :

  1. Component updates from parent to child; Because the parent component is created before the child, sowatcherThe order of execution should remain the order of execution.
  2. User customizationwatcherTake precedence over renderingwatcherImplementation; Because it’s user definedwatcherIs the renderingwatcherPreviously created (initComputed() > initWatch() > render watch).
  3. If a component is in the parent component’swatcherExecution period is destroyed, then it corresponds towatcherExecution can be skipped, so the parent component’swatcherIt should be executed first.

It then iterates through the queue, gets the corresponding watcher, checks watcher.before, which is where it executes the beforeUpdate hook function, and then executes watcher.run(). One thing to note here is that queue.length is evaluated each time it is traversed, because at watcher.run() it is very likely that the user will add a new watcher again, which will execute to queueWatcher again and walk up to if (! In the else branch of flushing:

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

I’m going to look backwards, find the first place where the ID of the watcher to be inserted is larger than the ID of the watcher in the current queue, insert the watcher into the queue by its ID, so the length of the queue changes.

Then let’s look at watcher.run() :

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

The run method is very simple, which is to execute this.get()(the updateComponent function we saved before) to update the patch trigger interface. The following logic determines whether the new value is equal to the old value, whether the new value is an object, whether deep is true, and then executes the corresponding callback as the first and second arguments. The catch.

Finally, the Watcher queue is cleared by executing resetSchedulerState() to restore some variables that control the process state to their initial values.

The realization of the nextTick

NextTick: flushSchedulerQueue nextTick: flushSchedulerQueue nextTick: flushSchedulerQueue

let isUsingMicroTask = false

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]()
  }
}

let timerFunc

if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  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)) {
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}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

It’s easy to understand if you know the Event Loop mechanism. In order to ensure browser and mobile compatibility, Vue had to do a demotion from MicroTask to MacroTask, using whichever was supported first, so timerFunc assignment is worth looking at the browser and the device. Flush the flushSchedulerQueue into the callbacks with an arrow function. The pending check ensures that timerFunc only executes the flushCallbacks once. TimerFunc will always execute the flushCallbacks on the next tick regardless of whether it uses macro or micro tasks. The logic of flushCallbacks is very simple: iterate through the callbacks and execute the corresponding callback function.

The reason for using callbacks instead of executing callbacks directly on nextTick is to ensure that executing nextTick multiple times does not start multiple asynchronous tasks, but instead pushes them into a single synchronous task, which is executed in sequence on the nextTick.

Here’s an example:

methods: {
    todo () {
        this.a = 1
        this.b = 2}}Copy the code

So let’s say I click on a button and trigger a todo function, update a and B, and when I assign to A, I fire a setter and call nextTick, and when I assign to B, I fire a setter and call nextTick again, assuming I have promise support, after I call nextTick for the first time, Setting pending to true causes the second call to nextTick to only push the callbacks and not generate new microtasks, so at the end of this round of macro tasks there should be two values in the callbacks but only one microtask. This makes sense to avoid starting multiple asynchronous tasks.

Because of the asynchronous queue mechanism, we can access the DOM directly at this time, which is not updated before. To ensure that we want to access the updated DOM, This.$nextTick() can be setTimeout or promise.resolve ().then(), but this.$nextTick() is platform-compatible. This.$nextTick() will be pushed again in the callbacks, and the timerFunc will be synchronized. The dom will be updated when the updateComponent function is triggered. Our callback in this.$nextTick() executes last, ensuring that we get the updated DOM.

The update is now distributed.

Array change detection

As mentioned above, the difference between array and Object change detection is caused by Object.defineProperty(). We usually define arrays as follows:

data(){
  return {
    arr: [1.2.3]}}Copy the code

We add a response to ARR as per the previous logic, but [1, 2, 3] as a child of ARR adds a response recursively, when we go to new Observer() again, as follows:

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

Entering the Array judgment, we only look at the branch of protoAugment(Value, arrayMethods) :

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

It’s very simple to change the orientation of the array prototype, so arrayMethods:

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

ArrayProto retains the prototype of the original Array, while arrayMethods is an object created based on the prototype of arrayProto. Next, 7 methods in Array prototype can change the contents of Array itself, respectively: Push, POP, Shift, unshift, splice, sort, reverse. We then iterate over them in order to add interceptors, overwriting arrays to add additional functionality without changing the original functionality. Because the other methods don’t actually operate on Array and they’re basically just going through the Array and looking it up and then generating a new Array and returning it, so there’s no point in intercepting it.

The first step of the interceptor is to implement the original array method, first of all to ensure that the original function, using the original example, where the “this” now refers to arR, arR is already responsive, first use it to obtain the Observer instance ob, [insert] Then, for the push, Unshift, and splice methods that can add elements, get the inserted element and call the OB. observeArray method, which looks like this:

observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
}
Copy the code

It’s very simple to iterate over and add a response to it, because the new element may be of object type. Ob.observearray () is followed by ob.dep.notify() to trigger dependency update.

At this point, you should understand my example:

export default {
    data() {
        return {
            arr: [1.2.3, { x: 1}}},methods: {
        todo() {
            /* Do not test together, as the following dependency updates will trigger a re-rendering of the entire component */
            this.arr[0] = 4 // Separate test: the interface does not change
            this.arr[3].x = 5 // Separate test: the interface changes in response
            this.arr = [1.2.3] // Separate test: the interface changes in response}}}Copy the code

Question4: Many people find it hard to understanddefineReactiveThere’s already one in thereDepExample, why inObserverI’m going to create one at the beginningDepThe instance?

Their priorities are different. As you can see from the $set and $delete methods, this deP triggers updates only when attributes are added or deleted, whereas the deP in defineReactive is one for each key of the object. Updates are triggered after the key is reassigned.

Added: Observer mode

I believe you in the interview, often asked about vUE’s responsive principle, in fact, patience reading above has been difficult to you. But until then, your answer might look something like this: Observer mode, so I’ll just add a quick sidebar. The Observer pattern is a design pattern.

Design patterns represent best practices and are generally adopted by experienced object-oriented software developers.

Usage scenario: The observer pattern is used when there is a one-to-many relationship between objects. Intent: Define a one-to-many dependency between objects so that when an object’s state changes, all dependent objects are notified and automatically updated.

Vue uses the observer mode, which is a good fit. The observer mode is divided into observed and observer. There is no doubt that the object defined in data is the observed, and the observer is the data that needs to respond in the template. The observer model is also known as publishing-subscriber model. Take a newspaper as an example. A newspaper needs to issue a new news magazine every day and deliver it to subscribers. So the newspaper society has a registration agency. You sign up as a user, and then we send you the newspaper. By vUE, you rely on collecting and distributing updates.

conclusion

The article has the wrong place welcome to correct, everybody diligently!!

Reference: Vue source code series -Vue Chinese community Vue. Js technology revealed