前言
Vue中组件分为全局组件和局部组件:
全局组件: 通过 Vue测试数据ponent(id,definition) 方法进行注册,并且可以在任何组件中被访问 局部组件: 在组件内的 components 属性中定义,只能在组件内访问下面是一个例子:
<div id="app"> {{ name }} <my-button></my-button> <aa></aa> </div> Vue测试数据ponents('my-button', { template: `<button>my button</button>` }); Vue测试数据ponents('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测试数据ponent 是定义在 Vue 构造函数上的一个函数,它接收 id 和 definition 作为参数:
id : 组件的唯一标识 definition : 组件的配置项在 src/global-api/index.js 中定义 Vue测试数据ponent 方法:
export function initGlobalApi (Vue) { Vue.options = {}; // 最终会合并到实例上,可以通过vm.$options._base直接使用 Vue.options._base = Vue; // 定义全局组件 Vue.options测试数据ponents = {}; initExtend(Vue); Vue.mixin = function (mixin) { this.options = mergeOptions(this.options, mixin); }; // 通过Vue测试数据ponents来注册全局组件 Vue测试数据ponents = function (id, definition) { const name = definition.name = definition.name || id; // 通过Vue.extend来创建Vue的子类 definition = this.options._base.extend(definition); // 将Vue子类添加到Vue.options测试数据ponents对象中,key为name this.options测试数据ponents[name] = definition; }; }
Vue测试数据ponent 帮我们做了俩件事:
通过 Vue.extend 利用传入的 definition 生成 Vue 子类 将 Vue 子类放到全局 Vue.options测试数据ponents 中那么 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测试数据ponent = Super测试数据ponent; 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 中的 components 和 Vue.options测试数据ponents 中通过 Vue测试数据ponent 定义的组件进行合并。
在 merge-options.js 中,我们为 strategies 添加合并 components 的策略:
strategies测试数据ponents = 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测试数据ponents 中的全局组件放到了合并后对象的原型上,而将 options 中 components 属性定义的局部组件放到了自身的属性上。这样当取值时,首先会从自身属性上查找,然后再到原型链上查找,也就是优先渲染局部组件,如果没有局部组件就会去渲染全局组件。
合并完 components 之后, 接下来要创建组件对应的虚拟节点:
function createVComponent (vm, tag, props, key, children) { const baseCtor = vm.$options._base; // 在生成父虚拟节点的过程中,遇到了子组件的自定义标签。它的定义放到了父组件的components中,所有通过父组件的$options来进行获取 // 这里包括全局组件和自定义组件,内部通过原型链进行了合并 let Ctor = vm.$options测试数据ponents[tag]; // 全局组件:Vue子类构造函数,局部组件:对象,合并后的components中既有对象又有构造函数,这里要利用Vue.extend统一处理为构造函数 if (typeof Ctor === 'object') { Ctor = baseCtor.extend(Ctor); } props.hook = { // 在渲染真实节点时会调用init钩子函数 init (vNode) { const child = vNode测试数据ponentInstance = 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.extend 将 components 中的对象转换为 Vue 子类构造函数 在虚拟节点上的 props 上添加钩子函数,方便在之后调用 执行 vNode 函数创建组件虚拟节点,组件虚拟节点会新增 componentOptions 属性来存放组件的一些选项在生成虚拟节点之后,便会通过虚拟节点来创建真实节点,如果是组件虚拟节点要单独处理:
// 处理组件虚拟节点 function createComponent (vNode) { let init = vNode.props?.hook?.init; init?.(vNode); if (vNode测试数据ponentInstance) { return true; } } // 将虚拟节点处理为真实节点 function createElement (vNode) { if (typeof vNode.tag === 'string') { if (createComponent(vNode)) { return vNode测试数据ponentInstance.$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测试数据ponentInstance = new Ctor(); child.$mount(); } };
此时便会通过 new Ctor() 来进行子组件的一系列初始化工作:
this._init initState ...Ctor 是通过 Vue.extend 来生成的,而在执行 Vue.extend 的时候,我们已经将组件对应的配置项传入。但是由于配置项中缺少 el 选项,所以要手动执行 $mount 方法来挂载组件。
在执行 $mount 之后,会将组件 template 创建为真实 DOM 并设置到 vm.$el 选项上。执行 props.hook.init 方法时,将组件实例放到了 vNode 的 componentInstance 属性上,最终在 createComponent 中会判断如果有该属性则为组件虚拟节点,并将其对应的 DOM ( vNode测试数据ponentInstance.$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 组件渲染内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!