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

目录
  • 引言与例子
    • 举一个工作中能用到的例子:
    • 实现
  • 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
      }
    }
  }

总结

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

(0)

相关推荐

  • 细说Vue组件的服务器端渲染的过程

    声明:需要读者对 NodeJs.Vue 服务器端渲染有一定的了解 现在,前后端分离与客户端渲染已经成为前端开发的主流模式,绝大部分的前端应用都适合用这种方式来开发,又特别是 React.Vue 等组件技术的发展,更是使这种方式深入人心. 但有一些应用,客户端渲染就会遇到一些问题了: 需要做 SEO(搜索引擎优化),但客户端渲染的 html 中几乎没有可用的信息 需要首屏快速加载,但客户端渲染一般是长时间的加载动画或者白屏 如果能把客户端渲染的组件化技术(React.Vue 等)与传统的后端渲染的

  • Vue中强制组件重新渲染的正确方法

    有时候,依赖 vue 响应方式来更新数据是不够的,相反,我们需要手动重新渲染组件来更新数据.或者,我们可能只想抛开当前的DOM,重新开始.那么,如何让vue以正确的方式重新呈现组件呢? 强制 Vue 重新渲染组件的最佳方法是在组件上设置:key. 当我们需要重新渲染组件时,只需更 key 的值,Vue 就会重新渲染组件. 这是一个非常简单的解决方案. 当然,你可能会对其他方式会更感兴趣: 简单粗暴的方式:重新加载整个页面 不妥的方式:使用 v-if 较好的方法:使用Vue的内置forceUpda

  • 加速vue组件渲染之性能优化

    背景 平时在用vue开发后台管理系统的时候,应该会用到大量的table这种组件,正常这种组件我们会在项目里做二次封装,然后针对表头title做参数化配置,如下: export default { data(){ return { tableTitle:[ { label:'省份', prop:'prop' }, { label:'城市', prop:'prop' }, { label:'汇总', prop:'prop', colconfig:[ { label:'下级', prop:'prop'

  • vue 强制组件重新渲染(重置)的两种方案

    数据通过异步操作后,对之前刚加载的数据进行变更后,发现数据不能生效 方案一 当数据变更后,通过watch 监听,先去销毁当前的组件,然后再重现渲染.使用 v-if 可以解决这个问题 <template> <third-comp v-if="reFresh"/> </template> <script> export default{ data(){ return { reFresh:true, menuTree:[] } }, watch

  • Vue强制组件重新渲染的方法讨论

    有时候,依赖 Vue 响应方式来更新数据是不够的,相反,我们需要手动重新渲染组件来更新数据.或者,我们可能只想抛开当前的DOM,重新开始.那么,如何让Vue以正确的方式重新呈现组件呢? 强制 Vue 重新渲染组件的最佳方法是在组件上设置:key. 当我们需要重新渲染组件时,只需更 key 的值,Vue 就会重新渲染组件. 这是一个非常简单的解决方案. 当然,你可能会对其他方式会更感兴趣: 简单粗暴的方式:重新加载整个页面 不妥的方式:使用 v-if 较好的方法:使用Vue的内置forceUpda

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

    目录 引言与例子 举一个工作中能用到的例子: 实现 extend 执行流程 1. 注册流程(以Vue.component()祖册为例子): 2. 执行流程 3. 渲染流程 总结 注: 本文目的是通过源码方向来讲component组件的渲染流程 引言与例子 在我们创建Vue实例时,是通过new Vue.this._init(options)方法来进行初始化,然后再执行$mount等,那么在组件渲染时,可不可以让组件使用同一套逻辑去处理呢? 答:当然是可以的,需要使用到Vue.extend方法来实现

  • Vue组件与生命周期详细讲解

    目录 写在前面 生命周期 图解 总结 写在前面 Vue.js的核心就是以简洁的模板语法将数据渲染进Dom系统.vue实例在渲染成html的时候往往要经过以下的步骤: 读取数据和方法,设置数据绑定和监听 解析template 将实例挂载到Dom,并将实例状态和视图绑定,在执行这些步骤的时候,vue还提供了一些生命周期的方法,用来在不同的阶段对代码做增添和修改. 生命周期 beforeCreate vue实例初始化,数据监听和方法属性挂载之前调用 created 数据监听和方法属性挂载之后调用,是最

  • Vue组件之间的通信方式详细讲解

    目录 前言 一.父级传数据给子级 1.传输固定的具体数据 2.动态语法 3.子组件调用父组件 二.子级传数据给父级 1.使用自定义事件 2.$refs的使用 3.同级别组价以及任意组件之间的数据传递 前言 在前面,我们已经了解了vue的组件以及vue组件之间的层级关系,这个在博主的往期博客,感兴趣的可以往前挪,地址是: 1.vue组件 2.vue组件的层级关系 本文主要编写记录的是,组件之间的通信的模式以及通信的方式,我们的组件之间只能调用自己的属性和自己的方法,不能调用其他组件的属性以及方法,

  • VUE组件传参超详细讲解

    目录 Vue.cli 中怎样使用自定义的组件 Vue 组件如何进行传值的 父组件向子组件传递数据 子组件向父组件传递数据 非父子组件之间传递数据 父传子例子 子传父例子 Vue组件data为什么必须是函数 组件的命名规范 Vue 组件里的定时器要怎么销毁 Vue.cli 中怎样使用自定义的组件 第一步:在 components 目录新建你的组件文件(CounterCom.vue),script一定要export default {} 第二步:在需要用的页面(组件)中导入: import Coun

  • Vue echarts画甘特图流程详细讲解

    vue项目中添加echarts,只需要增加echarts依赖,然后在main.js中引入echarts就可以使用了. 1.npm install echarts --save 2.修改main.js import * as echarts from 'echarts' Vue.prototype.$echarts = echarts 3.具体页面使用: <template> <div class="about"> <h1>This is echart

  • React生命周期与父子组件间通信知识点详细讲解

    目录 声明周期 声明周期解析 生命周期函数 Constructor componentDidMount componentDidUpdate componentWillUnmount 不常用的生命周期函数 认识组件间的通信 参数propTypes 限制单个元素 默认 Prop 值 对于函数式组件 子组件传递父组件 声明周期 很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期: React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能: 生命周期和

  • Sentinel整合Feign流程详细讲解

    修改84模块 84消费者调用提供者9003 Feign组件一般是消费侧 重点依赖 <!--SpringCloud openfeign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> 激活Sentinel对Fei

  • Android四大组件之Service服务详细讲解

    目录 一.Service是什么 二.Service 的启动方式 2.1.startService 显示启动 Service启动 Service 停止 2.2.bindService 绑定启动 使用bindService()方法启动Service unbindService 停止服务 三.Service 生命周期 startService启动的生命周期 bindService启动的生命周期 上一节:Activity 简介:在Android组件中最基本也是最为常见的四大组件: Activity Se

  • Python Flask实现图片验证码与邮箱验证码流程详细讲解

    目录 1. 图片验证码 1.1 工具类-utility.py 1.2 控制层-user.py 2. 邮箱验证码 2.1 准备 2.2 工具类-utility.py 2.3 控制层-user.py 1. 图片验证码 1.1 工具类-utility.py 将所有和图片验证码有关的方法放在类 ImageCode import random import string from io import BytesIO from PIL import Image, ImageFont, ImageDraw c

  • Java Controller实现参数验证与统一异常处理流程详细讲解

    目录 一,前期数据及类准备 1.1 统一状态码 1.2 统一返回格式 1.3 自定义接口API异常类 1.4 参数封装类 二,参数验证 2.1 pom.xml 2.2 修改参数封装类 2.3 controller 2.4 测试 三,统一异常处理 3.1 方法参数验证异常处理 3.2 其他异常处理 最近开发了比较多的接口,因为没有可参考的案例,所以一开始一直按照我的理解进行开发.开发多了发现自己每个结果都写了相同的代码:try() {} catch() {}, 和关于参数判空的:StringUti

随机推荐