preface

The previous article, Vue Source Code Interpretation (5) — Global API, detailed Vue’s global API implementation principle, this article will detail the implementation principle of each instance method.

The target

Learn more about the implementation principles of the following example methods.

  • vm.$set

  • vm.$delete

  • vm.$watch

  • vm.$on

  • vm.$emit

  • vm.$off

  • vm.$once

  • vm._update

  • vm.$forceUpdate

  • vm.$destroy

  • vm.$nextTick

  • vm._render

The source code interpretation

The entrance

/src/core/instance/index.js

This file is the entry file for the Vue instance, including the definition of the Vue constructor and the initialization of each instance method.

// The constructor of Vue
function Vue (options) {
  // Call the vue.prototype. _init method, which is defined in initMixin
  this._init(options)
}

// Define the vue.prototype. _init method
initMixin(Vue)
/** * Definition:  * Vue.prototype.$data * Vue.prototype.$props * Vue.prototype.$set * Vue.prototype.$delete * Vue.prototype.$watch */
stateMixin(Vue)
$on * vue.prototype. $once * vue.prototype. $off * vue.prototype. $emit */
eventsMixin(Vue)
/**
 * 定义:
 *   Vue.prototype._update
 *   Vue.prototype.$forceUpdate
 *   Vue.prototype.$destroy
 */
lifecycleMixin(Vue)
/** * Run installRenderHelpers to install a runtime convenience program on the Vue.prototype object ** Definition: * vue.prototype. $nextTick * vue.prototype. _render */
renderMixin(Vue)

Copy the code

Vm. $data, vm. $props

src/core/instance/state.js

These are two instance properties, not instance methods, which are briefly described here, although the implementation itself is simple

// data
const dataDef = {}
dataDef.get = function () { return this._data }
// props
const propsDef = {}
propsDef.get = function () { return this._props }
// Mount the data and props properties to the vue.prototype object
This.$data and this.$props can be used to access data and props
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)

Copy the code

vm.$set

/src/core/instance/state.js

Vue.prototype.$set = set
Copy the code

set

/src/core/observer/index.js

$set val * If target is an object and the key does not already exist, then set a response for the new key and perform dependency notification */
export function set (target: Array<any> | Object, key: any, val: any) :any {
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}Vue. Set (array, idx, val)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  Vue.set(obj, key, val)
  if (key intarget && ! (keyin Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // Cannot add dynamic add response attribute to Vue instance or $data, one of the uses of vmCount,
  // this.$data ob.vmCount = 1, indicating the root component, other subcomponents vm
  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
  }
  // Target is not a reactive object. New properties are set, but not reactive
  if(! ob) { target[key] = valreturn val
  }
  // Define a new attribute for the object, set the response via defineReactive, and trigger the dependency update
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

Copy the code

vm.$delete

/src/core/instance/state.js

Vue.prototype.$delete = del
Copy the code

del

/src/core/observer/index.js

/** * deletes the target object's array of specified keys * via the splice method. The object deletes the specified key * via the delete operator and performs the dependency notification */
export function del (target: Array<any> | Object, key: any) {
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)}// If target is an array, the splice method deletes the element with the specified index
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__

  // Avoid deleting Vue instance attributes or $data
  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 the attribute does not exist, end directly
  if(! hasOwn(target, key)) {return
  }
  // Delete the properties of an object with the delete operator
  delete target[key]
  if(! ob) {return
  }
  // Perform dependency notifications
  ob.dep.notify()
}

Copy the code

vm.$watch

/src/core/instance/state.js

/** * create watcher, return unwatch, and do 5 things: 3. Create an instance of Watcher * 4. If immediate is set, run cb * 5@param {*} expOrFn key
 * @param {*} Cb callback function *@param {*} $watch. The user may pass a configuration item * when calling this.$watch@returns Return the unwatch function, which is used to cancel watch listening */
Vue.prototype.$watch = function (
  expOrFn: string | Function, cb: any, options? :Object
) :Function {
  const vm: Component = this
  // Compatibility handling, because the cb set when the user calls vm.$watch may be an object
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  // Options. user represents the user watcher and the render watcher, which is instantiated in the updateComponent method
  options = options || {}
  options.user = true
  / / create a watcher
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // If the user sets immediate to true, the callback function is executed immediately
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}// Return an unwatch function to unlisten
  return function unwatchFn() {
    watcher.teardown()
  }
}

Copy the code

vm.$on

/src/core/instance/events.js

const hookRE = /^hook:/
/** * Listen for custom events on the instance, vm._event = {eventName: [fn1,... . } *@param {*} Event A single event name or an array of multiple event names *@param {*} Fn Callback function to execute when event is fired *@returns * /
Vue.prototype.$on = function (event: string | Array<string>, fn: Function) :Component {
  const vm: Component = this
  if (Array.isArray(event)) {
    // If an event is an array of event names, then $on is called recursively through each of these events
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
    // Store registered events and callbacks as key-value pairs in the VM._event object VM._event = {eventName: [fn1,... }
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    // hookEvent, which provides the opportunity to externally inject the declared cycle method into the component instance
    // For example, inject extra logic to the component's Mounted method from outside the component
    // This capability is implemented in combination with the Callhook method
    if (hookRE.test(event)) {
      vm._hasHookEvent = true}}return vm
}

Copy the code

Hookevents will be covered in more detail in the next article.

vm.$emit

/src/core/instance/events.js

_event[event] => CBS => loop CBS => cb(args) *@param {*} Event Event name *@returns * /
Vue.prototype.$emit = function (event: string) :Component {
  const vm: Component = this
  if(process.env.NODE_ENV ! = ='production') {
    // Convert the event name to smaller
    const lowerCaseEvent = event.toLowerCase()
    / / mean, HTML attributes case-insensitive, so you can't use the v - on to monitor the event name in the form of small hump (eventName), and should use the event name in the form of a hyphen (event name)
    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}". `)}}// Get the array of callback functions for the current event from the vm._event object and call the array of callback functions once, passing the supplied arguments
  let cbs = vm._events[event]
  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
}

Copy the code

vm.$off

/src/core/instance/events.js

/** * Remove the custom event listener, that is, find the corresponding event from the VM._event object, remove all events or remove the callback function for the specified event *@param {*} event 
 * @param {*} fn 
 * @returns * /
Vue.prototype.$off = function (event? : string |Array<string>, fn? :Function) :Component {
  const vm: Component = this
  $off() remove all listeners on the instance => vm._events = {}
  if (!arguments.length) {
    vm._events = Object.create(null)
    return vm
  }
  Event = [event1,...] , iterates over the event array, and calls vm.$off recursively
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn)
    }
    return vm
  }
  $off() = $off() = $off()
  const cbs = vm._events[event]
  if(! cbs) {// Indicates that the event is not registered
    return vm
  }
  if(! fn) {// If the fn callback function is not provided, all callbacks for this event are removed, vm._event[event] = null
    vm._events[event] = null
    return vm
  }
  // Remove the specified callback function for the specified event by finding the callback function from the event callback array and deleting it
  let cb
  let i = cbs.length
  while (i--) {
    cb = cbs[i]
    if (cb === fn || cb.fn === fn) {
      cbs.splice(i, 1)
      break}}return vm
}

Copy the code

vm.$once

/src/core/instance/events.js

/** * listens for a custom event, but only fires it once. $on + vm.$off *@param {*} event 
 * @param {*} fn 
 * @returns * /
Vue.prototype.$once = function (event: string, fn: Function) :Component {
  const vm: Component = this

  $on = $on; $on = $on; $on = $on
  function on() {
    vm.$off(event, on)
    fn.apply(vm, arguments)
  }
  on.fn = fn
  vm.$on(event, on)
  return vm
}

Copy the code

vm._update

/src/core/instance/lifecycle.js

/** * is responsible for updating the page. The entry position of the first rendering of the page and subsequent updates is also the entry position of patch */
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if(! prevVnode) {// For the first render, that is, to initialize the page, go here
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
    // Go here for responsive data update
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

Copy the code

vm.$forceUpdate

/src/core/instance/lifecycle.js

/** * Call the watcher.update method directly to force the component to rerender. * It only affects the instance itself and the child components inserted into the slot contents, not all child components */
Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

Copy the code

vm.$destroy

/src/core/instance/lifecycle.js

/** * Completely destroy an instance. Clean up its connections to other instances and unbind all its instructions and event listeners. * /
Vue.prototype.$destroy = function () {
  const vm: Component = this
  if (vm._isBeingDestroyed) {
    // Indicates that the instance was destroyed
    return
  }
  // Call the beforeDestroy hook
  callHook(vm, 'beforeDestroy')
  // Indicates that the instance has been destroyed
  vm._isBeingDestroyed = true
  // Remove yourself from the belly of your $parent ($children)
  const parent = vm.$parent
  if(parent && ! parent._isBeingDestroyed && ! vm.$options.abstract) { remove(parent.$children, vm) }// Remove the dependency listener
  if (vm._watcher) {
    vm._watcher.teardown()
  }
  let i = vm._watchers.length
  while (i--) {
    vm._watchers[i].teardown()
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  vm._isDestroyed = true
  // Call __patch__ to destroy the node
  vm.__patch__(vm._vnode, null)
  // Call the Destroyed hook
  callHook(vm, 'destroyed')
  // Disable all event listening for the instance
  vm.$off()
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null}}Copy the code

vm.$nextTick

/src/core/instance/render.js

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

nextTick

/src/core/util/next-tick.js

const callbacks = []
/** * Do two things: * 1, use the try catch flushSchedulerQueue packaging function, and then put it in the callbacks array * 2, if the pending to false, * If pending is true, flushCallbacks are already in the browser's task queue. * If pending is true, flushCallbacks are already in the browser's task queue. Pending is set to false again, indicating that the next flushCallbacks can enter the browser's task queue@param {*} Cb Receives a callback function => flushSchedulerQueue *@param {*} CTX context *@returns * /
export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  // Use the callbacks array to store the wrapped CB function
  callbacks.push(() = > {
    if (cb) {
      // Wrap the callback function with a try catch to catch errors
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    // Execute timerFunc and put the flushCallbacks function in the browser's task queue (the preferred microtask queue)
    timerFunc()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}

Copy the code

vm._render

/src/core/instance/render.js

/** * generate VNode by executing the render function * but with a lot of exception handling code */
Vue.prototype._render = function () :VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(
      _parentVnode.data.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
  }

  // Set the parent vnode. This allows the rendering function to access the data at the placeholder node.
  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`)
    // There is an error executing the render function
    // The development environment renders the error message, and the production environment returns the previous VNode to prevent rendering errors resulting in blank components
    /* istanbul ignore else */
    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 vNode is an array and contains only one element, then the value is flat
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]}Render returns an empty vNode when the render function fails
  if(! (vnodeinstanceof 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
  vnode.parent = _parentVnode
  return vnode
}

Copy the code

installRenderHelpers

src/core/instance/render-helpers/index.js

This method installs a number of abbreviated rendering-related utility functions on the instance that are used in the compiler generated render functions, such as vm._l compiled by v-for, and the most familiar h function (vm._c), which is not declared here, but is declared in the initRender function.

The installRenderHelpers method is called in renderMixin.

/** * mount the abbreviated render tool function * on the instance@param {*} Target Vue instance */
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 }Copy the code

If you’re interested in a method, you can dig into it yourself.

conclusion

  • What does vm.$set(obj, key, val) do?

    A:

    Vm.$set is used to add a new property to the responsive object, ensure that the new property is also responsive, and trigger the view update. NewProperty = ‘val’, this.arr[3] = ‘val’. Hence the vm.$set, which is an alias for vue.set.

    • Add a new reactive data to the object: Call defineReactive method to add reactive data to the object, then perform dep.notify for dependency notification and update the view

    • Add a new reactive data to the array: through splice


  • Interviewer: What does vm.$delete(obj, key) do?

    A:

    Vm.$delete is used to delete attributes on an object. If the object is reactive and you can ensure that the view updates are triggered. This method is mainly used to avoid the case where Vue cannot detect that an attribute is deleted. It is an alias for vue.delete.

    • Delete the element of the specified index in the array. This is done internally by splice

    • To delete a specified property on an object, first delete the property using the delete operator, and then execute dep.notify to update the view


  • Interviewer: What did vm.$watch(expOrFn, callback, [options]) do?

    A:

    Vm.$watch watches for changes in the result of an expression or function on the Vue instance. When this changes, the callback function is executed and two arguments are passed to the callback, the first being the updated new value and the second the old value.

    One thing to note here is that if you’re looking at an object, for example: Array. When you add an element to the array using an array method such as push, the new value passed by the callback is the same as the old one because they refer to the same reference, so be careful when looking at an object and seeing whether the old and new values are equal in the callback.

    The first argument of vm.$watch only accepts the key path of simple responsive data. For more complex expressions, it is recommended to use a function as the first argument.

    The inner workings of VM.$watch are:

    • Set options.user = true, indicating a user watcher

    • Instantiate a Watcher instance. When an update is detected, the Watcher triggers the callback function to execute, passing the old and new values as arguments to the callback function

    • Returns an unwatch function to cancel the observation


  • Interviewer: What does vm.$on(event, callback) do?

    A:

    Listen for custom events on the current instance that can be fired by vm.$emit, and the callback will receive any additional arguments passed to the event firing function (vm.$emit).

    Vm $on principle is very simple, is dealing with transfer of the event and the callback two parameters, the registration of events and back DiaoHan tens of key-value pairs stored to the vm. The _event object, vm. _events = {eventName: [cb1, cb2, …] . }.


  • $emit(eventName, […args]) do what?

    A:

    Fires the specified event on the current instance, with any additional arguments passed to the event’s callback function.

    Internally, it executes all the callbacks in vm._events[eventName].

    Remark: On and on and on and emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit emit On, VM. on, Vm. on, vm.on, and vm.emit are used internally to handle custom events.


  • Interviewer: What does vm.$off([event, callback]) do?

    A:

    Removing a custom event listener removes data from the VM._events object.

    • If no argument is provided, all event listening for the instance is removed

    • If only the event argument is provided, all listeners for that event on the instance are removed

    • If both arguments are provided, the listener for the event is removed from the instance


  • Interviewer: What does vm.$once(event, callback) do?

    A:

    Listen for a custom event, but the event is only fired once. Once triggered, the listener is removed.

    Its internal implementation principle is:

    • $off(event, wrapper) removes the event in addition to the user callback

    • Use vm.$on(event, wrapper function) to register the event


  • Interviewer: What does v._update (vnode, hydrating) do?

    A:

    This is an instance method used inside the source code, which is responsible for updating the page. It is the entry point for rendering the page. Internally, it determines whether to render for the first time or to update the page based on the presence of a prevVnode, thus passing different parameters when calling __patch__. This approach is not used in business development.


  • Interviewer asks: What does vm.$forceUpdate() do?

    A:

    Forcing a Vue instance to re-render, it only affects the component instance itself and the child components inserted into the slot contents, not all child components. The internal principle is as simple as calling vm._watcher.update() directly, which is the watcher.update() method, which triggers component updates.


  • Interviewer asks: What does vm.$destroy() do?

    A:

    Responsible for completely destroying an instance. Clean up its connections to other instances and unbind all its directives and event listeners. The beforeDestroy and Destroy hook functions are called during execution. This method is not used in most business development scenarios, and is generally operated by the V-if directive. Its internal principle is:

    • Call the beforeDestroy hook function

    • Destroy your relationship with your dad by removing yourself from his belly ($parent)

    • Remove the dependent listener with watcher.tearDown ()

    • Use the vm.__patch__(vnode, null) method to destroy a node

    • Call the destroyed hook function

    • Remove all event listeners using the vm.$off method


  • Interviewer: What did VM.$nextTick(CB) do?

    A:

    NextTick is an alias for Vue. NextTick, which is used to delay the execution of the cb callback function. This.

    this.key = 'new val'
    
    Vue.nextTick(function() {
      // The DOM is updated
    })
    Copy the code

    Its internal implementation process is:

    • This.key = ‘new val’, which triggers the dependency notification update, placing the watcher responsible for the update in the watcher queue

    • Put the function that refreshes the Watcher queue into the Callbacks array

    • Put a function to refresh the array of Callbacks in the browser’s asynchronous task queue

    • Vm.$nextTick(cb) to jump the queue and put the CB function directly into the callbacks array

    • A function to refresh the array of Callbacks is executed at some point in the future

    • It then executes a number of functions in the Callbacks array, triggering the execution of watcher.run and updating the DOM

    • Since the CB function is placed later in the Callbacks array, this ensures that the DOM update is completed before the CB function is executed


  • Interviewer: What did vm._render do?

    A:

    This method, which is not provided in the official documentation, is an instance method used within the source code and is responsible for generating the VNode. Its key code is a single line, executing the render function to generate vNode. But it adds a lot of exception handling code.

Form a complete set of video

Vue source code interpretation (6) — instance method

Please focus on

Welcome everyone to follow my gold mining account and B station, if the content has to help you, welcome everyone to like, collect + attention

link

  • Vue source code interpretation (1) – preface

  • Vue source code interpretation (2) — Vue initialization process

  • Vue source code interpretation (3) – response principle

  • Vue source code interpretation (4) — asynchronous update

  • Vue source code Interpretation (5) — Global API

  • Vue source code interpretation (6) — instance method

  • (7) — Hook Event

  • Vue source code interpretation (8) — compiler parsing

  • Vue source code interpretation (9) — compiler optimization

  • Vue source code interpretation (10) — compiler generation rendering function

  • (11) — Render helper

  • Vue source code interpretation (12) — patch

Learning exchange group

link