本文以局部組件的註冊方式介紹組件的初始化渲染,demo以下vue
new Vue({ el: '#app', template: `<div> <div>father component!</div> <my-component></my-component> </div>`, components:{ 'my-component': { template: '<div>children component!</div>' } } })
一、Vue源碼解析(一)-模版渲染介紹過,vue初始化時根據template函數生成render函數,本文render函數會調用vm._c('my-component'),_createElement判斷'my-component是註冊過的組件,所以以組件的方式生成vnodenode
updateComponent = function () { vm._update(vm._render(), hydrating); }; //template生成的render函數vm._render會調用vm._c('my-component') vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); }; function _createElement(){ //本例tag=‘my-component’,‘my-component’在components屬性中註冊過,所以以組件的方式生成vnode if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { vnode = createComponent(Ctor, data, context, children, tag); } } //本例Ctor參數{template: '<div>children component1!</div>'} function createComponent (Ctor){ //Vue構造函數 var baseCtor = context.$options._base; if (isObject(Ctor)) { //生成VuComponent構造函數 //此處至關於Ctor = Vue.extend({template: '<div>children component1!</div>'}), Vue.extend後面有介紹; Ctor = baseCtor.extend(Ctor); } //將componentVNodeHooks上的方法掛載到vnode上,組件初次渲染會用到componentVNodeHooks.init var data = {} mergeHooks(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 } ); } //component初始化和更新的方法,此處先介紹init var componentVNodeHooks = { init(vnode){ //根據Vnode生成VueComponent實例 var child = vnode.componentInstance = createComponentInstanceForVnode(vnode); //將VueComponent實例掛載到dom節點上,本文是掛載到<my-component></my-component>節點 child.$mount(hydrating ? vnode.elm : undefined, hydrating); } }
二、調用vm._update將vnode渲染爲瀏覽器dom,主要方法是遍歷vnode的全部節點,根據節點類型調用相關的方法進行解析,本文主要介紹components的解析方法createComponent:根據vnode生成VueComponent(繼承Vue)對象,
調用Vue.prototype.$mount方法渲染domreact
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) { //組件vnode節點渲染方法 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } //Vue源碼解析(一)中介紹過普通vnode節點渲染步驟 //根據vnode節點生成瀏覽器Element對象 vnode.elm = nodeOps.createElement(tag, vnode); var children = vnode.children; //遞歸將vnode子節點生成Element對象 createChildren(vnode, children, insertedVnodeQueue); //將生成的vnode.elm插入到瀏覽器的父節點當中 insert(parentElm, vnode.elm, refElm); } function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { var i = vnode.data; if (isDef(i = i.hook) && isDef(i = i.init)) { //i就是上面的componentVNodeHooks.init方法 i(vnode, false /* hydrating */, parentElm, refElm); } if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true } } function createComponentInstanceForVnode (){ var options = { _isComponent: true, parent: parent, propsData: vnodeComponentOptions.propsData, _componentTag: vnodeComponentOptions.tag, _parentVnode: vnode, _parentListeners: vnodeComponentOptions.listeners, _renderChildren: vnodeComponentOptions.children, _parentElm: parentElm || null, _refElm: refElm || null }; //上面提到的VueComponent構造函數Ctor,至關於new VueComponent(options) return new vnode.ComponentOptions.Ctor(options) }
3 、new VueComponent和new Vue的過程相似,本文就再也不作介紹segmentfault
上文提到過 Vue.extend方法(繼承Vue生成VueComponent構造函數)此處單獨介紹一下api
Vue.extend = function (extendOptions) { var Super = this; var Sub = function VueComponent (options) { this._init(options); }; //經典的繼承寫法 Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; Sub.options = mergeOptions( Super.options, extendOptions ); return Sub };
經過Vue.component也能夠全局註冊組件,不須要每次new vue的時候單獨註冊,demo以下:瀏覽器
var globalComponent = Vue.extend({ name: 'global-component', template: '<div>global component!</div>' }); Vue.component('global-component', globalComponent); new Vue({ el: '#app', template: `<div> <global-component></global-component> <my-component></my-component> </div>`, components:{ 'my-component': { template: '<div>children component!</div>' } } })
vue.js初始化時會先調用一次initGlobalAPI(Vue),給Vue構造函數掛載上一些全局的api,其中又會調用到
initAssetRegisters(Vue),其中定義了Vue.component方法,具體看下其實現app
var ASSET_TYPES = [ 'component', 'directive', 'filter' ]; //循環註冊ASSET_TYPES中的全局方法 ASSET_TYPES.forEach(function (type) { Vue[type] = function ( id, definition ) { if (!definition) { return this.options[type + 's'][id] } else { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && type === 'component') { validateComponentName(id); } if (type === 'component' && isPlainObject(definition)) { definition.name = definition.name || id; definition = this.options._base.extend(definition); } if (type === 'directive' && typeof definition === 'function') { definition = { bind: definition, update: definition }; } //全局的組件、指令和過濾器都掛載在Vue.options上 this.options[type + 's'][id] = definition; return definition } }; }); Vue.prototype._init = function (options) { vue初始化時將options參數和Vue.options組裝爲vm.$options vm.$options = mergeOptions( //Vue.options resolveConstructorOptions(vm.constructor), options || {}, vm ); }
本例組裝後的vm.$option.components值以下,proto中前3個屬性是內置全局組件
dom
在 Vue 中,父子組件的關係能夠總結爲 prop 向下傳遞,事件向上傳遞。父組件經過 prop 給子組件下發數據,子組件經過事件給父組件發送消息.先看看prop是怎麼工做的。demo以下:函數
new Vue({ el: '#app', template: `<div> <div>father component!</div> <my-component message="hello!"></my-component> </div>`, components:{ 'my-component':{ props: ['message'], template: '<span>{{ message }}</span>' } } })
一、template生成的render函數包含:_c('my-component',{attrs:{"message":"hello!"}})]
二、render => vnode => VueComponent,上文提到的VueComponent的構造函數調用了Vue.prototype._init,而且入參option.propsData:{message: "hello!"}
三、雙向綁定中介紹過Vue初始化時會對data中的全部屬性調用defineReactive方法,對data屬性進行監聽;
VueComponent對propsData也是相似的處理方法,initProps後propsData中的屬性和data同樣也是響應式的,propsData變化,相應的view也會發生改變this
function initProps (vm, propsOptions) { for (var key in propsOptions){ //defineReactive參照Vue源碼解析(二) defineReactive(props, key, value); //將propsData代理到vm上,經過vm[key]訪問propsData[key] proxy(vm, "_props", key); } }
四、propsData是響應式的了,但更經常使用的是動態props,按官網說法:「咱們能夠用v-bind來動態地將prop綁定到父組件的數據。每當父組件的數據變化時,該變化也會傳導給子組件」,那麼vue是如何將data的變化傳到給自組件的呢,先看demo
var vm = new Vue({ el: '#app', template: `<div> <my-component :message="parentMsg"></my-component> </div>`, data(){ return{ parentMsg:'hello' } }, components:{ 'my-component':{ props: ['message'], template: '<span>{{ message }}</span>' } } }) vm.parentMsg = 'hello world'
五、雙向綁定中介紹過vm.parentMsg變化,會觸發dep.notify(),通知watcher調用updateComponent;
又回到了updateComponent,以後的dom更新過程能夠參考上文的組件渲染邏輯,只是propsData值已是最新的vm.parentMsg的值了
//又見到了。。全部的dom初始化或更新都會用到 updateComponent = function () { vm._update(vm._render(), hydrating); }; Vue.prototype._update = function (vnode, hydrating) { var prevVnode = vm._vnode; vm._vnode = vnode; //Vue源碼解析(一)介紹過dom初始化渲染的源碼 if (!prevVnode) { // initial render vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ); } else { // 本文介紹dom更新的方法 vm.$el = vm.__patch__(prevVnode, vnode); } }
Vue源碼解析(一)介紹過vm.__patch__中dom初始化渲染的邏輯,本文再簡單介紹下vm.__patch關於component更新的邏輯:
function patchVnode (oldVnode, vnode){ //上文介紹過componentVNodeHooks.init,此處i=componentVNodeHooks.prepatch var data = vnode.data; if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode); } } var componentVNodeHooks = { init(){}, prepatch: function prepatch (oldVnode, vnode) { var options = vnode.componentOptions; var child = vnode.componentInstance = oldVnode.componentInstance; //更新組件 updateChildComponent( child, //此時的propsData已是最新的vm.parentMsg options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ); } } function updateChildComponent (vm, propsData){ //將vm._props[key]設置爲新的propsData[key]值,從而觸發view層的更新 var props = vm._props; props[key] = validateProp(key, vm.$options.props, propsData, vm); }
子組件向父組件通訊須要用到emit,先給出demo
var vm = new Vue({ el: '#app', template: `<div> <my-component @rf="receiveFn"></my-component> </div>`, methods:{ receiveFn(msg){ console.log(msg) } }, components:{ 'my-component':{ template: '<div>child</div>', mounted(){ this.$emit('rf','hello') } } } })
本例中子組件mount結束會觸發callHook(vm, 'mounted'),調用this.$emit('rf','hello'),從而調用父組件的receiveFn方法
Vue.prototype.$emit = function (event) { //本例cbs=vm._events['rf'] = receiveFn,vm._events涉及v-on指令解析,之後有機會詳細介紹下 var cbs = vm._events[event]; //截取第一位以後的參數 var args = toArray(arguments, 1); //執行cbs cbs.apply(vm, args); }
prop和emit是父子組件通訊的方式,非父子組件能夠經過event bus(事件總線)實現
var bus = new Vue(); var vm = new Vue({ el: '#app', template: `<div> <my-component-1></my-component-1> <my-component-2></my-component-2> </div>`, components:{ 'my-component-1':{ template: '<div>child1</div>', mounted(){ bus.$on('event',(msg)=>{ console.log(msg) }) } }, 'my-component-2':{ template: '<div>child2</div>', mounted(){ bus.$emit('event','asd') } } } })
emit方法上文已經介紹過,主要看下on方法,其實就是將fn註冊到vm._events上
Vue.prototype.$on = function (event, fn) { var vm = this; if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { this.$on(event[i], fn); } } else { (vm._events[event] || (vm._events[event] = [])).push(fn); } return vm };