Vue组件的渲染流程

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

引言与例子

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

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

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

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

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

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

  1. 首先实现弹窗样式和逻辑(不是本文主要目的,此处跳过),假设其中有一个简单的显示函数show(){this.visible = true}
  2. 要通过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和组件的options
Sub上赋值静态方法
缓存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
  1. 首先判断传入的Ctor是否已经执行了extend方法,没有执行的话执行一遍
  2. 然后判断是不是异步组件(如果是,调用createAsyncPlaceholder生成并返回)
  3. 然后处理data,创建data.hook中的钩子函数,比如init
  4. 最后调用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)
}
  1. init方法中,执行createComponentInstanceForVnode时会调用new Ctor(options)
  2. 在上边介绍extend方法中可以看到new Ctor时会调用Vue_init方法,执行Vue实例的初始化逻辑
  3. Vue.prototype._init方法初始化完毕,执行$mount是,会有下边代码这样一个判断,组件这时没有el,所以不会执行$mount函数
if (vm.$options.el) {
    vm.$mount(vm.$options.el);
}
  1. 手动执行$mount函数

3. 渲染流程

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

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

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

createComponent
  1. 因为在执行流程中,生成的vnode就是该函数中传入的vnode,并且在vnode创建时把data放在了vnode上,那么vnode.data.hook.init就可以获取到上边说的init函数,我们可以判断,如果有该值,就可以认定本次vnode为组件,并执行vnode.data.hook.init,init的内容详见上边
  2. init执行完毕后,Ctor的实例会被挂载到vnode.componentInstance上,并且已经生成了真实dom,可以在vnode.componentInstance.$el上获取到
  3. 最后执行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
      }
    }
  }

你可能感兴趣的:(JS,vue,vue源码,vue.js,前端,javascript)