前方高能预警,这是最新的一波Vue实战技巧,不用则已,一用惊人

    技术2022-07-11  82

    最近一直在开发后台管理系统,日复一日的重复着表单表格表格表单,标准的CV仔,感觉好无聊,如何能在这种无聊的开发过程中去提升自己,小编今天又整理了一波新的Vue实战技巧,这些技巧,不用则已,一用惊人。同时你也可以点击下面的链接阅读近期小编的文章。

    了解选项合并策略,自定义生命周期钩子函数 当你使用Vue的mixins的时候,是否有发现,如果混入的methods里面的方法与组件的方法同名,则会被组件方法覆盖,但是生命周期函数如果重名,混入的与组件自身的都会被执行,且执行顺序是先混入和自身,这是怎么做到的呢?

    1. 了解Vue合并策略

    在Vue中,不同的选项有不同的合并策略,比如 data,props,methods是同名属性覆盖合并,其他直接合并,而生命周期钩子函数则是将同名的函数放到一个数组中,在调用的时候依次调用 在Vue中,提供了一个api, Vue.config.optionMergeStrategies,可以通过这个api去自定义选项的合并策略。 在代码中打印

    console.log(Vue.config.optionMergeStrategies)

    通过上图可以看到Vue所有选项的合并策略函数,我们可以通过覆盖上面的方法,来自定合并策略函数,不过一般用不到。

    现在web前端的工资怎样?​www.zhihu.com

    2. 通过合并策略自定义生命周期函数背景 最近客户给领导反馈,我们的系统用一段时间,浏览器就变得有点卡,不知道为什么。问题出来了,本来想甩锅到后端,但是浏览器问题,没法甩锅啊,那就排查吧。 后来发现页面有许多定时器,ajax轮询还有动画,打开一个浏览器页签没法问题,打开多了,浏览器就变得卡了,这时候我就想如果能在用户切换页签时候将这些都停掉,不久解决了。百度里面上下检索,找到了一个事件visibilitychange,可以用来判断浏览器页签是否显示。有方法了,就写呗.

    export default { created() { window.addEventListener('visibilitychange', this.$_hanldeVisiblityChange) // 此处用了hookEvent,可以参考小编前一篇文章 this.$on('hook:beforeDestroy', () => { window.removeEventListener( 'visibilitychange', this.$_hanldeVisiblityChange ) }) }, methods: { $_hanldeVisiblityChange() { if (document.visibilityState === 'hidden') { // 停掉那一堆东西 } if (document.visibilityState === 'visible') { // 开启那一堆东西 } } } }

    通过上面的代码,可以看到在每一个需要监听处理的文件都要写一堆事件监听,判断页面是否显示的代码,一处两处还可以,文件多了就头疼了,这时候小编突发奇想,定义一个页面显示隐藏的生命周期钩子,把这些判断都封装起来,哪里需要点哪里,so easy(点读机记得广告费)。 自定义生命周期钩子函数 定义生命周期函数 pageHidden 与 pageVisible

    import Vue from 'vue' // 通知所有组件页面状态发生了变化 const notifyVisibilityChange = (lifeCycleName, vm) => { // 生命周期函数会存在$options中,通过$options[lifeCycleName]获取生命周期 const lifeCycles = vm.$options[lifeCycleName] // 因为使用了created的合并策略,所以是一个数组 if (lifeCycles && lifeCycles.length) { // 遍历 lifeCycleName对应的生命周期函数列表,依次执行 lifeCycles.forEach(lifecycle => { lifecycle.call(vm) }) } // 遍历所有的子组件,然后依次递归执行 if (vm.$children && vm.$children.length) { vm.$children.forEach(child => { notifyVisibilityChange(lifeCycleName, child) }) } } /** * 添加生命周期钩子函数 * @param {*} rootVm vue 根实例,在页面显示隐藏时候,通过root向下通知 */ export function init() { const optionMergeStrategies = Vue.config.optionMergeStrategies /* 定义了两个生命周期函数 pageVisible, pageHidden 为什么要赋值为 optionMergeStrategies.created呢 这个相当于指定 pageVisible, pageHidden 的合并策略与 created的相同(其他生命周期函数都一样) */ optionMergeStrategies.pageVisible = optionMergeStrategies.beforeCreate optionMergeStrategies.pageHidden = optionMergeStrategies.created } /** * 将事件变化绑定到根节点上面 * @param {*} rootVm */ export function bind(rootVm) { window.addEventListener('visibilitychange', () => { // 判断调用哪个生命周期函数 let lifeCycleName = undefined if (document.visibilityState === 'hidden') { lifeCycleName = 'pageHidden' } else if (document.visibilityState === 'visible') { lifeCycleName = 'pageVisible' } if (lifeCycleName) { // 通过所有组件生命周期发生变化了 notifyVisibilityChange(lifeCycleName, rootVm) } }) }

    应用

    在main.js主入口文件引入 import { init, bind } from './utils/custom-life-cycle' // 初始化生命周期函数, 必须在Vue实例化之前确定合并策略 init() const vm = new Vue({ router, render: h => h(App) }).$mount('#app') // 将rootVm 绑定到生命周期函数监听里面 bind(vm)

    2.在需要的地方监听生命周期函数

    export default { pageVisible() { console.log('页面显示出来了') }, pageHidden() { console.log('页面隐藏了') } }

    provide与inject,不止父子传值,祖宗传值也可以Vue相关的面试经常会被面试官问道,Vue父子之间传值的方式有哪些,通常我们会回答,props传值,$emit事件传值,vuex传值,还有eventbus传值等等,今天再加一种provide与inject传值,离offer又近了一步。(对了,下一节还有一种)

    使用过React的同学都知道,在React中有一个上下文Context,组件可以通过Context向任意后代传值,而Vue的provide与inject的作用于Context的作用基本一样 先举一个例子 使用过elemment-ui的同学一定对下面的代码感到熟悉

    晴空万里girl:web前端达到什么水平,才能找到工作?​zhuanlan.zhihu.com

    先举一个例子

    使用过elemment-ui的同学一定对下面的代码感到熟悉

    <template> <el-form :model="formData" size="small"> <el-form-item label="姓名" prop="name"> <el-input v-model="formData.name" /> </el-form-item> <el-form-item label="年龄" prop="age"> <el-input-number v-model="formData.age" /> </el-form-item> <el-button>提交</el-button> </el-form> </template> <script> export default { data() { return { formData: { name: '', age: 0 } } } } </script>

    看了上面的代码,貌似没啥特殊的,天天写啊。在el-form上面我们指定了一个属性size="small",然后有没有发现表单里面的所有表单元素以及按钮的 size都变成了small,这个是怎么做到的?接下来我们自己手写一个表单模拟一下自己手写一个表单 我们现在模仿element-ui的表单,自己自定义一个,文件目录如下

    自定义表单custom-form.vue

    <template> <form class="custom-form"> <slot></slot> </form> </template> <script> export default { props: { // 控制表单元素的大小 size: { type: String, default: 'default', // size 只能是下面的四个值 validator(value) { return ['default', 'large', 'small', 'mini'].includes(value) } }, // 控制表单元素的禁用状态 disabled: { type: Boolean, default: false } }, // 通过provide将当前表单实例传递到所有后代组件中 provide() { return { customForm: this } } } </script>

    在上面代码中,我们通过provide将当前组件的实例传递到后代组件中,provide是一个函数,函数返回的是一个对象

    自定义表单项custom-form-item.vue

    没有什么特殊的,只是加了一个label,element-ui更复杂一些

    <template> <div class="custom-form-item"> <label class="custom-form-item__label">{{ label }}</label> <div class="custom-form-item__content"> <slot></slot> </div> </div> </template> <script> export default { props: { label: { type: String, default: '' } } } </script>

    自定义输入框 custom-input.vue

    <template> <div class="custom-input" :class="[ `custom-input--${getSize}`, getDisabled && `custom-input--disabled` ]" > <input class="custom-input__input" :value="value" @input="$_handleChange" /> </div> </template> <script> /* eslint-disable vue/require-default-prop */ export default { props: { // 这里用了自定义v-model value: { type: String, default: '' }, size: { type: String }, disabled: { type: Boolean } }, // 通过inject 将form组件注入的实例添加进来 inject: ['customForm'], computed: { // 通过计算组件获取组件的size, 如果当前组件传入,则使用当前组件的,否则是否form组件的 getSize() { return this.size || this.customForm.size }, // 组件是否禁用 getDisabled() { const { disabled } = this if (disabled !== undefined) { return disabled } return this.customForm.disabled } }, methods: { // 自定义v-model $_handleChange(e) { this.$emit('input', e.target.value) } } } </script>

    在form中,我们通过provide返回了一个对象,在input中,我们可以通过inject获取form中返回对象中的项,如上代码inject:['customForm']所示,然后就可以在组件内通过this.customForm调用form实例上面的属性与方法了在项目中使用

    <template> <custom-form size="small"> <custom-form-item label="姓名"> <custom-input v-model="formData.name" /> </custom-form-item> </custom-form> </template> <script> import CustomForm from '../components/custom-form' import CustomFormItem from '../components/custom-form-item' import CustomInput from '../components/custom-input' export default { components: { CustomForm, CustomFormItem, CustomInput }, data() { return { formData: { name: '', age: 0 } } } } </script>

    执行上面代码,运行结果为:

    <form class="custom-form"> <div class="custom-form-item"> <label class="custom-form-item__label">姓名</label> <div class="custom-form-item__content"> <!--size=small已经添加到指定的位置了--> <div class="custom-input custom-input--small"> <input class="custom-input__input"> </div> </div> </div> </form>

    通过上面的代码可以看到,input组件已经设置组件样式为custom-input--small了 inject格式说明 除了上面代码中所使用的inject:['customForm']写法之外,inject还可以是一个对象。且可以指定默认值 修改上例,如果custom-input外部没有custom-form,则不会注入customForm,此时为customForm指定默认值

    { inject: { customForm: { // 对于非原始值,和props一样,需要提供一个工厂方法 default: () => ({ size: 'default' }) } } }

    如果我们希望inject进来的属性的名字不叫customForm,而是叫parentForm,如下代码

    inject: { // 注入的属性名称 parentForm: { // 通过 from 指定从哪个属性注入 from: 'customForm', default: () => ({ size: 'default' }) } }, computed: { // 通过计算组件获取组件的size, 如果当前组件传入,则使用当前组件的,否则是否form组件的 getSize() { return this.size || this.parentForm.size } }

    使用限制

    provide和inject的绑定不是可响应式的。但是,如果你传入的是一个可监听的对象,如上面的customForm: this,那么其对象的属性还是可响应的。Vue官网建议provide 和 inject 主要在开发高阶插件/组件库时使用。不推荐用于普通应用程序代码中。因为provide和inject在代码中是不可追溯的(ctrl + f可以搜),建议可以使用Vuex代替。 但是,也不是说不能用,在局部功能有时候用了作用还是比较大的。$dispatch和$dispatch,这是一种有历史的组件通信方式$dispatch与$dispatch是一种有历史的组件通信方式,为什么是有历史的,因为他们是Vue1.0提供的一种方式,在Vue2.0中废弃了。但是废弃了不代表我们不能自己手动实现,像许多UI库内部都有实现。本文以element-ui实现为基础进行介绍。同时看完本节,你会对组件的$parent,$children,$options有所了解。方法介绍$dispatch: $dispatch会向上触发一个事件,同时传递要触发的祖先组件的名称与参数,当事件向上传递到对应的组件上时会触发组件上的事件侦听器,同时传播会停止。$broadcast: $broadcast会向所有的后代组件传播一个事件,同时传递要触发的后代组件的名称与参数,当事件传递到对应的后代组件时,会触发组件上的事件侦听器,同时传播会停止(因为向下传递是树形的,所以只会停止其中一个叶子分支的传递)。$dispatch实现与应用

    1. 代码实现

    /** * 向上传播事件 * @param {*} eventName 事件名称 * @param {*} componentName 接收事件的组件名称 * @param {...any} params 传递的参数,可以有多个 */ function dispatch(eventName, componentName, ...params) { // 如果没有$parent, 则取$root let parent = this.$parent || this.$root while (parent) { // 组件的name存储在组件的$options.componentName 上面 const name = parent.$options.name // 如果接收事件的组件是当前组件 if (name === componentName) { // 通过当前组件上面的$emit触发事件,同事传递事件名称与参数 parent.$emit.apply(parent, [eventName, ...params]) break } else { // 否则继续向上判断 parent = parent.$parent } } } // 导出一个对象,然后在需要用到的地方通过混入添加 export default { methods: { $dispatch: dispatch } }

    2. 代码应用

    在子组件中通过$dispatch向上触发事件 import emitter from '../mixins/emitter' export default { name: 'Chart', // 通过混入将$dispatch加入进来 mixins: [emitter], mounted() { // 在组件渲染完之后,将组件通过$dispatch将自己注册到Board组件上 this.$dispatch('register', 'Board', this) } }

    在Board组件上通过$on监听要注册的事件

    export default { name: 'Board', created() { this.$on('register',(component) => { // 处理注册逻辑 }) } }

    $broadcast实现与应用

    1. 代码实现

    /** * 向下传播事件 * @param {*} eventName 事件名称 * @param {*} componentName 要触发组件的名称 * @param {...any} params 传递的参数 */ function broadcast(eventName, componentName, ...params) { this.$children.forEach(child => { const name = child.$options.name if (name === componentName) { child.$emit.apply(child, [eventName, ...params]) } else { broadcast.apply(child, [eventName, componentName, ...params]) } }) } // 导出一个对象,然后在需要用到的地方通过混入添加 export default { methods: { $broadcast: broadcast } }

    2. 代码应用

    在父组件中通过$broadcast向下触发事件

    import emitter from '../mixins/emitter' export default { name: 'Board', // 通过混入将$dispatch加入进来 mixins: [emitter], methods:{ //在需要的时候,刷新组件 $_refreshChildren(params) { this.$broadcast('refresh', 'Chart', params) } } }

    在后代组件中通过$on监听刷新事件|

    export default { name: 'Chart', created() { this.$on('refresh',(params) => { // 刷新事件 }) } }

    总结 通过上面的例子,同学们应该都能对$dispatch和$broadcast有所了解,但是为什么Vue2.0要放弃这两个方法呢?官方给出的解释是:”因为基于组件树结构的事件流方式实在是让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。这种事件方式确实不太好,我们也不希望在以后让开发者们太痛苦。并且 $dispatch 和 $broadcast 也没有解决兄弟组件间的通信问题。“ 确实如官网所说,这种事件流的方式确实不容易让人理解,而且后期维护成本比较高。但是在小编看来,不管黑猫白猫,能抓老鼠的都是好猫,在许多特定的业务场景中,因为业务的复杂性,很有可能使用到这样的通信方式。但是使用归使用,但是不能滥用,小编一直就在项目中有使用。插槽,我要钻到你的怀里 插槽,相信每一位Vue都有使用过,但是如何更好的去理解插槽,如何去自定义插槽,今天小编为你带来更形象的说明。默认插槽 大学毕业刚上班,穷鬼一个,想着每个月租房还要掏房租,所以小编决定买一个一居室,东拼西凑借了一堆债,终于凑够了首付,买了一个小小的毛坯房。我们可以把这个一居室的毛坯房想想成一个组件,这个房子的户型,面积,楼层都是固定的,但是室内如何装修,摆什么家具,这个却是由你来决定的,房间内部就可以理解为插槽,允许用户去自定义内容。1. 开发商终于将一居室开发完交房了

    <template> <!--这是一个一居室--> <div class="one-bedroom"> <!--添加一个默认插槽,用户可以在外部随意定义这个一居室的内容--> <slot></slot> </div> </template>

    2. 小编要开始装修了

    <template> <!--这里一居室--> <one-bedroom> <!--将家具放到房间里面,组件内部就是上面提供的默认插槽的空间--> <span>先放一个小床,反正没有女朋友</span> <span>再放一个电脑桌,在家还要加班写bug</span> </one-bedroom> </template> <script> import OneBedroom from '../components/one-bedroom' export default { components: { OneBedroom } } </script>

    具名插槽 过了几年,小编有了女朋友,准备结婚了,一居室房间肯定不行啊,丈母娘嫌小不同意,没办法,只能又凑钱买大房子,买了一个两居室(穷逼一个),因为是两居室,所以有了主卧和次卧之分,装修是否也不能把主卧和次卧装修的一模一样,所以就需要进行区分。将房子想想成组件,那么组件就有两个插槽,并且需要起名字进行区分。1. 开发商终于开发完交房了

    <template> <div class="two-bedroom"> <!--这是主卧--> <div class="master-bedroom"> <!---主卧使用默认插槽--> <slot></slot> </div> <!--这是次卧--> <div class="secondary-bedroom"> <!--次卧使用具名插槽--> <slot name="secondard"></slot> </div> </div> </template>

    2. 小编要卖血攒钱装修了

    <template> <two-bedroom> <!--主卧使用默认插槽--> <div> <span>放一个大床,要结婚了,嘿嘿嘿</span> <span>放一个衣柜,老婆的衣服太多了</span> <span>算了,还是放一个电脑桌吧,还要写bug</span> </div> <!--次卧,通过v-slot:secondard 可以指定使用哪一个具名插槽, v-slot:secondard 也可以简写为 #secondard--> <template v-slot:secondard> <div> <span>父母要住,放一个硬一点的床,软床对腰不好</span> <span>放一个衣柜</span> </div> </template> </two-bedroom> </template> <script> import TwoBedroom from '../components/slot/two-bedroom' export default { components: { TwoBedroom } } </script>

    作用域插槽

    装修的时候,装修师傅问我洗衣机是要放到卫生间还是阳台,一般情况下开发商会预留放洗衣机的位置。而这个位置可以理解为插槽传的参数,这个就是作用域插槽。

    1. 看一下卫生间插槽传了什么参数

    <template> <div class="two-bedroom"> <!--其他内容省略--> <div class="toilet"> <!--通过v-bind 可以向外传递参数, 告诉外面卫生间可以放洗衣机--> <slot name="toilet" v-bind="{ washer: true }"></slot> </div> </div> </template>

    2. 把洗衣机放到卫生间

    <template> <two-bedroom> <!--其他省略--> <!--卫生间插槽,通过v-slot="scope"可以获取组件内部通过v-bind传的值--> <template v-slot:toilet="scope"> <!--判断是否可以放洗衣机--> <span v-if="scope.washer">这里放洗衣机</span> </template> </two-bedroom> </template>

    插槽默认值

    小编的同事不想等期房,所以就买了二手房,二手房前业主都装修好了,可以直接入住。当然也可以重新装修,下面是同事买的二手房。

    1. 这是装修好的二手房

    <template> <div class="second-hand-house"> <div class="master-bedroom"> <!--插槽可以指定默认值,如果外部调用组件时没有修改插槽内容,则使用默认插槽--> <slot> <span>这里有一张水床,玩的够嗨</span> <span>还有一个衣柜,有点旧了</span> </slot> </div> <!--这是次卧--> <div class="secondary-bedroom"> <!--次卧使用具名插槽--> <slot name="secondard"> <span>这里有一张婴儿床</span> </slot> </div> </div> </template>

    2. 同事决定先把主卧装修了,以后结婚用

    <second-hand-house> <!--主卧使用默认插槽,只装修主卧--> <div> <span>放一个大床,要结婚了,嘿嘿嘿</span> <span>放一个衣柜,老婆的衣服太多了</span> <span>算了,还是放一个电脑桌吧,还要写bug</span> </div> </second-hand-house>
    Processed: 0.010, SQL: 9