Vue 中 $attrs, native, $listeners

1,763 阅读3分钟

前情提要

Vue + element-ui, 一个input框输入订单号,实现按回车或者输入框失去焦点时,向后端发起请求更新数据。

实现方案

定义一个方法searchByOrderId,在输入框上绑两个事件:keyup、blur,都触发searchByOrderId

<template>
    <div>
        <el-input
            v-model="orderId"
            placeholder="请输入订单号进行查询"
            @keyup.enter.native="searchByOrderId"
            @blur="searchByOrderId"
        />
    </div>
</template>
<script>
export default {
    data () {
        return {
            orderId: ''
        }
    },
    methods: {
        searchByOrderId () {
            // 使用 orderId 发请求
            if (this.orderId) {
                axios.get(this.orderId)
            }
        }
    }
}
</script>

存在的问题

  1. 输入1后,一直按回车,请求会按一次回车发一次。
  2. 输入1后,按回车发一次请求,失焦后又会发一次。

总的来说就是orderId没变化时,本该发一次请求就行,却发了多次。

解决方案

多定义一个data数据: oldOrderId,用来保存上一次的orderId。只有两次的值不同时才会重新发请求。

data () {
    return {
        orderId: '',
        oldOrderId: ''
    }
},
methods: {
    searchByOrderId () {
        if (this.orderId && this.oldOrderId !== this.orderId) {
            this.oldOrderId = this.orderId // 更新oldOrderId
            axios.get(this.orderId)
        }
    }
}

新的问题

本来想着手动减少请求次数,但后面发现一个BUG,如果用户复制一段orderId去查询,但接口报错(dialog提示),他关闭dialog错误提示后,再次回车重试,却不会发送请求了...(因为orderId没变)

后面实际代码就恢复到开始那样了(或者也可以搞个类似节流一样的),但有同事说属于过度优化,没有必要(一般用户也不会一直回车,恶意人员也不会手动爆接口)

例子的事情到此结束,让我们深入研究下这段代码

我们先看下 element-ui 源码 中 el-input 结构(简化版)

<template>
    <div class="el-input">
        <input
            type="text"
            v-bind="$attrs"
            @input="handleInput"
            @focus="handleFocus"
            @blur="handleBlur"
            @change="handleChange"
        />
    </div>
</template>
<script>
export default {
    inheritAttrs: false,
    // 函数中都会 $emit 对应的事件
    methods: {
        handleInput
        handleFocus
        handleBlur
        handleChange
    }
}
</script>

我们会发现其实它是一个 div 包裹一个input。而我们项目中在el-input 组件上 双向绑定了orderId,传递了一个placeholder属性。el-input 组件都没有处理,但是可以工作的。

$attrs

可以看到 element-ui 中 el-input 上有一个 v-bind="$attrs"而且在 script中有个inheritAttrs: false

vue文档对$attrs的解释 cn.vuejs.org/v2/guide/co…

Vue中有prop特性和非prop特性两种。

一般情况我们写的都是prop特性:父组件向子组件传一些props,子组件里用props来接收并定义它们的类型和默认值等等。

一个非 prop 的特性是指:传向一个组件,但是该组件并没有相应 prop 定义的特性。(通俗的说就是父组件向子组件传了,但子组件的props中并没有定义的那些特性)

正常情况父组件所有的非prop特性都会直接绑定在子组件的根元素上。(prop特性由于子组件props中接收了,所以随意绑定在哪个元素上)

$attrs即所有非prop特性(除了class和style)。(是一个对象)

// 父组件
<child
    class="cls"
    style="color: red"
    placeholder="请输入订单号进行查询"
    val="111"
    a="222"
/>
// 子组件
<div class="el-input">
    <input
      type="text"
    />
</div>
<script>
    export default {
        props: ['a']
    }
</script>

最后子组件渲染为

<div 
    class="el-input cls"
    style="color: red;"
    placeholder="请输入订单号进行查询"
    val="111"
">
    <input type="text" />
</div>

子组件中申明了porps: ['a'] 所以没有 a

所以 porp 特性有:a, 非 prop特性 有: class, style, placeholder, val

$attrs为: {placeholder: '请输入订单号进行查询', value: '111'}

可以看到所有的非 prop 特性都直接绑定在子组件的根元素上。

上面是默认情况

但如果你不希望组件的根元素继非 prop 承特性,你可以在组件的选项中设置inheritAttrs: false。注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定。

有了inheritAttrs: false 和 $attrs, 你就可以手动决定这些特性会被赋予哪个元素。在撰写基础组件的时候是常会用到的。(参考element-ui 中 el-input)

// 父组件
<child
    class="cls"
    style="color: red"
    placeholder="请输入订单号进行查询"
    val="111"
    a="222"
/>
// 子组件
<div class="el-input">
    <input
      type="text"
      v-bind="$attrs"
    >
</div>
<script>
    export default {
        inheritAttrs: false,
        props: ['a']
    }
</script>

此时子组件渲染为

<div 
    class="el-input cls" 
    style="color: red;
">
    <input 
        type="text"
        placeholder="请输入订单号进行查询"
        val="111"
    />
</div>

tips:

obj={a: 'aa', b: 'bb', c: 'cc'}

<div v-bind="obj" ></div>
// 相当于
<div 
    a="aa"
    b="bb"
    c="cc"
></div>


现在我们就知道了业务代码中placeholder为什么可以工作了。(它属于非prop特性,被手动绑定到了 el-input 的 input上面,而不是根元素div上。)

然后研究下给子组件绑定的事件为什么可以执行。

<template>
  <div>
    <el-input
      v-model="orderId"
      placeholder="请输入订单号进行查询"
      @keyup.enter.native="searchByOrderId"
      @blur="searchByOrderId"
    />
  </div>
</template>

根据element-ui 源码可以看到 el-input 组件在input上绑定了input focus blur change事件,并向父组件emit, 因此我们例子中blur是可以正常绑定执行。但没有keyup事件是没有被emit。 而例子中keyup事件是用 native 来绑定的。

.native

在一个组件的根元素上直接监听一个原生事件

  1. 不用native时 监听子组件点击事件
// 父组件
<child
    @myClick="handleClick"
/>
handleClick () {
    console.log('father handleClick')
}

// 子组件
<div @click="handleClick">
    child child child child
</div>
handleClick () {
    this.$emit('myClick')
}
  1. 使用native时 监听子组件点击事件(效果与上面相同)
// 父组件
<child
    @click.native="handleClick"
/>
handleClick () {
    console.log('father handleClick')
}

// 子组件
<div>
    child child child child
</div>

可以看到使用native可以 省去子组件 自身绑定click事件,并向父组件$emit 的部分。

使用场景即像上面例子一样。如果子组件click不需要做额外操作,只是向父组件 emit 对应的事件。此时就可以使用native来简化监听原生事件过程。

在上面element-ui例子中,我们直接在el-input组件上监听原生keyup事件:<el-input @keyup.enter.native="searchByOrderId" />el-input组件是一个div包裹一个input,按道理我们应该把keyup事件监听到input身上,但这里由于keyup事件会冒泡到div上面,所以没用额外操作。

但有些事件必须手动监听到input上面。$listeners闪亮登场。

$listeners

为了解决.native只能在根元素上直接监听一个原生事件。 vue提供了$listeners$listeners也是一个对象,里面包含了作用在这个组件上的(不含 .native 修饰器的)的事件监听器。 你可以手动决定子组件哪个元素 可以触发父组件的事件监听器。 cn.vuejs.org/v2/guide/co…

$listeners还可以实现隔代组件间通信。

// 父组件
<child1
    @myTest1="handleMyTest1"
    @myTest2="handleMyTest2"
/>
// 子组件1 child1
<div>
    <div>child1 child1</div>
    <child2
      v-on="$listeners"
    />
</div>
// 子组件1 的子组件 child2. 省去了这个组件 $emit('myTest1') $emit('myTest2')
<div>
    <div>child2 child2</div>
    <div @click="handleClick">
        child222 child222
    </div>
</div>
<script>
    export default {
        methods: {
            handleClick () {
                this.$emit('myTest1')
                this.$emit('myTest2')
            }
        }
    }
</script>

使用$listeners可以省去中间一层一层的$emit某个事件。

现在嵌套传递了两层,如果多层并且传递多个事件的话就非常省事了。当然,最好用的还是EventBus和Vuex, 一些特殊情况可以用这个骚操作。