Vue源码解析05-组件化

本文解析Vue组件从创建到挂载的全过程,包括组件构建、VNode生成及DOM挂载,揭示Vue组件化的内部机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Vue源码解析05-组件化

前言

组件(Component)是Vue.js框架强大的功能之一,我们想要编写一个完整、健壮的项目离不开Vue的组件,所以本渣从源码角度梳理了一下Vue组件的创建流程,在此做简单记录

个人认为Vue的组件分三步

  • 组件的创建(我们平常会使用Vue.component创建一个全局组件)
  • 组件的解析,也就是将组件转化为对应的VNode(平常我们说的虚拟DOM)
  • 组件的挂载(VNode向真实DOM转化并显示在界面上的过程)

组件的创建

官方文档指出,我们可以使用**Vue.component(id:String,definition:Object|function)**注册或获取一个全局组件,它接收两个参数:组件名称(id)和组件配置项
那么,先来看一下Vue.component都做了什么事情

  • 为了能够梳理出一个清晰的脉络和阅读代码的方便,我们只粘贴关键代码

Vue.Component的实现

Vue.component是在**initGlobalAPI()**方法中进行初始化的,具体查找过程因为不是重点,所以在此不做过多赘述.

//这里的type,指的是component、directive、filter,也就是说,这三者的处理方法大同小异
Vue[type] = function (
      id: string,//组件id
      definition: Function | Object//配置对象
    ): Function | Object | void {
      if (!definition) {
        //如果第二个参数不存在,系统认为是获取一个全局组件,则返回组件的构造方法
        return this.options[type + 's'][id]
      } else {
        ......
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          //重点:这里使用extend方法将definition转化为VueComponent构造函数
          definition = this.options._base.extend(definition)
        }
        ......
        //将组件的构造函数加入到options中相应的component、directive、filter对象中去
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  • 总结一下,Vue.component的主要作用就是执行了Vue.extend方法,生成了一个组件的构造函数VueComponent(),并将该构造方法挂载到Vue.components属性上

下面我们来看一下Vue.extend方法的实现

Vue.extend方法的实现


Vue.extend = function (extendOptions: Object): Function {
    const Super = this//这里的this指的是Vue
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }
    const name = extendOptions.name || Super.options.name

    //创建一个VueComponent的构造函数
    //VueComponent继承自Vue
    const Sub = function VueComponent(options) {
      this._init(options)
    }
    //继承于Vue,Super指的是Vue构造函数
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    /*
      * 这里进行了一些extend、mixin、use方法的继承
        和其他的一些方法的初始化、选项合并等
      */
    // 将构造函数缓存起来
    cachedCtors[SuperId] = Sub
    //返回VueComponent
    return Sub
  }

通过对上面代码的分析我们能大概清楚Vue.extend的作用:

  • 创建一个VueComponent构造函数并返回
  • VueComponent继承自Vue,同时继承了Vue中的extend、mixin、use等方法和一些其他的选项

那么我们最终可以得出结论:Vue组件的创建过程其实就是通过用户配置的一些组件信息,创建了一个继承自Vue的组件构造函数VueComponent,并将该构造函数挂载到Vue.components上

组件的解析

我们都知道Vue从创建到显示在界面上大致的理解为:带模板语法的HTML->VNode(虚拟DOM)->挂载真实DOM(展示在界面上)
组件的解析就是将组件转换为VNode的过程

组件解析的源头:_render

读过Vue源码的小伙伴肯定对_render函数不陌生,这个函数主要的作用就是生成VNode,因为生成VNode的过程比较复杂,所以我们这里只挑选跟Component组件有关的代码进行分析

组件的VNode的创建:从_createElement到createComponent

我们先做个简单的认知:_createElement是创建VNode节点的方法,其中就包含了创建HTML原声节点(div、p、span等标签)和组件Component

我们看一下_createElement的简化代码:

if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 先判断是否是保留标签(div、p等 )
    if (config.isReservedTag(tag)) {
      // 直接创建一个虚拟DOM
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else
      //如果没有pre命令
      if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
        //Ctor:组件的构造函数
        vnode = createComponent(Ctor, data, context, children, tag)
      } else {
        vnode = new VNode(
          tag, data, children,
          undefined, undefined, context
        )
      }
  } else {
    // 如果tag直接是一个组件的选项或者构造函数
    vnode = createComponent(tag, data, context, children)
  }

我们只取出了_createElement中于Component密切相关的核心代码,可以看出,我们是他调用的是createComponent()进行VNode的生成,下面我们再看一下createComponent的逻辑:
这里我们删除了对构造函数的校验等非核心操作

  export function createComponent(
  Ctor: Class<Component> | Function | Object | void,//组件的构造函数
  data: ?VNodeData,//组件data选项
  context: Component,//上下文
  children: ?Array<VNode>,//children
  tag?: string//组件name
): VNode | Array<VNode> | void {

  //关于异步组件的处理
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(asyncFactory,data,context,children,tag)
    }
  }
  data = data || {}

//解析构造函数中的options选项,主要作用是更新了Ctor.superOptions选项,
//主要是防止父组件的options更新后与当前组件的superOptions不一致
  resolveConstructorOptions(Ctor)

  if (isDef(data.model)) {
    //处理组件的v-model事件
    transformModel(Ctor.options, data)
  }

  //获取Ctor中的props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

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

  const listeners = data.on
  data.on = data.nativeOn

  //为data选项添加组件的钩子函数,init(),prepatch(),insert(),destroy()
  installComponentHooks(data)

  //获取组件的名称或者标签名
  const name = Ctor.options.name || tag
  //创建虚拟节点,自定义组件的VNode
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  //返回组件的vnode
  return vnode
}

从上述代码我们可以看出,crateComponent()的主要作用就是将组件的构造函数解析为VNode并返回

总结:组件的解析发生在Vue._render函数调用的时候,_render函数的主要作用是生成VNode,当我们的组件被解析到的时候会将组件的构造方法从Vue.components中取出,然后通过createComponent方法生成VNode并返回
同时,createComponent()方法对组件的v-model、functional、异步组件进行了处理,同时为组件的VNode添加了**init()、prepatch()、insert()、destroy()**等钩子函数

组件的挂载

最终我们的组件是需要真正挂载到界面上展示出来的.所以这就涉及到了Vue的挂载过程了.而我们的组件也是在此过程中挂载到界面上的.
为了把我们的问题简单化,这里只分析初次挂载的过程,真正更新的时候的挂载过程都是大同小异的

挂载的入口:mountComponent()

这里我们按照代码实际运行的顺序去分析一下DOM挂载的入口(精简中的精简版):

  export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  //调用声明周期beforeMount
  callHook(vm, 'beforeMount')
    // 定义组件更新函数,函数的内容就是调用Vue实例的_update方法
    updateComponent = () => {
      //核心代码
      vm._update(vm._render(), hydrating)
    }

  // 创建Watcher实例
  new Watcher(vm, updateComponent, noop, {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        //beforeCreate钩子
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  if (vm.$vnode == null) {
    //挂载完成的标记
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

分析上面的最终简化版的mountComponent()代码,真正执行挂载操作的其实是vm._update(vm._render())方法,它接收一个我们之前提到的_render()函数生成的VNode然后执行挂载操作

挂载的重点:patch()函数

这里特此说明一下:我们跳过了中间繁琐的非重点环节(因为查找函数调用顺序,在源码中很容易找到,不是介绍重点),直指挂载的核心函数patch()方法

  • patch函数的重点就是对比新老节点然后进行更新操作,如果是初次挂载,那么它会直接执行创建新节点的操作
  • 最终patch()函数,会返回一个最后生成的最新的真实DOM:vnode.elm,这个真实dom会被挂载到vue实例的$el上保存起来
  createElm(
          vnode,//VNode
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          // 在父组件中追加转换后的真实dom
          nodeOps.nextSibling(oldElm)
        )

createElm创建真实DOM

patch()函数中调用了createElm来生成真实的DOM,那么我们来看一下createElm的代码

   function createElm(
    vnode,//传入的vnode
    insertedVnodeQueue,
    parentElm,//父组件的真实DOM
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    //尝试创建组件实例
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    //下面是普通节点的处理
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      //插入到父节点中,这个时候挂载之前的原真实DOM还没有删除
      insert(parentElm, vnode.elm, refElm)
    }
  }

从上面的代码分析得知,createElm的主要作用就是调用各种方法将生成的真实DOM插入到父节点中,其中将组件DOM插入到父节点的方法便是createComponent

createComponent:创建组件DOM的真正执行者

createComponent的方法其实很简单,因为他的主要作用就是,调用了一个组件的init()钩子函数(这个钩子函数包含了组件的初始化、创建、挂载的生命周期),
然后,将自身的一个真实dom插入到父节点中

  //生成组件的真实DOM
  function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    //普通节点是没有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(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

上面代码中的i.hook(vnode,false),其实就是执行我们之前提到过的生成组件VNode的时候在VNode上的data中挂载的一个init()钩子函数,我们来看一下他的代码

  //组件的实例化及挂载
  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)
    }
  }

依然是一个简单明了的函数:

  • 先判断是否有keepAlive参数生效,如果有keepAlive生效的话,则只更新当前组件的一些涉及到parent或者插槽相关的东西,而不去patch组件本身
  • 初次调用init()的话,则执行组件实例的$mount()方法,其实这相当与在vue的组件树中的一个递归调用,组件这个时候就会进行初始化、创建、挂载的流程
  • 执行到挂载的时候会将自身的一个真实DOM挂载到父节点上,一直向上递推这个过程,直到根节点挂载到界面中,我们的界面就能够完整的显示出来了

总结

Vue组件最终经过了创建、编译解析和挂载操作,最终展示在了界面上.
简单可以概括成以下步骤:

  • 用户利用Vue.component生成一个继承自Vue类的VueComponent的构造函数,并将构造函数存储在Vue.components中
  • 界面渲染过程中,在render生成VNode的过程中,会调用Vue.components中相应的组件的构造函数生成对应的VNode
  • 挂载阶段,程序会递归调用组件的初始化、创建、挂载过程.从而实现了组件树的挂载展示

**PS:**拜读大神的源码真的收益匪浅,深刻认识到自己还是一个渣渣

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值