Vue 组件渲染详情

目录
  • 前言
  • 全局组件
  • Vue.extend
  • 组件渲染流程
  • 总结

前言

Vue中组件分为全局组件和局部组件:

  • 全局组件:通过Vue.component(id,definition)方法进行注册,并且可以在任何组件中被访问
  • 局部组件:在组件内的components属性中定义,只能在组件内访问

下面是一个例子:

<div id="app">
  {{ name }}
  <my-button></my-button>
  <aa></aa>
</div>
Vue.components('my-button', {
  template: `<button>my button</button>`
});
Vue.components('aa', {
  template: `<button>global aa</button>`
});
const vm = new Vue({
  el: '#app',
  components: {
    aa: {
      template: `<button>scoped aa</button>`
    },
    bb: {
      template: `<button>bb</button>`
    }
  },
  data () {
    return {
      name: 'ss'
    };
  }
});

页面中会渲染全局定义的my-button组件和局部定义的aa组件:

接下来笔者会详细讲解全局组件和局部组件到底是如何渲染到页面上的,并实现相关代码。

全局组件

Vue.component是定义在Vue构造函数上的一个函数,它接收iddefinition作为参数:

  • id: 组件的唯一标识
  • definition: 组件的配置项

src/global-api/index.js中定义Vue.component方法:

export function initGlobalApi (Vue) {
  Vue.options = {};
  // 最终会合并到实例上,可以通过vm.$options._base直接使用
  Vue.options._base = Vue;
  // 定义全局组件
  Vue.options.components = {};
  initExtend(Vue);
  Vue.mixin = function (mixin) {
    this.options = mergeOptions(this.options, mixin);
  };
  // 通过Vue.components来注册全局组件
  Vue.components = function (id, definition) {
    const name = definition.name = definition.name || id;
    // 通过Vue.extend来创建Vue的子类
    definition = this.options._base.extend(definition);
    // 将Vue子类添加到Vue.options.components对象中,key为name
    this.options.components[name] = definition;
  };
}

Vue.component帮我们做了俩件事:

  • 通过Vue.extend利用传入的definition生成Vue子类
  • Vue子类放到全局Vue.options.components

那么Vue.extend是如何创建出Vue的子类呢?下面我们来实现Vue.extend函数

Vue.extend

Vue.extend利用JavaScript原型链实现继承,我们会将Vue.prototype指向Sub.prototype.__proto__,这样就可以在Sub的实例上调用Vue原型上定义的方法了:

Vue.extend = function (extendOptions) {
  const Super = this;
  const Sub = function VueComponent () {
    // 会根据原型链进行查找,找到Super.prototype.init方法
    this._init();
  };
  Sub.cid = cid++;
  // Object.create将Sub.prototype的原型指向了Super.prototype
  Sub.prototype = Object.create(Super.prototype);
  // 此时prototype为一个对象,会失去原来的值
  Sub.prototype.constructor = Sub;
  Sub.options = mergeOptions(Super.options, extendOptions);
  Sub.component = Super.component;
  return Sub;
};

如果有小伙伴对JavaScript原型链不太了解的话,可以看笔者的这篇文章: 一文彻底理解JavaScript原型与原型链

核心的继承代码如下:

const Super = Vue
const Sub = function VueComponent () {
  // some code ...
};
// Object.create将Sub.prototype的原型指向了Super.prototype
Sub.prototype = Object.create(Super.prototype);
// 此时prototype为一个对象,会失去原来的值
Sub.prototype.constructor = Sub;

Object.create会创建一个新对象,使用一个已经存在的对象作为新对象的原型。这里将创建的新对象赋值给了Sub.prototype,相当于做了如下俩件事:

  • Sub.prototype = {}
  • Sub.prototype.__proto__ = Super.prototype

Sub.prototype赋值后,其之前拥有的constructor属性便会被覆盖,这里需要再手动指定一下Sub.prototype.constructor = Sub

最终Vue.extend会将生成的子类返回,当用户实例化这个子类时,便会通过this._init执行子类的初始化方法创建组件

组件渲染流程

在用户执行new Vue创建组件的时候,会执行this._init方法。在该方法中,会将用户传入的配置项和Vue.options中定义的配置项进行合并,最终放到vm.$options中:

function initMixin (Vue) {
  Vue.prototype._init = function (options = {}) {
    const vm = this;
    // 组件选项和Vue.options或者 Sub.options进行合并
    vm.$options = mergeOptions(vm.constructor.options, options);
    // ...
  };
  // ...
}

执行到这里时,mergeOptoins会将用户传入options中的componentsVue.options.components中通过Vue.component定义的组件进行合并。

merge-options.js中,我们为strategies添加合并components的策略:

strategies.components = function (parentVal, childVal) {
  const result = Object.create(parentVal); // 合并后的原型链为parentVal
  for (const key in childVal) { // childVal中的值都设置为自身私有属性,会优先获取
    if (childVal.hasOwnProperty(key)) {
      result[key] = childVal[key];
    }
  }
  return result;
};

components的合并利用了JavaScript的原型链,将Vue.options.components中的全局组件放到了合并后对象的原型上,而将optionscomponents 属性定义的局部组件放到了自身的属性上。这样当取值时,首先会从自身属性上查找,然后再到原型链上查找,也就是优先渲染局部组件,如果没有局部组件就会去渲染全局组件。

合并完components之后,接下来要创建组件对应的虚拟节点:

function createVComponent (vm, tag, props, key, children) {
  const baseCtor = vm.$options._base;
  // 在生成父虚拟节点的过程中,遇到了子组件的自定义标签。它的定义放到了父组件的components中,所有通过父组件的$options来进行获取
  // 这里包括全局组件和自定义组件,内部通过原型链进行了合并
  let Ctor = vm.$options.components[tag];
  // 全局组件:Vue子类构造函数,局部组件:对象,合并后的components中既有对象又有构造函数,这里要利用Vue.extend统一处理为构造函数
  if (typeof Ctor === 'object') {
    Ctor = baseCtor.extend(Ctor);
  }
  props.hook = { // 在渲染真实节点时会调用init钩子函数
    init (vNode) {
      const child = vNode.componentInstance = new Ctor();
      child.$mount();
    }
  };
  return vNode(`vue-component-${Ctor.id}-${tag}`, props, key, undefined, undefined, { Ctor, children });
}

function createVElement (tag, props = {}, ...children) {
  const vm = this;
  const { key } = props;
  delete props.key;
  if (isReservedTag(tag)) { // 是否为html的原生标签
    return vNode(tag, props, key, children);
  } else {
    // 创建组件虚拟节点
    return createVComponent(vm, tag, props, key, children);
  }
}

在创建虚拟节点时,如果tag不是html中定义的标签,便需要创建组件对应的虚拟节点。

组件虚拟节点中做了下面几件事:

  • 通过vm.$options拿到合并后的components
  • Vue.extendcomponents中的对象转换为Vue子类构造函数
  • 在虚拟节点上的props上添加钩子函数,方便在之后调用
  • 执行vNode函数创建组件虚拟节点,组件虚拟节点会新增componentOptions属性来存放组件的一些选项

在生成虚拟节点之后,便会通过虚拟节点来创建真实节点,如果是组件虚拟节点要单独处理:

// 处理组件虚拟节点
function createComponent (vNode) {
  let init = vNode.props?.hook?.init;
  init?.(vNode);
  if (vNode.componentInstance) {
    return true;
  }
}

// 将虚拟节点处理为真实节点
function createElement (vNode) {
  if (typeof vNode.tag === 'string') {
    if (createComponent(vNode)) {
      return vNode.componentInstance.$el;
    }
    vNode.el = document.createElement(vNode.tag);
    updateProperties(vNode);
    for (let i = 0; i < vNode.children.length; i++) {
      const child = vNode.children[i];
      vNode.el.appendChild(createElement(child));
    }
  } else {
    vNode.el = document.createTextNode(vNode.text);
  }
  return vNode.el;
}

在处理虚拟节点时,我们会获取到在创建组件虚拟节点时为props添加的init钩子函数,将vNode传入执行init函数:

props.hook = { // 在渲染真实节点时会调用init钩子函数
  init (vNode) {
    const child = vNode.componentInstance = new Ctor();
    child.$mount();
  }
};

此时便会通过new Ctor()来进行子组件的一系列初始化工作:

  • this._init
  • initState
  • ...

Ctor是通过Vue.extend来生成的,而在执行Vue.extend的时候,我们已经将组件对应的配置项传入。但是由于配置项中缺少el选项,所以要手动执行$mount方法来挂载组件。

在执行$mount之后,会将组件template创建为真实DOM并设置到vm.$el选项上。执行props.hook.init方法时,将组件实例放到了vNodecomponentInstance 属性上,最终在createComponent中会判断如果有该属性则为组件虚拟节点,并将其对应的DOM(vNode.componentInstance.$el)返回,最终挂载到父节点上,渲染到页面中。

整个渲染流程画图总结一下:

总结

明白了组件渲染流程之后,最后我们来看一下父子组件的生命周期函数的执行过程:

<div id="app">
  {{ name }}
  <aa></aa>
</div>
<script>
  const vm = new Vue({
    el: '#app',
    components: {
      aa: {
        template: `<button>aa</button>`,
        beforeCreate () {
          console.log('child beforeCreate');
        },
        created () {
          console.log('child created');
        },
        beforeMount () {
          console.log('child beforeMount');
        },
        mounted () {
          console.log('child mounted');
        }
      },
    },
    data () {
      return {
        name: 'ss'
      };
    },
    beforeCreate () {
      console.log('parent beforeCreate');
    },
    created () {
      console.log('parent created');
    },
    beforeMount () {
      console.log('parent beforeMount');
    },
    mounted () {
      console.log('parent mounted');
    }
  });
</script>

在理解了Vue的组件渲染流程后,便可以很轻易的解释这个打印结果了:

  • 首先会初始化父组件,执行父组件的beforeCreate,created钩子
  • 接下来会挂载父组件,在挂载之前会先执行beforeMount钩子
  • 当父组件开始挂载时,首先会生成组件虚拟节点,之后在创建真实及节点时,要new SubComponent来创建子组件,得到子组件挂载后的真实DOM:vm.$el
  • 而在实例化子组件的过程中,会执行子组件的beforeCreate,created,beforeMount,mounted钩子
  • 在子组件挂载完毕后,继续完成父组件的挂载,执行父组件的mounted钩子

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

(0)

相关推荐

  • Vue组件渲染与更新实现过程浅析

    目录 1. 模板编译 2. 组件渲染和更新 1. 模板编译 Vue的模板编译就是将模板字符串转换为渲染函数的过程.具体来说,当组件的生命周期执行到created和beforeMounted之间时,Vue会将模板(template)编译成渲染函数(render),render函数是一个纯JavaScript函数,由with语句构成,它接收一个Vue组件实例作为参数.当render函数执行时会调用h函数,生成虚拟DOM节点(vnode). 下面给出了常见的template模板以及模板编译后的结果:

  • 关于vue组件的更新机制 resize() callResize()

    目录 组件的更新机制 resize() callResize() 异步更新机制是如何实现的 组件的更新机制 resize() callResize() 假设有一段代码,通过isCollapse改变触发ref的子组件child触发方法resize(),借着触发callResize()方法. 这是vue组件的更新机制. 子组件是child,父组件是整个界面 ,在父组件上任意触发监听,调用方法resize(): resize()调用callResize(),callResize()把下面的3个方法的r

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

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

  • Vue中关于重新渲染组件的方法及总结

    目录 重新渲染组件的方法总结 重新加载整个页面 使用forceUpdate 总结 重新渲染组件的方法总结 有时Vue的反应性系统还不够,您只需要重新渲染组件即可. 重新渲染组件有以下几个办法: 可怕的方法:重新加载整个页面 可怕的方法:使用v-if 更好的方法:使用Vue的内置forceUpdate方法 最好的方法:在组件上进行key更改 重新加载整个页面 非常不建议这样做,我们来看下一个办法 v-if指令,该指令仅在组件为true时才渲染. 如果为false,则该组件在DOM中不存在. 下面我

  • Vue 父子组件的数据传递、修改和更新方法

    父子组件之间的数据关系,我这边将情况具体分成下面4种: 父组件修改子组件的data,并实时更新 子组件通过$emit传递子组件的数据,this.$data指当前组件的data(return{...})里的所有数据, this.$emit('data',this.$data); 之后通过父组件的getinputdata方法来接收数据 @data='getinputdata' 其中的data就是传过来的数据,通过修改这个数据就可以通过父组件实时更新子组件 getinputdata(data) { c

  • Vue手写实现组件初渲染

    目录 前言 生成虚拟节点 将虚拟节点处理为真实节点 总结 前言 在Vue进行文本编译之后,会得到代码字符串生成的render函数.本文会基于render函数介绍以下内容: 执行render函数生成虚拟节点 通过vm._update方法,将虚拟节点渲染为真实DOM 在vm.$mount方法中,文本编译完成后,要进行组件的挂载,代码如下: Vue.prototype.$mount = function (el) { // text compile code .... mountComponent(v

  • Vue组件更新数据v-model不生效的解决

    目录 组件更新数据v-model不生效 问题描述 原因分析 方法一 方法二 方法三 关于v-model失效的问题 解决办法 组件更新数据v-model不生效 问题描述 在使用Vue双向绑定(v-model)功能时,封装子组件通过Inject功能使用了父组件中的 model 中的属性进行双向绑定,此时在程序中去更新model的某个属性的值,发现子组件没有实时渲染. 原因分析 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化.尽管如此我们还是有一些办法来回避这些限制并保证它们的响

  • 简单聊一聊Vue3组件更新过程

    目录 前言 副作用渲染函数更新组件的过程 核心逻辑:patch流程 1.处理组件 2.处理普通元素 总结 前言 组件渲染的过程,本质上就是把各种把各种类型的 vnode 渲染成真实 DOM.我们也知道了组件是由模板.组件描述对象和数据构成的,数据的变化会影响组件的变化.组件的渲染过程中创建了一个带副作用的渲染函数,当数据变化的时候就会执行这个渲染函数来触发组件的更新.本文我们就具体分析一下组件的更新过程. 副作用渲染函数更新组件的过程 我们先来回顾一下带副作用渲染函数 setupRenderEf

  • Vue 组件渲染详情

    目录 前言 全局组件 Vue.extend 组件渲染流程 总结 前言 Vue中组件分为全局组件和局部组件: 全局组件:通过Vue.component(id,definition)方法进行注册,并且可以在任何组件中被访问 局部组件:在组件内的components属性中定义,只能在组件内访问 下面是一个例子: <div id="app"> {{ name }} <my-button></my-button> <aa></aa> &

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

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

  • 解决vue组件渲染没更新数据问题

    目录 问题: 现象: 原因 解决步骤 问题: 使用前端日期控件时 - 数据联动时数据绑定无效问题 现象: 选择A日期,想动态改变B日期数据,只有第一次选择时会动态改变B日期数据,第二次选择A日期时,B日期数据虽已改变,但是页面数据未改变 例如我要点击留样日期,销毁日期就得默认设置为留样日期之后的三个月,只有第一次点击,效果能正常显示,但是第二次点击,销毁日期就不会更新了… 原因 前端组件没有重新加载,一直保持旧数据 解决步骤 使用标志位让组件每次更改刷新一次 一.前端组件绑定点击事件 二.新增一

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

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

  • vue组件是如何解析及渲染的?

    前言 本文将对vue组件如何解析以及渲染做一个讲解. 我们可以通过Vue.component注册全局组件,之后可以在模板中进行使用 <div id="app"> <my-button></my-button> </div> <script> Vue.component("my-button", { template: "<button> 按钮组件</button>"

  • Vue 组件组织结构及组件注册详情

    目录 1.组件的组织 2.组件名 2.1 组件命名方式 3.全局注册 4.局部注册 1.组件的组织 通常一个应用会以一棵嵌套的组件树的形式来组织: 例如:我们可能会有页头.侧边栏.内容区等组件,每个组件又包含了其它的像导航链接.博文之类的组件. 为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别.这里有两种组件的注册类型:全局注册和局部注册. 至此,我们的组件都只是通过 Vue.component 全局注册的: Vue.component('my-component-name', {

  • Vue 组件化基本使用详情

    目录 1.什么叫做组件化 2.基本使用 前言: 有时候有一组html结构的代码,并且这个上面可能还绑定了事件.然后这段代码可能有多个地方都被使用到了,如果都是拷贝来拷贝去,很多代码都是重复的,包括事件部分的代码都是重复的.那么这时候我们就可以把这些代码封装成一个组件,以后在使用的时候就跟使用普通的html元素一样,拿过来用就可以了. 1.什么叫做组件化 vue.js 有两大法宝,一个是数据驱动,另一个就是组件化,那么问题来了,什么叫做组件化,为什么要组件化?接下来我就针对这两个问题一一解答,所谓

  • 封装一个Vue文件上传组件案例详情

    目录 前言 1. 子组件 2 父组件使用 3.效果 4.总结 前言 在面向特定用户的项目中,引 其他ui组件库导致打包体积过大,首屏加载缓慢,还需要根据UI设计师设计的样式,重写大量的样式覆盖引入的组件库的样式.因此尝试自己封装一个自己的组件,代码参考了好多前辈的文章 1. 子组件 <template> <div class="digital_upload"> <input style="display: none" @change=&

  • ajax请求+vue.js渲染+页面加载的示例

    1.导入js <script type="text/javascript" src="<c:url value="/resources/lib/jquery/jquery-1.11.0.min.js" />"></script> <!--标准mui.css--> <link href="<c:url value=" rel="external nofollo

随机推荐