cuixiaorui / vue-magic

共读 vue3
MIT License
112 stars 4 forks source link

emits property blocks $attrs injection #4736 #4

Open cuixiaorui opened 2 years ago

cuixiaorui commented 2 years ago

emits property blocks $attrs injection #4736

为什么要读他

可以学到什么

开始时间

2021-12-20

likui628 commented 2 years ago

问题

重现:https://codesandbox.io/s/zealous-surf-ngryc Parent:

<template>
  <div>
    <Mid @myEvent="myEventHandler" />
  </div>
</template>

<script>
import Mid from "./Mid.vue";

export default {
  components: { Mid },
  methods: {
    myEventHandler() {
      console.log("triggered");
    },
  },
};
</script>

Mid:

<template>
  <div>
    <Child v-bind="$attrs" />
  </div>
</template>

<script>
import Child from "./Child.vue";

export default {
  emits: ["myEvent"],
  inheritAttrs: false,
  components: { Child },
  mounted(){
      console.log("Mid: ",this.$attrs)
  }
};
</script>

Child:

<template>
  <button @click="$emit('myEvent')">Emit Child</button>
</template>

<script>
export default {
  mounted() {
    console.log("Child: ", this.$attrs);
  },
};
</script>

以上点击Child的Emit Child按钮不会触发myEventHandler,以下是当前的输出:

Child:  Proxy {__vInternal: 1}
Mid.vue:17 Mid:  Proxy {__vInternal: 1}

我们期待的应该是以下的输出:

Child:  Proxy {__vInternal: 1, onMyEvent: ƒ}
Mid.vue:15 Mid:  Proxy {__vInternal: 1, onMyEvent: ƒ}

这里可以知道其中的原因了,onMyEvent被忽略掉了,通过查询文档可知,

Events listed in the emits option will not be inherited by the root element of the component and also will be excluded from the $attrs property.

这个不能算是bug,所以原issue里提供了2种解决方案。

解决

  1. 通过props 代替emits https://github.com/vuejs/vue-next/issues/4736#issuecomment-934156497
  2. 再次emit事件 https://github.com/vuejs/vue-next/issues/4736#issuecomment-934191738

讨论

是否有个好的机制$attrsemits声明的onXXX可以继续保留? 我比较赞同@adamreisnz的观点 https://github.com/vuejs/rfcs/discussions/397#discussioncomment-1780988

思考

注释掉emits: ["myEvent"]也可能达成目的。

emits的意义在于确保不会造成冲突,Parent无法知道事件是Mid还是Child触发的。

资料

  1. 0031-attr-fallthrough.md
  2. Attribute Inheritance
  3. Disabling Attribute Inheritance
  4. emits property blocks $attrs injection
  5. emits
shuzong commented 2 years ago

issues问题🔗如下:

https://github.com/vuejs/vue-next/issues/4736

复现流程:

Parent:

<template>
  <div>
    <Mid @myEvent="myEventHandler" />
  </div>
</template>

<script>
import Mid from "./Mid.vue";

export default {
  components: { Mid },
  methods: {
    myEventHandler() {
      console.log("triggered");
    },
  },
};
</script>

Mid组件:

<template>
  <div>
    <Child v-bind="$attrs" />
  </div>
</template>

<script>
import Child from "./Child.vue";

export default {
  emits: ["myEvent"],
  inheritAttrs: false,
  components: { Child },
  mounted(){
      console.log("Mid: ",this.$attrs)
  }
};
</script>

Child组件:

<template>
  <button @click="$emit('myEvent')">Emit Child</button>
</template>

<script>
export default {
  mounted() {
    console.log("Child: ", this.$attrs);
  },
};
</script>

操作点击Child组件的Emit Child按钮,并没有触发Parent的myEventHandler事件

PS: 其实这个issues并不是真正的bug,却引发了讨论

首先咱们就来看看为什么,首先从Parent入手:

1.Parent要做的只有一件事,引入Mid组件,并将自身的myEventHandler方法传给Mid,传值方式为:@myEvent

2.Mid组件用emits接收了myEvent事件,并在mounted挂载阶段输出了this.$attrs

3.Child组件mounted挂载阶段输出了this.$attrs,并且在按钮的点击事件中调用了emit定义的myEvent

最后的结果为:

Child:  Proxy {__vInternal: 1}
Mid:  Proxy {__vInternal: 1}

并没有从$attrs中获取到myEvent,最后的myEvent也没有调用成功

$emit传递数据到Mid组件后并没有继续传递到Child,而是把$attrs传到Child组件上,而从刚才输出的MId可以发现,并没有输出$emit传递的myEvent,可以得知,$attrs并不会传递给定义给组件的emit选项,我们试着将Mid修改一下,去掉emits: ["myEvent"],可得到如下输出:

Child:  Proxy {__vInternal: 1, onMyEvent: ƒ}
Mid:  Proxy {__vInternal: 1, onMyEvent: ƒ}

由此得知,$attrs并不会传递emits选项中的事件,这引发了一个关于vue-next框架的讨论,有兴趣可以看看

vuejs/rfcs#397 (comment)

jp-liu commented 2 years ago

emits 造成的$attrs 中的 v-on 事件深层传递失败

对应 issues 号为: #4736

地址: emits property blocks $attrs injection

1.问题

问题: v-bind:$attrs 没有透传

Parent:

<template>
  <div>
    <Mid @myEvent="myEventHandler" />
  </div>
</template>

<script>
import Mid from './Mid.vue'

export default {
  components: { Mid },
  methods: {
    myEventHandler() {
      console.log('triggered')
    }
  }
}
</script>

Mid:

<template>
  <div>
    <Child v-bind="$attrs" />
  </div>
</template>

<script>
import Child from './Child.vue'

export default {
  emits: ['myEvent'],
  inheritAttrs: false,
  components: { Child },
  mounted() {
    console.log('Mid: ', this.$attrs)
  }
}
</script>

Child:

<template>
  <button @click="$emit('myEvent')">Emit Child</button>
</template>

<script>
export default {
  mounted() {
    console.log('Child: ', this.$attrs)
  }
}
</script>

以上点击 Child 的 Emit Child 按钮不会触发 myEventHandler,以下是当前的输出:

Child:  Proxy {__vInternal: 1}
Mid.vue:17 Mid:  Proxy {__vInternal: 1}

2.问题解析

其实这不是一个 bug 是一个需求

在我们封装高级组件/二次封装组件的时候,很多时候,需要利用 v-bind="$attrs" 绑定到子组件,继承传递 非 prop 的属性

vue2的时候, 非声明属性继承,是分为两类, 属性: $attrs事件: $listeners 所以,在 V2 中,分开传递给子组件没有什么问题,

但是在 V3 去除了 $listeners 这个 API ,使得所有非声明属性和事件传递,都会在 $attrs 里面进行传递,详情可以看 尤大 的文档 0031-attr-fallthrough.md

为了区分原生DOM事件和自定义事件, V3 提供了 emits 声明当前组件,对外暴露的事件

{
    emits: ['xxx']
}

这个时候, $attrs 里面需要排除 propsemits 两个部分的声明

官方文档声明 禁用 Attribute 继承

通过将 inheritAttrs 选项设置为 false,你可以使用组件的 $attrs property 将 attribute 应用到其它元素上,该 property 包括组件 propsemits property 中未包含的所有属性 (例如,classstylev-on 监听器等)。

3.解答

  1. 通过props 代替emits emits property blocks $attrs injection vuejs/vue-next#4736 (comment)
  2. 再次emit事件 emits property blocks $attrs injection vuejs/vue-next#4736 (comment)

这个 issues 的作者,是想在扩展组件的时候,可以通过 v-bind="$attrs" 处理子组件属性和事件

作者想通过 $attrs 访问祖先节点透传的事件, 但是被 emits 截胡了,想官方能够提供一种方式,所以产生了一个 讨论,大家可以去围观一下

sundada88 commented 2 years ago

1. 问题原因

emits 选项中声明的属性,不会再作为$attrs 中的一部分

This is especially important because of the removal of the .native modifier. Any listeners for events that aren't declared with emits will now be included in the component's $attrs, which by default will be bound to the component's root node.

这尤为重要,因为我们移除了 .native 修饰符。任何未在 emits 中声明的事件监听器都会被算入组件的 $attrs,并将默认绑定到组件的根节点上。

2. 问题思考

  1. 在 varlet组件库中,作者使用如下将 onClick 作为组件的一个 props 属性,那么我们使用组件可以通过 @click的方式来触发click事件

    <template>
    <div @click="handleClick">触发事件<div>
    </template>
    <script>
    export default {
       name: 'comp',
      props: {
         onClick: {
            type: Function
         }
      },
      methods: {
        handleClick(e) {
           this.onClick(e)
       }
      }
    }
    </scrip>

    在使用的时候后

    <comp @click="handleClick"></comp> // 此时通过原件的props传入,在使用的时候,可以通过  `@...`的方式传入

    这种方案似乎和https://github.com/vuejs/rfcs/discussions/397#discussioncomment-1796052 的解决方案一致

  2. 关于 emits选项,我认为适用在原生事件场景中

    <template>
    <div  @click="handleClick">
       clickTest
    </div>
    </template>
    <script>
    export default {
    methods: {
     handleClick() {
       this.$emit('click')
     }
    }
    }
    </script>

    如果此时我们在外使用组件然后触发click事件的时候 <comp @click="handleClick"> // 此时如果点击则会触发两次click事件,即执行两次handleClick。 所以上面这种情况就需要在原组件中定义emits属性来约束其向父组件触发事件的行为