在上一篇文章中我介绍了 vue从实例化到渲染到页面的具体流程,本文基于该文章介绍组件的创立到渲染的具体流程,咱们将从源码的⾓度来剖析Vue的组件外部是如何⼯作的,只有理解了外部的⼯作原理,能力让咱们使⽤它的时候更加得⼼应⼿。
vue源码绝对比较复杂,须要急躁重复了解及调试,不懂就多调试问百度,罗马不是一日建成的,置信保持就会有播种哈~
咱们先写个测试页面,具体调试能够下载vue.js,而后具体做断点debugger调试。
<script src="./vue.js"></script> <div id="app"></div> <script> var Child = { name: 'childcom', template: '<div>{{msg}}</div>', data() { return { msg: 'child msg', } } } var vm = new Vue({ el: '#app', render(h){ return h(Child) }, data: { message: 'Hello', } }) </script>
先上图剖析流程,绿线为组件化流程走向。
1. 执行render函数
由上一篇文章可知,页面渲染次要是执行vm._update(vm._render(), hydrating)
, 即先执行vm._render()
函数获取vnode虚构dom, 而后再vm._update(vnode)
把虚构dom更新到页面上,对应到理论开发中,也就是下面测试页中的 render(h){ return h(Child) }
, 此处的 h
即为 createElement
, createElement
通过参数重载之后调用 _createElement
_createElement 次要是依据tag,标签判断是组件还是一般node标签,返回对应的vnode虚构dom,所以外围在于 createComponent 函数
function _createElement ( context, tag, data, children, normalizationType) { if (typeof tag === 'string') { // 一般元素 创立虚构dom vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ); }else{ // 组件 创立虚构dom vnode = createComponent(tag, data, context, children); }}
2. 执行 createComponent 函数
createComponent 的逻辑也会有⼀些简单,然而剖析源码⽐较举荐的是只剖析核⼼流 程,分⽀流程能够之后针对性的看,所以这⾥针对组件渲染这3个关键步骤:
- 创立 Vue.extend 函数
// 在vue初始化initGlobalAPI的时候定义了 Vue.options._base = Vue // baseCtor.extend(Ctor) 等价于 Vue.extend(Ctor) const baseCtor = context.$options._base if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) }
接着看 enxtend 函数是如何构建 VueComponent 构造函数的
Vue.extend = function (extendOptions) { // this 即为 Vue var Super = this; // 创立 VueComponent 构造函数 var Sub = function VueComponent (options) { // 当咱们去实例化 Sub 的时候,就会执⾏ this._init 逻辑再次⾛到了 Vue 实例的初始化逻辑 this._init(options); }; // Object.create继承 Super, 也就是继承了 Vue Sub.prototype = Object.create(Super.prototype); // 更改constructor,指向本人,实现完满继承 Sub.prototype.constructor = Sub; Sub.cid = cid++; Sub.options = mergeOptions( Super.options, extendOptions ); Sub['super'] = Super; return Sub }
能够学习通过Object.create的形式继承类,这也是面试中常常会遇到的问题。VueComponent 继承了 Vue, 而后合并options,最初返回 Sub 构造函数。
- 装置组件钩⼦函数
// install component management hooks onto the placeholder nodeinstallComponentHooks(data)
整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩⼦函数合并到 data.hook 中,在 VNode 执⾏ patch 的过程中执⾏相干的钩⼦函数,具体的执⾏咱们稍后在介绍 patch 过程中会具体介绍。
- 实例化 vnode
// return a placeholder vnode var name = Ctor.options.name || tag; 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
通过以上三个步骤可知组件的实例化是由Vue.extend中 VueComponent 构造函数实现的,并且createComponent最终返回了vnode,以执行 vm._update(vnode)
渲染页面。
3. 执行 _update 函数
_update 执行 patch ,也就是调用 path 函数
function patch (oldVnode, vnode, hydrating, removeOnly) { // 通过vnode创立新的实在dom节点 createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ); // 移除老节点 if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0); } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode); } }
path的外围就在于 createElm,通过该函数将虚构dom创立为实在的dom,接下来看 createElm
function createElm (vnode,insertedVnodeQueue, parentElm,refElm,nested,ownerArray,index ) { // 如果是组件则执行 createComponent if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // createChildren 递归调用 createElm 生成vnode createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { // 实在dom元素 属性结构 invokeCreateHooks(vnode, insertedVnodeQueue); } // 父元素插入实在dom元素 insert(parentElm, vnode.elm, refElm); }
createComponent函数执行
let i = vnode.dataif (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false );}
在第二步 createComponent 的时候, 通过执行 installComponentHooks(data)
, 将 vnode.data 挂上了 init 等钩子函数, i(vnode, false );
也就是执行 上面的 init函数
var componentVNodeHooks = { init: function init (vnode, hydrating) { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch var mountedNode = vnode; // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode); } else { // 获取 Vue.extend 构造函数实例 var child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ); // 挂载实例 child.$mount(hydrating ? vnode.elm : undefined, hydrating); } }, // ....}
此处次要通过 createComponentInstanceForVnode 实例化 VueComponent 失去实例,并挂载。
function createComponentInstanceForVnode ( vnode, // we know it's MountedComponentVNode but flow doesn't parent // activeInstance in lifecycle state) { var options = { _isComponent: true, _parentVnode: vnode, parent: parent }; // check inline-template render functions var inlineTemplate = vnode.data.inlineTemplate; if (isDef(inlineTemplate)) { options.render = inlineTemplate.render; options.staticRenderFns = inlineTemplate.staticRenderFns; } // vnode.componentOptions.Ctor 等价于 VueComponent return new vnode.componentOptions.Ctor(options)}
合并options 并把 _isComponent 设置为 true, 而后 new VueComponent(options),因为VueComponent是Vue的子类,那么实例化将从新执行一遍 Vue._init(options)。 也就再执行了一遍 vm._update(vm._render(), hydrating)
。最初将执行回来 createElm
, 还是持续把 createElm
代码贴出来。
function createElm (vnode,insertedVnodeQueue, parentElm,refElm,nested,ownerArray,index ) { // 如果是组件则执行 createComponent if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // createChildren 递归调用 createElm 生成vnode createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { // 实在dom元素 属性结构 invokeCreateHooks(vnode, insertedVnodeQueue); } // 父元素插入实在dom元素 insert(parentElm, vnode.elm, refElm); }
如果该组件没有子组件那么 createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
为 false
, 那么将跳出递归一步步回到 patch
函数, 渲染页面。
其实,vue组件化过程就是先new Vue, 而后执行init初始化,接着判断是否含有组件, 如果没有则间接生成vode,最初通过path把vnode渲染到页面。如果有组件,则通过 createComponent函数,来创立 VueComponent构造函数 (继承 Vue)。而后在patch的时候通过 createElm函数中判断是否有组件,而后调用VueComponent函数,再执行一遍init初始化,直到没有组件被实例化完之后接着执行createElm来创立实在dom元素,最初把它渲染到页面上。具体能够参考下面的流程图来剖析!