细谈 vue - component 篇

本篇文章是细谈 vue 系列的第六篇。看过我这个系列文章的小伙伴都知道:文章贼长,看不下去的建议先点个赞当收藏,而后等有时间静下心来慢慢看,前端交流群:731175396。之前的文章传送门以下javascript

用过 vue 的小伙伴确定知道,在 vue 的开发中,component 可谓是随处可见,项目中的那一个个 .vue (SFC) 文件,可不就是一个个的组件么。html

那么,既然 component 这么核心,这么重要,为什么很差好来研究一波呢?前端

why not ?vue

​ — 鲁迅java

1、组件建立

以前咱们分析 vdom 的时候分析过一个函数 createElement,与它相同的是 createComponent,二者都是用来建立 vnode 节点的,若是是普通的 html 标签,则直接实例化一个普通的 vnode 节点,不然经过 createComponent 来建立一个 Component 类型的 vnode 节点node

一、createElement

这里仅列出不一样状况下 vnode 节点建立的代码webpack

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)
}
复制代码

二、createComponent

接下来,咱们先看 createComponent() 的定义,具体以下git

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 (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${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
}
复制代码
  • 在其内部,第一件事情就是将构造函数 Vue 赋值给变量 baseCtor ,并经过 extend 将参数 Ctor 进行扩展
const baseCtor = context.$options._base
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}
复制代码

这里咱们看到 $options._base ,其实就是构造函数 Vuegithub

// 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)
复制代码
  • 其次,紧接着,断定组件是否为异步组件、函数式组件或者抽象组件。具体每种状况的处理后面我再详细分析
// 异步组件
let asyncFactory
if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  if (Ctor === undefined) {
    return createAsyncPlaceholder(
      asyncFactory,
      data,
      context,
      children,
      tag
    )
  }
}

// 函数式组件
if (isTrue(Ctor.options.functional)) {
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// 抽象组件
if (isTrue(Ctor.options.abstract)) {
  const slot = data.slot
  data = {}
  if (slot) {
    data.slot = slot
  }
}
复制代码
  • 对于组件上的事件也有相关处理,它会提取组件上的事件监听器。它须要做为子组件的监听器,而并不是DOM监听器。因此须要将其替换为拥有 .native 修饰符的侦听器,让其能在父组件 patch 阶段可以获得处理
const listeners = data.on
data.on = data.nativeOn
复制代码
  • 而后,安装组件的钩子函数。它将 componentVNodeHooks 的钩子函数合并到 data.hook 中,而后 Component 类型的 vnode 节点在 patch 过程当中会执行相关的钩子函数,若是某个时机的钩子函数已经存在,则经过 mergeHook 将函数合并,即依次执行同一时机的这两个函数
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
}
复制代码

三、componentVNodeHooks

上面的 componentVNodeHooks 则是组件初始化的时候实现的几个钩子函数,分别有 initprepatchinsertdestroyweb

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 */)
      }
    }
  }
}
复制代码

接下来咱们来仔细看看 componentVNodeHooks 里面的四个钩子函数都作了些什么

  1. init :当 vnodekeep-alive 组件时、存在实例且没被销毁,为了防止组件流动,直接执行了 prepatch。不然直接经过执行 createComponentInstanceForVnode 建立一个 Component 类型的 vnode 实例,并进行 $mount 操做
  2. prepatch:将已有组件更新成最新的 vnode 上的数据,这里没啥好说的
  3. insertinsert 钩子函数
    • 首先会断定组件实例是否已经被 mounted,若没被渲染,则直接将 componentInstance 做为参数执行 mounted 钩子函数。
    • 其次,则是组件为 keep-alive 内置组件的状况。这里有个操做有点骚,就是当它已经 mounted 了的时候,进入 insert 阶段的时候,为了防止 keep-alive 子组件更新触发 activated 钩子函数,直接就放弃了 walking tree 的更新机制,而是直接将组件实例 componentInstance 丢到 activatedChildren 这个数组中。固然没有 mounted 的状况则直接触发 activated 钩子函数进行 mounted 便可
  4. destroy:组件销毁操做,这里一样对 keep-alive 组件作了兼容。若是不是 keep-alive 组件,直接执行 $destory 销毁组件实例,不然触发 deactivated 钩子函数进行销毁。

上面用的一些辅助函数以下

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)
}
复制代码
  • 最后实例化 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
)
return vnode
复制代码

createComponent 的所有过程就是:首先先构建 Vue 的子类构造函数,而后安装组件的钩子函数,最后实例化 VNode,而后返回。里面的不少操做都对 keep-alive 内置组件作了不少兼容。因此假如你用过 keep-alive 组件,而且恰巧看到这,相信你会有不少感悟。

2、配置合并

一般来讲,设计一款插件或者组件,为了保证其可定制化、可扩展性,通常会在自身定义一些默认配置,而后在内部作好 merge 配置项的操做,让你能在其初始化阶段进行自定义的配置。

固然,Vue 在这块设计也是如此。vue 中对于 options 合并策略其实我上面也列出过代码,具体在 src/core/instance/init.js 中(这里我只保留相关代码)。

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
      )
    }
    // ...
  }
}
复制代码

能看出来,合并策略有两个。一种是为 Component 组件的状况下,执行 initInternalComponent 进行内部组件配置合并,一种是非组件的状况,直接经过 mergeOptions 作配置合并。

一、normal merge

这里直接将 resolveConstructorOptions(vm.constructor) 的返回值和 options 进行合并

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
复制代码

咱们先来看下 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'
]
复制代码

接着,咱们再来看看 mergeOptions 的逻辑:它是 vue 核心合并策略之一,它主要功能就是将 parantchild 进行策略合并,而后返回一个新的对象,代码在 src/core/util/options.js 中。

  1. 首先会先对 child 上面的 propsinjectdirectives 进行 object format 操做(具体逻辑可自行研究,主要就是对其进行 object 转换操做)
  2. child._base 不存在,遍历 child.extendschild.mixins ,将其合并到 parent
  3. 遍历 parent,调用 mergeField 合并到变量 options
  4. 遍历 child,若 childparent 不存在的属性,则调用 mergeField 将该属性合并到 options
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
}
复制代码

vue 中除了对 options 的合并外,还有不少合并策略,感兴趣的能够本身去 src/core/util/options.js 中查阅研究

二、component merge

在分析 createComponent 的时候咱们了解到组件的构造函数是经过 Vue.extendVue 进行继承的,代码以下

// 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)
}
复制代码

咱们再来 Vue.extend, 它的定义在 src/core/global-api/extend.js 中(仅保留关键逻辑),它经过执行 mergeOptions()Super.options,即 Vue.options 合并到 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
  }
}
复制代码

而后在 componentVNodeHooksinit 钩子函数中,即子组件的初始化阶段,会执行 createComponentInstanceForVnode 进行组件实例的初始化。createComponentInstanceForVnode 函数中的 vnode.componentOptions.Ctor 指向的其实就是上面 Vue.extend 中返回的 Sub,因此执行 new 操做的时候会执行到 this._init(options),即 Vue._init(options) 操做,又由于 options._isComponent 的定义是 true,因此直接进入了 initInternalComponent 操做

// 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)
}
复制代码

initInternalComponent 只是作了一些简单的对象赋值,具体我就不分析了,代码以下:

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
  }
}
复制代码

讲到这,可能有些小伙伴会有些懵,我举个例子来讲明下

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

<script> export default { name: 'HelloWorld', props: { msg: String }, created () { console.log('this is child') } } </script>
复制代码

而后在父组件进行调用

<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>
复制代码

走完上面的合并策略后,vm.$options 的值大体以下

vm.$options = {
  parent: VueComponent, // 父组件实例
  propsData: {
    msg: 'Welcome to Your Vue.js App'
  },
  _componentTag: 'HelloWorld',
  _parentListeners: undefined,
  _parentVnode: VNode, // 父节点 vnode 实例
  _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')
      }
    ]
  }
}
复制代码

3、异步组件

在上面分析 createComponent 的时候,咱们留下几种特殊状况没有分析,其中一种就是异步组件的状况。它的场景是,当 Ctor.cid 未定义的状况下,则直接走异步组件建立的流程,具体代码以下

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
    )
  }
}
复制代码

在作具体分析前,咱们先经过官方示例来看下异步组件的用法和普通组件的用法有何不一样

// 普通组件
Vue.component('my-component-name', {
  // ... options ...
})

// 异步组件
Vue.component('async-webpack-example', function (resolve, reject) {
  // 这个特殊的 require 语法
  // 将指示 webpack 自动将构建后的代码,
  // 拆分到不一样的 bundle 中,而后经过 Ajax 请求加载。
  require(['./my-async-component'], resolve)
})
复制代码

在例子中,Vue 普通组件是一个对象,而异步组件则是一个工厂函数,它接收2个参数,一个 resolve 回调函数用来从服务器获取到组件定义的对象,另一个 reject 回调函数来代表加载失败。除了上面的写法外,异步组件还支持如下两种写法

// Promise 异步组件
Vue.component(
  'async-webpack-example',
  // `import` 函数返回一个 Promise.
  () => import('./my-async-component')
)

// 高级异步组件
const AsyncComponent = () => ({
  // 加载组件(最终应该返回一个 Promise)
  component: import('./MyComponent.vue'),
  // 异步组件加载中(loading),展现为此组件
  loading: LoadingComponent,
  // 加载失败,展现为此组件
  error: ErrorComponent,
  // 展现 loading 组件以前的延迟时间。默认:200ms。
  delay: 200,
  // 若是提供 timeout,而且加载用时超过此 timeout,
  // 则展现错误组件。默认:Infinity。
  timeout: 3000
})
Vue.component('async-component', AsyncComponent)
复制代码

一、resolveAsyncComponent

resolveAsyncComponent 主要功能就是对上面说起的 3 种异步组件建立方式进行支持,具体代码以下

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: ${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
  }
}
复制代码

首先咱们先来看看对于异步组件是如何加载的。这里咱们先跳过 resolveAsyncComponent 一开始就对咱们前面说起的高级异步组件作的处理。

在分析 resolveAsyncComponent 异步组件建立逻辑前,咱们先过看看其中会用到的一些核心的方法

  • forceRender:对组件强制进行从新渲染,而后在 render 完成的时候清掉工厂函数中当前的渲染实例 owners,顺带把 timerLoadingtimerTimeout 清除掉。

    $forceUpdate:调用 watcherupdate 方法,即组件的从新渲染。对 vue 中通常只有数据变动才会触发视图的从新渲染,而异步组件在加载过程当中数据是不会发生变化的,那么这个时候是不会触发组件从新渲染的,因此须要经过执行 $forceUpdate 强制对组件进行从新渲染

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()
  }
}
复制代码
  • once:利用闭包以及一个标识变量 called 保证其包装的函数只会执行一次
export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}
复制代码
  • resolve:内部 resolve 函数,首先会执行 ensureCtor 并将其返回值做为 factoryresolved 值。紧接着若 sync 异步变量为 false ,则直接执行 forceRender 强制让组件从新渲染,不然则清空 owners

    ensureCtor 则是为了保证能找到异步组件上定义的组件对象而定义的函数。若是发现它是普通对象,则直接经过 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
}
复制代码
  • reject:内部 reject 函数,异步组件加载失败时执行
const reject = once(reason => {
  process.env.NODE_ENV !== 'production' && warn(
    `Failed to resolve async component: ${String(factory)}` +
    (reason ? `\nReason: ${reason}` : '')
  )
  if (isDef(factory.errorComp)) {
    factory.error = true
    forceRender(true)
  }
})
复制代码

看完其中的核心方法后,接下来咱们具体异步组件是如何建立的。

  1. 咱们从 resolveAsyncComponent 的定义中知道该方法接收 2 个参数,一个是 factory 工厂函数,一个是 baseCtor ,即 Vue
  2. 而后在当前渲染实例存在、且在 factory.owners 中存在的状况下,即组件进入 pending 阶段,则直接将当前实例丢到 factory.owners 中。
  3. 然而,初始化异步组件的时候 factory 是不会有 owners 滴,那这个时候又该怎么办呢?很简单呗,直接执行 factory 工厂函数,并把内部定义的 resolvereject 函数做为其参数,这样咱们就能直接经过 resolvereject 作点事了,这些逻辑也正是对普通异步组件支持的逻辑,相关代码以下
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)
  // ...
}
复制代码
  • Promise 异步组件

vue 中,你可使用 webpack2+ + ES6 的方式来异步加载组件,以下

Vue.component(
  'async-webpack-example',
  // `import` 函数返回一个 Promise.
  () => import('./my-async-component')
)
复制代码

即执行完 res = factory(resolve, reject) 时,res 的值为 import('./my-async-component') 的返回值,是一个 Promise 对象。以后进入 Promise 异步组件的处理逻辑,异步组件加载成功后执行 resolve,加载失败则执行 reject

const res = factory(resolve, reject)
if (isPromise(res)) {
  // () => Promise
  if (isUndef(factory.resolved)) {
    res.then(resolve, reject)
  }
}
复制代码
  • 高级异步组件

其实这里所谓的高级,其实就是 vue 在 2.3.0+ 版本新增了加载状态处理的功能,即抛出了一些可配置的字段给用户,其中有 componentloadingerrordelaytimeout,其中 component 支持 Promise 异步组件加载的形式,具体案例代码以下

const AsyncComponent = () => ({
  // 加载组件(最终应该返回一个 Promise)
  component: import('./MyComponent.vue'),
  // 异步组件加载中(loading),展现为此组件
  loading: LoadingComponent,
  // 加载失败,展现为此组件
  error: ErrorComponent,
  // 展现 loading 组件以前的延迟时间。默认:200ms。
  delay: 200,
  // 若是提供 timeout,而且加载用时超过此 timeout,
  // 则展现错误组件。默认:Infinity。
  timeout: 3000
})
Vue.component('async-component', AsyncComponent)
复制代码

和刚分析 Promise 异步组件加载逻辑同样,若执行完 res = factory(resolve, reject)res.component 的返回值是 Promise 的话,直接执行 then 方法,代码以下

else if (isPromise(res.component)) {
  res.component.then(resolve, reject)
}
复制代码

紧接着就是对其它 4 个可配置字段的处理

  1. 首先断定是否自定义了 error 组件,若是有,执行 ensureCtor(res.error, baseCtor) 并将返回值直接赋值给 factory.errorComp
  2. 同理若传入了 loading 组件,则执行 ensureCtor(res.loading, baseCtor) 并将返回值直接赋值给 factory.loadingComp
  3. 紧接着,在定义了 loading 组件的逻辑中,若设置了 delay 值为 0,则直接将 factory.loading 值设为 true,不然延时 delay 执行,delay 未设置,延时默认为 200ms
  4. 最后,若设置了组件加载的 timeout 加载时长的话,若组件在 res.timeout 时间后还未加载成功,则直接执行 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)
}
复制代码

而后最后经过断定 factory.loading 进行不一样值的返回,从上面自定义字段 loading 的处理咱们得知,若自定义字段 delay 设为 0,则说明此次直接渲染 loading 组件,不然会直接延时并执行到 forceRender 方法,这样就会触发组件的从新渲染,从而再次执行 resolveAsyncComponent

sync = false
return factory.loading
  ? factory.loadingComp
	: factory.resolved
复制代码

而后咱们再次回到 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
}
复制代码

二、createAsyncPlaceholder

从上面的对 resolveAsyncComponent 的分析中,咱们得知,若是是第一次执行 resolveAsyncComponent ,返回值会是 undefined,固然,你将 delay 值设置为 0 的时候除外。为了不 Ctorundefined 时,致使节点信息没法捕获的状况,会直接经过 createAsyncPlaceholder 建立一个注释的 vnode 节点,做为异步组件的占位符,同时用来保留该 vnode 节点全部的原始信息。具体代码以下

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
}
复制代码

4、函数式组件

分析 createComponent 组件建立的时候咱们还留下一个问题没讲,那就是 functional component(函数式组件),具体场景以下

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

分析前,防止有小伙伴不是很了解函数式组件是啥,我先列举两个官方支持的函数式组件写法

// 方式一 render function
Vue.component('my-component', {
  functional: true,
  // Props 是可选项
  props: {
    // ...
  },
  // 为了弥补缺乏的实例
  // 咱们提供了第二个参数 context 做为上下文
  render: function (createElement, context) {
    // ...
  }
})

// 方式二 template functional
<template functional></template>
复制代码

想了解更多的,能够直接去官方文档先仔细阅读。

函数式组件官方定义:组件被标记成 functional,它无状态,无响应式 data,无实例,即没有 this 上下文。

下面咱们就来揭开函数式组件的面纱。

一、createFunctionalComponent

createFunctionalComponent 主要核心分为三步

  1. Ctor.options 中的 props 合并到新对象 props 中。若 Ctor.options 存在 props,直接遍历其 props,执行 validatePropCtor.options.props 当前属性进行校验并将当前属性复制给 props[key]。若 Ctor.options.props 未定义,则将 data 上定义好的 attrsprops 经过执行 mergeProps 函数合并到新对象 props 上。
  2. 执行 new FunctionalRenderContext 实例化 functional 组件的上下文,并执行 options 上的 render 函数实例化 vnode 节点
  3. 对实例化的 vnode 进行特殊的克隆操做并进行返回
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
  }
}
复制代码

上面说起的两个辅助函数以下

  1. cloneAndMarkFunctionalResult :为了不复用节点,fnContext 致使命名槽点不匹配的状况,直接在设置 fnContext 以前克隆节点,最后返回克隆好的 vnode
  2. mergePropsprops 合并策略
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]
  }
}
复制代码

二、FunctionalRenderContext

从文档中咱们知道函数式组件支持两种书写方式,第一种是 render function 的方式,另一种则是 <template functional> 单文件组件的方式。render function 的方式在 createFunctionalComponent 的处理中已经作了支持,它会直接执行 Ctor.options 上的 render 方法。在函数式组件渲染上下文构造函数 FunctionalRenderContext 中则是对 <template functional> 单文件组件的方式也进行了支持。

  1. 首先,它为了确保函数式组件的 createElement 函数可以得到惟一的上下文,将克隆的 parent 对象赋值给上下文 vm 变量 contextVmcontextVm._original 则赋值为 parent,当作其上下文来源的标记。其中有种比较临界的状况就是,若传入的上下文 vm 也是函数式上下文,这该怎么办呢?其实只要按照 _uid 存在的状况来逆向推进下逻辑便可,contextVm 接收 parentparent 接收 parent._original 便可,由于往上继续找,总能找着存在 _uidparent 不是。
  2. 接下来就是对函数式组件中 datapropslistenersinjections 等进行支持处理,这里对于 slots 作了一层转换处理,即将 normal slots 对象转换成 scoped slots
  3. 最后对 options._scopeId 存在与否的场景进行不一样的 createElement 节点建立操做
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)
  const needNormalization = !isCompiled

  this.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)
  }
}
复制代码

5、抽象组件

对于抽象组件,我曾经写过几篇文章对其进行分析,因此这里就再也不赘述了,想看的能够点击传送门自行去阅读。

总结

文章到这,通过了大篇幅的文字分析,咱们对 vue 中组件的建立(其中包括异步组件的建立、函数式组件的建立以及抽象组件的建立)、组件的钩子函数、组件配置合并等都有了一个较为全面的了解。

这里我也但愿各位小伙伴能在了解组件的这些原理之后,在自身业务开发中,能够结合业务进行最佳的组件开发实践,好比我我的曾因业务中的权限操做的统一管理而采用了我的认为的最佳方案 - 抽象组件,它很好的解决权限管理这一业务痛点

前端交流群:731175396,欢迎你们加入

我的准备从新捡回本身的公众号了,以后每周保证一篇高质量好文,感兴趣的小伙伴能够关注一波。

相关文章
相关标签/搜索