current position:Home>Source code analysis, Vue global API set, del, nexttick, use, mixin, etc

Source code analysis, Vue global API set, del, nexttick, use, mixin, etc

2022-04-29 04:28:10@huihui_ new

stay vue In the source src/core/global-api The directory index Next , Global is initialized API, as follows :

initGlobalAPI

import config from '../config'
import {
     initUse } from './use'
import {
     initMixin } from './mixin'
import {
     initExtend } from './extend'
import {
     initAssetRegisters } from './assets'
import {
     set, del } from '../observer/index'
import {
     ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'

import {
    
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
    
  // config
  const configDef = {
    }
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    
    configDef.set = () => {
    
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

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

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

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

You can see , stay initGlobalAPI First of all Vue Added some config Configuration properties , If these configurations are modified, a warning will be reported , This is important to the overall situation API No impact analysis . The next in Vue.util Added some methods to the properties of , Can pass Vue.util.[xx] To access these methods , But it's not recommended ,util The methods in may change from time to time , Of course, this is important to the overall situation API The analysis has no effect .
Next, give the constructor Vue Initialized some static API,set、delete、nextTick These methods

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

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

This code is for Vue Of options Some objects are initialized on the object , give the result as follows :
 Insert picture description here
adopt extend function , to Vue.components To add the keepAlive Components , That's why we can use it directly keepAlive The cause of the component

export function extend (to: Object, _from: ?Object): Object {
    
  for (const key in _from) {
    
    to[key] = _from[key]
  }
  return to
}

 Insert picture description here
Next, execute some initialization methods , In the constructor Vue To add the use、mixin、extend、components、directive、filter These methods . Next, the global method of mounting is analyzed in detail .

1.set

/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */
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)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

export function isUndef (v: any): boolean %checks {
    
  return v === undefined || v === null
}

export function isPrimitive (value: any): boolean %checks {
    
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    // $flow-disable-line
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

set Method can only work on arrays or objects , If the property value set is undefined, null, or primitive value A warning will be given , Next, judge the set properties , If it already exists in the array or object , Give a new value and return ;
Next, let's see if the object contains attributes __ob__( Flag indicating whether the object has listened ), The monitored object cannot be a vue example , Otherwise, the report will be wrong ;
If the object is not listening , The value is returned directly ,

defineReactive(ob.value, key, val)
ob.dep.notify()

If everything is OK , Add a responsive attribute to the object ( adopt defineReactive), And notify the dependent ( adopt notify) Make corresponding changes , These two methods are important methods of the response principle , I won't elaborate here , But from here we can see ,Vue.set This method is to set the dynamic new properties of an existing object to be responsive .
( Dynamically adding methods to objects or arrays , No, set Under the circumstances , Is not responsive , The author once stepped on this pit , So it's still necessary to learn the source code !!!)

2.delete

/** * Delete a property and trigger change if necessary. */
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 (Array.isArray(target) && isValidArrayIndex(key)) {
    
    target.splice(key, 1)
    return
  }
  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 (!hasOwn(target, key)) {
    
    return
  }
  delete target[key]
  if (!ob) {
    
    return
  }
  ob.dep.notify()
}

delete Methods and set The method is very similar , The previous logic is similar to set It's the same , If this property is not included in the object , Then return directly , Otherwise, through native Syntax delete This attribute , If there is __ob__ object , Then notify the dependency to make corresponding changes ;

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

There is also the above code in the source code , Use the prototype to mount set and del Method , That's why we can use this.$xx To call set and del.

3.nextTick

js The execution of is single threaded , The execution process of the main thread is a tick, And all asynchronous results come through “ Task queue ” To dispatch , This involves macro tasks and micro tasks , In the browser environment , common macro task Yes setTimeout、MessageChannel、postMessage、setImmediate; common micro task Yes MutationObsever and Promise.then, I won't elaborate here .
Let's look at nextTick Source code :

const callbacks = []
let pending = false
export function nextTick (cb?: Function, ctx?: Object) {
    
  let _resolve
  callbacks.push(() => {
    
    if (cb) {
    
      try {
    
        cb.call(ctx)
      } catch (e) {
    
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
    
      _resolve(ctx)
    }
  })
  if (!pending) {
    
    pending = true
    if (useMacroTask) {
    
      macroTimerFunc()
    } else {
    
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    
    return new Promise(resolve => {
    
      _resolve = resolve
    })
  }
}

First abandon _resolve The logic of ( This is to achieve promise Preparing for , As we'll see ),
This function will first send a message to callbacks Push an anonymous function into the array , When the anonymous function is executed, it will trigger the incoming callback, To determine pending, This pending At first it was false, After entering, it is assigned as true, That is to ensure that this logic will only be executed once , To determine useMacroTask, According to browser support , Determine different ways of execution .

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    
  macroTimerFunc = () => {
    
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
    
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    
    port.postMessage(1)
  }
} else {
    
  /* istanbul ignore next */
  macroTimerFunc = () => {
    
    setTimeout(flushCallbacks, 0)
  }
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    
  const p = Promise.resolve()
  microTimerFunc = () => {
    
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
} else {
    
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */
export function withMacroTask (fn: Function): Function {
    
  return fn._withTask || (fn._withTask = function () {
    
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

You can see that the priority will be to check whether the browser supports native setImmediate, If it is not supported, then check whether it supports native MessageChannel, If it doesn't support it, it will be downgraded to setTimeout 0; And for micro task The implementation of the , Then check whether the browser supports native Promise, If not, point directly to macro task The implementation of the , In fact, these functions will eventually execute flushCallbacks.

function flushCallbacks () {
    
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    
    copies[i]()
  }
}

You can see ,flushCallbacks Lieutenant general pending Assigned to false, Then the next... Is executed in turn tick All of them callback.
Besides , go back to _resolve Variable , In addition to the way of incoming callbacks ,nextTick Also support promise.then, When there is no cb When it comes in , Will execute :

if (!cb && typeof Promise !== 'undefined') {
    
  return new Promise(resolve => {
    
    _resolve = resolve
  })
}

The next tick When it is executed, it will go to else if Logic , You can go through .then The way to execute .

if (cb) {
    
  try {
    
     cb.call(ctx)
   } catch (e) {
    
     handleError(e, ctx, 'nextTick')
   }
} else if (_resolve) {
    
   _resolve(ctx)
}

From the above we can see that ,nextick Function is to collect all callback functions and put them in the next tick To execute .

$nextTick

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

In the constructor Vue The prototype is also mounted nextTick Method , That's why we can also use this.$nextTick To call nextTick Why .

4.use、mixin

Vue.use And mixin You can go to my other source code analysis From initialization vuex From the perspective of use、mixin, Inside through to Vue.use(Store) Process analysis , Yes use、mixin Your understanding is more vivid and specific .
about use function , In fact, the core is to execute the incoming plug-in install Method :

export function initUse (Vue: GlobalAPI) {
    
  Vue.use = function (plugin: Function | Object) {
    
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
    
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
    
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
    
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

about mixin function , The core is the implementation of mergeOptions function , That is to say, to Vue.options add to mixin object

export function initMixin (Vue: GlobalAPI) {
    
  Vue.mixin = function (mixin: Object) {
    
    this.options = mergeOptions(this.options, mixin)
    return this
  } 
}

among ,mergeOptions There are different merging strategies for different attributes in the function , For example, hook function

/** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */
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
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  const extendsFrom = child.extends
  if (extendsFrom) {
    
    parent = mergeOptions(parent, extendsFrom, 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
  for (key in parent) {
    
    mergeField(key)
  }
  for (key in child) {
    
    if (!hasOwn(parent, key)) {
    
      mergeField(key)
    }
  }
  function mergeField (key) {
    
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
LIFECYCLE_HOOKS.forEach(hook => {
    
  strats[hook] = mergeHook
})
function mergeHook (
  parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function>
): ?Array<Function> {
    
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

For hook functions , Will pass parentVal.concat(childVal) Merge into an array and mix into Vue.options On the object , The execution order is from front to back .

if (child.mixins) {
    
  for (let i = 0, l = child.mixins.length; i < l; i++) {
    
    parent = mergeOptions(parent, child.mixins[i], vm)
  }
}

For the incoming mixin Objects have mixins Attribute , First the mixins The hook function in the property is merged into parent Back , Re merger mixin Hook function in object , The final execution sequence is :
Vue.options The hook function above –>mixin On the object mixins Hook function on property object –>mixin Hook function on object

5.extend

... To be continued

copyright notice
author[@huihui_ new],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2022/117/202204270551339253.html

Random recommended