Vue 2.0 父子组件通讯
CommanderXL opened this issue · 0 comments
父子组件通讯
在Vue
中,父子组件基本的通讯方式就是父组件通过props
属性将数据传递给子组件,这种数据的流向是单向的,当父props
属性发生了改变,子组件所接收到的对应的属性值也会发生改变,但是反过来却不是这样的。子组件通过event
自定义事件的触发来通知父组件自身内部所发生的变化。
Vue props 是如何传递以及父 props 更新如何使得子模板视图更新
还是从一个实例出发:
// 模板
<div id="app">
<child-component :message="val"></child-component>
</div>
// js
Vue.component('child-component', {
props: ['message']
template: '<div>this is child component, I have {{message}}</div>',
})
new Vue({
el: '#app',
data() {
return {
val: 'parent val'
}
},
mounted () {
setTimeout(() => {
this.val = 'parent val which has been changed after 2s'
}, 2000)
}
})
最终页面渲染出的内容为:
this is child component, I have parent val
2s后文案变更为:
this child component, I hava parent val which has been changed after 2s
接下来我们就来看下父子组件是如何通过props
属性来完成数据的传递的。
首先根组件开始实例化,完成一系列的初始化的内容。首先将val
转化为响应式的数据,并调用Vue.prototype.$mount
方法完成vnode
的生成,真实dom
元素的挂载等功能:
Vue.prototype._init = function (options) {
...
initState(vm)
...
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
...
}
Vue.prototype.$mount
方法内部:
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);
var options = this.$options;
// resolve template/el and convert to render function
if (!options.render) {
var template = options.template;
if (template) {
...
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
...
var ref = compileToFunctions(template, {
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
// 生成render函数
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
}
}
return mount.call(this, el, hydrating)
};
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) {
...
}
// 挂载前
callHook(vm, 'beforeMount');
var updateComponent;
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
...
} else {
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
完成模板的编译,同时生成render
函数,这个render
函数在实际实行生成vnode
时,会将作用域绑定到对应的vm
实例的作用域下,即在创建vnode
的环节当中,始终访问的是当前这个vm
实例,子vnode
创建时是没法直接访问到父组件中定义的数据的。除非通过props
属性来完成数据由父组件向子组件的传递。
;(function() {
with (this) {
return _c(
'div',
{
attrs: {
id: 'app'
}
},
[
_c('my-component', {
attrs: {
message: val
}
})
],
1
)
}
})
完成模板的编译生成render
函数后,调用_c
方法,对应访问vue
实例的_c
方法,开始创建对应的vnode
,注意这里val
变量,即vue
实例上data
属性定义的val
,在创建对应的vnode
前,实例已经调用initState
方法将val
转化为响应式的数据。因此在创建vnode
过程中,访问val
即访问它的getter
。
在访问过程中Dep.target
已经被设定为当前vue
实例的watcher
(具体见mountComponent
方法内部创建watcher
对象),因此会将当前的watcher
加入到val
的dep
当中。这样便完成了val
的依赖收集的工作。
在创建VNode
时,又分为:
- 内置标签(即标准规定的标签)元素的 VNode(
built in VNode
) - 本文要讨论的自定义的标签元素的 VNode(
component VNode
)
其中内置标签的VNode
的没有需要特别说明的地方,就是调用VNode
的构造函数完成创建过程。
但是在创建自定义标签元素的VNode
时,完成一些重要的操作(因为本文是讲解 props 传递,所以挑出和 props 相关的部分):
function createComponent () {
...
// 注意这个方法。它完成了从父组件对应的props字段获取值的作用,具体到本例子,就是获取到了message字段的值
// 这样就完成了props从父组件传递到子组件的功能
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
...
// 给component初始化挂载钩子函数,在VNode实例化成vue component会调相关的钩子函数
installComponentHooks(data);
...
// 创建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
}
在createComponent
创建VNode
的过程中需要注意的是:Vue
的父子组件传递props
属性的时候都是在子组件上直接写自定义的dom attrs
:
<div id="app">
<child-component :message="val"></child-component>
</div>
但是在模板编译后,统一将dom
节点(不管是built in
的节点还是自定义component
节点)上的属性转化为attrs
对象(见代码片段 111),在创建VNode
过程中调用了extractPropsFromVNodeData
这个方法完成从attrs
对象上获取到这个component
所需要的props
属性,获取完成后还会将attrs
对象上对应的key
值删除。因此这个key
值对应的是要传入子component
的数据,而非原生dom
属性,最终由VNode
生成真实dom
的时候是不需要这些自定义数据的,因此需要删除。
当然如果你在子组件中传入了props
数据,但是在子组件中没有定义相关的props
属性,那么这个 props 属性最终会渲染到子组件的真实的dom
元素上,不过控制台也会出现报错:
> [Vue warn]: Property or method "message" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property
当完成了my-component
的VNode
创建后,开始创建它的父VNode
,即根VNode
。
(function() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, <my-component-vnode>, 1)
}
}
)
vm._render()
方法调用完成后,即所有的VNode
都创建完成,开始递归将VNode
渲染成真实的dom
节点,同时挂载到document
当中(见上方调用的mountComponent
内部vm.update(vm._render())
)。
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
if (vm._isMounted) {
// 触发beforeUpdate钩子函数
callHook(vm, 'beforeUpdate');
}
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var prevActiveInstance = activeInstance;
activeInstance = vm;
vm._vnode = vnode;
// 第一次渲染
if (!prevVnode) {
// initial render
// 初始化render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
);
...
} else {
// updates
// 将prevVnode和vnode进行patch操作并更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
...
}
在将VNode
递归渲染成真实的dom
节点过程当中:
对于**自定义标签元素(即组件)**的渲染,首先完成组件vue实例
的初始化。又重复到上文一开始的Vue.prototype._init
方法。在实例化my-component
组件的过程中,还是通过调用initState
方法,将定义的props
属性中的message
属性转化为响应式的数据。� 在此之前,my-component
组件上的message
属性已经被初始化为从父组件传递过来的值。因此在页面初次渲染的时候,my-component
通过定义的props
属性从父组件上获取到的值为parent val
(上面的例子中定义的)
这样便完成了父组件通过props
属性向子组件传递数据。
父 props 的改变是如何影响到子 component 的视图的更新
在子组件生成 VNode 的过程中会对应创建 render watcher,通过 props 从父组件传递给子组件的数据是在父作用域下获取得到的。因此,props 的 Dep 中会将这个 watcher 作为依赖添加进去。那么当父组件中的数据发生了改变,便会调用这个响应式数据Dep.notify()
方法去通知相关的订阅者去完成更新,其中就包括子组件的 render watcher。
Vue 父子组件如何传递/绑定自定义事件的
那么在子组件需要和父组件进行通讯的时候,所使用的events
事件又是如何实现的呢?
// 模板
<div id="app">
<child-component @foo="bar"></child-component>
</div>
// js
Vue.component('child-component', {
props: ['message']
template: '<div @click="foo">this is child component, I have {{message}}</div>',
methods: {
foo () {
this.$emit('foo', 'this is child component')
}
}
})
new Vue({
el: '#app',
data() {
return {
val: 'parent val'
}
},
methods: {
foo (val) {
console.log(val)
}
}
})
当点击<child-component>
时,会在控制台输出this is child component
。那我们来看下整个过程是如何进行的:
首先在模板编译的过程:
;(function() {
with (this) {
return _c(
'div',
{
attrs: {
id: 'app'
}
},
[
_c('my-component', {
staticClass: 'my-component',
attrs: {
message: val
},
on: {
test: test
}
})
],
1
)
}
})
在创建my-component
的component VNode
过程中,通过传入data
数据上定义的on
属性。这个时候test
访问的还是在父组件上定义的test
方法。
//
function createComponent (
Ctor,
data,
context,
children,
tag
) {
...
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
// 获取父 component 作用域中定义的 listeners。这个 listeners 会作为 componentOptions 的属性传递进 VNode 当中
// 注意这个地方和 DOM listeners 的区别。DOM listeners是使用的浏览器原生的事件系统
var listeners = data.on;
...
// install component management hooks onto the placeholder node
installComponentHooks(data);
// return a placeholder vnode
var name = Ctor.options.name || tag;
// 将从父 component 作用作用域定义的 listeners 作为 VNode 的 componentOptions 传入 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
);
/* istanbul ignore if */
return vnode
}
接下来在将这个VNode
实例成vue component
的时候:
Vue.prototype._init = function (options) {
var vm = this;
// 实例化子vue component
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
// 从VNode的componentOptions属性上获取关于这个vue component定义的属性
initInternalComponent(vm, options);
} else {
...
}
// expose real self
vm._self = vm;
initLifecycle(vm);
// 初始化vm的上绑定的自定义事件
initEvents(vm);
...
};
// 实例化vue component自定义事件
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);
}
}
function updateComponentListeners (
vm,
listeners,
oldListeners
) {
// 设置全局target对象
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
target = undefined;
}
// 给全局target对象挂载自定义事件
function add (event, fn, once) {
if (once) {
target.$once(event, fn);
} else {
target.$on(event, fn);
}
}
在将VNode
实例化过程当中,调用initEvents
方法,获取在这个VNode
上绑定的从父组件传递下来的方法,并缓存至对应事件的回调函数数组当中。当你在子组件当中去$emit
对应的事件的时候,便会执行对应的回调函数。这里父子间的event
事件机制实际上是利用了发布订阅的设计模式。
这个是有关父子组件自定义事件的机制。这里也顺带讲下 Vue 是如何绑定原生 DOM 事件的。
在代码片段 xxx 当中,生成 VNode 的环节当中,会将 nativeOn 赋值给data.on
(data 上保存了将 VNode 渲染成真实 DOM 节点的数据)。当开始渲染真实 DOM 元素的时候:
function createElm () {
...
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
...
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) { i.create(emptyNode, vnode); }
if (isDef(i.insert)) {
insertedVnodeQueue.push(vnode);
}
}
}
当 data 有值的时候,那么就开始执行 DOM 相关属性更新的工作。即执行在 cbs 上有关 create 阶段所有的回调函数,其中包括:
var platformModules = [
attrs, // attrs 属性
klass, // class
events, // 原生 dom 事件
domProps,
style,
transition
]
其中我们来看下有关 events,即原生 dom 事件是如何绑定到 DOM 元素上的。
var events = {
create: updateDOMListeners,
update: updateDOMListeners
}
function updateDOMListeners (oldVnode, vnode) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
target$1 = vnode.elm; // 真实的 dom 节点
normalizeEvents(on);
updateListeners(on, oldOn, add$1, remove$2, vnode.context);
target$1 = undefined;
}
function add$1 (
event,
handler,
once$$1,
capture,
passive
) {
handler = withMacroTask(handler); // 强制放到 marcoTask 当中去执行
if (once$$1) { handler = createOnceHandler(handler, event, capture); }
target$1.addEventListener(
event,
handler,
supportsPassive
? { capture: capture, passive: passive }
: capture
);
}
function remove$2 (
event,
handler,
capture,
_target
) {
(_target || target$1).removeEventListener(
event,
handler._withTask || handler,
capture
);
}
在初次渲染DOM节点的时候,传入的 oldVNode 为一个空的 VNode,即拿这个空的 VNode 和即将要渲染的 VNode 进行原生DOM事件的 diff 工作。在updateDOMListeners
方法当中还是继续调用updateListeners
方法去进行事件的绑定,这个时候绑定事件的函数使用的是add$1
,即调用DOM提供的addEventListener
方法去完成原生DOM事件的绑定工作。在这里我们也可以看出去 Vue 提供的事件修饰符在这里进行配置生效。这样便完成了原生的DOM事件的绑定。
.sync修饰符-数据双向绑定
在 2.3.0+ 版本,Vue 提供了一种可以对 props 进行数据双向绑定的语法糖。基本的使用方法为:
事实上是 Vue 在将模板编译成渲染函数时,会将带有.sync
标识符的 props 自动添加一个自定义的事件update:message
事件:
{
...
on: {
'update:message': function ($event) {
message = $event
}
}
...
}
那么当你在子组件当中去调用update:message
方法的时候,并传入值的时候即会更新 message 的值。这个 message 的值即在父组件当中的数据。这样便完成了数据的双向绑定。
// vm._update(vm._render())
// patch
// prepatch 方法
// updateChildComponent 完成 props 等属性的 setter 操作 _props
是在实例初始化过程中定义的一个内部属性,同时调用defineReactive
方法完成将响应式数据存放到_props
属性上。在VNode
的patch
过程中,如果有属性发生了变化,那么会调用这个属性的setter
方法完成值的变更操作,继而完成视图的更新。当然了,如果组件在定义的过程,没有定义props
属性,那么在实例初始化的过程中,_props
属性也不会被创建。只有组件上定义过props
属性,在初始化的过程中才会定义这个内部属性。