Vue source code analysis -- normalization option

Posted by dodgei on Sat, 19 Feb 2022 18:27:00 +0100

The article was first published on personal blog Small gray space

Normalization options

When new Vue creates a Vue instance for initialization, the first operation is the merge option. In the merge option, there are different merge strategies for different scenarios

if (options && options._isComponent) {
  // When initializing subcomponents, merge the options of subcomponents
  initInternalComponent(vm, options)
} else {
  // Merge options when calling new Vue externally
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

You can see that when you create a Vue instance, you call mergeOptions to merge options. In the process of merging options, the first thing to do is to normalize options

/**
 * Normalize the projects input directives respectively
 */
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)

Let's take a look at the specific implementation of the above three functions

Normalized props

According to the instructions in the Vue usage document, there are two formats for defining props: array and object. Let's take a look at the use of these two formats first

// Lists props as an array
props: ['name', 'age']

// Lists props as objects. Lists props as objects. You can specify the data type of each prop
props: {
  name: string,
  age: age,
  address: object
}

Since props has two definition formats, it will be inconvenient to use it. Therefore, it is necessary to standardize props and unify it into object format. Now let's look at the implementation of normalizeProps

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name

  if (Array.isArray(props)) {
    // List props in array format
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        // Each item in the array must be a string. Each item is stored in an object. The key of each item refers to the most object, and the corresponding value is {type: null}
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    // If props is listed in object format, isplanobject determines whether a value is an ordinary object
    for (const key in props) {
      val = props[key]
      // Convert the key of the object into small hump format
      name = camelize(key)
      // Evaluate using ternary expressions
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

After the analysis of the above code, we can finally summarize the results of props standardization for different writing methods

For array format

props: ['name', 'age']

The final conversion is as follows

props: {
  name: {
    type: null
  },
  age: {
    type: null
  }
}

For object formats

props: {
  name: string,
  age: number,
  address: {
    type: object 
  }
}

The final conversion is as follows

props: {
  name: {
    type: string
  },
  age: {
    type: number
  },
  address: {
    type: object
  }
}

Normalized inject

Inject cannot be used alone in Vue, and it needs to be used together with provide. Let's take a look at Vue's explanation of provide/inject

To allow an ancestor component to inject a dependency into all its descendants, no matter how deep the component level is, and it will always take effect when its upstream and downstream relationships are established

The inject option should be:

  • An array of strings, or
  • An object whose key is the local binding name and whose value is:

    • Search for the key (string or Symbol) used in the available injection content, or
    • An object whose:

      • from property is the key (string or Symbol) used for searching in the available injected content
      • default property is the value used in case of degradation

Now let's look at the implementation of normalizeInject. The inject option can be written in two ways: array and object. As with props, it will eventually be converted to object format

function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  // Array format
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      // from: attribute is the key (string or Symbol) used for searching in the available injected content
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    // Object format
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

Normalized directive

Let's first look at the usage of instruction options. Vue allows us to customize instructions, and it provides five hook functions bind, inserted, update, componentupdated and unbind. For specific usage, please refer to the official user-defined instruction document. In addition to defining hook functions in the form of objects, the official also provides a function abbreviation, such as:

{
  directives: {
    'color-swatch': function(el, binding) {
        el.style.backgroundColor = binding.value
    }
  }
}

The writing of the function will trigger the same behavior in the bind and update hooks, and do not care about other hooks. This behavior is the defined function. Therefore, when normalizing directives, the writing of functions will assign behavior to bind and update hooks.

function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      // Function shorthand will eventually be converted to object form
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

Function cache

In the above code, there is a code to optimize the performance, which is worthy of reference and learning in future development.

When converting parameters to hump format, the inverted value will be cached after each call to the function. The next time, the value in the cache will be used first instead of re executing the function to improve performance. This is a typical optimization of space for time, and it is also a classic application of partial function

export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null) // Create an empty object cache data
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str)) // If there is a cache, the cache is used. If there is no cache, the function is executed to obtain the result
  }: any)
}


const camelizeRE = /-(\w)/g
// Convert the writing of a-b form into hump form aB
// The cached function is called here to cache the conversion results
export const camelize = cached((str: string): string => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})

summary

In the above three methods, respectively normalize the projects input directives, and the three options will eventually be converted into the object format.

Topics: Javascript Front-end Vue.js