preface

Recently, I have been reading the source code of Vue and found a few practical tips, which I share with you.

You can also read my previous Vue article

  • “SAO Operation” in VUE development
  • Discover hidden Vue tips in the source code!
  • $nextTick() : $nextTick()

Hidden in the source code skills

When Vue is instantiated, the vue.prototype._init method is called first, where the options returned by the mergeOptions() method will be used in all initialization functions. This code is as follows:

vm.$options = mergeOptions(

 resolveConstructorOptions(vm.coustructor),

    options || {},

    vm

)

Copy the code

The mergeOptions method is in the core/util/options.js file, and the last method returns an options object.

return options

Copy the code

This means that the mergeOptions method will eventually return the merged options as the vm.$options value.

Custom options merge methods

What if we add custom attributes in options? Examples from the government:

new Vue({

  customOption'foo'.

  createdfunction ({

    console.log(this.$options.customOption) // => 'foo'

  }

})

Copy the code

A customOption is passed when creating the Vue instance: customOptions, which is accessible in created via this.$options.customOption. The mergeOptions function is used to merge custom options. For the merge of custom options, the default merge strategy is used:

// When an option has no corresponding policy function, the default policy is used

const strat = strats[key] || defaultStrat

Copy the code

The defaultStrat function is defined in the options.js file, source code below

/ * *

 * Default strategy.

* /


const defaultStrat = function (parentVal: any, childVal: any) :any {

  return childVal === undefined

    ? parentVal

    : childVal

}

Copy the code

DefaultStart does what it sounds like, and is a default merge strategy: use the child option as long as it is not undefined, use the parent option otherwise. The net effect is that what you initialize is what you get.

But also provides a global configuration, called the Vue Vue. Config. OptionMergeStrategies, this is the option of merger strategy object, so you can specify a particular option through it the merger strategy, often merge strategy is used to specify a custom options, Such as can give customOption option specifies a merge strategy, only need to config. OptionMergeStrategies add and option strategies with the same function:

Vue.config.optionMergeStrategies.customOption = function (parentVal, childVal{

 return parentVal ? (parentVal + childVal) : childVal

}

Copy the code

The merge strategy for the custom customOption is to return childVal if parentVal is not present, otherwise return the sum of the two.

Here’s an example:

// Create a subclass

const Sub = Vue.extend({

 customOption1

})



// Create an instance with a subclass

const vm = new Sub({

 customOption2.

    created () {

     console.log(this.$options.customOption) / / 3

    }

})

Copy the code

The result is to print the number 3 in the Created method. Customize the policy function for merge options, which is very useful.

vm.$createElement

In initRender, the vm.$createElement() method is defined. InitRender is in core/instance/render.js. In this function there is the following code:

  // bind the createElement fn to this instance

  // so that we get proper render context inside it.

  // args order: tag, data, children, normalizationType, alwaysNormalize

  // internal version is used by render functions compiled from templates

  vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false)

  // normalization is always applied for the public version, used in

  // user-written render functions.

  vm.$createElement = (a, b, c, d) = > createElement(vm, a, b, c, d, true)

Copy the code

This code adds two methods to the Vue instance object:

  • vm._c
  • vm.$createElement

These two methods are essentially wrappers around the inner function createElement.

render: function (createElement) {

 return createElement('h2'.'title')

}

Copy the code

As you know, the first argument to the render function is createElement, which is used to create a virtual node, so you can write this as well

render: function ({

 return this.$createElement('h2'.'title')

}

Copy the code

The above two pieces of code are completely equivalent.

provide & inject

Finally, look at the principles of provide and inject.

If a component uses the provide option, data specified by the provide option will be injected into all child components of the component. Optionally inject the child component with the Inject option, in which case the component gets data provided by the parent component.

Provide and inject are initialized in vue.prototype. _init:

initLifecycle(vm)

initEvents(vm)

initRender(vm)

callHook(vm, 'beforeCreate')

initInjections(vm)

initState(vm)

initProvide(vm)

callHook(vm, 'created')

Copy the code

Note that the initinjection function is called before the initProvide function, which means that for any component, initialize the Inject option first before the provide option.

Let’s look at initInjections function, it defined in SRC/core/instance/inject js file

export function initInjections (vm: Component{

  const result = resolveInject(vm.$options.inject, vm)

  if (result) {

    / / to omit...

  }

}

Copy the code

The function takes the component instance object as an argument, first defines the Result constant inside the function, and then executes the code inside the if statement after checking that result is true.

The value of the result constant is the return value of the resolveInject function.

export function resolveInject (inject: any, vm: Component): ?Object {

  if (inject) {

    / / to omit...

    return result

  }

}

Copy the code

As you can see, the resolveInject function takes two parameters, the Inject option and the component instance object. If inject is true, the code inside the if statement block will be executed and result will be returned, otherwise undefined will be returned.

The code inside the if statement block:

const result = Object.create(null)

const keys = hasSymbol

  ? Reflect.ownKeys(inject).filter(key= > {

    /* istanbul ignore next */

    return Object.getOwnPropertyDescriptor(inject, key).enumerable

  })

  : Object.keys(inject)

Copy the code

First create an empty Object with Object.create(null) and assign it to result. We then define the keys constant that holds each key name of the Inject option object.

HasSymbol is evaluated first, if true, use reflect.ownkeys to get all enumerable key names in the Inject Object, otherwise use Object.keys.

The advantage of using reflect.ownkeys is that the Symbol type can be supported as the key name.

Next, use the for loop to iterate through the array of keys you just obtained

for (let i = 0; i < keys.length; i++) {

 const key = keys[i];

    const provideKey = inject[key].from

    let source = vm

    while (source) {

      if (source._provided && hasOwn(source._provided, provideKey)) {

          result[key] = source._provided[provideKey]

          break

      }

      source = source.$parent

    }

}

Copy the code

The provideKey constant holds the value of the FROM property of the injected object defined by each Inject option.

In the canonicalization phase, the Inject option is normalized as an object, and the object must contain the FROM attribute.

inject: ['data1'.'data2']

Copy the code

The vm.$options.inject option will be changed to:

{

  'data1': { from'data1' },

  'data2': { from'data2' }

}

Copy the code

The provided object itself has a provideKey. If so, the injected data is found: Source. _provided[provideKey], and assigns it to the property of the same name of the Result object.

If false, execute source = source.$parent; Reassigning the source variable to reference the parent, and so on, completes the need to look up data from the parent until it is found.

If data cannot be found:

if(! source) {

  if ('default' in inject[key]) {

    const provideDefault = inject[key].default

    result[key] = typeof provideDefault === 'function'

      ? provideDefault.call(vm)

      : provideDefault

  } else if(process.env.NODE_ENV ! = ='production') {

    warn(`Injection "${key}" not found`, vm)

  }

}

Copy the code

Check whether the Default option is defined in the Inject [key] object. If the default option is defined, the data provided by the default option is used as the injection data. Otherwise, developers will be prompted in non-production environments.

Finally, if data is found, result is returned. However, the value of the result constant may not exist, so a result judgment is required. If true, the injected data was successfully retrieved, and the contents of the if block are executed.

toggleObserving(false)

Object.keys(result).forEach(key= > {

  /* istanbul ignore else */

  if(process.env.NODE_ENV ! = ='production') {

    defineReactive(vm, key, result[key], () => {

      warn(

        `Avoid mutating an injected value directly since the changes will be ` +

        `overwritten whenever the provided component re-renders. ` +

        `injection being mutated: "${key}"`.

        vm

      )

    })

  } else {

    defineReactive(vm, key, result[key])

  }

})

toggleObserving(true)

Copy the code

Define a variable on the current component instance object VM with the same injected name and assign the obtained value by iterating through the Result constant and calling defineReactive.

The toggleObsesrving(false) function is called to turn reactive definitions off and on before using defineReactive, which results in the attribute’s value not being converted to reactive when defining a property using defineReactive.

What provide does is simple: look at the contents of initProvide:

export function initProvide (vm: Component{

  const provide = vm.$options.provide

  if (provide) {

    vm._provided = typeof provide === 'function'

      ? provide.call(vm)

      : provide

  }

}

Copy the code

Again, the component instance object is received as a parameter. Define provide, which is a reference to the vm.$options.provide option, followed by the if judgment statement, which is executed only if the provide option exists.

Procide can be either an object or a function, so use Typeof to detect the type, execute the function to get the data if it is a function, and otherwise simply provide itself as data. Finally, the data is assigned to the vm._provided property of the component instance object. Inject data from the vm._provided property of the parent component instance.

Final note: Provide and Inject are mainly used when developing high-level plug-in/component libraries. Not recommended for use in normal application code.

At the end

For more articles, please go to Github, if you like, please click star, which is also a kind of encouragement to the author.