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

 更新时间:2022年06月28日 10:45:16   作者:不在五行中  
在Vue核心中除了响应式原理外,视图渲染也是重中之重,下面这篇文章主要给大家介绍了关于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组件渲染流程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • VSCode前端Vue项目引入Element-ui组件三步简单操作方法

    VSCode前端Vue项目引入Element-ui组件三步简单操作方法

    elementui相当于一个库,封装好的内容,我们引入到vue项目中,就可用库中的内容,这篇文章主要给大家介绍了关于VSCode前端Vue项目引入Element-ui组件的三步简单操作方法,需要的朋友可以参考下
    2024-07-07
  • vue3+element-plus props中的变量使用 v-model 报错及解决方案

    vue3+element-plus props中的变量使用 v-model 报错及解决方案

    这篇文章主要介绍了vue3+element-plus props中的变量使用 v-model 报错及解决方案,prop 是单向数据流,这里只能用:model-value,不能用v-model,本文给大家介绍的非常详细,需要的朋友可以参考下
    2023-10-10
  • Vite结合whistle实现一劳永逸开发环境代理方案

    Vite结合whistle实现一劳永逸开发环境代理方案

    这篇文章主要为大家介绍了Vite结合whistle实现一劳永逸开发环境代理方案,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • vue 项目优雅的对url参数加密详解

    vue 项目优雅的对url参数加密详解

    这篇文章主要为大家介绍了vue 项目优雅的对url参数加密详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • 详解Vue整合axios的实例代码

    详解Vue整合axios的实例代码

    本篇文章主要介绍了详解Vue整合axios的实例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • antd vue 如何调整checkbox默认样式

    antd vue 如何调整checkbox默认样式

    这篇文章主要介绍了antd vue 如何调整checkbox默认样式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • vue上传项目到git时,如何忽略node_modules文件夹

    vue上传项目到git时,如何忽略node_modules文件夹

    这篇文章主要介绍了vue上传项目到git时,如何忽略node_modules文件夹,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • element可编辑表格验证问题解决

    element可编辑表格验证问题解决

    本文主要介绍了element可编辑表格验证问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • vue radio单选框,获取当前项(每一项)的value值操作

    vue radio单选框,获取当前项(每一项)的value值操作

    这篇文章主要介绍了vue radio单选框,获取当前项(每一项)的value值操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • VUE中的v-if与v-show区别介绍

    VUE中的v-if与v-show区别介绍

    这篇文章介绍了VUE中v-if与v-show的区别,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-03-03

最新评论