This is the 18th day of my participation in Gwen Challenge

preface

In the previous learning to create a Vue instance, the initialization of the VM object will merge parameters, merge parameters have a lot of merge strategy, today to see how Vue specifically merge parameters.

Review how options are handled at the end of mergeOptions:

  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
Copy the code

As you can see, the mergeField function returns different policies for different keys. It contains two variables: strats and defalutStrat, which hold the specific merge policy.

defaultStrat

The default merge strategy is to check if the property exists on the child and use the value on the parent if it does not.

const defaultStrat = function (parentVal: any, childVal: any) :any {
  return childVal === undefined
    ? parentVal
    : childVal
}
Copy the code

strats

Let’s take a look at what strats has in general:

As you can see, these are mainly lifecycle hook functions, data, computed, filters, Methods, props, Watch, and so on defined in our Vue component

If you look at the original definition of Strats, it is an empty Object

const strats = config.optionMergeStrategies

optionMergeStrategies: Object.create(null)
Copy the code

Life cycle function merge

Let’s look at how the lifecycle functions are merged:

LIFECYCLE_HOOKS.forEach(hook= > {
  strats[hook] = mergeHook
})

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function|?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}
Copy the code

Here is a multi-level nested ternary operator that checks whether childVal exists, and returns parentVal if it does not;

If childVal exists, parentVal is determined to exist, and if so, childVal is merged into parentVal to return

If parentVal does not exist, check whether childVal is an array. If parentVal is an array, return childVal as an array.

Finally, an array of merged hooks is returned using the dedupeHooks function.

The data merging

strats.data = function (parentVal: any, childVal: any, vm? : Component): ?Function {
  if(! vm) {if (childVal && typeofchildVal ! = ='function') { process.env.NODE_ENV ! = ='production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}
Copy the code

The data merge basically calls mergeDataOrFn, so let’s look at this function:

export function mergeDataOrFn (parentVal: any, childVal: any, vm? : Component): ?Function {
  if(! vm) {// in a Vue.extend merge, both should be functions
    if(! childVal) {return parentVal
    }
    if(! parentVal) {return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this.this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this.this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}
Copy the code

If no VM instance exists (created by vue.extend), check whether childVal exists. If not, return parentVal. Next check if parentVal exists, and return childVal if not. If both exist, mergeData is called

If there is a VM (created by new Vue), this refers to the VM in childVal and parentVal; Finally, call mergeata merge directly

Look at the mergeData function:

function mergeData (to: Object.from:?Object) :Object {
  if (!from) return to
  let key, toVal, fromVal

  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // in case the object is already observed...
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    if(! hasOwn(to, key)) { set(to, key, fromVal) }else if( toVal ! == fromVal && isPlainObject(toVal) && isPlainObject(fromVal) ) { mergeData(toVal, fromVal) } }return to
}
Copy the code

The function takes two arguments, to and from, to merge from the FROM object into the TO object.

OwnKeys and Object.keys to determine whether there is a symbol attribute; But note that reflects. ownKeys returns all of the Object’s own properties, including non-enumerable and Symbol, while Object.keys enumerates only the Object’s own enumerable properties, not Symbol. Therefore, it is not recommended to use this method if the object has non-enumerable attributes. You can click here for object properties.

Iterate over all properties in the FROM object. If the current key is already a listening property, the next one is directly traversed. Otherwise, take out the corresponding key value from the two objects to be merged and use hasOwn function (encapsulating hasOwnProperty) to check whether the to object has the same key. If not, call set function to bind to the TO object.

Otherwise, the to object itself has the same key, and the corresponding values of the two objects are judged to be consistent. If they are inconsistent and both objects are objects, mergeData is recursively executed.

export function isPlainObject (obj: any) :boolean {
  return _toString.call(obj) === '[object Object]'
}
Copy the code

Watch merger

The Watch code is much simpler than data.

strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object, vm? : Component, key: string): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if(! childVal)return Object.create(parentVal || null)
  if(process.env.NODE_ENV ! = ='production') {
    assertObjectType(key, childVal, vm)
  }
  if(! parentVal)return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}
Copy the code

In Firefox, Object. Prototype has a Watch attribute, so check that the Watch Object is not the browser’s own. (const nativeWatch = ({}).watch)

If childVal is empty, object.create is called to create a new Object modeled after parentVal and return the new Object

If parentVal is empty, childVal is returned directly

ParentVal is shallow copied to RET by calling extend(ret, parentVal) when neither parentVal nor childVal is null

Next, we iterate through childVal. If the current key exists in the parent object, we convert the parent to an array. Then we call concat to concatenate the child as well. Otherwise return the key from child to RET in array format.

Take a look at the extend implementation:

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

Props, the methods, inject, computed merge

The methods for props, Methods, Inject, computed are the same.

strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object, vm? : Component, key: string): ?Object {
  if(childVal && process.env.NODE_ENV ! = ='production') {
    assertObjectType(key, childVal, vm)
  }
  if(! parentVal)return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}
Copy the code

Dojo.provide merger

Provide merges in the same way as data by calling mergeDataOrFn

strats.provide = mergeDataOrFn
Copy the code

Merge components, directives, and filters

Components, directives, filter merges are calls to the mergeAssets method.

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

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})
Copy the code

Take a look at the mergeAssets method:

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object, vm? : Component, key: string) :Object {
  const res = Object.create(parentVal || null)
  if(childVal) { process.env.NODE_ENV ! = ='production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}
Copy the code

The mergeAssets implementation is also uncomplicated. Let res inherit parentVal, and then determine whether childVal has a value.

Vue.mixin

Vue officially provides us with mixins to distribute reuse functionality in components. Mixins can contain any option of Vue components.

var mixin = {
  ...
}

new Vue({
  mixins: [mixin],
  ...
})
Copy the code

The documentation also indicates that it has different merge strategies for different options.

When a component and mixin have options of the same name, the data objects are merged recursively, and the component data takes precedence in the event of a conflict.

The hook function of the same name is merged into an array, with the object’s hook function inserted before the component’s own hook function.

The methods, componets, merge directives for the same object, the two objects key name conflict, take the component object’s key/value pair.

Ha, ha, ha, ha, ha, ha, ha, ha, ha. Review the implementation of this:

 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)
      }
    }
  }
Copy the code

We can also call vue. mixin directly to implement a global mixin.

Vue.mixin({
  created: function () {
    var myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

new Vue({
  myOption: 'hello! '
})
Copy the code

Take a look at the internal implementation of vue.mixin:

Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
}
Copy the code

Vue.mixin is a direct call to mergeOption. The mixin object is childVal in our source code and has the highest priority, so be careful when using global hook mixins.

Vue3 changes

Previously we know that data merge is recursive merge object, is a deep merge. However, Vue3 takes care of this. Mixin and extend merge data ata shallow level and do not recursively look for objects.

Official examples:

const Mixin = {
  data() {
    return {
      user: {
        name: 'Jack'.id: 1}}}}const CompA = {
  mixins: [Mixin],
  data() {
    return {
      user: {
        id: 2}}}}Copy the code

$data in Vue2. X

{
  user: {
    id: 2.name: 'Jack'}}Copy the code

$data result in Vue3:

{
  user: {
    id: 2}}Copy the code