• 最新的一波Vue实战技巧,不用则已,一用惊人


    Vue中,不同的选项有不同的合并策略,比如 data,props,methods是同名属性覆盖合并,其他直接合并,而生命周期钩子函数则是将同名的函数放到一个数组中,在调用的时候依次调用

    Vue中,提供了一个api, Vue.config.optionMergeStrategies,可以通过这个api去自定义选项的合并策略。

    在代码中打印

    console.log(Vue.config.optionMergeStrategies)
    

      

     通过合并策略自定义生命周期函数

    背景

    发现页面有许多定时器,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') {
            // 开启那一堆东西
          }
        }
      }
    }
    

    通过上面的代码,可以看到在每一个需要监听处理的文件都要写一堆事件监听,判断页面是否显示的代码,一处两处还可以,文件多了就头疼了,这时候小编突发奇想,定义一个页面显示隐藏的生命周期钩子,把这些判断都封装起来

    自定义生命周期钩子函数

    定义生命周期函数 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)
        })
      }
    }
    
    // 添加生命周期函数
    export function init() {
      const optionMergeStrategies = Vue.config.optionMergeStrategies
      // 定义了两个生命周期函数 pageVisible, pageHidden
      // 为什么要赋值为 optionMergeStrategies.created呢
      // 这个相当于指定 pageVisible, pageHidden 的合并策略与 created的相同(其他生命周期函数都一样)
      optionMergeStrategies.pageVisible = optionMergeStrategies.beforeCreate
      optionMergeStrategies.pageHidden = optionMergeStrategies.created
    }
    
    
    // 将事件变化绑定到根节点上面
    // rootVm vue根节点实例
    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)
        }
      })
    }
    

    应用

    1. 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('页面隐藏了')
      }
    }
    

      

    provideinject,不止父子传值,祖宗传值也可以

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

    使用过React的同学都知道,在React中有一个上下文Context,组件可以通过Context向任意后代传值,而Vueprovideinject的作用于Context的作用基本一样

    先举一个例子

    使用过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,这个是怎么做到的?接下来我们自己手写一个表单模拟一下

    自己手写一个表单

    自定义表单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'
          })
        }
      }
    }
    

      

    使用限制

    1.provideinject的绑定不是可响应式的。但是,如果你传入的是一个可监听的对象,如上面的customForm: this,那么其对象的属性还是可响应的。

    2.Vue官网建议provideinject 主要在开发高阶插件/组件库时使用。不推荐用于普通应用程序代码中。因为provideinject在代码中是不可追溯的(ctrl + f可以搜),建议可以使用Vuex代替。 但是,也不是说不能用,在局部功能有时候用了作用还是比较大的。

     

    插槽,我要钻到你的怀里

    插槽,相信每一位Vue都有使用过,但是如何更好的去理解插槽,如何去自定义插槽,今天小编为你带来更形象的说明。

    默认插槽

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

      

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

    具名插槽

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

      

    <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>

    作用域插槽

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

      

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

    插槽默认值

    <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>
    

      

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

    dispatchbroadcast,这是一种有历史的组件通信方式

    dispatchbroadcast是一种有历史的组件通信方式,为什么是有历史的,因为他们是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监听要注册的事件

    $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 也没有解决兄弟组件间的通信问题。“

    确实如官网所说,这种事件流的方式确实不容易让人理解,而且后期维护成本比较高。但是在小编看来,不管黑猫白猫,能抓老鼠的都是好猫,在许多特定的业务场景中,因为业务的复杂性,很有可能使用到这样的通信方式。但是使用归使用,但是不能滥用,小编一直就在项目中有使用。

  • 相关阅读:
    紫色飞猪的研发之旅--07client-go实现进入pod模拟终端登录
    紫色飞猪的研发之旅--06go自定义状态码
    紫色飞猪的研发之旅--05go封装http请求
    紫色飞猪的研发之旅--04client-go客户端
    紫色飞猪的研发之旅--03golang:获取cookie
    支持remote write和exemplar的prometheus服务
    从头编写一个时序数据库
    解析Prometheus PromQL
    老板:把系统从单体架构升级到集群架构!
    小白自制Linux开发板 三. Linux内核与文件系统移植
  • 原文地址:https://www.cnblogs.com/songyao666/p/13201784.html
Copyright © 2020-2023  润新知