Vue | 17 组件深入-处理边界情况

内容提要:

  1. 元素和组件的访问:根实例、父组件实例、子组件实例和子元素的访问方式以及依赖注入
  2. 程式化事件监听器
  3. 循环应用的产出原因及解决方式
  4. 使用inline Templates、X-Templates替代模板定义
  5. 数据更新的控制:强制更新和加载静态内容的方式

这页假设你已经读了基本组件 Components Basics,如果你不了解组件知识,首先读它。

这页记录了所有和处理边界情况有关的功能,即一些需要对Vue的规则做一些小调整的一些情况。然而,这些功能都有劣势或危险的场景。这些需要在每一种情况中注意,所以当我们使用这些功能的时候要引起注意。

元素&组件访问

在大部分情况下,我们最好避免去触达其他组件实例内部或维护DOM元素。然而,有些情况下这么做是合适的。

访问根实例

在每一个共同new Vue 实例创建的子组件,我们能够通过$root属性访问根元素。例如,在这个根实例:

// 根实例
new Vue({
    data: {
        foo: 1
  },
    computed: {
        bar: function () { /* ... */ }
    },
    methods: {
        baz: function () { /* ... */ }
    }
})

所有的子组件现在都能够使用这个实例并且使用它作为一个全局存储:

// 获取根数据
this.$root.foo

// 设置根数据
this.$root.foo = 2

// 访问根计算属性
this.$root.bar

// 调用根方法
this.$root.baz()

这对于这个Demos 或只有几个组件的小应用是方便的。然而,这个模式不能很好的拓展到中型或大型的应用,所以我们强烈推荐你使用 Vuex去管理大部分情况的状态。

访问父级组件实例

类似于$root,$parent属性能够用于访问一个子组件的父级实例。这可能对于作为一个懒惰的方式去访问或使用prop传递数据是诱人的。

在大部分情况下,深入到父级实例会使你的应用调试和理解困难,尤其是如果你改变父级的数据。以后再看这个组件的时候,找出改变来自哪里会非常困难。

然而有一些情况下,特别是共享组件库,这时可能是合适的。例如,在和JavaScript APIs进行交互而不渲染HTML的抽象组件内,比如假设的Google Maps组件:

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

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

记住,无论如何,组件使用这个模式构建仍然是内部脆弱的。例如,假设我们增加了一个新的google-map-region标签组件,当google-map-markers标签出现在里面的时候,它应该仅仅渲染数据该区域的标记:

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

然后在google-map-markers标签内部你可能发现自己需要一些类似于这样的hack:

var map = this.$parent.map || this.$parent.$parent.map

这可能很快的失去控制。这就是为什么要像任意深度的派生组件去提供上下文信息,我们提供了推荐方案依赖注入 dependency injection

访问子组件元素&子元素

尽管存在props和事件,有时你可能仍然需要在JavaScript中去直接访问一个子组件。为了实现这个你可以使用ref属性为子组件分配一个ID引用。例如:

<base-input ref="usernameInput"></base-input>

现在在组件中你已经定义了这个ref,你能这样使用:

this.$refs.usernameInput

来访问base-input标签实例。当你想这么做的时候可能是有用的,例如,程序化的从一个父级组件聚焦于这个输入框。在这种情况下,与使用一个ref类似,在base-input标签组件内部去提供访问一个特定的元素,例如:

<input ref="input">

甚至可以通过其父级组件定义的方法:

methods:{
    // 用来从父级组件聚焦于输入框
    focus: function () {
        this.$refs.input.foucs()
    }
}

因此允许父组件去在base-input标签内部聚焦于输入框:

this.$refs.usernameInput.focus

refv-for一起使用的时候,你得到的ref将是一个包含映射数据源的子组件的数组。

$refs在组件被渲染后填充,它们不是响应式的。这只意味着为直接操作子组件而封装的“逃生舱”—你应该避免在模板内部或计算属性访问$refs.

依赖注入

在前面,当我们描写 Accessing the Parent Component Instance的时候,我们展示了一个例子像这样:

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

在这个组件里,所有的google-map标签的子节点需要访问getMap方法,以确定和哪个地图交互。不幸的是,使用$parent属性不能很好的拓展到嵌套更深的组件。这就是使用依赖注入的必要性,使用两个新的实例选项provideinject

provide选项允许我们提供指定的数据/方法给我们的派生组件。在这里例子中,google-map标签内部包含getMap方法。

provide:function () {
    return {
        getMap: this.getMap
    }
}

然后在任何的派生组件中,我们能够使用inject选项接收我们想要添加在这个实例上的属性。

inject: ['getMap']

你可以在这里查看完整代码full example here,相比于$parent的优势是这个用法使我们能够在任何派生的组件访问getMap。而不暴露出整个的实例。这允许我们持续研发该组件,不用害怕我们会改变或移除一些子组件依赖的东西。这些组件之间的接口仍然是明确定义的。正如props一样。

实际上,你可以认为依赖注入作为“大范围的props”的一种,除了:

  • 原始组件不需要知道哪些派生组件使用了它提供的属性
  • 派生组件不需要知道派生的组件从哪里来

然而,依赖注入有一些缺点。它将你的应用以目前的组织形式耦合了起来,使得重构变得困难。被提供的属性也不是响应式的。这是出于设计的考虑,因为使用它们创建一个中心数据存储的伸缩性和使用$root来实现相同的目的一样差。如果你想要共享的这个属性是你的应用所特有的,而非通用的,如果你想在祖先控件中更新所提供的数据,那么意味着你可以需要一个像Vuex一样的解决方案了。

学习更多依赖注入在the API doc

程序化的事件监听器

到目前为止,你已经看到$emit的用法,使用v-on监听,但是Vue实例在事件界面也提供了其他方法。我们能:

  • 使用$on(eventName, eventHandler)监听事件
  • 使用$once(eventName, eventHandler)一次性监听事件
  • 使用$off(eventName, eventHandler)停止监听一个事件

正常情况下你不必使用这些,除非你需要手动监听一个组件实例。它们也是有用作为一个代码组织工具。例如,你可能常常看到这种集成第三方库的模式:

// 一次性将这个日期选择器附加到输入框上
// 它会被挂载到DOM上
mounted: function () {
    // Pikaday 是一个第三方日期选择去库
    this.picker = new Pikaday({
        field: this.$refs.input,
        format:'YYYY-MM-DD'
    })
},
    // 在组件被销毁之前,也销毁这个日期选择器
    beforeDestory: function () {
        this.picker.destory()
    }

这有两个潜在的问题:

  • 它需要保存picker给组件实例,如果可能的话最好只有生命周期钩子能够访问到它。这并不是严重的问题,但被认为很混乱。
  • 我们创建的代码独立于我们的清理代码,这使我们很难程式化的清理我们创建的任何代码。

你能够解决这两个问题用一个程序化的监听器:

mounted:function () {
    var picker = new Pikaday({
        field: this.$refs.input,
        format: 'YYYY-MM-DD'
    })
    
    this.$once('hook:beforeDestory', function () {
        picker.destory()
    })
}

使用这个策略,我们甚至可以在多个输入元素使用Pikaday,在它自己被销毁之后每一个新实例会自动清理它:

mounted:function () {
    attachDatepicker: function (refName) {
        var picker = new Pikaday({
            field: this.$refs[refName],
            format: 'YYYY-MM-DD'
        })
        
        this.$once('hook:beforeDestory',function () {
            picker.destory()
        })
    }
}

完整代码看 this fiddle。然而,如果你发现你自己不得不在一个单独的组件中做许多创建和清理的工作,最好的解决方案常常是更为标准化的组件。在这种情况下,我们推荐可复用的input-datepicker标签组件。

为了学到更多关于程式化的监听器,请查看Events Instance MethodsAPI。

注意:Vue的事件系统和浏览器的 EventTarget API是不同的。尽管他们工作方式类似,$emit,$on,和$off不是dispatchEvent, addEventListener, 和removeEventListener

循环引用

递归组件

组件在他们自己的模板中调用他们自己,然而,他们只能通过name操作符来做这件事:

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

当你使用Vue.component全局注册一个组件的时候,全局ID将自动作为组件的name操作符设置。

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

如果你不仔细,递归组件也能够导致无限循环:

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

一个像上面的组件将产生“超过最大尺寸”的错误,所有请确保递归调用是条件性的(例如:使用一个最终会得到falsev-if)。

组件间的循环引用

假设你需要创建一个文件目录树,例如Finder或File Explorer.你可能使用这个有一个tree-folder组件的模板:

<p>
    <span>{{ folder.name }}</span>
    <tree-folder-contents :children="folder.children"/>
</p>

然后一个tree-folder-contents组件使用这个模板:

<ul>
    <li v-for="child in children">
        <tree-folder v-if="child.children" :folder="child"/>
        <span v-else>{{ child.name }}</span>
    </li>
</ul>

当你仔细观察的时候,你会发现这些组件被其他组件派生,由祖先组件渲染这颗树的悖论!当使用Vue.component全局注册组件的时候,这个悖论会被自动解决,如果你是这样做的,你可以跳过这里。

然而,如果你使用一个模块系统requireing/importing组件,例如通过Webpack或Browserify,你将得到一个错误:

Failed to mount component: template or render function not defined.

为了解释发生了什么,让我们称我们的组件为A和B。组件系统需要A,但是A需要B,B需要A,但是A需要B,等等,它被卡在了这个循环里,不知道如何不经过其中一个组件而完全解析出其他组件。为了修复这个问题,我们需要给模块系统一个它能指明的点,“A实际上需要B,但是不需要首先解析B”。

在我们的例子里,把tree-folder组件设为那个点。我们知道子组件创建了悖论是tree-folder-contents组件,所以我们等到beforeCreate生命周期钩子去注册它:

beforeCreate:function() {
	this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者,当你本地注册组件的时候你能使用Webpack的异步import

components: {
    TreeFolderContents: () => import('./tree-folder-contents.vue')
}

问题被解决!

代替模板定义

内联模板

当inline-template特殊属性出现在一个子组件的时候,组件将使用它的内联内容作为他的模板,而不是将其作为被分发的内容。这时的模板的制作更为灵活。

<my-component inline-template>
    <div>
        <p>这是被编译的作为自己的模板. </p>
        <p>Not parent's transclusion content.</p>
    </div>
</my-component>

然而,模板的作用域更加难以理解,作为最佳实践,宁愿使用template操作符在组件内部定义模板或在.vue文件内部使用元素来定义模板。

X-Templates

另一种定义模板的方式是使用text/x-template类型在script元素内部,然后通过一个id引用模板。例如:

<script type="text/x-template" id="hello-world-template">
	<p>Hello hello hello</p>
</script>
Vue.component('hello-world',{
  template: '#hello-world-template'  
})

这些可以用于模板特别大的demos或极小的应用,但是其他的情况应该避免,因为他们将该模板和组件的其它定义分开。

控制更新

感谢Vue的响应系统,如果你正取的使用它,那么你更新数据的时候它总是知道。有一些边缘的情况,然而,你想要去强制更新尽管实际上响应式数据没有发生变化。然而,有一些其他情况你想要阻止非必要的更新。

强制更新

如果你发现你自己需要再在Vue内做强制更新,99.99%的情况下,是你在某个地方犯了一个错误。

你可能没有注意到 with arraysobjects改变检测警告,或你可能依赖于没有被Vue响应系统追踪的状态。例如:data

然而,如果你发现你已经排除了上述情况,你想在极其罕见的情况中手动更新,你能够使用$forceUpdate做。

使用v-once创建低开销的静态组件

在Vue渲染HTML元素是非常快的,但有时你可能有一个组件包含了很多静态内容。在这些情况下,你可以确保内容仅仅被计算一次,然后在根元素使用v-once指令去从缓存增加内容,像这样:

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

再说一次,试着不去复用这个模式。当你不得不去渲染很多静态内容的时候,在一些罕见的情况下这很方便,除非你注意到加载很缓慢否则这是没有必要的,因为它会在以后引起很多困惑。例如,另一个开发人员这不熟悉v-once的用法,或在模板中丢失了它。它可能会花费几个小时计算出模板为什么没有正确更新。

猜你喜欢

转载自blog.csdn.net/wudizhanshen/article/details/84960557