34条我能告诉你的Vue之实操篇

907 阅读8分钟

7种组件通信方式随你选

组件通信是 Vue 的核心知识,掌握这几个知识点,面试开发一点问题都没有。

props/@on+$emit

用于实现父子组件间通信。通过 props 可以把父组件的消息传递给子组件:

// parent.vue    
<child :title="title"></child>

// child.vue
props: {
    title: {
        type: String,
        default: '',
    }
}

这样一来 this.title 就直接拿到从父组件中传过来的 title 的值了。注意,你不应该在子组件内部直接改变 prop,这里就不多赘述,可以直接看官网介绍。

而通过 @on+$emit 组合可以实现子组件给父组件传递信息:

// parent.vue
<child @changeTitle="changeTitle"></child>

// child.vue
this.$emit('changeTitle', 'bubuzou.com')

attrs和listeners

Vue_2.4 中新增的 $attrs/$listeners 可以进行跨级的组件通信。$attrs 包含了父级作用域中不作为 prop 的属性绑定(classstyle 除外),好像听起来有些不好理解?没事,看下代码就知道是什么意思了:

// 父组件 index.vue
<list class="list-box" title="标题" desc="描述" :list="list"></list>

// 子组件 list.vue
props: {
    list: [],
},
mounted() {
    console.log(this.$attrs)  // {title: "标题", desc: "描述"}
}

在上面的父组件 index.vue 中我们给子组件 list.vue 传递了4个参数,但是在子组件内部 props 里只定义了一个 list,那么此时 this.$attrs 的值是什么呢?首先要去除 props 中已经绑定了的,然后再去除 classstyle,最后剩下 titledesc 结果和打印的是一致的。基于上面代码的基础上,我们在给 list.vue 中加一个子组件:

// 子组件 list.vue
<detail v-bind="$attrs"></detial>

// 孙子组件 detail.vue
// 不定义props,直接打印 $attrs
mounted() {
    console.log(this.$attrs)  // {title: "标题", desc: "描述"}
}

在子组件中我们定义了一个 v-bind="$attrs" 可以把父级传过来的参数,去除 propsclassstyle 之后剩下的继续往下级传递,这样就实现了跨级的组件通信。

$attrs 是可以进行跨级的参数传递,实现父到子的通信;同样的,通过 $listeners 用类似的操作方式可以进行跨级的事件传递,实现子到父的通信。$listeners 包含了父作用域中不含 .native 修饰的 v-on 事件监听器,通过 v-on="$listeners" 传递到子组件内部。

// 父组件 index.vue
<list @change="change" @update.native="update"></list>

// 子组件 list.vue
<detail v-on="$listeners"></detail>

// 孙子组件 detail.vue
mounted() {
    this.$listeners.change()
    this.$listeners.update() // TypeError: this.$listeners.update is not a function
}

provide/inject组合拳

provide/inject 组合以允许一个祖先组件向其所有子孙后代注入一个依赖,可以注入属性和方法,从而实现跨级父子组件通信。在开发高阶组件和组件库的时候尤其好用。

// 父组件 index.vue
data() {
    return {
        title: 'bubuzou.com',
    }
}
provide() {
    return {
        detail: {
            title: this.title,
            change: (val) => {
                console.log( val )
            }
        }
    }
}

// 孙子组件 detail.vue
inject: ['detail'],
mounted() {
    console.log(this.detail.title)  // bubuzou.com
    this.detail.title = 'hello world'  // 虽然值被改变了,但是父组件中 title 并不会重新渲染
    this.detail.change('改变后的值')  // 执行这句后将打印:改变后的值 
}

provideinject 的绑定对于原始类型来说并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。这也就是为什么在孙子组件中改变了 title,但是父组件不会重新渲染的原因。

EventBus

以上三种方式都是只能从父到子方向或者子到父方向进行组件的通信,而我就比较牛逼了😀,我还能进行兄弟组件之间的通信,甚至任意2个组件间通信。利用 Vue 实例实现一个 EventBus 进行信息的发布和订阅,可以实现在任意2个组件之间通信。有两种写法都可以初始化一个 eventBus 对象:

  1. 通过导出一个 Vue 实例,然后再需要的地方引入:

    // eventBus.js
    import Vue from 'vue'
    export const EventBus = new Vue()
    

    使用 EventBus 订阅和发布消息:

    import {EventBus} from '../utils/eventBus.js'
    
    // 订阅处
    EventBus.$on('update', val => {})
    
    // 发布处
    EventBus.$emit('update', '更新信息')
    

  2. main.js 中初始化一个全局的事件总线:

    // main.js
    Vue.prototype.$eventBus = new Vue()
    

    使用:

    // 需要订阅的地方
    this.$eventBus.$on('update', val => {})
    
    // 需要发布信息的地方
    this.$eventBus.$emit('update', '更新信息')
    

如果想要移除事件监听,可以这样来:

this.$eventBus.$off('update', {})

上面介绍了两种写法,推荐使用第二种全局定义的方式,可以避免在多处导入 EventBus 对象。这种组件通信方式只要订阅和发布的顺序得当,且事件名称保持唯一性,理论上可以在任何 2 个组件之间进行通信,相当的强大。但是方法虽好,可不要滥用,建议只用于简单、少量业务的项目中,如果在一个大型繁杂的项目中无休止的使用该方法,将会导致项目难以维护。

Vuex进行全局的数据管理

Vuex 是一个专门服务于 Vue.js 应用的状态管理工具。适用于中大型应用。Vuex 中有一些专有概念需要先了解下:

  • State:用于数据的存储,是 store 中的唯一数据源;

  • Getter:类似于计算属性,就是对 State 中的数据进行二次的处理,比如筛选和对多个数据进行求值等;

  • Mutation:类似事件,是改变 Store 中数据的唯一途径,只能进行同步操作;

  • Action:类似 Mutation,通过提交 Mutation 来改变数据,而不直接操作 State,可以进行异步操作;

  • Module:当业务复杂的时候,可以把 store 分成多个模块,便于维护;

对于这几个概念有各种对应的 map 辅助函数用来简化操作,比如 mapState,如下三种写法其实是一个意思,都是为了从 state 中获取数据,并且通过计算属性返回给组件使用。

computed: {
    count() {
        return this.$store.state.count
    },
    ...mapState({
        count: state => state.count
    }),
    ...mapState(['count']),
},

又比如 mapMutations, 以下两种函数的定义方式要实现的功能是一样的,都是要提交一个 mutation 去改变 state 中的数据:

methods: {
    increment() {
        this.$store.commit('increment')
    },
    ...mapMutations(['increment']),
}

接下来就用一个极简的例子来展示 Vuex 中任意2个组件间的状态管理。1、 新建 store.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        count: 0,
    },
    mutations: {
        increment(state) {
            state.count++
        },
        decrement(state) {
            state.count--
        }
    },
})

2、 创建一个带 storeVue 实例

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './utils/store'

new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app')

3、 任意组件 A 实现点击递增

<template>
    <p @click="increment">click to increment:{{count}}</p>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
export default {
    computed: {
        ...mapState(['count'])
    },
    methods: {
        ...mapMutations(['increment'])
    },
}
</script>

4、 任意组件 B 实现点击递减

<template>
    <p @click="decrement">click to decrement:{{count}}</p>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
export default {
    computed: {
        ...mapState(['count'])
    },
    methods: {
        ...mapMutations(['decrement'])
    },
}
</script>

以上只是用最简单的 vuex 配置去实现组件通信,当然真实项目中的配置肯定会更复杂,比如需要对 State 数据进行二次筛选会用到 Getter,然后如果需要异步的提交那么需要使用Action,再比如如果模块很多,可以将 store 分模块进行状态管理。对于 Vuex 更多复杂的操作还是建议去看Vuex 官方文档,然后多写例子。

Vue.observable实现mini vuex

这是一个 Vue2.6 中新增的 API,用来让一个对象可以响应。我们可以利用这个特点来实现一个小型的状态管理器。

// store.js
import Vue from 'vue'

export const state = Vue.observable({
    count: 0,
})

export const mutations = {
    increment() {
        state.count++
    }
    decrement() {
        state.count--
    }
}

// parent.vue
<template>
    <p>{{ count }}</p>
</template>
<script>
import { state } from '../store'
export default {
    computed: {
        count() {
            return state.count
        }
    }
}
</script>

// child.vue
import  { mutations } from '../store'
export default {
    methods: {
        handleClick() {
            mutations.increment()
        }
    }
}

children/root

通过给子组件定义 ref 属性可以使用 $refs 来直接操作子组件的方法和属性。

<child ref="list"></child>

比如子组件有一个 getList 方法,可以通过如下方式进行调用,实现父到子的通信:

this.$refs.list.getList()

除了 $refs 外,其他3个都是自 Vue 实例创建后就会自动包含的属性,使用和上面的类似。

6类可以掌握的修饰符

表单修饰符

表单类的修饰符都是和 v-model 搭配使用的,比如:v-model.lazyv-model-trim 以及 v-model.number 等。

  • .lazy:对表单输入的结果进行延迟响应,通常和 v-model 搭配使用。正常情况下在 input 里输入内容会在 p 标签里实时的展示出来,但是加上 .lazy 后则需要在输入框失去焦点的时候才触发响应。

    <input type="text" v-model.lazy="name" />
    <p>{{ name }}</p>
    

  • .trim:过滤输入内容的首尾空格,这个和直接拿到字符串然后通过 str.trim() 去除字符串首尾空格是一个意思。

  • .number:如果输入的第一个字符是数字,那就只能输入数字,否则他输入的就是普通字符串。

事件修饰符

Vue 的事件修饰符是专门为 v-on 设计的,可以这样使用:@click.stop="handleClick",还能串联使用:@click.stop.prevent="handleClick"

<div @click="doDiv">
    click div
    <p @click="doP">click p</p>
</div>

  • .stop:阻止事件冒泡,和原生 event.stopPropagation() 是一样的效果。如上代码,当点击 p 标签的时候,div 上的点击事件也会触发,加上 .stop 后事件就不会往父级传递,那父级的事件就不会触发了。

  • .prevent:阻止默认事件,和原生的 event.preventDefault() 是一样的效果。比如一个带有 href 的链接上添加了点击事件,那么事件触发的时候也会触发链接的跳转,但是加上 .prevent 后就不会触发链接跳转了。

  • .capture:默认的事件流是:捕获阶段-目标阶段-冒泡阶段,即事件从最具体目标元素开始触发,然后往上冒泡。而加上 .capture 后则是反过来,外层元素先触发事件,然后往深层传递。

  • .self:只触发自身的事件,不会传递到父级,和 .stop 的作用有点类似。

  • .once:只会触发一次该事件。

  • .passive:当页面滚动的时候就会一直触发 onScroll 事件,这个其实是存在性能问题的,尤其是在移动端,当给他加上 .passive 后触发的就不会那么频繁了。

  • .native:现在在组件上使用 v-on 只会监听自定义事件 (组件用 $emit 触发的事件)。如果要监听根元素的原生事件,可以使用 .native 修饰符,比如如下的 el-input,如果不加 .native 当回车的时候就不会触发 search 函数。

    <el-input type="text" v-model="name" @keyup.enter.native="search"></el-input>
    

串联使用事件修饰符的时候,需要注意其顺序,同样2个修饰符进行串联使用,顺序不同,结果大不一样。@click.prevent.self 会阻止所有的点击事件,而 @click.self.prevent 只会阻止对自身元素的点击。

鼠标按钮修饰符

  • .left:鼠标左键点击;

  • .right:鼠标右键点击;

  • .middle:鼠标中键点击;

键盘按键修饰符

Vue 提供了一些常用的按键码:

  • .enter

  • .tab

  • .delete (捕获“删除”和“退格”键)

  • .esc

  • .space

  • .up

  • .down

  • .left

  • .right

另外,你也可以直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符,比如可以通过如下的代码来查看具体按键的键名是什么:

<input @keyup="onKeyUp">

onKeyUp(event) {
    console.log(event.key)  // 比如键盘的方向键向下就是 ArrowDown
}

.exact修饰符

.exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。

<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button v-on:click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button v-on:click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button v-on:click.exact="onClick">A</button>

.sync修饰符

.sync 修饰符常被用于子组件更新父组件数据。直接看下面的代码:

// parent.vue
<child :title.sync="title"></child>

// child.vue
this.$emit('update:title', 'hello')

子组件可以直接通过 update:title 的形式进行更新父组件中声明了 .syncprop。上面父组件中的写法其实是下面这种写法的简写:

<child :title="title" @update:title="title = $event"></child>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用

如果需要设置多个 prop,比如:

<child :name.sync="name" :age.sync="age" :sex.sync="sex"></child>

可以通过 v-bind.sync 简写成这样:

<child v-bind.sync="person"></child>

person: {
    name: 'bubuzou',
    age: 21,
    sex: 'male',
}

Vue 内部会自行进行解析把 person 对象里的每个属性都作为独立的 prop 传递进去,各自添加用于更新的 v-on 监听器。而从子组件进行更新的时候还是保持不变,比如:

this.$emit('update:name', 'hello')

6种方式编写可复用模块

今天需求评审了一个需求,需要实现一个详情页,这个详情页普通用户和管理员都能进去,但是展示的数据有稍有不同,但绝大部分是一样的;最主要的区别是详情对于普通用户是纯展示,而对于管理员要求能够编辑,然后管理员还有一些别的按钮权限等。需求看到这里,如果在排期的时候把用户的详情分给开发A做,而把管理员的详情分给B去做,那这样做的结果就是开发A写了一个详情页,开发B写了一个详情页,这在开发阶段、提测后的修改 bug 阶段以及后期迭代阶段,都需要同时维护这 2 个文件,浪费了时间浪费了人力,所以你可以从中意识到编写可复用模块的重要性。

Vue 作者尤大为了让开发者更好的编写可复用模块,提供了很多的手段,比如:组件、自定义指令、渲染函数、插件以及过滤器等。

组件

组件是 Vue 中最精髓的地方,也是我们平时编写可复用模块最常用的手段,但是由于这块内容篇幅很多,所以不在这里展开,后续会写相关的内容进行详述。

使用混入mixins

什么是混入呢?从代码结构上来看,混入其实就是半个组件,一个 Vue 组件可以包括 templatescriptstyle 三部分,而混入其实就是 script 里面的内容。一个混入对象包含任意组件选项,比如 datamethodscomputedwatch 、生命周期钩子函数、甚至是 mixins 自己等,混入被设计出来就是旨在提高代码的灵活性、可复用性。

什么时候应该使用混入呢?当可复用逻辑只是 JS 代码层面的,而无 template 的时候就可以考虑用混入了。比如需要记录用户在页面的停留的时间,那我们就可以把这段逻辑抽出来放在 mixins 里:

// mixins.js
export const statMixin = {
    methods: {
        enterPage() {},
        leavePage() {},
    },
    mounted() {
        this.enterPage()
    },
    beforeDestroyed() {
        this.leavePage()
    }
}

然后在需要统计页面停留时间的地方加上:

import { statMixin } from '../common/mixins'
export default {
    mixins: [statMixin]
}

使用混入的时候要注意和组件选项的合并规则,可以分为如下三类:

  • data 将进行递归合并,对于键名冲突的以组件数据为准:

    // mixinA 的 data
    data() {
        obj: {
            name: 'bubuzou',
        },
    }
    
    // component A
    export default {
        mixins: [mixinA],
        data(){
            obj: {
                name: 'hello',
                age: 21
            },
        },
        mounted() {
            console.log( this.obj )  // { name: 'bubuzou', 'age': 21 }    
        }
    }
    

  • 对于生命周期钩子函数将会合并成一个数组,混入对象的钩子将先被执行:

    // mixin A
    const mixinA = {
        created() {
            console.log( '第一个执行' )
        }
    }
    
    // mixin B
    const mixinB = {
        mixins: [mixinA]
        created() {
            console.log( '第二个执行' )
        }
    }
    
    // component A
    export default {
        mixins: [mixinB]
        created() {
            console.log( '最后一个执行' )
        }
    }
    

  • 值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

自定义指令

除了 Vue 内置的一些指令比如 v-modelv-if 等,Vue 还允许我们自定义指令。在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。比如我们可以通过自定义一个指令来控制按钮的权限。我们期望设计一个如下形式的指令来控制按钮权限:

<button v-auth="['user']">提交</button>

通过在按钮的指令里传入一组权限,如果该按钮只有 admin 权限才可以提交,而我们传入一个别的权限,比如 user,那这个按钮就不应该显示了。接下来我们去注册一个全局的指令:

// auth.js
const AUTH_LIST = ['admin']

function checkAuth(auths) {
    return AUTH_LIST.some(item => auths.includes(item))
}

function install(Vue, options = {}) {
    Vue.directive('auth', {
        inserted(el, binding) {
            if (!checkAuth(binding.value)) {
                el.parentNode && el.parentNode.removeChild(el)
            }
        }
    })
}

export default { install }

然后我们需要在 main.js 里通过安装插件的方式来启用这个指令:

import Auth from './utils/auth'
Vue.use(Auth)

使用渲染函数

这里将使用渲染函数实现上面介绍过的的权限按钮。使用方式如下,把需要控制权限的按钮包在权限组件 authority 里面,如果有该权限就显示,没有就不显示。

<authority :auth="['admin']">
    <button>提交</button>
</authority>

然后我们用渲染函数去实现一个 authority 组件:

<script>
const AUTH_LIST = ['admin', 'user', 'org']

function checkAuth(auths) {
    return AUTH_LIST.some(item => auths.includes(item))
}
export default {
    functional: true,
    props: {
        auth: {
            type: Array,
            required: true
        }
    },
    render(h, context) {
        const { props,  scopedSlots} = context
        return checkAuth(props.auth) ? scopedSlots.default() : null
    }
}
</script>

全局注册这个组件:

// main.js
import Authority from './components/authority'
Vue.component('authority', Authority)

使用过滤器

Vue 提供了自定义过滤器的功能,主要应用场景是想要将数据以某种格式展示出来,而原始数据又不符合这种格式的时候。比如有一组关于人的数据,如下:

[{    name: '张茂',    population: 'young',}, {    name: '王丽',    population: 'middle',}, {    name: '郝鹏程',    population: 'child',}]

其中有一项是关于按照年龄划分的群体类型 population,而它是用 code 进行标识的,我们希望在展示的时候能够显示成对应的中文意思,比如 young 显示成青年。那我们就可以定义一个如下的局部过滤器:

export default {
    filters: {
        popuFilters(value) {
            if (!value) { return '未知' }
            let index = ['child', 'lad', 'young', 'middle', 'wrinkly'].indexOf(value)
            return index > 0 && ['儿童', '少年', '青年', '中年', '老年'][index] || '未知'
        }
    }
}

使用过滤器的时候只要在 template 中这样使用即可:

<p>{{ item.population | popuFilters }}</p>

自定义插件

在某些情况下,我们封装的内容可能不需要使用者对其内部代码结构进行了解,其只需要熟悉我们提供出来的相应方法和 api 即可,这需要我们更系统性的将公用部分逻辑封装成插件,来为项目添加全局功能,比如常见的 loading 功能、弹框功能等。

开发 Vue 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。可以通过如下4种方式来自定义插件:

MyPlugin.install = function (Vue, options) {
  // 1\. 添加全局方法或 property
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2\. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3\. 注入组件选项
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4\. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

然后需要在入口文件,比如 main.js 中注册插件:

import MyPlugin from './plugins/plugins.js'
Vue.use(MyPlugin)

3种方式手写优雅代码

平时写项目的时候我们都是在第一时间完成需求功能的开发、提测修改 bug 等,然后开开心心的等待着发布生产以为没啥事情了。其实回过头来细细的看我们平时写的代码,可能会发现很多地方都是值得优化的,比如对于很多重复性很强的代码,比如对于某些写得很繁杂的地方。优雅的代码可以化机械为自动、化繁为简,看人开了如沐春风,心情大好。这里列了几个在 Vue 中一定会遇到的问题,然后通过优雅的方式进行解决。

自动化导入模块

在开发一个稍微大点的项目的时候,会习惯将路由按照模块来划分,然后就可能会出现如下这种代码:

// router.js
import Vue from 'vue'
import Router from 'vue-router'
// 导入了一大堆路由文件
import mediator from './mediator'
import judges from './judges'
import disputeMediation from './disputeMediation'
import onlineMediation from './onlineMediation'
import useraction from './useraction'
import organcenter from './organcenter'
import admin from './admin'

let routeList = []
routeList.push(mediator, judges, disputeMediation, onlineMediation, useraction, organcenter, admin)

export default new Router({
    mode: 'history',
    routes: routeList,
})

其实真实的远远不止这么点,就我本地项目而言就有20几个路由文件,写了一大堆的导入代码,显得很臃肿,更无奈的是每当需要新增一个路由模块,还得再次 import 再次 push,那么有没有什么办法可以解决这个问题呢?答案自然是有的。

利用 webpackrequire.context 就可以很优雅的解决这个问题,使用语法如下:

require.context(
    directory,  // 搜索的目录
    useSubdirectories = true,  // 是否搜索子目录
    regExp = /^\.\/.*$/,  // 匹配的目标文件格式
    mode = 'sync'  // 同步还是异步
)

有了这个语法,我们就能很容易的写出下面的代码:

import Vue from 'vue'
import Router from 'vue-router'

let routeList = []
let importAll = require.context('@/publicResource/router', false, /\.js$/)
importAll.keys().map(path => {
    // 因为 index.js 也在 @/publicResource/router 目录下,所以需要排除
    if (!path.includes('index.js')) {          
        //兼容处理:.default 获取 ES6 规范暴露的内容; 后者获取 commonJS 规范暴露的内容
        let router = importAll(path).default || importAll(path)
        routeList(router)
    }
})

export default new Router({
    mode: 'history',
    routes: routeList,
})

我这里准备了一些 

【Java核心技术资料】 

【JAVA核心总结】 

【524页中高级程序员必看Java资料】 

【《JavaGuide面试突击》v3.0肝出来了!】

 【Java高级笔试宝典覆盖近3年Java笔试中98%高频知识点吊打100家大厂面试官】 

【Java大厂面试题】 

【阿里架构师花近十年时间整理出来的Java核心知识pdf(Java岗)】

 【524页《Java中高级程序员必备核心知识》总结,令人犹如醍醐灌顶】

 等一系列的Java架构资料 需要的话扫一扫免费获取 无套路