Open CommanderXL opened 5 years ago
在Vue中,父子组件基本的通讯方式就是父组件通过props属性将数据传递给子组件,这种数据的流向是单向的,当父props属性发生了改变,子组件所接收到的对应的属性值也会发生改变,但是反过来却不是这样的。子组件通过event自定义事件的触发来通知父组件自身内部所发生的变化。
Vue
props
event
还是从一个实例出发:
// 模板 <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元素的挂载等功能:
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属性来完成数据由父组件向子组件的传递。
render
vm
;(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。
_c
vue
data
initState
getter
在访问过程中Dep.target已经被设定为当前vue实例的watcher(具体见mountComponent方法内部创建watcher对象),因此会将当前的watcher加入到val的dep当中。这样便完成了val的依赖收集的工作。
Dep.target
watcher
mountComponent
dep
在创建VNode时,又分为:
VNode
built in 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:
createComponent
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的时候是不需要这些自定义数据的,因此需要删除。
built in
component
attrs
extractPropsFromVNodeData
key
当然如果你在子组件中传入了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。
my-component
(function() { with (this) { return _c('div', { attrs: { "id": "app" } }, <my-component-vnode>, 1) } } )
vm._render()方法调用完成后,即所有的VNode都创建完成,开始递归将VNode渲染成真实的dom节点,同时挂载到document当中(见上方调用的mountComponent内部vm.update(vm._render()))。
vm._render()
document
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(上面的例子中定义的)
vue实例
Vue.prototype._init
message
parent val
这样便完成了父组件通过props属性向子组件传递数据。
在子组件生成 VNode 的过程中会对应创建 render watcher,通过 props 从父组件传递给子组件的数据是在父作用域下获取得到的。因此,props 的 Dep 中会将这个 watcher 作为依赖添加进去。那么当父组件中的数据发生了改变,便会调用这个响应式数据Dep.notify()方法去通知相关的订阅者去完成更新,其中就包括子组件的 render watcher。
Dep.notify()
那么在子组件需要和父组件进行通讯的时候,所使用的events事件又是如何实现的呢?
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。那我们来看下整个过程是如何进行的:
<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方法。
on
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 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事件机制实际上是利用了发布订阅的设计模式。
initEvents
$emit
这个是有关父子组件自定义事件的机制。这里也顺带讲下 Vue 是如何绑定原生 DOM 事件的。
在代码片段 xxx 当中,生成 VNode 的环节当中,会将 nativeOn 赋值给data.on(data 上保存了将 VNode 渲染成真实 DOM 节点的数据)。当开始渲染真实 DOM 元素的时候:
data.on
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事件的绑定。
updateDOMListeners
updateListeners
add$1
addEventListener
在 2.3.0+ 版本,Vue 提供了一种可以对 props 进行数据双向绑定的语法糖。基本的使用方法为:
事实上是 Vue 在将模板编译成渲染函数时,会将带有.sync标识符的 props 自动添加一个自定义的事件update:message事件:
.sync
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属性,在初始化的过程中才会定义这个内部属性。
_props
defineReactive
patch
setter
父子组件通讯
在
Vue
中,父子组件基本的通讯方式就是父组件通过props
属性将数据传递给子组件,这种数据的流向是单向的,当父props
属性发生了改变,子组件所接收到的对应的属性值也会发生改变,但是反过来却不是这样的。子组件通过event
自定义事件的触发来通知父组件自身内部所发生的变化。Vue props 是如何传递以及父 props 更新如何使得子模板视图更新
还是从一个实例出发:
最终页面渲染出的内容为:
接下来我们就来看下父子组件是如何通过
props
属性来完成数据的传递的。首先根组件开始实例化,完成一系列的初始化的内容。首先将
val
转化为响应式的数据,并调用Vue.prototype.$mount
方法完成vnode
的生成,真实dom
元素的挂载等功能:Vue.prototype.$mount
方法内部:完成模板的编译,同时生成
render
函数,这个render
函数在实际实行生成vnode
时,会将作用域绑定到对应的vm
实例的作用域下,即在创建vnode
的环节当中,始终访问的是当前这个vm
实例,子vnode
创建时是没法直接访问到父组件中定义的数据的。除非通过props
属性来完成数据由父组件向子组件的传递。完成模板的编译生成
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
时,又分为:built in VNode
)component VNode
)其中内置标签的
VNode
的没有需要特别说明的地方,就是调用VNode
的构造函数完成创建过程。但是在创建自定义标签元素的
VNode
时,完成一些重要的操作(因为本文是讲解 props 传递,所以挑出和 props 相关的部分):在
createComponent
创建VNode
的过程中需要注意的是:Vue
的父子组件传递props
属性的时候都是在子组件上直接写自定义的dom attrs
:但是在模板编译后,统一将
dom
节点(不管是built in
的节点还是自定义component
节点)上的属性转化为attrs
对象(见代码片段 111),在创建VNode
过程中调用了extractPropsFromVNodeData
这个方法完成从attrs
对象上获取到这个component
所需要的props
属性,获取完成后还会将attrs
对象上对应的key
值删除。因此这个key
值对应的是要传入子component
的数据,而非原生dom
属性,最终由VNode
生成真实dom
的时候是不需要这些自定义数据的,因此需要删除。当然如果你在子组件中传入了
props
数据,但是在子组件中没有定义相关的props
属性,那么这个 props 属性最终会渲染到子组件的真实的dom
元素上,不过控制台也会出现报错:当完成了
my-component
的VNode
创建后,开始创建它的父VNode
,即根VNode
。vm._render()
方法调用完成后,即所有的VNode
都创建完成,开始递归将VNode
渲染成真实的dom
节点,同时挂载到document
当中(见上方调用的mountComponent
内部vm.update(vm._render())
)。在将
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
事件又是如何实现的呢?当点击
<child-component>
时,会在控制台输出this is child component
。那我们来看下整个过程是如何进行的:首先在模板编译的过程:
在创建
my-component
的component VNode
过程中,通过传入data
数据上定义的on
属性。这个时候test
访问的还是在父组件上定义的test
方法。接下来在将这个
VNode
实例成vue component
的时候:在将
VNode
实例化过程当中,调用initEvents
方法,获取在这个VNode
上绑定的从父组件传递下来的方法,并缓存至对应事件的回调函数数组当中。当你在子组件当中去$emit
对应的事件的时候,便会执行对应的回调函数。这里父子间的event
事件机制实际上是利用了发布订阅的设计模式。这个是有关父子组件自定义事件的机制。这里也顺带讲下 Vue 是如何绑定原生 DOM 事件的。
在代码片段 xxx 当中,生成 VNode 的环节当中,会将 nativeOn 赋值给
data.on
(data 上保存了将 VNode 渲染成真实 DOM 节点的数据)。当开始渲染真实 DOM 元素的时候:当 data 有值的时候,那么就开始执行 DOM 相关属性更新的工作。即执行在 cbs 上有关 create 阶段所有的回调函数,其中包括:
其中我们来看下有关 events,即原生 dom 事件是如何绑定到 DOM 元素上的。
在初次渲染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
事件:那么当你在子组件当中去调用
update:message
方法的时候,并传入值的时候即会更新 message 的值。这个 message 的值即在父组件当中的数据。这样便完成了数据的双向绑定。// vm._update(vm._render()) // patch // prepatch 方法 // updateChildComponent 完成 props 等属性的 setter 操作
_props
是在实例初始化过程中定义的一个内部属性,同时调用defineReactive
方法完成将响应式数据存放到_props
属性上。在VNode
的patch
过程中,如果有属性发生了变化,那么会调用这个属性的setter
方法完成值的变更操作,继而完成视图的更新。当然了,如果组件在定义的过程,没有定义props
属性,那么在实例初始化的过程中,_props
属性也不会被创建。只有组件上定义过props
属性,在初始化的过程中才会定义这个内部属性。