美文网首页
【vue学习】处理边界情况

【vue学习】处理边界情况

作者: 前端菜篮子 | 来源:发表于2019-07-26 15:52 被阅读0次
    image

    访问元素 & 组件

    在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。

    $root

    • 所有的子组件都可以将这个实例作为一个全局 store来访问或使用
    • 对于 demo 或非常小型的有少量组件的应用来说这是很方便的。
    • 不过这个模式扩展到中大型应用来说就不然了。
    • 因此在绝大多数情况下,我们强烈推荐使用 Vuex 来管理应用的状态。

    $parent

    • $root 类似,$parent 属性可以用来从一个子组件访问父组件的实例。

    • 它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

    • 触达父级组件会使得你的应用更难调试和理解

    • parent与children的关系

    <!-- parent -->
    <template>
        <div>
            <h3>我是爹爹,哈哈哈</h3>
            <slot></slot>
        </div>
    </template>
    <script>
    export default {
        data(){
            return {
                parentText:'text999999999'
            }
        },
        mounted(){
            console.log(this.$children, "children")
        }
    }
    </script>
    
    <!-- child -->
    <template>
        <div>
            我是娃...
        </div>
    </template>
    <script>
    export default {
        data(){
            return {
                childText:'childtttttttt'
            }
        },
        mounted(){
            console.log(this.$parent, "parent")
        }
    }
    </script>
    

    child组件被parent组件包裹

    image
    打印结果:
    image
    image
    child组件被div包裹(div包裹几层都一样:应该原生的都不算吧?)
    image
    image
    image
    • prop写法
      image
    • $parent写法
      image
    • 官网栗子

    在一些可能适当的时候,你需要特别地共享一些组件库。举个例子,在和 JavaScript API 进行交互而不渲染 HTML 的抽象组件内,诸如这些假设性的 Google 地图组件一样:

    <google-map>
      <google-map-markers v-bind:places="iceCreamShops">
      </google-map-markers>
    </google-map>
    

    这个 <google-map> 组件可以定义一个 map 属性,所有的子组件都需要访问它。在这种情况下 <google-map-markers> 可能想要通过类似 this.$parent.getMap 的方式访问那个地图,以便为其添加一组标记。

    哈哈哈,然后可能有小伙伴就会用到this.$parent.$parent.map(孙子访问爷爷),然后就失控了

    $refs

    我们自己的项目中,这个貌似用的比较多

    image

    $refs 只会在组件渲染完成之后生效(后面$nextTick单独一篇介绍下),并且它们不是响应式的。这仅作为一个用于直接操作子组件的【逃生舱】—— 你应该避免在模板或计算属性中访问 $refs

    provide&inject

    上面$parent一节讲到过this.$parent.$parent.map(孙子访问爷爷),失控了。
    那如果真有这种需求呢?这就是依赖注入的用武之地,它用到了两个新的实例选项:provideinject

    • parent包裹child的情况
      image
    • div包裹child/gchild:即当前调用div的组件中定义provide选项,childgchild组件中定义inject
    • provide 选项允许我们指定我们想要提供给后代组件的数据/方法。该选项应该是一个对象或返回一个对象的函数
    provide: function () {
      return {
        getMap: this.getMap
      }
    }
    // 或者
    provide: {
        foo: 'bar'
    }
    
    • provide/inject应用
    • 然后在任何后代组件里,我们都可以使用 inject 选项来接收指定的我们想要添加在这个实例上的属性。
      inject 选项应该是:
    一个字符串数组,或
    一个对象,对象的 key 是本地的绑定名,value 是:
        在可用的注入内容中搜索用的 key (字符串或 Symbol),或
        一个对象,该对象的:
            from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
            default 属性是降级情况下使用的 value
            
    inject: ['getMap']
    //或
    inject: {
        simpleIndex:{from:'simpleIndex', default:1000 }
    }
    
    • Vue-cli 3.0 + Typescript 环境下
    //父组件 provide
    @Provide()
    public componentActivity = this.getProvide()
    private getProvide() {
        return 'aaaaaa'
    }
    // 或者
    @Provide()
    public componentActivity = {name:'aaaaaa'}
    
    
    //后代组件 Inject
    @Inject()
    private componentActivity: string
    private created() {
       console.info(this.componentActivity)   // 'aaaaaa'
    }
    

    程序化的事件侦听器

    你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的

    官方案例

    1. 你可能经常看到这种集成一个第三方库的模式:
    // 一次性将这个日期选择器附加到一个输入框上
    // 它会被挂载到 DOM 上。
    mounted: function () {
      // Pikaday 是一个第三方日期选择器的库
      this.picker = new Pikaday({
        field: this.$refs.input,
        format: 'YYYY-MM-DD'
      })
    },
    // 在组件被销毁之前,
    // 也销毁这个日期选择器。
    beforeDestroy: function () {
      this.picker.destroy()
    }
    
    1. 这里有两个潜在的问题:
    • 它需要在这个组件实例中保存这个 picker,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。
    • 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。
    • 你应该通过一个程序化的侦听器解决这两个问题:
    mounted: function () {
      var picker = new Pikaday({
        field: this.$refs.input,
        format: 'YYYY-MM-DD'
      })
    
      this.$once('hook:beforeDestroy', function () {
        picker.destroy()
      })
    }
    
    1. 使用了这个策略,我甚至可以让多个输入框元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己:
    mounted: function () {
      this.attachDatepicker('startDateInput')
      this.attachDatepicker('endDateInput')
    },
    methods: {
      attachDatepicker: function (refName) {
        var picker = new Pikaday({
          field: this.$refs[refName],
          format: 'YYYY-MM-DD'
        })
    
        this.$once('hook:beforeDestroy', function () {
          picker.destroy()
        })
      }
    }
    

    $on & $off &once【vue事件总线EventBus了解下】

    1. 该章节学习自: vue篇之事件总线--简书:程序汪

    2. EventBus(事件总线)的简介:Vue中可以来作为事件的沟通桥梁,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件;但也就是太方便所以若使用不慎,就会造成难以维护的灾难,因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。

    3. 如何使用EventBus

    • 初始化
    /**单独一个js文件event-bus.js,
       定义一个变量EventBus
       局部定义
    */
    /**实质上它是一个不具备 DOM 的组件,
       它具有的仅仅只是它实例方法而已,
       因此它非常的轻便
    */
    import Vue from 'vue'
    export const EventBus = new Vue()
    
    /**或者直接在在main.js中初始化也可以
    (这是个全局的定义,后面的具体案例先不管这个)
    */
    Vue.prototype.$EventBus = new Vue()
    
    • 发送事件:假设你有两个子组件: DecreaseCountIncrementCount ,分别在按钮中绑定了 decrease()increment() 方法。这两个方法做的事情很简单,就是数值递减(增) 1 ,以及角度值递减(增) 180 。
    <!--在这两个方法中,通过 
    EventBus.$emit(channel: string, callback(payload1,…)) 
    监听 decreased (和 incremented )频道。
    -->
    <!-- DecreaseCount.vue -->
    <template>
        <button @click="decrease()">-</button>
    </template>
    <script> import { EventBus } from "../event-bus.js";
        export default {
            name: "DecreaseCount",
            data() {
                return {
                    num: 1,
                    deg:180
                };
            },
            methods: {
                decrease() {
                    EventBus.$emit("decreased", {
                        num:this.num,
                        deg:this.deg
                    });
                }
            }
        }; 
    </script>
    
    <!-- IncrementCount.vue -->
    <template>
        <button @click="increment()">+</button>
    </template>
    <script> import { EventBus } from "../event-bus.js";
        export default {
            name: "IncrementCount",
            data() {
                return {
                    num: 1,
                    deg:180
                };
            },
            methods: {
                increment() {
                    EventBus.$emit("incremented", {
                        num:this.num,
                        deg:this.deg
                    });
                }
            }
        };
     </script>
     <!-- 
     上面的示例,在 DecreaseCount 和 IncrementCount 
     分别发送出了 decreased 和 incremented频道。
     接下来,我们需要在另一个组件中接收这两个事件,
     保持数据在各组件之间的通讯。
     -->
    
    • 接收事件:现在我们可以在组件 App.vue 中使用 EventBus.$on(channel: string, callback(payload1,…))监听 DecreaseCountIncrementCount 分别发送出了 decreasedincremented 频道。
    <!-- App.vue -->
    <template>
        <div id="app">
            <div class="container" :style="{transform: 'rotateY(' + degValue + 'deg)'}">
                <div class="front">
                    <div class="increment">
                        <IncrementCount />
                    </div>
                    <div class="show-front"> {{fontCount}} </div>
                    <div class="decrement">
                        <DecreaseCount />
                    </div>
                </div>
    
                <div class="back">
                    <div class="increment">
                        <IncrementCount />
                    </div>
                    <div class="show-back"> {{backCount}} </div>
                    <div class="decrement">
                        <DecreaseCount />
                    </div>
                </div> 
            </div>
        </div>
    </template>
    
    <script>
        import IncrementCount from "./components/IncrementCount";
        import DecreaseCount from "./components/DecreaseCount";
        import { EventBus } from "./event-bus.js";
        export default {
            name: "App",
            components: {
                IncrementCount,
                DecreaseCount
            },
            data() {
                return {
                    degValue:0,
                    fontCount:0,
                    backCount:0
                };
            },
            mounted() {
                EventBus.$on("incremented", ({num,deg}) => {
                    this.fontCount += num
                    this.$nextTick(()=>{
                        this.backCount += num
                        this.degValue += deg;
                    })
                });
                EventBus.$on("decreased", ({num,deg}) => {
                    this.fontCount -= num
                    this.$nextTick(()=>{
                        this.backCount -= num
                        this.degValue -= deg;
                    })
                });
            }
        }; 
    </script>
    
    • 用一张图来描述示例中用到的 EventBus 之间的关系:


      image
    • 如果你只想监听一次事件的发生,可以使用 EventBus.$once(channel: string, callback(payload1,…))

    • 移除事件监听者:

    import { eventBus } from './event-bus.js'
    EventBus.$off('decreased', {})
    
    • 你也可以使用 EventBus.$off('decreased') 来移除应用内所有对此事件的监听。或者直接调用EventBus.$off() 来移除所有事件频道, 注意不需要添加任何参数 。
    • 上面就是 EventBus 的使用方式,是不是很简单。PS:每次使用 EventBus 时都需要在各组件中引入 event-bus.js
    • 事实上,我们还可以通过别的方式,让事情变得简单一些。那就是创建一个全局的 EventBus
    1. 全局的 EventBus

    全局EventBus,虽然在某些示例中不提倡使用,但它是一种非常漂亮且简单的方法,可以跨组件之间共享数据。它的工作原理是发布/订阅方法,通常称为 Pub/Sub

    image
    我们从上图中可以得出以下几点:
    a.有一个全局EventBus
    b.所有事件都订阅它
    c.所有组件也发布到它,订阅组件获得更新
    
    总结一下:
    a.所有组件都能够将事件发布到总线,
    b.然后总线由另一个组件订阅,
    c.然后订阅它的组件将得到更新
    
    var EventBus = new Vue();
    Object.defineProperties(Vue.prototype, {
        $bus: {
            get: function () {
                return EventBus
            }
        }
    })
    /**
    现在,这个特定的总线使用两个方法 $on 和 $emit 一个用于创建发出的事件,它就是$emit 
    另一个用于订阅 $on 
    */
    this.$bus.$emit('nameOfEvent',{ ... pass some event data ...});
    
    this.$bus.$on('nameOfEvent',($event) => {
        // ...
    })
    

    现在我们创建两个简单的组件:一个 ShowMessage 的组件用来显示信息,另外创建一个 UpdateMessage 的组件,用来更新信息。

    <!-- UpdateMessage.vue -->
    <template>
        <div class="form">
            <div class="form-control">
                <input v-model="message" >
                <button @click="updateMessage()">更新消息</button>
            </div>
        </div>
    </template>
    <script>
    export default {
            name: "UpdateMessage",
            data() {
                return {
                    message: "这是一条消息"
                };
            },
            methods: {
                updateMessage() {
                    this.$bus.$emit("updateMessage", this.message);
                }
            },
            beforeDestroy () {
                $this.$bus.$off('updateMessage')
            }
        };
     </script>
    
    <!-- ShowMessage.vue -->
    <template>
        <div class="message">
            <h1>{{ message }}</h1>
        </div>
    </template>
    
    <script> 
    export default {
            name: "ShowMessage",
            data() {
                return {
                    message: "我是一条消息"
                };
            },
            created() {
                var self = this
                this.$bus.$on('updateMessage', function(value) {
                    self.updateMessage(value);
                })
            },
            methods: {
                updateMessage(value) {
                    this.message = value
                }
            }
        }; 
    </script>
    
    1. EventBus注册在全局上时,路由切换时会重复触发事件
    • 看了篇文章:vue中eventbus被多次触发

    • 全局定义的事件是不会跟随组件的生命周期函数进行状态改变的。

    • 切换路由时,如果不手动清除事件的话,它会注册多次。

    • 手动清除事件

    created() {
        this.bus.$off('clickBus');
        //在每次创建事件之前,手动清除定义的事件
        //根据实际的业务需求也可以在beforeDestroy()和destroyed()
    },
    mounted(){
        this.bus.$on('clickBus', (e) => {})
    }
    

    $once【定时器销毁案例看下】

    1. 这个首先看下官方案例,即【程序化的事件侦听器】刚开始讲到的那个
    2. 方案1:


      image

      该方案有两点不好的地方,引用尤大的话来说就是:

    • (1)它需要在这个组件实例中保存这个数据timer,这是多余的。
    • (2)我们的建立定时器代码独立于我们的清理代码(定时器需要卸载页面的时候卸载),这使得我们比较难于程序化的清理我们建立的所有东西(意思是执行代码和清除代码不在一起)。
    1. 方案2(同官网案例)


      image

    循环引用

    递归组件(自己调自己)

    组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事(动态组件和异步组件一章中我们提起过name的作用):

    name: 'unique-name-of-my-component'
    

    当你使用 Vue.component 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项。

    Vue.component('unique-name-of-my-component', {
      // ...
    })
    

    稍有不慎,递归组件就可能导致无限循环:

    name: 'stack-overflow',
    template: '<div><stack-overflow></stack-overflow></div>'
    

    A组件与B组件相互调用

    这个我们也在动态组件和异步组件的“异步组件”中讲到了
    组件之间的循环引用

    模板定义的替代品

    A. 内联模板inline-template

    Vue 学习笔记 — inline-template

    定义一个私有子组件时,如果子组件的template过长会使得代码非常难以阅读


    image

    这时可以使用内联模版


    image

    不过,inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。

    B. <script type="text/x-template">

    这个的意义应该和inline-template差不多,用法如下图:

    image

    控制更新

    Vue响应式系统,它始终知道何时进行更新 (如果你用对了的话)。不过还是有一些边界情况,你想要强制更新($forceUpdate),尽管表面上看响应式的数据没有发生改变。也有一些情况是你想阻止不必要的更新(v-once)。

    强制更新$forceUpdate

    如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。

    • 你可能还没有留意到数组或对象的变更检测注意事项(后面另行学习),或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。
    • 一个对象数组,我们尝试直接给某个item增加一个属性,发现页面上没有效果;直接将length变成0来清空数组,也没有效果,关键代码如下:
      image
    • 上面是我们按照vue的规范去写的,是可以实现变化的,关键代码如下:
      image
    • 可是如果我们不想利用$set去设置,非要按照我们第一种方式去写,可以实现么?答案是可以的,就是利用$forceUpdate了,因为你修改了数据,但是页面层没有变动,说明数据本身是被修改了,但是vue没有监听到而已,用$forceUpdate就相当于按照最新数据给渲染一下。
      image

    低开销静态组件v-once

    渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once 特性以确保这些内容只计算一次然后缓存起来,就像这样:

    Vue.component('terms-of-service', {
      template: `
        <div v-once>
          <h1>Terms of Service</h1>
          ... a lot of static content ...
        </div>
      `
    })
    

    啥也不多说了:不要过度使用这个模式。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新

    总结一句

    边界情况这块内容挺多的,但是基本上实际开发应用中都不咋用

    相关文章

      网友评论

          本文标题:【vue学习】处理边界情况

          本文链接:https://www.haomeiwen.com/subject/uildrctx.html