Vue常用的高级特性

102 阅读1分钟

Vue常用的高级特性

自定义组件的 v-model

官方文档

  1. 一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件

父组件 index.vue

<template>
    <div>
        <div>{{ text1 }}</div>
        <childA v-model="text1" />

        <div>{{ text2 }}</div>
        <!-- 自定义 v-model 的原理 -->
        <childA :value="text2" @input="text2 = $event" />
    </div>
</template>

<script>
    import childA from './components/childA.vue'
    export default {
        components: {
            childA
        },
        data() {
            return {
                text1: '测试',
                text2: '原理'
            }
        }
    }
</script>

子组件 childA.vue

<template>
    <div>
        <input :value="value" @input="$emit('input', $event.target.value)">
    </div>
</template>

<script>
    export default {
        props: {
            value: {
                type: String,
                default: ''
            }
        }
    }
</script>
  1. 单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。 model 选项可以用来避免这样的冲突

父组件 index.vue

<template>
    <div>
        <div>{{ boo }}</div>
        <childB v-model="boo" />
    </div>
</template>

<script>
    import childB from './components/childB.vue'
    export default {
        components: {
            childB
        },
        data() {
            return {
                boo: false
            }
        }
    }
</script>

子组件 childB.vue

<template>
    <div>
        <input type="checkbox" :checked="aaa" @input="$emit('change', $event.target.checked)">
    </div>
</template>

<script>
    export default {
        model: {
            prop: 'aaa',
            event: 'change'
        },
        props: {
            aaa: Boolean
        }
    }
</script>

$listeners

官方文档

$listeners : 当前组件所有的事件监听器

$listeners简单应用

父组件 index.vue

<template>
    <div>
        <childA @click="handleClick" @test="handleTest" />
    </div>
</template>

<script>
    import childA from './components/childA.vue'
    export default {
        components: {
            childA
        },
        data() {
            return {}
        },
        methods: {
            handleClick() {
                console.log('通过childB触发了click')
            },
            handleTest() {
                console.log('通过childB触发了test')
            }
        }
    }
</script>

子组件 childA.vue

<template>
    <div>
        <!-- 将 组件childA 所有的事件监听器指向 子组件childB -->
        <childB v-on="$listeners" />
    </div>
</template>

<script>
    import childB from './childB.vue'
    export default {
        components: {
            childB
        },
        mounted() {
            console.log(this.$listeners)
            // 打印 组件childA 所有事件监听器
            // click: ƒ invoker()
            // test: ƒ invoker()
        }
    }
</script>

子组件 childA.vue 的子组件 childB.vue

<template>
    <div>
        <button @click="handleClick">触发click</button>
        <button @click="handleTest">触发test</button>
    </div>
</template>

<script>
    export default {
        mounted() {
            console.log(this.$listeners)
        },
        methods: {
            handleClick() {
                this.$emit('click')
            },
            handleTest() {
                this.$emit('test')
            }
        }
    }
</script>

利用$listeners使封装的组件是一个完全透明的包裹器了

父组件 index.vue

<template>
    <div>
        <div>{{ val }}</div>
        <baseInput v-model="val" label="测试:" />
    </div>
</template>

<script>
    import baseInput from './components/base-input.vue'
    export default {
        components: {
            baseInput
        },
        data() {
            return {
                val: '123'
            }
        },
        methods: {}
    }
</script>

子组件 base-input.vue

<template>
    <div>
        <label>
            {{ label }}
            <input :value="value" v-bind="$attrs" v-on="inputListeners">
        </label>
    </div>
</template>

<script>
    export default {
        inheritAttrs: false, // 不希望组件的`div`根元素继承非 props 的属性,利用`v-bind="$attrs"`将属性绑定到`input`原型元素上
        props: {
            label: {
                type: String,
                default: ''
            },
            value: {
                type: String,
                default: ''
            }
        },
        computed: {
            inputListeners: function() {
                // `Object.assign` 将所有的对象合并为一个新对象
                return Object.assign({},
                    // 我们从父级添加所有的监听器
                    this.$listeners,
                    // 然后我们添加自定义监听器,或覆写一些监听器的行为
                    {
                        // 这里确保组件配合 `v-model` 的工作
                        input: event => {
                            this.$emit('input', event.target.value)
                        }
                    }
                )
            }
        }
    }
</script>

现在 <base-input> 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 <input> 元素一样使用了:所有跟它相同的 attribute 和监听器都可以工作,不必再使用 .native 监听器。

.sync 修饰符

官方文档

自定义组件的 v-model 有相同之处

  1. 自定义组件的v-model:子组件$emit('input', xxx),父组件v-model接收
  2. .sync 修饰符:子组件$emit('update:title', xxx), 父组件:title.sync接收
  3. 它们的目的都是为了双向绑定

父组件 index.vue

<template>
    <div>
        <div>我是父组件title:{{ title1 }}</div>
        <!-- 传统方式:不一定是 @update:title,子组件 $emit 什么就是 @ 什么 -->
        <childA :title="title1" @update:title="title1 = $event" />

        <div>我是父组件title:{{ title2 }}</div>
        <!-- 缩写方式,即 .sync 修饰符:规定子组件必须是 $emit('update:title', xxx) -->
        <childA :title.sync="title2" />
    </div>
</template>

<script>
    import childA from './components/childA.vue'
    export default {
        components: {
            childA
        },
        data() {
            return {
                title1: '',
                title2: ''
            }
        },
        methods: {}
    }
</script>

子组件 childA

<template>
    <div>
        <div>我是子组件title:{{ title }}</div>
        <input v-model="text" type="text" @input="handleInput">
    </div>
</template>

<script>
    export default {
        props: {
            title: {
                type: String,
                default: ''
            }
        },
        data() {
            return {
                text: ''
            }
        },
        methods: {
            handleInput() {
                this.$emit('update:title', this.text)
            }
        }
    }
</script>

$nextTick

等数据改变并渲染结束再执行 $nextTick 的回调

<template>
    <div>
        <ul ref="ulRef1">
            <li v-for="item in list1" :key="item">{{ item }}</li>
        </ul>
        <button @click="add1">添加</button>
        <ul ref="ulRef2">
            <li v-for="item in list2" :key="item">{{ item }}</li>
        </ul>
        <button @click="add2">添加</button>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                list1: [1, 2, 3],
                list2: [1, 2, 3]
            }
        },
        methods: {
            add1() {
                this.list1.push(4)
                this.list1.push(5)
                this.list1.push(6)

                // 数据被改变,但是还没完成渲染
                console.log(this.$refs.ulRef1.children.length) // 3

                this.$nextTick(() => {
                    // 数据被改变,且完成了渲染
                    console.log(this.$refs.ulRef1.children.length) // 6
                })
            },
            async add2() {
                this.list2.push(4)
                this.list2.push(5)
                this.list2.push(6)

                // 数据被改变,但是还没完成渲染
                console.log(this.$refs.ulRef2.children.length) // 3
                await this.$nextTick()

                // 数据被改变,且完成了渲染
                console.log(this.$refs.ulRef2.children.length) // 6
            }
        }
    }
</script>

作用域插槽

作用域插槽: 让插槽能够访问子组件中的数据

父组件 index.vue

<template>
    <div>
        <childA>
            <template v-slot:default="slotProps">
                {{ slotProps.user.firstName }}
            </template>
        </childA>
    </div>
</template>

<script>
    import childA from './components/childA.vue'
    export default {
        components: {
            childA
        },
        data() {
            return {
                list1: [1, 2, 3],
                list2: [1, 2, 3]
            }
        }
    }
</script>

子组件 childA.vue

<template>
    <span>
        <slot :user="user">
            {{ user.lastName }}
        </slot>
    </span>
</template>

<script>
    export default {
        data() {
            return {
                user: {
                    firstName: 'James',
                    lastName: 'Leblanc'
                }
            }
        }
    }
</script>

动态组件 与 keep-alive

动态组件

keep-alive

自定义指令

官方文档

全局自定义指令

指令挂在 Vue

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
    // 当被绑定的元素插入到 DOM 中时……
    inserted: function(el) {
        // 聚焦元素
        el.focus()
    }
})

局部自定义指令

指令挂在当前组件

<template>
    <div>
        <!-- v-focus 显示的时候自动获得焦点 -->
        <input v-if="show" v-focus type="text" />

        <!-- v-background 显示的时候添加样式 -->
        <div v-if="show" v-background="'#f17f7f'">0</div>

        <button @click="toggle">切换显示</button>
    </div>
</template>

<script>
    export default {
        directives: {
            focus: {
                bind(el, binding, vnode, oldVnode) {
                    console.log('%c bind---', 'color:blue')
                    console.log('el---', el)
                    console.log('binding---', binding)
                    console.log('vnode---', vnode)
                    console.log('oldVnode---', oldVnode)
                },
                inserted(el) {
                    console.log('%c inserted---', 'color:blue')
                    el.focus()
                },
                update() {
                    console.log('%c update---', 'color:blue')
                },
                componentUpdated() {
                    console.log('%c componentUpdated---', 'color:blue')
                },
                unbind() {
                    console.log('%c unbind---', 'color:blue')
                }
            },
            background: {
                inserted(el, binding) {
                    console.log('el---', el)
                    console.log('binding---', binding)
                    console.log(el.style)
                    Object.assign(el.style, {
                        width: '100px',
                        lineHeight: '100px',
                        background: binding.value,
                        fontSize: '50px',
                        textAlign: 'center',
                        transition: 'all 3s'
                    })
                }
            }
        },
        data() {
            return {
                show: true
            }
        },
        methods: {
            toggle() {
                this.show = !this.show
            }
        }
    }
</script>

mixins

mixins