内容简介:组件是Vue的一个重要核心,我们在进行项目工程化时,会将页面的结构组件化,组件化意味着独立和共享。编写Vue组件是掌握Vue开发的核心基础,Vue官网也花了大篇幅介绍了组件的知识,并且也深入讲解了Vue的使用。这一节内容,我们将深入Vue组件部分的源码,了解组件注册的实现思路和组件渲染挂载的基本流程。这将让我们今后在解决vue组件相关问题上更加得心应手。熟悉Vue的开发流程的都知道,Vue组件在使用之前需要进行注册,而注册的方式有两种,全局注册和局部注册。在进入源码分析之前,我们先回忆一下两者的用法,以便后
组件是Vue的一个重要核心,我们在进行项目工程化时,会将页面的结构组件化,组件化意味着独立和共享。编写Vue组件是掌握Vue开发的核心基础,Vue官网也花了大篇幅介绍了组件的知识,并且也深入讲解了Vue的使用。这一节内容,我们将深入Vue组件部分的源码,了解组件注册的实现思路和组件渲染挂载的基本流程。这将让我们今后在解决vue组件相关问题上更加得心应手。
5.1 组件两种注册方式
熟悉Vue的开发流程的都知道,Vue组件在使用之前需要进行注册,而注册的方式有两种,全局注册和局部注册。在进入源码分析之前,我们先回忆一下两者的用法,以便后续掌握两者的差异。
5.1.1 全局注册
Vue.component('my-test', { template: '<div>{{test}}</div>', data () { return { test: 1212 } } }) var vm = new Vue({ el: '#app', template: '<div id="app"><my-test><my-test/></div>' }) 复制代码
其中组件的全局注册需要在全局实例化Vue前调用,注册之后可以用在任何新创建的 Vue 实例的模板中调用。
5.1.2 局部注册
var myTest = { template: '<div>{{test}}</div>', data () { return { test: 1212 } } } var vm = new Vue({ el: '#app', component: { myTest } }) 复制代码
当只需要在某个局部用到某个组件时,可以使用局部注册的方式进行组件注册,此时局部注册的组件只能在注册该组件的组件内部使用。
5.1.3 注册过程
简单回顾两种组件注册方式后,我们来看注册过程到底发生了什么,我们以全局组件注册为例。它通过 Vue.component(name, {...})
进行组件注册, Vue.component
的定义在 Vue
源码的初始化中。
// 初始化全局api initAssetRegisters(Vue); var ASSET_TYPES = [ 'component', 'directive', 'filter' ]; function initAssetRegisters(Vue){ // 定义ASSET_TYPES中每个属性的方法,其中包括component ASSET_TYPES.forEach(function (type) { // type: component,directive,filter Vue[type] = function (id,definition) { if (!definition) { // 直接返回注册组件的构造函数 return this.options[type + 's'][id] } ... if (type === 'component') { // 验证component组件名字是否合法 validateComponentName(id); } if (type === 'component' && isPlainObject(definition)) { // 组件名称设置 definition.name = definition.name || id; // Vue.extend() 创建子组件,返回子类构造器 definition = this.options._base.extend(definition); } // 为Vue.options 上的component属性添加将子类构造器 this.options[type + 's'][id] = definition; return definition } }); } 复制代码
源码中全局注册组件的实际是调用构造函数 Vue
的静态方法 extend
,并且为 Vue.options
上的 component
属性添加子类构造器。其中extend方法的定义我们在 深入剖析Vue源码 - 选项合并(上) 中的子类构造器一段中有介绍这里不赘述,总结而言: Vue.extend
创建了一个基于父Vue的子类,创建过程会继承父类的方法,并对父类子类的配置进行合并,最终返回一个子类的构造器。这个子组件的名称默认以 Vue.component()
的第一个参数作为组件名,当组件选项有name属性时,则用name属性覆盖。当 Vue.component()
不传递第二个选项参数时,会返回已经注册过的子类构造器。
接下来留下一个问题,局部注册和全局注册的在实现上的区别在哪里?
5.2 组件Vnode创建
组件注册过后,会在实例 options.component
增加一个组件的配置属性,这个属性是一个子的 Vue
构造器。然而这个组件何时创建,何时进行实例化,何时渲染,何时挂载基础钩子是这一小节分析的核心。
5.2.1 Vnode创建流程图
5.2.2 具体流程分析
我们将上图的流程简单概括为以下几点:
-
-
Vue
根实例初始化会执行vm.$mount(vm.$options.el)
实例挂载的过程,按照之前 深入剖析Vue源码 - 完整渲染过程 所讲的逻辑,完整流程会经历render
函数生成Vnode
,以及Vnode
生成真实DOM
的过程。
-
-
-
render
函数生成Vnode
过程中,子会优先父执行生成Vnode
过程,子执行过程中遇到子组件占位符如(<test></test>
)时,会判断该占位符是否是注册过的组件标签,如果符合条件,则进入createComponent
创建子组件的过程,如果为一般标签,则执行new Vnode
过程。
-
-
-
createComponent
是创建组件Vnode
的过程,创建过程会合并子和父构造器的选项配置,并安装组件相关的钩子,最后通过new Vnode()
生成以vue-component
开头的Virtual DOM
-
-
-
render
函数执行过程也是一个循环递归调用创建Vnode
的过程,执行3,4步之后,完整的生成了一个包含各个子组件的Vnode tree
-
这其中一二步的源码是前一节所分析过的,我们重点分析遇到子组件占位符时差异的处理。
// 内部执行将render函数转化为Vnode的函数 function _createElement(context,tag,data,children,normalizationType) { ··· if (typeof tag === 'string') { // 子节点的标签为普通的html标签,直接创建Vnode if (config.isReservedTag(tag)) { vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ); // 子节点标签为注册过的组件标签名,则子组件Vnode的创建过程 } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 创建子组件Vnode vnode = createComponent(Ctor, data, context, children, tag); } } } 复制代码
其中核心是在判断该子组件的占位符是否为已注册过的组件,在介绍全局注册时我们已经知道了,一个组件全局注册后,Vue实例的 options.component
对象上会新增一个带有构造器的组件选项。因此是否拥有这个选项也成为判断组件是否注册的标准。
// 需要明确组件是否已经被注册 function resolveAsset (options,type,id,warnMissing) { // 标签为字符串 if (typeof id !== 'string') { return } // 这里是 options.component var assets = options[type]; // 这里的分支分别支持大小写,驼峰的命名规范 if (hasOwn(assets, id)) { return assets[id] } var camelizedId = camelize(id); if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } var PascalCaseId = capitalize(camelizedId); if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } // fallback to prototype chain var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; if (warnMissing && !res) { warn( 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, options ); } // 最终返回子类的构造器 return res } 复制代码
子组件创建Vnode的过程是调用 createComponent
方法。
// 创建子组件过程 function createComponent ( Ctor, // 子类构造器 data, context, // vm实例 children, // 子节点 tag // 子组件占位符 ) { ··· // Vue.options里的_base属性存储Vue构造器 var baseCtor = context.$options._base; // 针对局部组件注册场景 if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor); } data = data || {}; // 构造器配置合并 resolveConstructorOptions(Ctor); // 挂载组件钩子 installComponentHooks(data); // return a placeholder vnode var name = Ctor.options.name || tag; // 创建子组件vnode,名称以 vue-component- 开头 var vnode = new VNode(("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),data, undefined, undefined, undefined, context,{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },asyncFactory); return vnode } 复制代码
这里将大部分的代码都拿掉了,只留下创建vnode相关的代码,最终会通过 new Vue
实例化一个名称以 vue-component-
开头标记名称的 Vnode
节点返回。其中两个关键的步骤是配置合并和安装组件钩子函数,选项合并的内容可以查看这个系列的前两节,这里看看 installComponentHooks
安装组件钩子函数时发生了什么。
// 组件内部自带钩子 var componentVNodeHooks = { init: function init (vnode, hydrating) { }, prepatch: function prepatch (oldVnode, vnode) { }, insert: function insert (vnode) { }, destroy: function destroy (vnode) { } }; var hooksToMerge = Object.keys(componentVNodeHooks); // 将componentVNodeHooks 钩子函数合并到组件data.hook中 function installComponentHooks (data) { var hooks = data.hook || (data.hook = {}); for (var i = 0; i < hooksToMerge.length; i++) { var key = hooksToMerge[i]; var existing = hooks[key]; var toMerge = componentVNodeHooks[key]; // 如果钩子函数存在,则执行mergeHook$1方法合并 if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge; } } } function mergeHook$1 (f1, f2) { // 返回一个依次执行f1,f2的函数 var merged = function (a, b) { f1(a, b); f2(a, b); }; merged._merged = true; return merged } 复制代码
组件默认自带几个钩子函数,这些钩子函数在后续 patch
过程中会在不同阶段执行, installComponentHooks
函数的目的是将这些默认的钩子函数和自定义的钩子函数合并,合并的原则是如果钩子函数存在,则合并两个函数,在执行阶段会依次执行。
5.2.3 局部注册和全局注册的区别
在说到全局注册和局部注册的用法时留下了一个问题,局部注册和全局注册两者的区别在哪里。上文源码分析讲到全局注册却没有提及局部注册,其实局部注册的原理同样简单,我们使用局部注册组件时会通过在父组件选项配置中的 component
添加子组件的对象配置,这和全局注册后在Vue的 options.component
添加子组件构造器的结果很相似。区别在于:
- 1.局部注册添加的对象配置是在某个组件下,而全局注册添加的子组件是在根实例下。
- 2.局部注册添加的是一个子组件的配置对象,而全局注册添加的是一个子类构造器。
因此局部注册中缺少了一步构建子类构造器的过程,这个过程放在哪里进行呢? 回到 createComponent
的源码,源码中根据传入对象和构造器的分类区分局部和全局注册组件,而局部注册依然会调用 父类的 extend
方法去创建子类构造器。
// 针对局部组件注册场景 if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor); } 复制代码
5.3 组件Vnode渲染真实DOM
有了完整的 Vnode tree
,接下来是根据 Vnode tree
渲染真实的 DOM
5.3.1 真实节点渲染流程图
5.3.2 具体流程分析
-
- 经过
vm._render()
生成完整的Virtual Dom
树后,紧接着执行Vnode渲染真实DOM的过程,即vm.update()
,而update
的核心方法是vm.__patch__
- 经过
-
-
vm.__patch__
内部会通过createElm
去创建真实的DOM
元素,期间遇到子Vnode
会递归调用createElm
方法。
-
-
- 递归调用过程中,判断该节点类型为组件类型是通过
createComponent
方法判断的,该方法和渲染Vnode
阶段的方法createComponent
不同,他会调用子组件的init
初始化钩子函数,并完成组件的DOM
插入。
- 递归调用过程中,判断该节点类型为组件类型是通过
-
- 而
init
初始化钩子函数的核心是new
实例化这个子组件,实例化子组件的过程又回到合并配置,初始化生命周期,初始化事件中心,初始化渲染的过程
- 而
-
- 完成所有子组件的实例化和节点挂载后,最后才回到根节点的挂载。
__patch__
核心代码是通过 createElm
创建真实节点,当创建过程中遇到子 vnode
时,会调用 createChildren
, createChildren
的目的是对子vnode递归调用 createElm
创建子组件节点。
// 创建真实dom function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) { ··· // 递归创建子组件真实节点,直到完成所有子组件的渲染才进行根节点的真实节点插入 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } ··· var children = vnode.children; // createChildren(vnode, children, insertedVnodeQueue); ··· insert(parentElm, vnode.elm, refElm); } function createChildren(vnode, children, insertedVnodeQueue) { for (var i = 0; i < children.length; ++i) { // 遍历子节点,递归调用创建真实dom节点的方法 - createElm createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i); } } 复制代码
对子组件的处理,放在 createComponent
方法中, createComponent
的核心是会判断这个 Vnode
是否为子组件,如果条件满足,则执行组件注册时安装的 init
方法(由于在组件注册过程中会安装一系列的钩子函数,所以是否有钩子函数可以作为判断组件的唯一条件)。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { var i = vnode.data; // 是否有钩子函数可以作为判断是否为组件的唯一条件 if (isDef(i = i.hook) && isDef(i = i.init)) { // 执行init钩子函数 i(vnode, false /* hydrating */); } ··· } 复制代码
由于前面在介绍组件内部钩子函数时跳过了每个钩子内部实现功能的介绍,所以我们需要回头分析 init
钩子函数的执行逻辑(其中忽略 keeplive
分支逻辑)。
var componentVNodeHooks = { // 忽略keepAlive过程 var child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance); child.$mount(hydrating ? vnode.elm : undefined, hydrating); } function createComponentInstanceForVnode(vnode, parent) { ··· // 实例化Vue子组件实例 return new vnode.componentOptions.Ctor(options) } 复制代码
init
执行过程中会调用 createComponentInstanceForVnode
对子组件进行实例化。 调用 createComponent
是一个递归调用实例化所有子组件的过程,只有会将所有的子组件实例化,并挂载到对应父节点上后,才最后进行根节点的挂载。
5.4 小结
这一节内容是组件部分的基础,介绍了组件注册到挂载渲染流程,组件的注册核心是定义了一个子类构造器,渲染过程中遵循先子后父的思想,逐级递归对子组件完成生成 vnode
到挂载真实 dom
的挂载,最后完成根节点的挂载。从源码内部分析完组件流程怎么工作后,接下来会介绍一些组件高级用法的实现原理。
以上所述就是小编给大家介绍的《深入剖析Vue源码 - 组件基础》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Flink 核心组件原理多图剖析
- 深入剖析Vue源码 - 组件进阶
- 剖析 Android 架构组件之 ViewModel
- Jetty9源码剖析 - 基础组件 - QueuedThreadPool
- SpringCloud组件 & 源码剖析:Eureka服务注册方式流程全面分析
- 蚂蚁金服分布式链路跟踪组件 SOFATracer 总览 | 剖析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。