Vue2.x source code - initialization: stateMixin(Vue), eventsMixin(Vue), lifecycleMixin(Vue), renderMixin(Vue)

Keywords: Vue

Previous: Vue2.x source code - initialization: initMixin(Vue)

This article mainly looks at the four blends of stateMixin, eventsMixin, lifecycle mixin and renderMixin.


These methods are mainly used to mount some methods on the prototype of Vue instances:

1. stateMixin: data related $data, $props, $set, $del, $watch
2. eventsMixin: events $on, $off, $once, $emit
3. Lifecycle mixin: component and instance update_ Update, $forceUpdate, lifecycle $destroy
4. renderMixin: shorthand rendering tool function installRenderHelpers, component rendering $nextTick_ render

1, stateMixin(Vue)

In the src/core/instance/state.js file

export function stateMixin (Vue: Class<Component>) {
  //Use the get methods of dataDef and propsDef to obtain the data and props on the instance
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  //Setting the set method in the development environment throws a warning message
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  //Responsively bind data and props to $data and $props,
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  //Set $set $delete
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  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()
    }
  }
}

First, define dataDef and propsDef to set the get and set methods of response $data and $props; Then add $set, $delete, $watch methods to the instance;

Note: the instance root $data cannot be replaced; props is read-only and cannot be modified.

let obj = {
	a:1
}
this.$data = obj ///This direct assignment to $data will cause a warning (the default copy of the object is a deep copy, that is, a reference)

watch
The watch method is mainly to create a watcher object, which involves the principle of response; For convenience, I'll take out the $watch code separately

//Set $watch
 Vue.prototype.$watch = function (
   expOrFn: string | Function, //User manual monitoring
   cb: any, //Callback function after listening for changes
   options?: Object //parameter
 ): Function {
   const vm: Component = this
   //If cb is an object, execute createwatch, obtain the handler attribute in the cb object, and continue to recursively execute $watch until cb is not an object
   if (isPlainObject(cb)) {
     return createWatcher(vm, expOrFn, cb, options)
   }
   options = options || {}
   //It is marked as the user watcher, and is used to listen to the watch function when assigning a new value to data
   options.user = true
   //Add a watcher for expOrFn
   const watcher = new Watcher(vm, expOrFn, cb, options)
   //immediate==true execute cb callback immediately
   if (options.immediate) {
     try {
       cb.call(vm, watcher.value)
     } catch (error) {
       handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
     }
   }
   //Cancel observation function
   return function unwatchFn () {
     //Removes the current watcher from the subscriber list of all dependencies
     watcher.teardown()
   }
 }
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)
}

createWatcher determines whether the handler is an object. If yes, mount the handler to options. If not, take out the handler attribute of the object; If the handler is a string, find the handler from the vm to assign a value; Finally, execute the $watch method on the instance;

The Watcher involved in watch will be described in detail later. I won't elaborate here.

set,del
The set and del methods are in the src/core/observer/index file

export function set (target: Array<any> | Object, key: any, val: any): any {
  //Development environment, undefined, original object
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  //Is target an array and key a valid array index
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    //Compare key and target.length to reassign the array length
    target.length = Math.max(target.length, key)
    //Replace the value of the key position
    target.splice(key, 1, val)
    return val
  }
  //If the key already exists in the target or the key belongs to the original attribute of the Object, it is assigned directly
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  //Avoid adding a responsiveness attribute to the Vue instance or its root $data at run time -- declare it in advance in the data option
  //Get the observe instance. You can refer to the observe instance object
  const ob = (target: any).__ob__
  //isVue is true, indicating that target is a Vue instance
  //The vmCount attribute exists, indicating that it is root $data
  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] = val
    return val
  }
  //Add as a responsive property and notify data updates
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

set is used to add the val attribute value of the key attribute name to the target in response;

export function del (target: Array<any> | Object, key: any) {
  //Development environment, undefined, original object
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  //Is target an array and key a valid array index
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    //Directly delete the value of the key position
    target.splice(key, 1)
    return
  }
  //Avoid deleting the attribute on the Vue instance, or set its root $data to null
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  //If there is no key in the target, this attribute is returned directly
  if (!hasOwn(target, key)) {
    return
  }
  //delete attribute and notify data update
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}

The del method deletes the attribute of the key attribute name on the target object.

set and del methods cannot operate on properties on Vue instance objects and properties of root data objects ($data).

2, eventsMixin(Vue)

In the src/core/instance/events.js file

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  //In a publish subscribe relationship, one plays the role of subscription
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    //The event is an array, and the $on method is called repeatedly until it is found that the event is not an array
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      //Check under the constructor of vue_ Does the events object have a current event
      //If it does not exist, create an array. If it does exist, add the callback fn of the subscription to the array_ The current event property of events
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // Check whether the current event starts with hook:, if so, set the current vue_ The status of hasHookEvent is true
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }
  //It is used to listen to a custom event, but it is triggered only once. Remove the listener after the first trigger
 Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    //An on method is provided internally,
    function on () {
      //Calling $off removes the event listener
      vm.$off(event, on)
      //this of fn points to vm
      fn.apply(vm, arguments)
    }
    //The passed in parameter FN is replaced by on in $on. When the user manually unloads, vm.$off('xxx',fn) cannot find the FN event. Here, FN is passed into on.fn;
    //$off will judge internally that if the fn attribute of an item in the callback function list is the same as fn, the event can be successfully removed
    on.fn = fn
    //Execute on method
    vm.$on(event, on)
    return vm
  }
//Remove custom event listener
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // When no parameters are received, it means that all event listeners in the vue constructor should be unloaded
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // If the event is an array, $off is called in a loop
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // If the current event does not exist, it will be returned directly
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }
    //If there is no callback function, remove the current event directly
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // If there is a callback, check the current subscription array, delete the current callback function, and exit
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }
  //In a publish subscribe relationship, one acts as a publisher
  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      //Check whether there is a problem with the naming of the event (v-on cannot be used to listen to hump events, and whether it already exists on the instance)
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event]
    //If there is a callback for the current event to be published, publish the event to the subscribed event in turn
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}
//src/core/util/error.js
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    //The event callback function is bound to the vm
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // Avoid triggering capture multiple times during nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

On events subscribe to events, emit triggers events, and off deletes events. Once is essentially an on event, but once can only be triggered once.

3, Lifecycle mixin (Vue)

In the src/core/instance/lifecycle.js file

export function lifecycleMixin (Vue: Class<Component>) {
  //_ The update method is used to update the component content and is also the entry of the patch
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    //$el and $el of elements before cache update_ vnode
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    //The current instance is set as active
    const restoreActiveInstance = setActiveInstance(vm)
    //Pass in the latest vnode
    vm._vnode = vnode
    // prevVnode does not exist, indicating that it is the first rendering
    // __ patch__ It is mainly to convert vnode into dom and render it in the view
    if (!prevVnode) {
      // First rendering
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // Render again (update)
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    //Call the method in return to return the original active instance
    restoreActiveInstance()
    // Previous el additions_ vue_ Property and set to null
    if (prevEl) {
      prevEl.__vue__ = null
    }
    //__ vue__  Point to the Vue instance received when updating
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // If the parent $parent of the current instance is a high-level component, its $el is also updated
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }
  // $forceUpdate method, calling the instance's_ watcher, force the update once and trigger the updated life cycle
  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }
  //Completely destroy an instance. Clean up its connection with other instances and unbind all its instructions and event listeners
  //Trigger beforeDestroy and destroyed lifecycles
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    //If already started, interrupt to avoid repeated destruction
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // Removes the current object from the parent node
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // Unloads the watcher object on the current instance
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // Removing a reference from a frozen object may not have an observer
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    vm._isDestroyed = true
    // Invokes the destroy hook on the currently rendered tree
    vm.__patch__(vm._vnode, null)
    callHook(vm, 'destroyed')
    // Close all instance listeners
    vm.$off()
    // Related properties are not set to null
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

_ update mainly uses__ patch__ Method passes in different parameters to realize rendering and updating; The setActiveInstance method is used here, which is mainly used to cache the last instance object and return the current instance object__ patch__ Return to the previous instance object after completion, which is generally used for direct nesting of components.

4, renderMixin(Vue)

In the src/core/instance/render.js file

export function renderMixin (Vue: Class<Component>) {
  // Mount the shorthand rendering tool functions on the instance. These are runtime code
  installRenderHelpers(Vue.prototype)
  //Delay the callback until after the next DOM update cycle
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  //Execute the render function to generate vnode and exception handling
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    //Create the vnode of the subcomponent and execute normalizeScopedSlots
    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }
    // Set parent vnode
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      currentRenderingInstance = vm
       // Execute the render function to generate vnode
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // Error in render function
      // The development environment renders an error message, and the production environment returns the previous vnode to prevent rendering errors from leading to blank components
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // If the returned array contains only one node, convert it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // If the rendering function fails, an empty vnode is returned
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // Set parent node
    vnode.parent = _parentVnode
    return vnode
  }
}

Execute the render function to generate vnodes. If the execution fails, an incorrect rendering result or previous vnodes will be returned to prevent rendering errors from causing blank components; If the render function does not generate vnode, an empty vnode is returned;

This is mainly responsible for the production of vnode, and then some exception handling methods.

The createElement method will be described in detail later, but there is no more description here.

installRenderHelpers

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

Mount the short function name of common rendering tool functions on the Vue.prototype prototype.

nextTick
In the src/core/util/next-tick.js file

const callbacks = []
let pending = false
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // callback is an array defined above that stores the following methods
  callbacks.push(() => {
    // If the callback function exists, it is bound to cb.call(ctx) on the vue instance_ resolve(ctx)
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  //padding status bit
  if (!pending) {
    pending = true
    timerFunc()
  }
  // If the callback function does not exist and the current environment supports Promise, a Promise object is returned and assigned to_ resolve
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
let timerFunc
//Does the current environment support promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // Set an empty counter to force the refresh of the micro task queue
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available
  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 (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
// Empty the padding flag, cache the data in the copies, empty the callbacks array, and traverse and execute each method
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

The callback function is cached in the callbacks array, then the timerFunc method is invoked. TimerFunc will invoke the flushCallbacks method according to different environments through different means to execute every cache in callbacks.

The core idea of nextTick is to process tasks through asynchronous methods; vue will give priority to use promise.then, MutationObserver and setImmediate according to the current environment. If they are not supported, set timeout will be used to delay the function until the DOM is updated. (the reason is that the macro task consumes more than the micro task, so the micro task is used first, and the macro task with the largest consumption is used last)

Posted by venom999 on Tue, 02 Nov 2021 21:49:50 -0700