This article is the sixth in the vUE series. I have seen this series of articles partners know: the article thief is long, can not read the suggestion of the first point when the collection, and then have time to calm down to slowly read, front-end communication group: 731175396. Previous article portals are as follows

  • Vue Core – VDOM
  • In Detail on Vue-Slot
  • In Detail on Vue-Transition
  • The Vue-Transition-Group
  • Vue – Abstract Components In Action

If you have used VUE, you will know that components are everywhere in vue development. The.vue (SFC) files in a project are components.

So, if Component is so central and important, why not take a look at a wave?

According to the not?

Lu xun –

Component creation

CreateElement createComponent is used to create a vNode. If it is a normal HTML tag, it will instantiate a normal VNode. Otherwise, create a Component vNode using createComponent

1, the createElement method

Only the code for vNode creation in different cases is listed here

if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined.undefined, context
    )
  } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
    // component
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // unknown or unlisted namespaced elements
    // check at runtime because it may get assigned a namespace when its
    // parent normalizes children
    vnode = new VNode(
      tag, data, children,
      undefined.undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}
Copy the code

2, the createComponent

Next, let’s look at createComponent() as defined below

export function createComponent (
  Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeofCtor ! = ='function') {
    if(process.env.NODE_ENV ! = ='production') {
      warn(`Invalid Component definition: The ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
    data, undefined.undefined.undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}
Copy the code
  • Inside it, the first thing to do is to get the constructorVueAssign to a variablebaseCtor, and through theextendThe parameterCtorextend
const baseCtor = context.$options._base
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}
Copy the code

Here we see $options._base, which is the constructor Vue

// src/core/global-api/index.js
Vue.options._base = Vue

// src/core/instance/init.js
// 1. initMixin()
if (options && options._isComponent) {
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}
// 2. initInternalComponent()
const opts = vm.$options = Object.create(vm.constructor.options)
Copy the code
  • Next, determine whether the component is asynchronous, functional, or abstract. I’ll talk more about how to deal with each case later
// Asynchronous components
let asyncFactory
if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  if (Ctor === undefined) {
    return createAsyncPlaceholder(
      asyncFactory,
      data,
      context,
      children,
      tag
    )
  }
}

// Functional components
if (isTrue(Ctor.options.functional)) {
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// Abstract the component
if (isTrue(Ctor.options.abstract)) {
  const slot = data.slot
  data = {}
  if (slot) {
    data.slot = slot
  }
}
Copy the code
  • There is also processing for events on the component, which extracts event listeners on the component. It needs to be a listener for a child component, not a DOM listener. So you need to replace it with owning.nativeModifier so that it can be processed during the parent component patch phase
const listeners = data.on
data.on = data.nativeOn
Copy the code
  • Then, install the component’s hook function. It will becomponentVNodeHooksMerges the hook function todata.hookAnd thenComponentThe type ofvnodeNodes in thepatchProcedure executes the associated hook function, and passes if the hook function already exists at some pointmergeHookMerge functions, that is, execute the two functions in sequence at the same time
installComponentHooks(data)

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if(existing ! == toMerge && ! (existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } }const hooksToMerge = Object.keys(componentVNodeHooks)

function mergeHook (f1: any, f2: any) :Function {
  const merged = (a, b) = > {
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}
Copy the code

3, componentVNodeHooks

ComponentVNodeHooks init, PrePatch, Insert, destroy

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {const mountedNode: any = vnode
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if(! componentInstance._isMounted) { componentInstance._isMounted =true
      callHook(componentInstance, 'mounted')}if (vnode.data.keepAlive) {
      if (context._isMounted) {
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if(! componentInstance._isDestroyed) {if(! vnode.data.keepAlive) { componentInstance.$destroy() }else {
        deactivateChildComponent(componentInstance, true /* direct */)}}}}Copy the code

ComponentVNodeHooks What do the four hook functions in componentVNodeHooks do

  1. init: whenvnodekeep-aliveComponent, instance exists and has not been destroyed. To prevent component flow, execute directlyprepatch. Otherwise, execute it directlycreateComponentInstanceForVnodeTo create aComponentThe type ofvnodeInstance, and proceed$mountoperation
  2. prepatch: Updates existing components to the latestvnodeThere’s nothing to tell here
  3. insert:insertHook function
    • It first determines whether the component instance has beenmounted, if not rendered, will be directlycomponentInstanceExecute as parametermountedHook function.
    • Second, the component iskeep-aliveThe case for built-in components. There’s an operation here that’s a little bit snappy, which is when it’s alreadymountedWhen it is, enterinsertIn order to preventkeep-aliveChild component update triggeredactivatedThe hook function is abandonedwalking treeUpdate mechanism, but directly to the component instancecomponentInstancePut it in theactivatedChildrenIn this array. Of course notmountedIs triggered directlyactivatedHook function proceedmountedCan be
  4. destroyThe component destruction operation is also true herekeep-aliveComponents are compatible. If it is notkeep-aliveComponent, directly executed$destoryDestroys the component instance or firesdeactivatedThe hook function is destroyed.

Some of the helper functions used above are as follows

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
) :Component {
  const options: InternalComponentOptions = {
    _isComponent: true._parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}
Copy the code
  • Final instantiationVNodeAnd then return
const name = Ctor.options.name || tag
const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
  data, undefined.undefined.undefined, context,
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
)
return vnode
Copy the code

The whole process of createComponent is to first build the Vue subclass constructor, then install the component’s hook function, finally instantiate VNode, and then return. Many of these operations are compatible with the Keep-Alive built-in components. So if you’ve ever used the Keep-alive component and happened to see this, you’ll have a lot to learn.

2. Merge configurations

In general, to ensure the customization and scalability of a plug-in or component, you define some default configurations for the plug-in or component, and then perform internal operations of the Merge configuration items, so that you can customize the configurations during its initialization.

Of course, Vue does the same here. For the options in the vue merge strategy: actually, I also list the code above, concrete in SRC/core/instance/init. Js in here I only keep related code).

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options? :Object) {
    const vm: Component = this
    // ...
    // merge options
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // ...}}Copy the code

As you can see, there are two strategies for merging. One is for Component components, execute initInternalComponent to merge internal Component configuration. The other is for non-component components, directly merge configuration through mergeOptions.

1, normal merge

Here will directly resolveConstructorOptions (vm) constructor) return values and the options to merge

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
Copy the code

Let’s take a look at the definition of vue. options

// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
	// ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type= > {
    Vue.options[type + 's'] = Object.create(null)})// ...
}

// src/shared/constants.js
export const ASSET_TYPES = [
  'component'.'directive'.'filter'
]
Copy the code

MergeOptions is one of vue’s core merge strategies. Its main function is to merge parant and child, and return a new object in SRC /core/util/options.js.

  1. The first one will be rightchildThe aboveprops,inject,directivesforobject formatOperation (specific logic can be studied, mainly on itsobjectConversion operation)
  2. ifchild._baseDoes not exist, traversalchild.extendschild.mixins, merge it toparent
  3. traverseparent, the callmergeFieldMerge into variablesoptions
  4. traversechildIf,childparentProperty that does not existmergeFieldMerge the property tooptions
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)

  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
  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

In addition to merging options, there are also many merging strategies in VUE. If you are interested, you can refer to SRC /core/util/options.js for research

2, component merge

When analyzing createComponent, we learned that the component’s constructor extends Vue via vue.extend, as shown below

// src/core/global-api/index.js
Vue.options._base = Vue
// src/core/vdom/create-component.js
const baseCtor = context.$options._base
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}
Copy the code

Vue.extend, defined in SRC /core/global-api/ exten.js (with only the key logic), extends super.options, Options merged into sub. options

export function initExtend (Vue: GlobalAPI) {
	// ...
  Vue.extend = function (extendOptions: Object) :Function {
    extendOptions = extendOptions || {}
    const Super = this
    // ...
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // ...
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    // ...
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // ...
    return Sub
  }
}
Copy the code

Then in componentVNodeHooks init hook function, namely the subcomponents initialization phase, executes createComponentInstanceForVnode component instance initialization. CreateComponentInstanceForVnode function the vnode.com ponentOptions. Ctor point is above the Vue. The extend returned in the Sub, _init(options), Vue._init(options), and since options._isComponent is defined as true, So we go directly to the initInternalComponent operation

// componentVNodeHooks init()
const child = vnode.componentInstance = createComponentInstanceForVnode(
  vnode,
  activeInstance
)
// createComponentInstanceForVnode()
export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
) :Component {
  const options: InternalComponentOptions = {
    _isComponent: true._parentVnode: vnode,
    parent
  }
  // ...
  return new vnode.componentOptions.Ctor(options)
}
Copy the code

InitInternalComponent does a few simple object assignments that I won’t go into. The code looks like this:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}
Copy the code

Now, if you’re a little confused at this point, let me give you an example

<template>
  <div class="hello">
    {{ msg }}
  </div>
</template>

<script>
export default {
  name: 'HelloWorld'.props: {
    msg: String
  },
  created () {
    console.log('this is child')}}</script>
Copy the code

The call is then made in the parent component

<template>
  <div class="home">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'home'.components: {
    HelloWorld
  },
  created () {
    console.log('this is parent')}}</script>
Copy the code

After the merge strategy, the vm.$options values are roughly as follows

vm.$options = {
  parent: VueComponent, // Parent component instance
  propsData: {
    msg: 'Welcome to Your Vue.js App'
  },
  _componentTag: 'HelloWorld'._parentListeners: undefined._parentVnode: VNode, // Parent vNode instance
  _propKeys: ['msg']._renderChildren: undefined.__proto__: {
    components: {
      HelloWorld: function VueComponent(options) {}},directives: {},
    filters: {},
    _base: function Vue(options) {},
    _Ctor: {},
    created: [
      function created() {
        console.log('this is parent')},function created() {
        console.log('this is child'}]}}Copy the code

Asynchronous components

When analyzing createComponent above, we left out a few special cases, one of which was the case of asynchronous components. In this scenario, if ctor. cid is not defined, the asynchronous component creation process is directly followed, with the following code

let asyncFactory
if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  if (Ctor === undefined) {
    // return a placeholder node for async component, which is rendered
    // as a comment node but preserves all the raw information for the node.
    // the information will be used for async server-rendering and hydration.
    return createAsyncPlaceholder(
      asyncFactory,
      data,
      context,
      children,
      tag
    )
  }
}
Copy the code

Before we dive into the details, let’s take a look at the official example to see how asynchronous components are used differently than normal components

// Common components
Vue.component('my-component-name', {
  // ... options ...
})

// Asynchronous components
Vue.component('async-webpack-example'.function (resolve, reject) {
  // This particular require syntax
  // will instruct WebPack to automatically build the code,
  // Split into different bundles and load them via Ajax requests.
  require(['./my-async-component'], resolve)
})
Copy the code

In our example, the Vue generic component is an object, while the asynchronous component is a factory function that takes two arguments, a resolve callback to fetch the component-defined object from the server, and a Reject callback to indicate a load failure. In addition to the above, asynchronous components can be written in two other ways

// Promise asynchronous component
Vue.component(
  'async-webpack-example'.// the 'import' function returns a Promise.
  () = > import('./my-async-component'))// Advanced asynchronous components
const AsyncComponent = () = > ({
  // Load the component (eventually return a Promise)
  component: import('./MyComponent.vue'),
  // An asynchronous component is loading
  loading: LoadingComponent,
  // Failed to load, display this component
  error: ErrorComponent,
  // Displays the delay time before loading components. Default value: 200ms.
  delay: 200.// If a timeout is provided and the load time exceeds this timeout,
  // Displays the error component. Default: Infinity.
  timeout: 3000
})
Vue.component('async-component', AsyncComponent)
Copy the code

1, resolveAsyncComponent

ResolveAsyncComponent supports the three asynchronous component creation methods described above

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
) :Class<Component> | void {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    // already pending
    factory.owners.push(owner)
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if(owner && ! isDef(factory.owners)) {const owners = factory.owners = [owner]
    let sync = true
    let timerLoading = null
    let timerTimeout = null; (owner: any).$on('hook:destroyed'.() = > remove(owners, owner))

    const forceRender = (renderCompleted: boolean) = > {
      for (let i = 0, l = owners.length; i < l; i++) {
        (owners[i]: any).$forceUpdate()
      }

      if (renderCompleted) {
        owners.length = 0
        if(timerLoading ! = =null) {
          clearTimeout(timerLoading)
          timerLoading = null
        }
        if(timerTimeout ! = =null) {
          clearTimeout(timerTimeout)
          timerTimeout = null}}}const resolve = once((res: Object | Class<Component>) = > {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if(! sync) { forceRender(true)}else {
        owners.length = 0}})const reject = once(reason= >{ process.env.NODE_ENV ! = ='production' && warn(
        `Failed to resolve async component: The ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : ' '))if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)}})const res = factory(resolve, reject)

    if (isObject(res)) {
      if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isPromise(res.component)) {
        res.component.then(resolve, reject)

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            factory.loading = true
          } else {
            timerLoading = setTimeout(() = > {
              timerLoading = null
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender(false)
              }
            }, res.delay || 200)}}if (isDef(res.timeout)) {
          timerTimeout = setTimeout(() = > {
            timerTimeout = null
            if(isUndef(factory.resolved)) { reject( process.env.NODE_ENV ! = ='production'
                  ? `timeout (${res.timeout}ms)`
                  : null
              )
            }
          }, res.timeout)
        }
      }
    }

    sync = false
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}
Copy the code

First let’s look at how asynchronous components are loaded. Here we’ll skip over what resolveAsyncComponent does to the advanced asynchronous components we mentioned earlier in the first place.

Before looking at the resolveAsyncComponent asynchronous component creation logic, let’s take a look at some of the core methods used

  • ForceRender: Force the component to rerender, then remove the owners of the factory function when render is done, and remove timerLoading and timerTimeout.

    $forceUpdate: Calls Watcher’s update method, which is a re-rendering of the component. In vUE, only data changes will trigger the view to be rerendered. In asynchronous components, data does not change during loading, so the component will not be rerendered by executing $forceUpdate to force it to be rerendered

const forceRender = (renderCompleted: boolean) = > {
  for (let i = 0, l = owners.length; i < l; i++) {
    (owners[i]: any).$forceUpdate()
  }

  if (renderCompleted) {
    owners.length = 0
    if(timerLoading ! = =null) {
      clearTimeout(timerLoading)
      timerLoading = null
    }
    if(timerTimeout ! = =null) {
      clearTimeout(timerTimeout)
      timerTimeout = null
    }
  }
}

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}
Copy the code
  • once: Uses closures and an identity variablecalledEnsure that its wrapped functions are executed only once
export function once (fn: Function) :Function {
  let called = false
  return function () {
    if(! called) { called =true
      fn.apply(this.arguments)}}}Copy the code
  • Resolve: The internal resolve function, which first executes ensureCtor and returns its resolved value for the factory. Then, if the sync asynchronous variable is false, execute forceRender directly to force the component to rerender, otherwise empty the owners

    EnsureCtor is a function defined to ensure that a component object defined on an asynchronous component can be found. If it is found to be a normal object, it is converted directly to the component’s constructor via vue.extend

const resolve = once((res: Object | Class<Component>) = > {
  factory.resolved = ensureCtor(res, baseCtor)
  if(! sync) { forceRender(true)}else {
    owners.length = 0}})function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}
Copy the code
  • rejectInternal:rejectFunction that is executed when the asynchronous component load fails
const reject = once(reason= >{ process.env.NODE_ENV ! = ='production' && warn(
    `Failed to resolve async component: The ${String(factory)}` +
    (reason ? `\nReason: ${reason}` : ' '))if (isDef(factory.errorComp)) {
    factory.error = true
    forceRender(true)}})Copy the code

After looking at the core methods, let’s look at how asynchronous components are created.

  1. We learn fromresolveAsyncComponentIt is known from the definition that the method takes two arguments, one of which isfactoryThe factory function, one isbaseCtor, i.e.,Vue.
  2. Then the current render instance exists, and infactory.ownersExists in the case that the component enterspendingPhase, the current instance is thrown directly tofactory.ownersIn the.
  3. However, when an asynchronous component is initializedfactoryThere will not beownersWell, what should I do then? It’s simple. Just do itfactoryFactory function, and put the internally definedresolverejectFunction as an argument, so we can go right throughresolverejectDoing something, this logic is exactly what is supported for normal asynchronous components, and the code is shown below
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
  // already pending
  factory.owners.push(owner)
}
if(owner && ! isDef(factory.owners)) {const owners = factory.owners = [owner]
  let sync = true; (owner: any).$on('hook:destroyed'.() = > remove(owners, owner))
  const forceRender = (renderCompleted: boolean) = > {
    // ...
  }
  const resolve = once((res: Object | Class<Component>) = > {
    // ...
  })
  const reject = once(reason= > {
    // ...
  })
  const res = factory(resolve, reject)
  // ...
}
Copy the code
  • PromiseAsynchronous components

In VUE, you can load components asynchronously using WebPack2 + + ES6, as shown below

Vue.component(
  'async-webpack-example'.// the 'import' function returns a Promise.
  () = > import('./my-async-component'))Copy the code

After res = factory(resolve, reject), res is the return value of import(‘./my-async-component’), which is a Promise object. The processing logic of the Promise asynchronous component is then entered. Resolve is executed if the asynchronous component is loaded successfully, and reject is executed if the asynchronous component fails

const res = factory(resolve, reject)
if (isPromise(res)) {
  // () => Promise
  if (isUndef(factory.resolved)) {
    res.then(resolve, reject)
  }
}
Copy the code
  • Advanced asynchronous component

In the 2.3.0+ version, vue has added the loading state processing function, that is, it throws some configurable fields to the user, including Component, loading, error, delay, timeout, Component supports the form of Promise asynchronous component loading. The case code is as follows

const AsyncComponent = () = > ({
  // Load the component (eventually return a Promise)
  component: import('./MyComponent.vue'),
  // An asynchronous component is loading
  loading: LoadingComponent,
  // Failed to load, display this component
  error: ErrorComponent,
  // Displays the delay time before loading components. Default value: 200ms.
  delay: 200.// If a timeout is provided and the load time exceeds this timeout,
  // Displays the error component. Default: Infinity.
  timeout: 3000
})
Vue.component('async-component', AsyncComponent)
Copy the code

If res = factory(resolve, Reject) and res.component returns a Promise, execute the then method as follows

else if (isPromise(res.component)) {
  res.component.then(resolve, reject)
}
Copy the code

This is followed by processing of the other four configurable fields

  1. First determine if it is customerrorComponent, if any, executeensureCtor(res.error, baseCtor)And assigns the return value directly tofactory.errorComp
  2. Same thing if it’s passed inloadingComponent is executedensureCtor(res.loading, baseCtor)And assigns the return value directly tofactory.loadingComp
  3. And then, in the definitionloadingComponent logic, if setdelayIf the value is 0, thefactory.loadingA value fortrue, otherwise delaydelayTo perform,delayThe default delay is 200ms
  4. Finally, if the component load is settimeoutLoad time if the component is inres.timeoutIf the load fails, run the command directlyrejectTo throw the wrong
if (isDef(res.error)) {
  factory.errorComp = ensureCtor(res.error, baseCtor)
}

if (isDef(res.loading)) {
  factory.loadingComp = ensureCtor(res.loading, baseCtor)
  if (res.delay === 0) {
    factory.loading = true
  } else {
    timerLoading = setTimeout(() = > {
      timerLoading = null
      if (isUndef(factory.resolved) && isUndef(factory.error)) {
        factory.loading = true
        forceRender(false)
      }
    }, res.delay || 200)}}if (isDef(res.timeout)) {
  timerTimeout = setTimeout(() = > {
    timerTimeout = null
    if(isUndef(factory.resolved)) { reject( process.env.NODE_ENV ! = ='production'
        ? `timeout (${res.timeout}ms)`
        : null
      )
    }
  }, res.timeout)
}
Copy the code

Finally, different values are returned by determining factory.loading. As we know from the processing of the custom field loading above, if the custom field delay is set to 0, it means that the loading component is directly rendered this time. Otherwise it will simply delay and execute to the forceRender method, which will trigger a re-rendering of the component, which will execute resolveAsyncComponent again

sync = false
return factory.loading
  ? factory.loadingComp
	: factory.resolved
Copy the code

Then we return to the actions we skipped at the beginning of the resolveAsyncComponent

if (isTrue(factory.error) && isDef(factory.errorComp)) {
  return factory.errorComp
}

if (isDef(factory.resolved)) {
  return factory.resolved
}

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
  return factory.loadingComp
}
Copy the code

2, createAsyncPlaceholder

If you execute resolveAsyncComponent for the first time, the return value of resolveAsyncComponent will be undefined, unless you set delay to 0. In order to prevent the node information from being captured when Ctor is undefined, an annotated VNode is created directly from createAsyncPlaceholder, which will be used as a placeholder for asynchronous components and retain all the original information about the vNode. The specific code is as follows

export function createAsyncPlaceholder (
  factory: Function, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag: ? string) :VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}
Copy the code

Functional components

One issue left unaddressed when analyzing createComponent component creation is the functional Component, which is described in the following scenario

// functional component
if (isTrue(Ctor.options.functional)) {
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}
Copy the code

In case you don’t know what a functional component is, let me list two officially supported ways to write functional components

// render function
Vue.component('my-component', {
  functional: true.// Props is optional
  props: {
    // ...
  },
  // To make up for missing instances
  // We provide the second argument context as the context
  render: function (createElement, context) {
    // ...}})// template functional
<template functional></template>
Copy the code

For more information, go directly to the official documentation and read it carefully.

Official definition of a functional component: The component is marked functional and has no state, no responsive data, and no instance, i.e. no this context.

Let’s take the veil off functional components.

1, createFunctionalComponent

CreateFunctionalComponent main core is divided into three steps

  1. willCtor.optionsIn thepropsMerge to new objectpropsIn the. ifCtor.optionsThere areprops, directly traverses itprops, the implementation ofvalidatePropCtor.options.propsCurrent property validates and copies the current property toprops[key]. ifCtor.options.propsIf not defined, willdataWell defined aboveattrspropsBy performingmergePropsFunction merged into the new objectpropsOn.
  2. performnew FunctionalRenderContextinstantiationfunctionalComponent context, and executeoptionsOn therenderFunction instantiationvnodenode
  3. For instantiatedvnodePerform a special clone operation and return
export function createFunctionalComponent (
  Ctor: Class<Component>,
  propsData: ?Object,
  data: VNodeData,
  contextVm: Component,
  children: ?Array<VNode>
) :VNode | Array<VNode> | void {
  const options = Ctor.options
  const props = {}
  const propOptions = options.props
  if (isDef(propOptions)) {
    for (const key in propOptions) {
      props[key] = validateProp(key, propOptions, propsData || emptyObject)
    }
  } else {
    if (isDef(data.attrs)) mergeProps(props, data.attrs)
    if (isDef(data.props)) mergeProps(props, data.props)
  }

  const renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  )

  const vnode = options.render.call(null, renderContext._c, renderContext)

  if (vnode instanceof VNode) {
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
  } else if (Array.isArray(vnode)) {
    const vnodes = normalizeChildren(vnode) || []
    const res = new Array(vnodes.length)
    for (let i = 0; i < vnodes.length; i++) {
      res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext)
    }
    return res
  }
}
Copy the code

The two helper functions mentioned above are as follows

  1. cloneAndMarkFunctionalResult: To avoid reusing nodes,fnContextCauses the named slot point to mismatch the case directly in the settingfnContextThe node is cloned before, and the cloned node is returnedvnode
  2. mergeProps:propsMerge strategy
function cloneAndMarkFunctionalResult (vnode, data, contextVm, options, renderContext) {
  const clone = cloneVNode(vnode)
  clone.fnContext = contextVm
  clone.fnOptions = options
  if(process.env.NODE_ENV ! = ='production') {
    (clone.devtoolsMeta = clone.devtoolsMeta || {}).renderContext = renderContext
  }
  if (data.slot) {
    (clone.data || (clone.data = {})).slot = data.slot
  }
  return clone
}

function mergeProps (to, from) {
  for (const key in from) {
    to[camelize(key)] = from[key]
  }
}
Copy the code

2, FunctionalRenderContext

We know from the documentation that functional components can be written in two ways, the first as a render function and the other as a < Template functional> single-file component. Render the function of the way in the treatment of createFunctionalComponent support has been done, it will direct execution Ctor. The options on the render method. The < Template functional> single-file component approach is also supported in the functional component render context constructor.

  1. First, it is designed to ensure that functional componentscreateElementThe function is able to get a unique context that will be clonedparentObject to the contextvmvariablecontextVm.contextVm._originalThe assignment isparent, as a token of its context source. One of the more critical cases is if the context is passed invmIt’s also a functional context. How do I do that? As long as you follow_uidTo reverse the logic,contextVmreceiveparent.parentreceiveparent._originalCan, because go up to continue to look, always can find existence_uidparentIt isn’t.
  2. The next step is to look at functional componentsdata,props,listeners,injectionsSupport processing, here forslotsI’ve done a layer of transformation, and I’m going tonormal slotsObject converted toscoped slots
  3. Finally,options._scopeIdThe presence or absence of different scenarioscreateElementCreating a node
export function FunctionalRenderContext (
  data: VNodeData,
  props: Object,
  children: ?Array<VNode>,
  parent: Component,
  Ctor: Class<Component>
) {
  const options = Ctor.options
  let contextVm
  if (hasOwn(parent, '_uid')) {
    contextVm = Object.create(parent)
    contextVm._original = parent
  } else {
    contextVm = parent
    parent = parent._original
  }
  const isCompiled = isTrue(options._compiled)
  constneedNormalization = ! isCompiledthis.data = data
  this.props = props
  this.children = children
  this.parent = parent
  this.listeners = data.on || emptyObject
  this.injections = resolveInject(options.inject, parent)
  this.slots = () = > {
    if (!this.$slots) {
      normalizeScopedSlots(
        data.scopedSlots,
        this.$slots = resolveSlots(children, parent)
      )
    }
    return this.$slots
  }

  Object.defineProperty(this.'scopedSlots', ({
    enumerable: true,
    get () {
      return normalizeScopedSlots(data.scopedSlots, this.slots())
    }
  }: any))

  // support for compiled functional template
  if (isCompiled) {
    // exposing $options for renderStatic()
    this.$options = options
    // pre-resolve slots for renderSlot()
    this.$slots = this.slots()
    this.$scopedSlots = normalizeScopedSlots(data.scopedSlots, this.$slots)
  }

  if (options._scopeId) {
    this._c = (a, b, c, d) = > {
      const vnode = createElement(contextVm, a, b, c, d, needNormalization)
      if (vnode && !Array.isArray(vnode)) {
        vnode.fnScopeId = options._scopeId
        vnode.fnContext = parent
      }
      return vnode
    }
  } else {
    this._c = (a, b, c, d) = > createElement(contextVm, a, b, c, d, needNormalization)
  }
}
Copy the code

5. Abstract components

I’ve written several articles about abstract components, so I won’t go over them here. Click on the portal to read for yourself.

  • In Detail on Vue-Transition
  • The Vue-Transition-Group
  • Vue – Abstract Components In Action

conclusion

This article, after a large length of text analysis, we have a comprehensive understanding of vUE component creation (including asynchronous component creation, functional component creation and abstract component creation), component hook function, component configuration merge and so on.

Here I also hope you can friend after understanding the principle of component, in its own business development, can be the best combination of business component development practices, such as my personal for permissions in the business operation of unified management and use the individual thinks the best solution – abstract component, it is very good rights management is the business pain points

Front end communication group: 731175396, welcome to join