准备工作
为了探究父子组件的数据传递以及事件触发, 父组件选用模板:
<div id="app"><button-counter :count-total="count" v-on:increment="incremenTotal"></button-counter></div>
子组件选用模板
<button v-on:click="incrementCounter">{{ counter }}</button>
父组件通过props传递参数count到子组件,子组件通过incrementCounter函数中的$emit触发父组件的incremenTotal函数,具体过程如下:
如何进行参数传递
button-counter组件中props传递的值,会在genData$2中经过genProps函数处理生成
"{"count-total":count}"
绑定的事件会经过genHandler函数处理生成
on:{"increment":incremenTotal}
所以生成的render函数为
"_c('button-counter',{attrs:{"count-total":count},on:{"increment":incremenTotal}})"
根据前面的编译器原理生成完整的render函数为:
"_c('div',{attrs:{"id":"app"}},[_c('button-counter',{attrs:{"count-total":count},on:{"increment":incremenTotal}})],1)"
调用渲染函数
vnode = render.call(vm._renderProxy, vm.$createElement);
调用渲染函数,先创建子组件Vnode
[_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); }
,执行
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
}
获取props值
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组件的解析
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存在,则循环它,依次调用每一个监听器回调,并将所有参数传递给监听器回调。