vue2-组件化实现

简介: 我们在前面的文章对vue的创建渲染等都或多或少有所了解,现在我们再继续对串联其中的 组件化实现 进行学习,从单个组件的创建渲染到组件的递归创建渲染。

我们在前面的文章对vue的创建渲染等都或多或少有所了解,现在我们再继续对串联其中的 组件化实现 进行学习,从单个组件的创建渲染到组件的递归创建渲染。


1.根组件实例化


根组件的实现和子组件的实现其实有所区别,根组件是我们手动调用Vue构造函数实例化,而子组件实例化的构造函数是继承自vue的 VueComponent,我们先来看看根组件实现。


1.1入口


我们知道组件实例化是在 new Vue 中进行的,所以对于根实例来说,其入口十分明确,就在开发者主动调用构造函数初始化中。


new Vue({
  el: '#app',
  render: h => h(App)
})
复制代码

1.2实例化


结合之前的分析我们知道,实例化将调用 _init 函数进行组件的初始化

let uid = 0
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // 每个组件都有唯一UID
    vm._uid = uid++
    // 组件对象不可以监测
    // 在observe部分可以看到具体判断
    vm._isVue = true
    // 配置合并
    // merge options
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    // 首次实例化_isComponent为定义走else逻辑
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 各种数据定义的初始化
    // 生命周期的调用
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm)
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
    // 挂载节点
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
复制代码


其中配置合并将执行 resolveConstructorOptions 函数,对于根实例来说 resolveConstructorOptions 就是将 Vue.options 的配置合并到组件实例中


export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    // ...
  }
  return options
}
复制代码

Vue.options 对象则是包含了各种全局注册的conponentsfiltersdirectives等。如此,通过 Vue.mixinVue.useVue.extend 等方法添加的资源即可在组件中调用了。


1.3创建虚拟节点


组件实例化的最后一步,是挂载节点,我们应该知道它其实是在组件实例化流程中进行的,我们这边将其单独拎出来作为下一步来分析。


vm.$mount 中实际先执行 render 函数,其调用在 watcher 实例的回调函数 updateComponent

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码


vm._render 执行的是我们的 rneder 函数,render 函数来源为开发者定义或 template 转化。在 render 中调用 _createElement 进行虚拟节点的创建。

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  //...
  // 子节点扁平化
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  // ...
  let vnode, ns
  // 标签节点
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    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))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  }
  // ...
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    return vnode
  } else {
    return createEmptyVNode()
  }
}
复制代码


从简化后的 _createElement 来看,其通过 tag 来判断是否是创建普通虚拟节点,如果是的话就执行 new VNode 逻辑。


1.4渲染


执行了 render 函数之后,意味为虚拟节点已经准备完毕,接下来就是执行渲染逻辑了。其调用依然在 updateComponent

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码


我们来看看 vm._update

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  // 此时$el已经由配置#app转化为真实节点
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  // _vnode更新为最新的虚拟节点
  vm._vnode = vnode
  if (!prevVnode) {
    // 首次渲染时将使用真实节点作为虚拟节点
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
}
复制代码

__patch__ 的逻辑我们就不细说了,等后面分析子组件的时候再去分析


1.5根组件小结


至此我们就完成了根组件从创建到渲染的分析,过程比较简单,我们来梳理下流程


  1. 开发者调用 new Vue 实例化组件,resolveConstructorOptions 将合并全局资源

  2. render 中执行 _createElement 创建虚拟节点 vnode,注意是整个组件的虚拟节点

  3. 执行 __patch__ 逻辑完成渲染

2.子组件实例化


我们今天的主要任务是弄清楚子组件的实例化,这样才能弄清楚vue组件化实现的机制


2.1创建组件入口


在根组件生成虚拟节点的时候,遇到自定义组件会调用 createComponent 创建相应的组件虚拟节点,组件标签 => 组件虚拟节点


export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    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))) {
      // 创建组件节点
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // 创建组件节点
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    return vnode
  } else {
    return createEmptyVNode()
  }
}
复制代码

2.2创建组件节点


我们来看看创建组件节点 createComponent 的逻辑


export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // _base === Vue
  const baseCtor = context.$options._base
  // 调用Vue.extend生成组件构造函数
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  // ...
  data = data || {}
  // 合并配置
  resolveConstructorOptions(Ctor)
  // 提取props值并校验
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  const listeners = data.on
  data.on = data.nativeOn
  // 安装组件管理相关钩子
  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
  )
  // 返回组件虚拟节点
  return vnode
}
复制代码

前面分析了 createComponent的主要流程,我们将其分为以下几个部分进行进一步分析


  1. Vue.extend生成组件构造函数

  2. resolveConstructorOptions合并配置

  3. installComponentHooks安装组件管理相关钩子

  4. new VNode 生成组件虚拟节点

2.2.1 Vue.extend生成组件构造函数

Vue.extend = function (extendOptions: Object): Function {
  // extendOptions是我们的组件配置
  // 包括created methods data components等
  extendOptions = extendOptions || {}
  // this===Vue
  const Super = this
  const SuperId = Super.cid
  // 往extendOptions._Ctor扩展属性保存此构造函数
  // 当遇到相同的配置时就不需要再生成构造函数
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }
  // 校验组件名
  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }
  // VueComponent定义
  const Sub = function VueComponent (options) {
    // 调用_init执行组件初始化的流程
    this._init(options)
  }
  // 原型继承
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  // 合并组件配置
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super
  // prop/computed初始化暂不分析
  // ...
  // 扩展extension/mixin/plugin等函数
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use
  // 扩展ASSET_TYPES中定义的函数包括component/directive/filter
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // 注册自身以支持循环调用
  if (name) {
    Sub.options.components[name] = Sub
  }
  // 缓存各种配置
  // 在后面将用于检查配置是否更新
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)
  // 缓存子构造器
  cachedCtors[SuperId] = Sub
  return Sub
}
复制代码


通过分析可以得到 Vue.extend 的主要功能就是继承了 Vue 的原型,定义了新的构造函数 VueComponentVue 进行区分,在构造函数中也很简单,就是执行 _init 函数。我们还是来总结下吧


  • 校验组件名 validateComponentName

  • 生成新的构造函数 Sub = VueComponent

  • 合并配置,包括组件配置与全局配置,并定义到 Sub.options

  • 扩展子构造函数

  • 缓存配置及构造函数

2.2.2 resolveConstructorOptions合并配置


resolveConstructorOptions 这里的功能其实和 Vue.extend 中的合并配置差不多,区别就是会在这边再次校验缓存的配置是否有更新


export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    // 判断配置是否更新
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}
复制代码


2.2.3 安装组件管理相关钩子

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  // 遍历hooksToMerge合并componentVNodeHooks
  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
    }
  }
}
复制代码


我们再来看看 componentVNodeHooks 的定义

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      // ...
    } 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')
    }
  },
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}
复制代码


通过 installComponentHooks 函数为组件数据 data.hook 添加 componentVNodeHooks 中定义的钩子函数,钩子函数的具体实现我们具体调用的时候再进行分析,目前只需要知道在这边进行定义添加就行。


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


值得注意的是,此处的 children 并不是组件的根节点或者其它子节点,因为其定义在外层组件的 render 函数中,这边的 children 其实是关于 slot 的实现。


我们继续看看组件 vnode 的实例化


export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag  // 组件名
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions // 组件配置
    this.componentInstance = undefined // 组件实例(实例化之前为undefined)
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}
复制代码

通过参数对比可以发现组件虚拟节点的参数主要是 tag data context componentOptions asyncFactory,其中 asyncFactory 和异步组件相关,与正常 vnode 不同之处在于其 tag 为组件名及 componentOptions 中保存了组件相关配置


2.3组件实例化


我们在前面的 1 -> 2 中梳理了组件的入口,组件构造函数的生成及组件节点 vnode 的生成,主要逻辑在于 _createElementcreatecomponent 中。至此我们创建了组件的 vnode,但是组件的实例化及渲染并没有涉及,其实组件实例化的过程在 patch 函数中。


2.3.1组件实例化入口


我们回到最开始的 patch 中。

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  // _vnode更新为最新的虚拟节点
  vm._vnode = vnode
  if (!prevVnode) {
    // 首次渲染时将使用真实节点作为虚拟节点
    // 对于子组件来说此时vm.$el为undefined
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
}
复制代码


关于 patch 的具体流程可以参考vue2-patch流程分析vueDiff 算法解读,现在我们来重点分析其中的组件相关实现。因为源码比较多,我们会省略一些代码。我们来看看 patch 最终执行的函数 createPatchFunction


export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend
  // ...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...
    let isInitialPatch = false
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        // ...
        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
复制代码


我们没有看到与组件相关的代码,但是能发现其主要逻辑是执行节点创建函数 createElm,我们接着分析

function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    vnode.isRootInsert = !nested // for transition enter check
    // 组件实例化入口
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    // 真实节点创建相关
    // ...
复制代码


2.3.2组件实例化实现


createElement 的执行中能看到创建组件的判断,我们分析其实现

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 */)
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      return true
    }
  }
}
复制代码

此时会先判断 vnode.data.hook.init 是否存在,如果存在则调用,这步是组件实例化的关键入口。init 的定义其实在上面我们分析过,存在 componentVNodeHooks 中,我们现在来分析下其实现。


2.3.2.1组件实例化调用
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    // kept-alive components, treat as a patch
    const mountedNode: any = vnode // work around flow
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
}
复制代码

init 的逻辑主要是实现两步


  • 执行 createComponentInstanceForVnode 并赋值给
  • vnode.componentInstance
  • 调用 child.$mount 完成渲染逻辑

我们先看看 createComponentInstanceForVnode

export function createComponentInstanceForVnode (
  vnode: any,
  parent: any
): Component {
  // 定义了构造函数的入参options
  // 仅包含三个参数_isComponent===true _parentVnode为组件vnode parent则会取到当前activeInstance也就是父组件实例
  // 关于activeInstance的实现也很简单,大家可以自己去看看lifecycle中的_update实现即可。
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // ...
  // 最后的结果是返回componentOptions.Ctor实例化的结果
  // vnode.componentOptions.Ctor的生成我们在前面分析过
  // 这边注意options不是开发者的组件配置
  // 开发者对组件的配置数据其实在生成Ctor时就已经定义在静态属性Ctor.options中了
  return new vnode.componentOptions.Ctor(options)
}
复制代码
2.3.2.2组件初始化


我们继续复习下 Ctor 的定义


const Sub = function VueComponent (options) {
  this._init(options)
}
复制代码

其中 _init 函数就是我们熟悉的组件初始化的函数


export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++
    let startTag, endTag
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // 此时会执行initInternalComponent
      initInternalComponent(vm, options)
    } else {
      // ...
    }
    // 组件初始化的其它逻辑
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    // 我们在组件中一般是不会定义el值的
    // 所以不会直接调用vm.$mount进行渲染
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
复制代码

我们接着分析下 initInternalComponent(vm, options) 的逻辑

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // 定义了$options继承自Ctor.options
  const opts = vm.$options = Object.create(vm.constructor.options)
  // 同时将_parentVnode与parent属性赋值到vm.$options
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode
  // 同时将组件vnode的数据赋值到vm.$options
  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
  }
}
复制代码
2.3.2.3组件挂载渲染

至此,子组件的实例化也完成了,我们再看看前面所说的两步走的第二步


child.$mount(undefined)
复制代码

$mount 函数我们前面也有分析过,就是再执行组件的挂载渲染了


3.组件相关实现


本篇文章的重点组件实例化其实已经分析完,我们再来看看与组件相关的其它方面的实现

3.1 组件节点的处理


我们知道在创建虚拟节点的时候会有个组件节点,而我们在页面上看到的真实渲染是没有组件节点这个东西的,我们来看看组件节点是如何被跳过的,组件节点下面的节点又是如何找到正确的挂载父节点的。


我们知道在渲染组件的时候,会执行其中的 render 函数,而 render 函数外面又包了一层,在 core/instance/render.js

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  // _parentVnode就是我们的组件节点
  const { render, _parentVnode } = vm.$options
  // ...
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    currentRenderingInstance = vm
    vnode = render.call(vm._renderProxy, vm.$createElement) 
  } catch (e) {
    // ...
  } finally {
    currentRenderingInstance = null
  }
  // ...
  // set parent
  vnode.parent = _parentVnode
  return vnode
}
复制代码

通过分析可以看到,在 _render 中,其实会将组件节点 _parentVnode 作为 parent 赋值给 vnode,而 vnode 来自于组件的 render 函数,所以实际在组件 _render 中,组件节点是不会被渲染在组件中的,而组件 render 返回的 vnode 会作为根节点返回。


以上代码解释了如何跳过组件节点的编译,我们再看看组件根节点是如何被挂载在父节点上的


patch 函数下的 createComponent

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    // 我们调用init函数完成了组件的实例化及真实节点创建
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    if (isDef(vnode.componentInstance)) {
      // 在这调用initComponent
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}
复制代码


其实节点的正确挂载逻辑在 initComponent

function initComponent (vnode, insertedVnodeQueue) {
  if (isDef(vnode.data.pendingInsert)) {
    insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
    vnode.data.pendingInsert = null
  }
  // 在这将vndoe.elm赋值为vnode.componentInstance.$el
  // vnode.componentInstance指向组件实例
  // 其$el实际在_update函数中生成,实际是__patch__函数的结果
  vnode.elm = vnode.componentInstance.$el
  if (isPatchable(vnode)) {
    invokeCreateHooks(vnode, insertedVnodeQueue)
    setScope(vnode)
  }
}
复制代码

所以其原理是将组件创建的真实节点先赋值给组件节点上,对于组件根节点的 vnode 来说,vnode.parent.elm === vnode.elm


3.2 其它组件管理钩子


我们在上面定义了组件的管理钩子,但是后面只分析了其中 init 的实现,我们现在来看看其它钩子的实现。


3.2.1 prepatch

prepatch 的调用其实在 diffpreVnode 中,当我们的组件节点有更新时就会先调用 prepatch


if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  i(oldVnode, vnode)
}
复制代码


prepatch 中主要调用 updateChildComponent

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


因为在更新节点的时候,我们的组件其实是已经完成创建的,所以组件实例已经存在。在更新中如果发现组件节点 vnode 的有更新,则需要更新组件节点对应的组件实例相关数据,这就是 updateChildComponent 的主要任务。


export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = true
  }
  // 更新组件节点
  vm.$options._parentVnode = parentVnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render
  if (vm._vnode) { // update child tree's parent
    vm._vnode.parent = parentVnode
  }
  vm.$options._renderChildren = renderChildren
  // 更新属性/事件
  vm.$attrs = parentVnode.data.attrs || emptyObject
  vm.$listeners = listeners || emptyObject
  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }
  // update listeners
  listeners = listeners || emptyObject
  const oldListeners = vm.$options._parentListeners
  vm.$options._parentListeners = listeners
  updateComponentListeners(vm, listeners, oldListeners)
}
复制代码

3.2.2 insert


在我们组件 patch 的最后一步,会执行 invokeInsertHook

// insertedVnodeQueue是在patch过程中收集的子组件
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
复制代码
function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i])
    }
  }
}
复制代码

invokeInsertHook 会遍历触发组件的 insert 钩子

insert (vnode: MountedComponentVNode) {
  const { context, componentInstance } = vnode
  if (!componentInstance._isMounted) {
    componentInstance._isMounted = true
    // 逻辑很简单 就是调用生命周期mounted
    callHook(componentInstance, 'mounted')
  }
}  
复制代码

这也解释了为什么子组件的 mounted 会比父组件早,因为在父组件的 patch 中,会调用子组件的 patch,而子组件的 patch 是先完成的。当然 insertedVnodeQueue 其实会有个存储的设计,因为 mounted 必须在根节点挂载后才能,所以并不是组件 patch 完成就会立即调用 insert 钩子。具体实现大家感兴趣可以自己去看看。


3.2.3 destroy


当移除组件节点的时候会触发 destroy

function removeVnodes (vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx]
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch)
        invokeDestroyHook(ch)
      } else { // Text node
        removeNode(ch.elm)
      }
    }
  }
}
复制代码
function invokeDestroyHook (vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    // 在这触发destroy
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  if (isDef(i = vnode.children)) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}
复制代码

我们来看看 destroy 的逻辑

destroy (vnode: MountedComponentVNode) {
  const { componentInstance } = vnode
  if (!componentInstance._isDestroyed) {
    if (!vnode.data.keepAlive) {
      componentInstance.$destroy()
    } else {
      deactivateChildComponent(componentInstance, true /* direct */)
    }
  }
}
复制代码

逻辑也很简,就是调用组件实例的 $destroy,我们就不继续分析了。


总结

本篇文章分析了vue组件化的实现,包括Vue实例VueComponent实例的创建及渲染过程,篇幅比较长,很多方面讲的也不行。后面如果有时间的话会继续分析组件生命周期及节点更新函数,没有时间的话就先算了。anyway good good staduy day day up


相关文章
|
8月前
|
JavaScript 前端开发 小程序
Vue | Vue.js 组件化基础 - 脚手架
Vue | Vue.js 组件化基础 - 脚手架
Vue | Vue.js 组件化基础 - 脚手架
|
2天前
|
JavaScript 前端开发 开发者
Vue组件化:Vue组件的创建与使用
【4月更文挑战第24天】Vue.js框架以其简单和高效的组件化开发著称,允许将UI拆分为独立、可复用组件,提升开发效率和代码可维护性。组件创建分为全局注册(影响所有Vue实例)和局部注册(限当前实例)。
|
8月前
|
JavaScript 前端开发 IDE
Vue3-组件化
Vue3-组件化
45 0
|
9月前
|
JSON JavaScript 前端开发
|
7月前
|
移动开发 JavaScript 前端开发
Vue学习笔记_组件化
Vue学习笔记_组件化
28 0
|
7月前
|
JavaScript 前端开发
|
9月前
|
JavaScript 前端开发 CDN
vue怎么进行模块化开发
Vue.js天生支持模块化开发,使用Vue.js进行模块化开发的步骤如下: 1.安装Vue.js:使用npm命令或者cdn链接方式安装Vue.js。 2.创建Vue实例:在JavaScript中,使用Vue构造函数创建一个Vue实例进行模块化开发。 3.创建组件:使用Vue.component注册组件,创建组件模板、组件方法等。 4.模板中使用组件:在模板中使用组件。 5.导入组件:在另一个组件中导入我们创建的组件。 6.注册组件:使用Vue.component注册第五步中导入的组件。
194 0
|
9月前
|
JavaScript 前端开发 Java
【vue系列-06】vue的组件化编程
【vue系列-06】vue的组件化编程
102 0
|
9月前
|
JavaScript 前端开发