组件实例初始化时会调用 this._init()
方法:
Vue.prototype._init = function (options?: Object) {
// ...
// 组件
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.
// 如果是组件,通过设置 options.parent 来建立与父组件间的联系
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
initRender(vm)
//...
}
初始化过程中又会调用initRender方法,如下:
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
// 通过 template 编辑生成 render 函数时入参为 _c
// e.g. render(_c){ return _c("div")}
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
// 手写 render 函数为的入参
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
从源码最后面可以看到,initRender
时会给当前组件实例vm
,定义两个响应式的值,分别为$attrs
和$listeners
, 其中attrs
初始值为 父组件 vnode data中attrs
值或者为空对象,$listeners
同样的情况,要么取父组件 vnode data中_parentListeners
中值,要么是空对象。
其中 _parentListeners
值在 初始化时赋值 ,如果是组件(options._isComponent
),会调用initInternalComponent
方法:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent // 父组件 vm实例
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
更新阶段 patchVnode
, 会调用组件实例上的 prepatch hook
:
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
// ... 省略
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// ... 省略
prepatch
执行过程中会调用 updateChildComponent
,会重新给组件实例上$attrs
、$listeners
上赋值:
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// ... 省略
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject
// ... 省略
}
A组件与C组件怎么通信,我们有多少种解决方案?
在很多开发情况下,我们只是想把A组件的信息传递给C组件,如果使用props 绑定来进行信息的传递,虽然能够实现,但是代码并不美观。
在vue2.4中,为了解决该需求,引入了$attrs
和$listeners
, 新增了inheritAttrs
选项。 在版本2.4以前,默认情况下父作用域的不被认作props
的属性属性百年孤独,将会“回退”且作为普通的HTML特性应用在子组件的根元素上。如下列的例子:
father.vue
组件:
<template>
<child :name="name"
:age="age"
:infoObj="infoObj"
@updateInfo="updateInfo"
@delInfo="delInfo" />
template>
<script>
import Child from './child.vue'
export default {
name: 'father',
components: { Child },
data () {
return {
name: 'myName',
age: 22,
infoObj: {
from: '北京',
hobby: ['1', '2', '3']
}
}
},
methods: {
updateInfo () {
console.log('update info')
},
delInfo () {
console.log('delete info')
}
}
}
script>
child.vue
组件:
<template>
<grand-son :height='height' :weight='weight' @addInfo="addInfo" v-bind="$attrs" v-on="$listeners" />
template>
<script>
import GrandSon from './grandSon'
export default {
name: 'child',
components: { GrandSon },
props: ['name'],
data () {
return {
height: '180cm',
weight: '70kg'
}
},
created () {
console.log(this.$attrs, 'child $attrs')
// 结果:age, infoObj, 因为父组件共传来name, age, infoObj三个值,由于name被 props接收了,所以只有age, infoObj属性
console.log(this.$listeners, 'child $listeners') // updateInfo: f, delInfo: f
},
methods: {
addInfo () {
console.log('add info')
}
}
}
script>
grandSon.vue
组件:
<template>
<div>
{{ $attrs }}
div>
template>
<script>
export default {
props: ['weight'],
created () {
console.log(this.$attrs, 'grandSon attrs')
console.log(this.$listeners, 'grandSon listeners') // updateInfo: f, delInfo: f, addInfo: f
// this.$listeners.updateInfo() //和下面$emit同等效果
// this.$emit('updateInfo') // 可以触发 father 组件中的updateInfo函数
}
}
script>
如有错误,请指出,感谢查看。