关于$attrs和$listeners的层级穿透-by DireW

2,273 阅读1分钟

 前提:

        vuejs在跨组件传递数据的时候,常用的有以下几个方式:

  1. 子组件用props接收父组件的数据,子组件内部值改变的时候通过$emit事件告知父组件。
  2. Vuex,不仅是父子组件,整个vue工程的所有页面都可以在store中赋值,并且取值,达到数据传递的效果。
  3. Event Bus,事件总线,通过new Vue()得到一个实例,在各自需要的地方引进该实例进行事件监听$on和事件触发$emit。必须保证先有$on之后,再由$emit触发同名事件之后才能正常对事件做操作。
  4. 多层级的组件,由外层向里层传递数据,由里层向外层触发事件,主要场景是中间层级不关心传递给下一级的属性,以及不关心下一级触发的指定事件。

场景:

        父组件有指定的属性需要传递给孙子组件,孙子组件注册对应的事件要告知父组件。作为中间层级的子组件,如果按照第一种方式,需要将自己从父组件接收到的数据主动传递到孙子组件。以及主动接收孙子组件的事件,并告知给父组件。在传递多个属性和事件的时候会造成代码冗余。

        在多层级的组件场景中,从2.4.0版本开始,vuejs提供了穿透属性和事件的写法,以下是官方文档截图: 



实际使用:

        那么,在传递的过程当中,如果子组件和孙子组件拥有同样的属性以及触发了相同的事件。在父组件层面会有什么响应效果呢?

  • 父组件代码:

<template>
    <div class="parent-page">
        <h3>这里是parent,以下是child信息</h3>    
            <child      
                child-name="小冰原"      
                real-name="小小冰原"      
                @save-money="onSaveMoney"
                @grand-son-request="onGrandSonRequest"      
                @child-request="onChildRequest"    />  
    </div>
</template>

<script>  
import Child from "./child";  
export default {    
    name: "parent",    
    components: {Child},    
    data() {      
        return {      
        };    
    },    
    created() {    
    },    
    methods: {      
        onSaveMoney(information) {        
            console.log(`这里是parent:后代赚钱了,${information.alias}赚了${information.amount}`);      
        },      
        onGrandSonRequest() {        
            console.log('这里是parent,要响应grand-son的request');      
        },      
        onChildRequest() {        
            console.log('这里是parent,要响应child的request');      
        }    
    }  
}
</script>

<style scoped>
.parent-page {  
    width: 1000px;  
    margin: 0 auto;  
    padding: 30px;
}
</style>

  • 子组件代码:

<template>  
    <div class="child-page">    
        <div>      
            child name: {{ childName }}    
        </div>    
        <div class="btns">      
            <el-button @click="saveMoney">儿子赚钱</el-button>      
            <el-button @click="request">儿子请求</el-button>    
        </div>    
        <div>      
            <h3>我是child,以下是grand-son:</h3>      
            <div class="grand-son-div">        
                <grand-son v-bind="$attrs" v-on="$listeners" />      
            </div>    
        </div>  
    </div>
</template>

<script>  
import GrandSon from "./grand-son";  
export default {    
    name: "child",    
    components: {GrandSon},    
    props: {      
        childName: {        
            type: String,        
            default: ''      
        },    
    },    
    data() {      
        return {};    
    },    
    created() {    
    },    
    methods: {      
        saveMoney() {
            console.log('儿子赚钱了,要告诉parent');
            this.$emit('save-money', {alias: '儿子', amount: 500});
        },      
        request() {
            console.log('儿子要求帮助');        
            this.$emit('child-request');
        }    
    }  
}
</script>

<style scoped>  
.grand-son-div {    
    background-color: antiquewhite;
    padding: 10px;  
}  
.child-page {
    background-color: azure;
    padding: 10px;  
}
</style>

  • 孙子组件代码:

<template>  
    <div>
        <div>      
            grand-son childName: {{ childName }}
        </div>    
        <div>
            grand-son realName: {{ realName }}
        </div>    
        <div class="btns">  
            <el-button @click="saveMoney">孙子赚钱</el-button>  
            <el-button @click="request">孙子请求</el-button>    
        </div>  
    </div>
</template>

<script>  
export default {    
    name: "grand-son",
    props: ['childName', 'realName'],
    data() { 
        return {};
    },
    created() {    },
    methods: { 
        saveMoney() {
            console.log('孙子赚钱了,要告诉parent组件'); 
            this.$emit('saveMoney', {alias: '孙子', amount: 100});
        },      
        request() {        
            console.log('孙子要求帮助');        
            this.$emit('grand-son-request');      
        }    
    }  
}
</script>

<style scoped>
</style>

  • 页面效果:


  • 按键触发的代码效果:


总结:

  1. 父组件往下传值,如果子组件已经匹配了,那么孙子组件将得不到该配置。
  2. 如果子组件没对应的配置,将有$attrs将其传递到孙子组件,如果有匹配的就直接使用该配置。
  3. 孙子组件往上传递事件,如果该事件名称与子组件名称重复,那么该事件无法传递到父组件。
  4. 如果孙子组件触发的事件在子组件中没有重名事件,则可以正常传递给父组件。

扩展:

        一般的vuejs项目开发当中,都是直接使用第三方框架,比如element-ui,iView等组件库。如果第三方组件不满足业务操作要求,则可以再多封装一层,比如动态设置下拉框的内容。使用$attrs和$listeners的时候,第三方的配置仍然可以从二次封装的组件中传递进去。并且在最外面能监听到第三方的组件事件。