【vue学习】处理边界情况

访问元素 & 组件

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 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组件包裹

打印结果:


child组件被div包裹(div包裹几层都一样:应该原生的都不算吧?)


  • prop写法
  • $parent写法
  • 官网栗子

在一些可能适当的时候,你需要特别地共享一些组件库。举个例子,在和 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

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

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

provide&inject

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

  • parent包裹child的情况
  • 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 之间的关系:

  • 如果你只想监听一次事件的发生,可以使用 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

我们从上图中可以得出以下几点:
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:

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

循环引用

递归组件(自己调自己)

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 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过长会使得代码非常难以阅读

这时可以使用内联模版

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

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

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

控制更新

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

强制更新$forceUpdate

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

  • 你可能还没有留意到数组或对象的变更检测注意事项(后面另行学习),或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。
  • 一个对象数组,我们尝试直接给某个item增加一个属性,发现页面上没有效果;直接将length变成0来清空数组,也没有效果,关键代码如下:
  • 上面是我们按照vue的规范去写的,是可以实现变化的,关键代码如下:
  • 可是如果我们不想利用$set去设置,非要按照我们第一种方式去写,可以实现么?答案是可以的,就是利用$forceUpdate了,因为你修改了数据,但是页面层没有变动,说明数据本身是被修改了,但是vue没有监听到而已,用$forceUpdate就相当于按照最新数据给渲染一下。
低开销静态组件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 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新

总结一句

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值