信息发布→ 登录 注册 退出

Vue组件的渲染流程详细讲解

发布时间:2026-01-11

点击量:
目录
  • 引言与例子
    • 举一个工作中能用到的例子:
    • 实现
  • extend
    • 执行流程
      • 1. 注册流程(以Vue.component()祖册为例子):
      • 2. 执行流程
      • 3. 渲染流程
    • 总结

      注: 本文目的是通过源码方向来讲component组件的渲染流程

      引言与例子

      在我们创建Vue实例时,是通过new Vuethis._init(options)方法来进行初始化,然后再执行$mount等,那么在组件渲染时,可不可以让组件使用同一套逻辑去处理呢?

      答:当然是可以的,需要使用到Vue.extend方法来实现。

      举一个工作中能用到的例子:

      需求:我们在项目中实现一个像element-uiMessage Box弹窗,在全局注册(Vue.use)后,能像alert方法一样,调用函数就可以弹出

      实现

      (先简单说下vueuse方法基础使用,use注册时,如果是函数会执行函数,如果是对象,会执行对象中的install方法进行注册)

      根据需求,我们在调用use方法后,需要实现两个目的:将组件注册并直接挂载到dom上,将方法放在Vue.prototype下;

      • 首先实现弹窗样式和逻辑(不是本文主要目的,此处跳过),假设其中有一个简单的显示函数show(){this.visible = true}
      • 要通过use的方式注册组件,就要有一个install方法,在方法中首先调用Vue.extend(messageBox组件),然后调用该对象的$mount()方法进行渲染,最后将生成的DOM节点messageBox.$el上树,然后上show方法放到Vue.prototype上,就完成了
      function install(Vue) {
          // 生成messageBox 构造函数
          var messageBox = Vue.extend(this);
          messageBox = new messageBox();
          // 挂载组件,生成dom节点(这里没传参,所以只是生成dom并没有上树)
          messageBox.$mount();
          // 节点上树
          document.body.appendChild(messageBox.$el);
          // 上show方法挂载到全局
          Vue.prototype.$showMessageBox = messageBox.show;
      }
      

      根据例子,我们来看一下这个extend方法:

      extend

      Vue中,有一个extend方法,组件的渲染就是通过调用extend创建一个继承于Vue的构造函数。
      extend中的创建的主要过程是:

      在内部创建一个最终要返回的构造函数SubSub函数内部与Vue函数相同,都是调用this._init(options) 继承Vue,合并Vue.options和组件的optionsSub上赋值静态方法 缓存Sub构造函数,并在extend方法开始时判断缓存,避免重复渲染同一组件 返回Sub构造函数(要注意extend调用后返回的是个还未执行的构造函数 Sub)

      // 注:mergeOptions方法是通过不同的策略,将options中的属性进行合并
      
      Vue.extend = function (extendOptions: Object): Function {
          extendOptions = extendOptions || {}
          const Super = this // 父级构造函数
          // 拿到cid,并通过_Ctor属性缓存,判断是否已经创建过,避免重复渲染同一组件
          const SuperId = Super.cid
          const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
          if (cachedCtors[SuperId]) {
            return cachedCtors[SuperId]
          }
      
          // name校验+抛出错误
          const name = extendOptions.name || Super.options.name
          if (process.env.NODE_ENV !== 'production' && name) {
            validateComponentName(name)
          }
      
          // 创建构造函数Sub
          const Sub = function VueComponent (options) {
            this._init(options)
          }
          // 继承原型对象
          Sub.prototype = Object.create(Super.prototype)
          Sub.prototype.constructor = Sub
          Sub.cid = cid++ // cid自增
          // 父级options与当前传入的组件options合并
          Sub.options = mergeOptions(
            Super.options,
            extendOptions
          )
          Sub['super'] = Super // 缓存父级构造函数
      
          // For props and computed properties, we define the proxy getters on the Vue instances at extension time, on the extended prototype. This avoids Object.defineProperty calls for each instance created.
          // 对于props和computed属性,我们在扩展时在扩展原型的Vue实例上定义代理getter。这避免了object。为创建的每个实例调用defineProperty。
          if (Sub.options.props) {
            initProps(Sub)
          }
          if (Sub.options.computed) {
            initComputed(Sub)
          }
      
          // 将全局方法放在Sub上,允许进一步调用
          Sub.extend = Super.extend
          Sub.mixin = Super.mixin
          Sub.use = Super.use
      
          // create asset registers, so extended classes
          // can have their private assets too.
          ASSET_TYPES.forEach(function (type) {
            Sub[type] = Super[type]
          })
          // enable recursive self-lookup
          if (name) {
            Sub.options.components[name] = Sub
          }
      
          // keep a reference to the super options at extension time.
          // later at instantiation we can check if Super's options have
          // been updated.
          Sub.superOptions = Super.options
          Sub.extendOptions = extendOptions
          Sub.sealedOptions = extend({}, Sub.options)
      
          // 对应上边的_Ctor属性缓存
          cachedCtors[SuperId] = Sub
          return Sub
        }
      }
      

      看完export函数后,思考下,生成组件时是一个怎样的执行流程呢?

      执行流程

      1. 注册流程(以Vue.component()祖册为例子):

      用户在调用Vue.component时,其实就只执行了三行代码

      // 简化版component源码
      Vue.component = function (id,definition) {
          definition.name = definition.name || id
          // _base指向的是new Vue()时的这个Vue实例,调用的是Vue实例上的extend方法
          definition = this.options._base.extend(definition)
          this.options.components[id] = definition
          return definition
      }
      

      获取并赋值组件的name definition.name调用根Vue上的extend方法
      将组件放到options.components
      返回definition

      (如果是异步组件的话,只会走后边两步,不会执行extend)

      在下文中,我们会将extend方法返回的Sub对象称为Ctor

      在创建组件时,我们实际只是为组件执行了extend方法,但在option.components中传入的组件不会被执行extend方法,在3.渲染流程中会执行

      2. 执行流程

      createElement函数执行时,根据tag字段来判断是不是一个组件,如果是组件,执行组件初始化方法createComponent

      createComponent

      • 首先判断传入的Ctor是否已经执行了extend方法,没有执行的话执行一遍
      • 然后判断是不是异步组件(如果是,调用createAsyncPlaceholder生成并返回)
      • 然后处理data,创建data.hook中的钩子函数,比如init
      • 最后调用new VNode()生成节点

      先看下createElement函数源码,然后在底下主要说下init函数

      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
        }
      
        // _base指向的是new Vue()时的这个Vue实例
        const baseCtor = context.$options._base
      
        // 如果extend没有执行过,在这里执行
        if (isObject(Ctor)) {
          Ctor = baseCtor.extend(Ctor)
        }
      
        // 报错处理
        if (typeof Ctor !== 'function') {
          if (process.env.NODE_ENV !== 'production') {
            warn(`Invalid Component definition: ${String(Ctor)}`, context)
          }
          return
        }
      
        // 异步处理
        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 = data || {}
      
        
        resolveConstructorOptions(Ctor)
        if (isDef(data.model)) {
          transformModel(Ctor.options, data)
        }
        const propsData = extractPropsFromVNodeData(data, Ctor, tag)
        if (isTrue(Ctor.options.functional)) {
          return createFunctionalComponent(Ctor, propsData, data, context, children)
        }
        const listeners = data.on
        data.on = data.nativeOn
        if (isTrue(Ctor.options.abstract)) {
          const slot = data.slot
          data = {}
          if (slot) {
            data.slot = slot
          }
        }
        
        // 重点 创建init方法
        installComponentHooks(data)
      
        // return a placeholder vnode
        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
        )
        return vnode
      }
      

      让我们看下init方法

      init,prepatch,insert,destroy等方法在源码中是创建在componentVNodeHooks对象上,通过installComponentHooksinstallComponentHooks方法判断data.hook中是否有该值,然后进行合并处理等操作实现的,在这里,我们不考虑其他的直接看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 {
            // 挂载到vnode上,方便取值
            // 在这个函数中会new并返回extend生成的Ctor
            const child = vnode.componentInstance = createComponentInstanceForVnode(
              vnode,
              activeInstance
            )
            // 重点
            child.$mount(hydrating ? vnode.elm : undefined, hydrating)
          }
        }
        
      // 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
        }
        // 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)
      }
      
      • init方法中,执行createComponentInstanceForVnode时会调用new Ctor(options)
      • 在上边介绍extend方法中可以看到new Ctor时会调用Vue_init方法,执行Vue实例的初始化逻辑
      • Vue.prototype._init方法初始化完毕,执行$mount是,会有下边代码这样一个判断,组件这时没有el,所以不会执行$mount函数
      if (vm.$options.el) {
          vm.$mount(vm.$options.el);
      }
      
      • 手动执行$mount函数

      3. 渲染流程

      在组件渲染流程createElm函数中,有一段代码

      if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
      }
      

      所以,组件的生成和判断都是在createComponent函数中发生的

      createComponent

      • 因为在执行流程中,生成的vnode就是该函数中传入的vnode,并且在vnode创建时把data放在了vnode上,那么vnode.data.hook.init就可以获取到上边说的init函数,我们可以判断,如果有该值,就可以认定本次vnode为组件,并执行vnode.data.hook.init,init的内容详见上边
      • init执行完毕后,Ctor的实例会被挂载到vnode.componentInstance上,并且已经生成了真实dom,可以在vnode.componentInstance.$el上获取到
      • 最后执行initComponentinsert,将组件挂载
      function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
          let i = vnode.data
          if (isDef(i)) {
            const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      
            // 在判断是否定义的同时,把变量做了改变,最终拿到了i.hook.init(在extend函数中注册的Ctor的init方法)
            if (isDef(i = i.hook) && isDef(i = i.init)) {
              // 执行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.
            //调用init hook之后,如果vnode是子组件
            //它应该创建一个子实例并挂载它。孩子
            //组件还设置了占位符vnode的elm。
            //在这种情况下,我们只需返回元素就可以了。
      
            // componentInstance是组件的ctor实例,有了代表已经创建了vnode.elm(真实节点)
            if (isDef(vnode.componentInstance)) {
              initComponent(vnode, insertedVnodeQueue)
              insert(parentElm, vnode.elm, refElm)
              if (isTrue(isReactivated)) {
                reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
              }
              return true
            }
          }
        }

      总结

      在线客服
      服务热线

      服务热线

      4008888355

      微信咨询
      二维码
      返回顶部
      ×二维码

      截屏,微信识别二维码

      打开微信

      微信号已复制,请打开微信添加咨询详情!