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

注: 本文目的是通过源码方向来讲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
      }
    }
  }

总结

到此这篇关于Vue组件渲染流程的文章就介绍到这了,更多相关Vue组件渲染流程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:

  • vue 强制组件重新渲染(重置)的两种方案
  • Vue中强制组件重新渲染的正确方法
  • Vue强制组件重新渲染的方法讨论
  • 加速vue组件渲染之性能优化
  • 细说Vue组件的服务器端渲染的过程
张贴在2