Skip to content

组件实现原理

render 阶段

对于子组件的编译过程,其实和普通标签一样,最终都会编译成_c函数(对应_createElement函数),但是在render阶段生成Vnode的时候就有所不同了:

javascript
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...
  if (typeof tag === 'string') {
    let Ctor
    // 1. 如果是普通标签,那么直接创建 vnode
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // 2. 否则的话,可能是组件,这个时候通过 resolveAsset 去 $options 中查找对应的 组件配置
    // 如果是全局组件注册,会返回一个构造函数,如果是局部组件注册,会返回 组件配置,也就是options
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  }
  // ...
}

_c方法会判断标签的类型,如果不是普通标签,那么此时可能是组件标签,需要查找组件的定义。如果找到了组件的定义,那么调用createComponent为组件生成Vnode:

javascript
export function createComponent (
 Ctor: Class<Component> | Function | Object | void, // 组件的配置 or 组件的构造函数
  data: ?VNodeData,
  context: Component,  // 当前的实例
  children: ?Array<VNode>,
  tag?: string // 组件名称
): VNode | Array<VNode> | void {

  // * 这里的 Ctor 有几种形式
  // * 1. 全局形式定义的 component,那么 Ctor 是构造函数形式
  // * 2. 局部定义的 component,那么是 对象形式。会对对象形式进行 extend 处理
  const baseCtor = context.$options._base

  // 局部注册的组件为配置形式
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  installComponentHooks(data)

 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
}

其中调用了installComponentHooks会为data添加几个方法,如下:

javascript
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
   // ...
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // ...
  },

  insert (vnode: MountedComponentVNode) {
   // ...
  },

  destroy (vnode: MountedComponentVNode) {
   // ...
  }
}

patch 阶段

patch阶段创建真实节点时:

javascript
function createElm () {
  ...
  // 如果是组件,则创建相应节点
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
}

// 创建Component
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }

    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      return true
    }
  }
}

如果当前是组件的Vnode,此时会调用init方法:

javascript
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  // 创建子组件
  const child = vnode.componentInstance = createComponentInstanceForVnode(
    vnode,
    activeInstance
  )
  child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}

此时会调用createComponentInstanceForVnode方法,生成子组件实例(其中activeInstance指的是当前正在更新的组件,子组件是在父组件渲染时去渲染的,因此表示父组件实例):

javascript
export function createComponentInstanceForVnode (
  vnode: any,
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode, // 指向组件对应的 vnode
    parent
  }
  
  // render 阶段 createComponent 生成vnode的时候,
  // 数据都存在componentOptions中的,包括组件构造函数 Ctor
  return new vnode.componentOptions.Ctor(options)
}

子组件实例化阶段

找到组件的构造函数后,进行实例化,这个过程中在options合并时有所不同:

javascript
export function initInternalComponent(vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  const parentVnode = options._parentVnode
  // 组件的父亲组件
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  // 组件的配置选项
  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  // * _renderChildren 是插槽内部的节点 (在没有scope的情况下)
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

}

实例化后,得到子组件实例,开始进行mount,编译更新完成后,将其添加到parent上:

javascript
child.$mount(hydrating ? vnode.elm : undefined, hydrating)

组件小结

最后总结一下Compnent的流程:

  1. 编译完成后,开始进行render生成Vnode。此时在_c函数中,如果不是普通标签的标签,说明可能是定义的组件。
  2. 根据可能是组件的标签,根据名称到options中查找对应的组件定义。如果找到了,它可能是构造函数(比如使用Vue.component全局定义的),它也可能是对象形式(比如局部定义的)。
  3. 找到定义后,创建一个Vnode,将构造函数等信息全部存放到componentOptions属性中,并且添加init,inserthook
  4. patch阶段,创建真实节点时,先判断有没有init方法。如果存在,说明可能是组件。此时需要取到之前保存的组件构造函数,并进行实例化,并且挂载到当前真实节点上。

异步组件实现原理

异步组件的基本使用如下:

javascript
// 直接定义一个promise处理回调
Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

// 直接定义一个promise
Vue.component(
  'async-webpack-example',
  // 这个动态导入会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

// 或者返回一个对象
const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

异步组件的处理和子组件一样,都是在render阶段处理的:

javascript
let asyncFactory
// 异步组件的 Ctor 为函数形式,但是没有 cid
if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  // 解析 Ctor,如果能拿到构造函数,说明加载完成了或存在占位Vnode
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  if (Ctor === undefined) {
    // 否则,创建一个替代 Vnode
    return createAsyncPlaceholder(
      asyncFactory,
      data,
      context,
      children,
      tag
    )
  }
}

看一下resolveAsyncComponent的处理:

javascript
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
  }
  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }
 // ...
}

参数中factory对应了组件定义时的内容。后面会开始加载组件,如果正在加载,那么loadingtrue,此时显示loadingComp,如果resolved存在,那么会显示加载完成的组件resolved

其次是owners的处理,owners表示哪些组件使用了该异步组件。这样在异步组件加载完成时,就能通知哪些组件去更新了。

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

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))
 // ...
}

最后,开始正在的加载过程:

javascript
// 加载完成时,更新所有使用该异步组件的组件
const forceRender = (renderCompleted: boolean) => {
  for (let i = 0, l = owners.length; i < l; i++) {
    (owners[i]: any).$forceUpdate()
  }
} 

// 仅加载一次,然后缓存起来,下一次直接使用
const resolve = once((res: Object | Class<Component>) => {
  // 加载完成的构造函数
  factory.resolved = ensureCtor(res, baseCtor)
  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)) {
  // 如果是 promise
  if (isPromise(res)) {
    // 如果是 () => Promise 会使用 then 处理加载回调
    if (isUndef(factory.resolved)) {
      res.then(resolve, reject)
    }
  } else if (isPromise(res.component)) {
    // 如果是对象形式 { component: () => Promise }
    res.component.then(resolve, reject)

    // 此时还会添加其他的占位 component
    if (isDef(res.error)) {
      factory.errorComp = ensureCtor(res.error, baseCtor)
    }
    if (isDef(res.loading)) {}
    if (isDef(res.timeout)) {}
  }
}

异步组件小结

最后小结一下异步组件的实现:异步组件与普通的组件最大的不同是在render阶段构造函数的处理。如果异步组件还未开始加载,那么会返回占位组件的构造函数。并且开始进行加载组件,并且在组件加载完成时,遍历用到了该异步组件的组件,进行$forceUpdate()