Vue 2.4+ $attrs 和 $listeners 源码解读

一、源码部分

组件实例初始化时会调用 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组件怎么通信,我们有多少种解决方案?

  1. 我们使用VueX来进行数据管理,但是如果项目中多个组件共享状态比较少,项目比较小,并且全局状态比较少,那使用VueX来实现该功能,并没有发挥出VueX的威力。
  2. 使用B来做中转站,当A组件需要把信息传给C组件时,B接受A组件的信息,然后利用属性传给C组件,这是一种解决方案,但是如果嵌套的组件过多,会导致代码繁琐,代码维护比较困难;如果C中状态的改变需要传递给A, 使用事件系统一级级往上传递 。本来
  3. 自定义一个Vue 中央数据总线,这个情况适合碰到组件跨级传递消息,但是使用VueX感觉又有点浪费的项目中,但是缺点是,碰到多人合作时,代码的维护性较低,代码可读性低

在很多开发情况下,我们只是想把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>

如有错误,请指出,感谢查看。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值