Interpretation of Vue source code -- global API

Posted by zeth369 on Fri, 25 Feb 2022 02:24:52 +0100

When learning becomes a habit, knowledge becomes common sense. Thank you for your likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

target

Deeply understand the implementation principle of the following global API s.

  • Vue.use
  • Vue.mixin
  • Vue.component
  • Vue.filter
  • Vue.directive
  • Vue.extend
  • Vue.set
  • Vue.delete
  • Vue.nextTick

Source code interpretation

From the first article in the series Interpretation of Vue source code (1) -- Preface It can be seen from the introduction to the source directory structure in that most of Vue's global APIs are implemented in the / SRC / core / global API directory. These global API source code reading entries are in / SRC / core / global API / index JS file.

entrance

/src/core/global-api/index.js

/**
 * Initialize Vue's many global API s, such as:
 *   Default configuration: Vue config
 *   Tool method: Vue util. xx
 *   Vue.set,Vue.delete,Vue.nextTick,Vue.observable
 *   Vue.options.components,Vue.options.directives,Vue.options.filters,Vue.options._base
 *   Vue.use,Vue.extend,Vue.mixin,Vue.component,Vue.directive,Vue.filter
 *   
 */
export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  // Many default configuration items for Vue
  configDef.get = () => config

  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }

  // Vue.config
  Object.defineProperty(Vue, 'config', configDef)

  /**
   * Expose some tools and methods. Don't use these tools and methods easily. You know these tools and methods well and know the risks of using them
   */
  Vue.util = {
    // Warning log
    warn,
    // Similar options merge
    extend,
    // Merge options
    mergeOptions,
    // Set responsive
    defineReactive
  }

  // Vue.set / delete / nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // Responsive method
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  // Vue.options.compoents/directives/filter
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // Mount the Vue constructor to Vue options._ Base
  Vue.options._base = Vue

  // In Vue options. Add built-in components to components, such as keep alive
  extend(Vue.options.components, builtInComponents)

  // Vue.use
  initUse(Vue)
  // Vue.mixin
  initMixin(Vue)
  // Vue.extend
  initExtend(Vue)
  // Vue.component/directive/filter
  initAssetRegisters(Vue)
}

Vue.use

/src/core/global-api/use.js

/**
 * Define Vue Use, responsible for installing plug-ins for Vue, did the following two things:
 *   1,Judge whether the plug-in has been installed. If it is installed, it will end directly
 *   2,Install the plug-in and execute the install method of the plug-in
 * @param {*} plugin install Method or an object containing the install method
 * @returns Vue example
 */
Vue.use = function (plugin: Function | Object) {
  // List of installed plug-ins
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  // Judge whether the plugin has been installed to ensure that it is not installed repeatedly
  if (installedPlugins.indexOf(plugin) > -1) {
    return this
  }

  // Put the Vue constructor in the first parameter position and pass these parameters to the install method
  const args = toArray(arguments, 1)
  args.unshift(this)

  if (typeof plugin.install === 'function') {
    // If the plugin is an object, execute its install method to install the plugin
    plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
    // Execute the direct plugin method to install the plug-in
    plugin.apply(null, args)
  }
  // Add a newly installed plug-in to the plug-in list
  installedPlugins.push(plugin)
  return this
}

Vue.mixin

/src/core/global-api/mixin.js

/**
 * Define Vue Mixin is responsible for global blending options, affecting all Vue instances created later. These instances will merge global blending options
 * @param {*} mixin Vue Configuration object
 * @returns Return Vue instance
 */
Vue.mixin = function (mixin: Object) {
  // Merge mixin objects on Vue's default configuration items
  this.options = mergeOptions(this.options, mixin)
  return this
}

mergeOptions

src/core/util/options.js

/**
 * Merge two options. When the same configuration item appears, the child option will overwrite the configuration of the parent option
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  // Standardize props, inject and directive options to facilitate the processing of subsequent programs
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Process the extensions and mixins on the original child object, execute mergeOptions respectively, and merge these inherited options into the parent
  // The objects processed by mergeOptions will contain_ base attribute
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  // Traverse parent options
  for (key in parent) {
    mergeField(key)
  }

  // Traverse the sub options. If the parent option does not have the configuration, merge it. Otherwise, skip it. Because the parent and child have the same attribute, it has been processed when processing the parent option above, and the value of the sub option is used
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }

  // For merge options, childVal takes precedence over parentVal
  function mergeField (key) {
    // strat is the merge policy function. If the key conflicts, childVal will overwrite parentVal
    const strat = strats[key] || defaultStrat
    // If childVal exists, childVal is preferred; otherwise, parentVal is used
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

Vue.component,Vue.filter,Vue.directive

/src/core/global-api/assets.js

The implementation of these three API s is quite special, but the principle is very similar, so they are implemented together.

const ASSET_TYPES = ['component', 'directive', 'filter']

/**
 * Define Vue component,Vue.filter,Vue. Direct these three methods
 * What these three methods do is similar, that is, in this options. Store the corresponding configuration on XX
 * Like Vue The result of component (componame, {XX}) is this options. components. Compname = component constructor
 * ASSET_TYPES = ['component', 'directive', 'filter']
 */
ASSET_TYPES.forEach(type => {
  /**
   * For example: Vue component(name, definition)
   * @param {*} id name
   * @param {*} definition Component constructor or configuration object 
   * @returns Return component constructor
   */
  Vue[type] = function (
    id: string,
    definition: Function | Object
  ): Function | Object | void {
    if (!definition) {
      return this.options[type + 's'][id]
    } else {
      if (type === 'component' && isPlainObject(definition)) {
        // If name exists in the component configuration, use it; otherwise, use id directly
        definition.name = definition.name || id
        // extend is Vue In this case, the constructor of definition becomes new ()
        definition = this.options._base.extend(definition)
      }
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition }
      }
      // this.options.components[id] = definition
      // When instantiating, merge the globally registered components into the components of the configuration object of each component through mergeOptions
      this.options[type + 's'][id] = definition
      return definition
    }
  }
})

Vue.extend

/src/core/global-api/extend.js

/**
 * Each instance constructor, including Vue, has a unique
 * cid. This enables us to create wrapped "child
 * constructors" for prototypal inheritance and cache them.
 */
Vue.cid = 0
let cid = 1

/**
 * Extend the subclass based on Vue, which also supports further extension
 * Some default configurations can be passed during expansion, just as Vue also has some default configurations
 * If the default configuration conflicts with the base class, options will be merged
 */
Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid

  /**
   * Using the cache, if it exists, the constructor in the cache is returned directly
   * Under what circumstances can this cache be utilized?
   *   If you call Vue multiple times The same configuration item (extendOptions) is used during extend, and the cache will be enabled
   */
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }

  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }

  // Define the Sub constructor, just like the Vue constructor
  const Sub = function VueComponent(options) {
    // initialization
    this._init(options)
  }
  // Inherit Vue through prototype inheritance
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  // Option merge: merge Vue configuration items into your own configuration items
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  // Record your own base class
  Sub['super'] = Super

  // Initialize props and proxy the props configuration to sub.prototype_ Props object
  // Pass this. In the component_ Props mode can be accessed
  if (Sub.options.props) {
    initProps(Sub)
  }

  // Initialize computed and proxy the computed configuration to the Sub.prototype object
  // In the component, you can use this Computedkey access
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // Three static methods, extend, mixin and use, are defined to allow subclasses to be further constructed on the basis of Sub
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // Define three static methods: component, filter and directive
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })

  // The principle of recursive components. If the component sets the name attribute, it will register itself in its components option
  if (name) {
    Sub.options.components[name] = Sub
  }

  // Keep references to base class options when extending.
  // Later, when instantiating, we can check whether the option of Super has an update
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // cache
  cachedCtors[SuperId] = Sub
  return Sub
}

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

Vue.set

/src/core/global-api/index.js

Vue.set = set

set

/src/core/observer/index.js

/**
 * Via Vue Set or this$ The set method sets the value val for the specified key of the target
 * If the target is an object and the key does not exist, set the response for the new key, and then execute the 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)}`)
  }
  // Update the elements of the array with the specified subscript, Vue Set (array, IDX, Val), and realize the responsive update through the splice method
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // Update the existing properties of the object, Vue Set (obj, key, Val) and execute the update
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // You cannot add dynamic responsive attributes to Vue instances or $data, which is one of the uses of vmCount,
  // this.$data ob Vmcount = 1, indicating the VM of the root component and other sub components Vmcount is 0
  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 responsive object. The new attribute will be set, but it will not be processed in a responsive manner
  if (!ob) {
    target[key] = val
    return val
  }
  // Define a new attribute for the object, set the response through the defineReactive method, and trigger the dependency update
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

Vue.delete

/src/core/global-api/index.js

Vue.delete = del

del

/src/core/observer/index.js

/**
 * Via Vue Delete or VM$ Delete deletes the specified key of the target object
 * The array is implemented through the splice method, and the object deletes the specified key through the delete operator and executes 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, delete the element of the specified subscript through the splice method
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__

  // Avoid deleting the properties of Vue instances or the data of $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 through the delete operator
  delete target[key]
  if (!ob) {
    return
  }
  // Execute dependency notification
  ob.dep.notify()
}

Vue.nextTick

/src/core/global-api/index.js

Vue.nextTick = nextTick

nextTick

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

For more detailed analysis of the nextTick method, you can see the previous article Interpretation of Vue source code (4) -- asynchronous update.

const callbacks = []
/**
 * Accomplish two things:
 *   1,Wrap the flushSchedulerQueue function with try catch and put it into the callbacks array
 *   2,If pending is false, it means that there is no flushCallbacks function in the browser's task queue
 *     If pending is true, it indicates that the flushCallbacks function has been put into the browser's task queue,
 *     When the flushCallbacks function is to be executed, pending will be set to false again, indicating that the next flushCallbacks function can enter
 *     The browser's task queue
 * pending Function: ensure that there is only one flushCallbacks function in the browser's task queue at the same time
 * @param {*} cb Receive 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 try catch to facilitate error capture
      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 (micro task queue is preferred)
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

summary

  • Interviewer: Vue What did use (plugin) do?

    Answer:

    To install the plugin plug-in is to execute the install method provided by the plug-in.

    • First, judge whether the plug-in has been installed
    • If not, execute the install method provided by the plug-in to install the plug-in. The plug-in decides what to do

  • Interviewer: Vue What did mixin (options) do?

    Answer:

    Responsible for merging options configuration on Vue's global configuration. Then, when each component generates vnode, the global configuration will be merged into the configuration of the component itself.

    • Standardize the format of props, inject, and directive options on the options object
    • Handle extensions and mixins on options and merge them into the global configuration respectively
    • Then, the options configuration and the global configuration are merged. When the options conflict, the options configuration will overwrite the global configuration

  • Interviewer: Vue What did component (compname, COMP) do?

    Answer:

    Responsible for registering global components. In fact, it is to register the component configuration on the components option of the global configuration (options.components), and then each sub component will merge the global components option into the local components configuration item when generating vnode.

    • If the second parameter is empty, it means to get the component constructor of compName
    • If Comp is a component configuration object, use Vue The extend method obtains the component constructor. Otherwise, proceed to the next step directly
    • Set the component information on the global configuration, this options. components. compName = CompConstructor

  • Interviewer: Vue What did directive ('My directive ', {XX}) do?

    Answer:

    Register the my directive globally, and then each subcomponent will merge the global directives option into the local directives option when generating vnode. The principle is the same as Vue Component method:

    • If the second parameter is null, the configuration object of the specified instruction is obtained
    • If it is not empty, if the second parameter is a function, the configuration object {bind: the second parameter, update: the second parameter} will be generated
    • Then set the instruction configuration object to the global configuration, this options. directives['my-directive'] = {xx}

  • Interviewer: Vue What does filter ('My filter ', function (VAL) {XX}) do?

    Answer:

    Be responsible for registering the filter my filter globally, and then each sub component will merge the global filters option into the local filters option when generating vnode. The principle is:

    • If the second parameter is not provided, get the callback function of my filter filter
    • If the second parameter is provided, set this options. filters['my-filter'] = function(val) {xx}.

  • Interviewer: Vue What does extend (options) do?

    Answer:

    Vue.extend creates a subclass based on Vue, and the parameter options will be used as the default global configuration of the subclass, just like the default global configuration of Vue. So through Vue Extend extends a subclass. One of its great uses is to build in some public configurations for subclasses of subclasses.

    • Define a subclass constructor. Like Vue, it is also called_ init(options)
    • Merge the Vue configuration and options. If the options conflict, the options will overwrite the Vue configuration items
    • Define a global API for subclasses, with the value Vue, such as sub.extend = super Extend, so that the subclass can also extend other subclasses
    • Return subclass Sub

  • Interviewer: Vue What did set (target, key, Val) do

    Answer:

    Because Vue cannot detect ordinary new properties (such as this.myObject.newProperty = 'hi'), Vue Set is to add a property to the responsive object, which can ensure that the new property is also responsive and trigger the view update.

    • Update the element of the array with the specified subscript: Vue Set (array, IDX, Val). The internal response update is realized through the splice method
    • Update the existing properties of the object: Vue Set (obj, key, Val), update directly = > obj [key] = Val
    • You cannot dynamically add root level responsive data to Vue instances or $data
    • Vue.set(obj, key, val). If obj is not a responsive object, obj[key] = val will be executed, but no responsive processing will be performed
    • Vue.set(obj, key, val). Add a new key to the responsive object obj. Set the responsive object through the defineReactive method and trigger the dependency update

  • Interviewer: Vue What does delete (target, key) do?

    Answer:

    Delete the property of the object. If the object is responsive, make sure the deletion triggers an update of the view. This method is mainly used to avoid the restriction that Vue cannot detect that the property is deleted, but you should rarely use it. Of course, you can't delete the responsive attribute at the root level.

    • Vue.delete(array, idx), which deletes the element with the specified subscript. It is implemented internally through the splice method
    • Delete a property on the responsive object: Vue Delete (obj, key). Internally, delete obj is executed Key, and then execute dependency update

  • Interviewer: Vue What did nexttick (CB) do?

    Answer:

    Vue. The function of nexttick (cb) method is to delay the execution of callback function cb, which is generally used for this Key = newval after changing the data, you want to get the changed DOM data immediately:

    this.key = 'new val'
    
    Vue.nextTick(function() {
      // DOM updated
    })

    Its internal execution process is:

    • this.key = 'new val, trigger dependency notification update, and put the watcher in charge of updating into the watcher queue
    • Put the function of refreshing the watcher queue into the callbacks array
    • Put a function to refresh the callbacks array in the asynchronous task queue of the browser
    • Vue.nextTick(cb) to jump the queue and put the cb function into the callbacks array
    • The function of refreshing the callbacks array will be executed at a certain time in the future
    • Then execute many functions in the callbacks array to trigger the watcher Execute run and update DOM
    • Since the cb function is put into the callbacks array later, this ensures that the DOM update is completed first, and then the cb function is executed

link

Thank you for your likes, collections and comments. I'll see you next time.

When learning becomes a habit, knowledge becomes common sense. Thank you for your likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

Topics: Javascript Front-end ECMAScript TypeScript Vue.js