爲了探究父子組件的數據傳遞以及事件觸發, 父組件選用模板:vue
<div id="app"><button-counter :count-total="count" v-on:increment="incremenTotal"></button-counter></div>
複製代碼
子組件選用模板node
<button v-on:click="incrementCounter">{{ counter }}</button>
複製代碼
父組件經過props傳遞參數count到子組件,子組件經過incrementCounter函數中的$emit觸發父組件的incremenTotal函數,具體過程以下:react
button-counter組件中props傳遞的值,會在genData$2中通過genProps函數處理生成數組
"{"count-total":count}"
複製代碼
綁定的事件會通過genHandler函數處理生成bash
on:{"increment":incremenTotal}
複製代碼
因此生成的render函數爲app
"_c('button-counter',{attrs:{"count-total":count},on:{"increment":incremenTotal}})"
複製代碼
根據前面的編譯器原理生成完整的render函數爲:async
"_c('div',{attrs:{"id":"app"}},[_c('button-counter',{attrs:{"count-total":count},on:{"increment":incremenTotal}})],1)"
複製代碼
調用渲染函數函數
vnode = render.call(vm._renderProxy, vm.$createElement);
複製代碼
調用渲染函數,先建立子組件Vnodeui
[_c('button-counter',{attrs:{"count-total":count},on:{"increment":incremenTotal}}]
複製代碼
_c表示vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); }
,執行this
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType)
}
複製代碼
其中tag爲button-counter
,data爲{attrs:{"count-total":count},on:{"increment":incremenTotal}}
,其他參數均爲undefined,而_createElement函數爲
...
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// platform built-in elements
// 若是是內置標籤
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 判斷函數是否已經註冊,註冊則建立組件
// component
vnode = createComponent(Ctor, data, context, children, tag);
} 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
} else {
return createEmptyVNode()
}
複製代碼
Ctor = resolveAsset(context.$options, 'components', tag)
獲取已註冊組件button-counter的構造函數,建立組件createComponent函數爲
function createComponent (
Ctor,
data,
context,
children,
tag
) {
// 獲取Vue的構造函數VueComponent
var baseCtor = context.$options._base;
resolveConstructorOptions(Ctor);
...
// extract props,處理props傳遞的數據,淺拷貝
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
// 獲取監聽函數的對象
var listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;
// install component management hooks onto the placeholder node
// 給建立的子組件,添加鉤子
installComponentHooks(data);
// Core爲子組件button-counter的構造函數
var name = Ctor.options.name || tag;
// 建立新的Vnode
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
}
複製代碼
resolveConstructorOptions(Ctor)函數執行後,button子組件的參數Ctor.options爲
{
components: {button-counter: ƒ},
data: ƒ (),
directives: {},
filters: {},
methods: {incrementCounter: ƒ},
name: "button-counter",
props: {countTotal: {type: ƒ, default: 0}},
template: "<button v-on:click="incrementCounter">{{ counter }}</button>",
_Ctor: {0: ƒ},
_base: ƒ Vue(options)
}
複製代碼
接着,installComponentHooks函數爲button-counter組件添加的鉤子爲
destroy: ƒ destroy(vnode)
init: ƒ init(vnode, hydrating)
insert: ƒ insert(vnode)
prepatch: ƒ prepatch(oldVnode, vnode)
// data的格式以下
{
attrs: {},
hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
on: undefined
}
複製代碼
組件button-counter生成新的Vnode以下:
{
asyncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ VueComponent(options), // button-counter
children: undefined,
listeners: {increment: ƒ},
propsData: {countTotal: 0},
tag: "button-counter"
},
context: f Vue,
data: {
attrs: {},
hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ},
on: undefined
},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "vue-component-1-button-counter",
text: undefined,
child: undefined
}
複製代碼
button-counter組件的Vnode建立完畢,再回到建立id爲app的元素節點,整個Vnode爲
{
asyncFactory: undefined,
asyncMeta: undefined,
children: [VNode],
componentInstance: undefined,
componentOptions: undefined,
context: Vue實例,
data: {attrs: {id: "app"}},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "div",
text: undefined,
child: undefined
}
複製代碼
在patch過程當中,createElm函數建立元素
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
複製代碼
對於div元素,createComponent函數返回false,button-counter組件則會執行如下代碼:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
// 執行組件初始化的鉤子
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */);
}
// 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.
...
}
}
複製代碼
只有組件data屬性中才具備鉤子函數,子組件初始化以下:
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 {
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
}
複製代碼
根據組件Vnode建立組件實例
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;
}
return new vnode.componentOptions.Ctor(options)
}
複製代碼
new vnode.componentOptions.Ctor(options)
其中options爲
{
parent: id爲app的Vue實例,
_isComponent: true,
_parentVnode: 父組件中button-counter組件的Vnode
}
複製代碼
新建子組件button實例,進入到了
var Sub = function VueComponent (options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
複製代碼
子組件的構造函數繼承了父組件。接着初始化子組件的參數
initInternalComponent(vm, options);
複製代碼
合併父子組件的參數後vm.$options參數爲
{
parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}.
propsData: {countTotal: 0},
_componentTag: "button-counter",
_parentListeners: {increment: ƒ},
_parentVnode: VNode {tag: "vue-component-1-button-counter", data: {…}, children: undefined, text: undefined, elm: undefined, …},
_renderChildren: undefined
}
複製代碼
button子組件的參數爲
{
components: {button-counter: ƒ}
data: ƒ (),
directives: {},
filters: {},
methods: {incrementCounter: ƒ},
name: "button-counter",
props: {
countTotal: {type: ƒ, default: 0}
},
template: "<button v-on:click="incrementCounter">{{ count }}</button>",
_Ctor: {0: ƒ},
_base: ƒ Vue(options)
}
複製代碼
button子組件會在原型上繼承該對象,button子組件爲
{
parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
propsData: {countTotal: 0},
_componentTag: "button-counter",
_parentListeners: {increment: ƒ},
_parentVnode: VNode {tag: "vue-component-1-button-counter", data: {…}, children: undefined, text: undefined, elm: undefined, …},
_renderChildren: undefined,
__proto__: Object // 繼承上面的對象
}
複製代碼
上面的參數通過初始化props
if (opts.props) {
initProps(vm, opts.props);
}
複製代碼
具體爲
var propsData = vm.$options.propsData || {};
var props = vm._props = {};
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
var keys = vm.$options._propKeys = [];
var isRoot = !vm.$parent;
// root instance props should be converted
// root實例的props屬性應該被轉成響應式數據
if (!isRoot) {
toggleObserving(false);
}
// static props are already proxied on the component's prototype // during Vue.extend(). We only need to proxy props defined at // instantiation here. if (!(key in vm)) { proxy(vm, "_props", key); } }; toggleObserving(true); } 複製代碼
規格化後的props從其父組件傳入的props數據中或者使用new建立的propsData參數中,篩選出須要的數據保存在vm._props中,而後在vm上設置一個代理,經過vm.x訪問vm._props.x。
button子組件根據編譯器解析生成的render函數爲:
with(this){return _c('button',{on:{"click":incrementCounter}},[_v(_s(counter))])}
複製代碼
this指向button-counter組件的構造函數,接着button子組件會調用$mount函數,進行模板解析,生成Vnode
{
asyncFactory: undefined,
asyncMeta: undefined,
children: [VNode],
componentInstance: undefined,
componentOptions: undefined,
context: button-counter子組件的構造函數,
data: {on: {click: f}},
elm: undefined,
fnContext: undefined,
fnOptions: undefined,
fnScopeId: undefined,
isAsyncPlaceholder: false,
isCloned: false,
isComment: false,
isOnce: false,
isRootInsert: true,
isStatic: false,
key: undefined,
ns: undefined,
parent: undefined,
raw: false,
tag: "button",
text: undefined,
child: undefined
}
複製代碼
context表示父組件中button-counter組件的構造實例,接着進入patch過程
// 此時oldVnode爲空
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
}
複製代碼
再次進入createElm函數的createComponent函數,此時vnode.data中是不存在鉤子函數的,故 能夠直接跳過這個函數,遞歸將button組件的子元素掛載到button元素上來即vnode.elm,最後返回給vm.$el,子組件的建立過程結束,因而又再次來到createComponent函數,進行組件的初始化
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
複製代碼
並返回true,即createElm函數return結束,此時id=app的節點已建立完畢,最後掛載到其父節點body上 ,代碼以下
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
...
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
複製代碼
刪除以前的老節點後,整個父子組件的渲染結束。
button組件初始化事件initEvent函數爲
function initEvents (vm) {
vm._events = Object.create(null);
vm._hasHookEvent = false;
// init parent attached events
var listeners = vm.$options._parentListeners;
if (listeners) {
updateComponentListeners(vm, listeners);
}
}
複製代碼
此時vm.$options._parentListeners爲:
{
increment: ƒ ()
}
複製代碼
繼續執行
function updateComponentListeners (
vm,
listeners,
oldListeners
) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
target = undefined;
}
複製代碼
updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm)
這個過程當中由target.$on(event, fn)
註冊了函數,$on函數爲:
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
複製代碼
將監聽函數push到vm._events數組中,再點擊按鈕,觸發$emit函數
Vue.prototype.$emit = function (event) {
var vm = this;
var cbs = vm._events[event];
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
var args = toArray(arguments, 1);
var info = "event handler for \"" + event + "\"";
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm
};
複製代碼
將事件監聽器從vm._events中取出,賦值給cbs,若cbs存在,則循環它,依次調用每個監聽器回調,並將全部參數傳遞給監聽器回調。