組件是可復用的Vue實例,一個組件本質上是一個擁有預定義選項的一個Vue實例,組件和組件之間通過一些屬性進行聯繫。 組件有兩種註冊方式,分別是全局註冊和局部註冊,前者通過Vue.component()註冊,後者是在創建Vue實例的時候在components屬性里指定,例如: 渲染DOM為: 其中He ...
組件是可復用的Vue實例,一個組件本質上是一個擁有預定義選項的一個Vue實例,組件和組件之間通過一些屬性進行聯繫。
組件有兩種註冊方式,分別是全局註冊和局部註冊,前者通過Vue.component()註冊,後者是在創建Vue實例的時候在components屬性里指定,例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="vue.js"></script> </head> <body> <div id="app"> <child title="Hello Wrold"></child> <hello></hello> <button @click="test">測試</button> </div> <script> Vue.component('child',{ //全局註冊 props:['title'], template:"<p>{{title}}</p>" }) var app = new Vue({ el:'#app', components:{ hello:{template:'<p>Hello Vue</p>'} //局部組件 }, methods:{ test:function(){ console.log(this.$children) console.log(this.$children[1].$parent ===this) } } }) </script> </body> </html>
渲染DOM為:
其中Hello World是全局註冊的組件渲染出來的,而Hello Vue是局部組件渲染出來的。
我們在測試按鈕上綁定了一個事件,點擊按鈕後輸出如下:
可以看到Vue實例的$children屬性是個數組,對應的是當前實例引用的所有組件的實例,其中$children[0]是全局組件child的實例,而children[1]是局部組件hello的實例。
而this.$children[1].$parent ===this輸出為true則表示對於組件實例來說,它的$parent指向的父組件實例,也就是例子里的根組件實例。
Vue內部也是通過$children和$parent屬性實現了組件和組件之間的關聯的。
組件是可以無限復用的,比如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="vue.js"></script> </head> <body> <div id="app"> <child title="Hello Wrold"></child> <child title="Hello Vue"></child> <child title="Hello Rose"></child> </div> <script> Vue.component('child',{ props:['title'], template:"<p>{{title}}</p>" }) var app = new Vue({el:'#app'}) </script> </body> </html>
渲染為:
註:對於組件來說,需要把data屬性設為一個函數,內部返回一個數據對象,因為如果只返回一個對象,當組件復用時,不同的組件引用的data為同一個對象,這點和根Vue實例不同的,可以看官網的例子:點我點我
例1:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="vue.js"></script> </head> <body> <div id="app"> <child ></child> </div> <script> Vue.component('child',{ data:{title:"Hello Vue"}, template:"<p>{{title}}</p>" }) var app = new Vue({el:'#app'}) </script> </body> </html>
運行時瀏覽器報錯了,如下:
報錯的內部實現:Vue註冊組件時會先執行Vue.extend(),然後執行mergeOptions合併一些屬性,執行到data屬性的合併策略時會做判斷,如下:
strats.data = function ( //data的合併策略 第1196行 parentVal, childVal, vm ) { if (!vm) { //如果vm不存在,對於組件來說是不存在的 if (childVal && typeof childVal !== 'function') { //如果值不是一個函數,則報錯 "development" !== 'production' && warn( 'The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm ); return parentVal } return mergeDataOrFn(parentVal, childVal) } return mergeDataOrFn(parentVal, childVal, vm) };
源碼分析
以這個例子為例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="vue.js"></script> </head> <body> <div id="app"> <child title="Hello Wrold"></child> </div> <script> Vue.component('child',{ props:['title'], template:"<p>{{title}}</p>" }) var app = new Vue({el:'#app',}) </script> </body> </html>
Vue內部會執行initGlobalAPI()函數給大Vue增加一些靜態方法,其中會執行一個initAssetRegisters函數,該函數會給Vue的原型增加一個Vue.component、Vue.directive和Vue.filter函數函數,分別用於註冊組件、指令和過濾器,如下
function initAssetRegisters (Vue) { //初始化component、directive和filter函數 第4850行 /** * Create asset registration methods. */ ASSET_TYPES.forEach(function (type) { //遍歷//ASSET_TYPES數組 ASSET_TYPES是一個數組,定義在339行,等於:['component','directive','filter'] Vue[type] = function ( id, definition ) { if (!definition) { return this.options[type + 's'][id] } else { /* istanbul ignore if */ if ("development" !== 'production' && type === 'component') { validateComponentName(id); } if (type === 'component' && isPlainObject(definition)) { //如果是個組件 definition.name = definition.name || id; definition = this.options._base.extend(definition); //則執行Vue.extend()函數 ;this.options._base等於大Vue,定義在5050行 } if (type === 'directive' && typeof definition === 'function') { definition = { bind: definition, update: definition }; } this.options[type + 's'][id] = definition; //將definition保存到this.options[type + 's']里,例如組件保存到this.options['component']裡面 return definition } }; }); }
Vue.extend()將使用基礎Vue構造器,創建一個“子類”。參數是一個包含組件選項的對象,也就是註冊組件時傳入的對象,如下:
Vue.extend = function (extendOptions) { //初始化Vue.extend函數 第4770行 extendOptions = extendOptions || {}; var Super = this; var SuperId = Super.cid; var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}); if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } var name = extendOptions.name || Super.options.name; if ("development" !== 'production' && name) { validateComponentName(name); } var Sub = function VueComponent (options) { //定義組件的構造函數,函數最後會返回該函數 this._init(options); }; /*中間進行一些數據的合併*/ // cache constructor cachedCtors[SuperId] = Sub; return Sub }; }
以例子為例,當載入完後,我們在控制台輸入console.log(Vue.options["components"]),輸出如下:
可以看到child組件的構造函數被保存到Vue.options["components"]["child“]裡面了。其他三個KeepAlive、Transition和TransitionGroup是Vue的內部組件
當vue載入時會執行模板生成的render函數,例子里的render函數等於:
執行_c('child',{attrs:{"title":"Hello Wrold"}})函數時會執行vm.$createElement()函數,也就是Vue內部的createElement函數,如下
function createElement ( //創建vNode 第4335行 context, tag, data, children, normalizationType, alwaysNormalize ) { if (Array.isArray(data) || isPrimitive(data)) { //如果data是個數組或者是基本類型 normalizationType = children; children = data; //修正data為children data = undefined; //修正data為undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE; } return _createElement(context, tag, data, children, normalizationType) //再調用_createElement } function _createElement ( //創建vNode context, //context:Vue對象 tag, //tag:標簽名或組件名 data, children, normalizationType ) { /*略*/ if (typeof tag === 'string') { //如果tag是個字元串 var Ctor; ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag); if (config.isReservedTag(tag)) { //如果tag是平臺內置的標簽 // platform built-in elements vnode = new VNode( //調用new VNode()去實例化一個VNode config.parsePlatformTagName(tag), data, children, undefined, undefined, context ); } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { //如果該節點名對應一個組件,掛載組件時,如果某個節點是個組件,則會執行到這裡 // component vnode = createComponent(Ctor, data, context, children, tag); //創建組件Vnode } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ); } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children); } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) { applyNS(vnode, ns); } if (isDef(data)) { registerDeepBindings(data); } return vnode //最後返回VNode } else { return createEmptyVNode() } }
resolveAsset()用於獲取資源,也就是獲取組件的構造函數(在上面Vue.extend裡面定義的構造函數),定義如下:
function resolveAsset ( //獲取資源 第1498行 options, type, id, warnMissing ) { /* istanbul ignore if */ if (typeof id !== 'string') { return } var assets = options[type]; // check local registration variations first if (hasOwn(assets, id)) { return assets[id] } //先從當前實例上找id var camelizedId = camelize(id); if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } //將id轉化為駝峰式後再找 var PascalCaseId = capitalize(camelizedId); if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } //如果還沒找到則嘗試將首字母大寫查找 // fallback to prototype chain var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; //最後通過原型來查找 if ("development" !== 'production' && warnMissing && !res) { warn( 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, options ); } return res }
例子里執行到這裡時就可以獲取到在Vue.extend()里定義的Sub函數了,如下:
我們點擊這個函數時會跳轉到Sub函數,如下:
回到_createElement函數,獲取到組件的構造函數後就會執行createComponent()創建組件的Vnode,這一步對於組件來說很重要,它會對組件的data、options、props、自定義事件、鉤子函數、原生事件、非同步組件分別做一步處理,對於組件的實例化來說,最重要的是安裝鉤子吧,如下:
function createComponent ( //創建組件Vnode 第4182行 Ctor:組件的構造函數 data:數組 context:Vue實例 child:組件的子節點 Ctor, data, context, children, tag ) { /*略*/ // install component management hooks onto the placeholder node installComponentHooks(data); //安裝一些組件的管理鉤子 /*略*/ 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 ); //創建組件Vnode return vnode //最後返回vnode }
installComponentHooks()會給組件安裝一些管理鉤子,如下:
function installComponentHooks (data) { //安裝組件的鉤子 第4307行 var hooks = data.hook || (data.hook = {}); //嘗試獲取組件的data.hook屬性,如果沒有則初始化為空對象 for (var i = 0; i < hooksToMerge.length; i++) { //遍歷hooksToMerge里的鉤子,保存到hooks對應的key裡面 var key = hooksToMerge[i]; hooks[key] = componentVNodeHooks[key]; } }
componentVNodeHooks保存了組件的鉤子,總共有四個:init、prepatch、insert和destroy,對應組件的四個不同的時期,以例子為例執行完後data.hook等於如下:
最後將虛擬VNode渲染為真實DOM節點的時候會執行n createelm()函數,該函數會優先執行createComponent()函數去創建組件,如下:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { //創建組件節點 第5590行 ;註:這是patch()函數內的createComponent()函數,而不是全局的createComponent()函數 var i = vnode.data; //獲取vnode的data屬性 if (isDef(i)) { //如果存在data屬性(組件vnode肯定存在這個屬性,普通vnode有可能存在) var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; //這是keepAlive邏輯,可以先忽略 if (isDef(i = i.hook) && isDef(i = i.init)) { //如果data里定義了hook方法,且存在init方法 i(vnode, false /* hydrating */, parentElm, refElm); } // 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. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true } } }
createComponent會去執行組件的init()鉤子函數:
init: function init ( //組件的安裝 第4110行 vnode, //vnode:組件的占位符VNode hydrating, //parentElm:真實的父節點引用 parentElm, //refElm:參考節點 refElm ) { if ( //這是KeepAlive邏輯 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 { var child = vnode.componentInstance = createComponentInstanceForVnode( //調用該方法返回子組件的Vue實例,並保存到vnode.componentInstance屬性上 vnode, activeInstance, parentElm, refElm ); child.$mount(hydrating ? vnode.elm : undefined, hydrating); } },
createComponentInstanceForVnode會創建組件的實例,如下:
function createComponentInstanceForVnode ( //第4285行 創建組件實例 vnode:占位符VNode parent父Vue實例 parentElm:真實的DOM節點 refElm:參考節點 vnode, // we know it's MountedComponentVNode but flow doesn't parent, // activeInstance in lifecycle state parentElm, refElm ) { var options = { _isComponent: true, parent: parent, _parentVnode: vnode, _parentElm: parentElm || null, _refElm: refElm || null }; // check inline-template render functions var inlineTemplate = vnode.data.inlineTemplate; //嘗試獲取inlineTemplate屬性,定義組件時如果指定了inline-template特性,則組件內的子節點都是該組件的模板 if (isDef(inlineTemplate)) { //如果inlineTemplate存在,我們這裡是不存在的 options.render = inlineTemplate.render; options.staticRenderFns = inlineTemplate.staticRenderFns; } return new vnode.componentOptions.Ctor(options) //調用組件的構造函數(Vue.extend()裡面定義的)返回子組件的實例,也就是Vue.extend()里定義的Sub函數 }
最後Vue.extend()里的Sub函數會執行_init方法對Vue做初始化,初始化的過程中會定義組件實例的$parent和父組件的$children屬性,從而實現父組件和子組件的互連,組件的大致流程就是這樣子