Vue官方文档笔记2——深入了解组件
Vue官方文档笔记2——深入了解组件
1.组件注册
1.1 组件名
在全局注册的时候,组件名就是 Vue.component 的第一个参数,组件命名采用驼峰式命名法或者短横线分隔命名法
1 | Vue.component('my-component-name', { /* ... */ }) |
1.2 全局注册
通过 Vue.component 来创建组件,在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中;
1 | Vue.component('component-a', { /* ... */ }) |
1 | <div id="app"> |
1.3 局部注册
通过一个普通的 JavaScript
对象来定义组件,然后在 components
选项中定义你想要使用的组件; components
对象中的每个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象,例如:
1 | var ComponentA = { /* ... */ } |
注意:局部注册的组件在其子组件中不可用;
1.4 模块系统
在模块系统中局部注册:
首先需要在局部注册之前导入每个你想使用的组件,然后再加入到组件属性中,例如在ComponentB中引入ComponentA和ComponentC 组件,进行逻辑处理后在 export
出去;
1 | import ComponentA from './ComponentA' |
基础组件的自动化全局注册:
可以使用 require.context
只全局注册这些非常通用的基础组件;在应用入口文件 (比如 src/main.js
) 中全局导入基础组件的示例代码(加载顺序以及正则的相关知识):
1 | import Vue from 'vue' |
注意:全局注册的行为必须在根 Vue 实例 (通过 new Vue
) 创建之前发生;
2.Prop
2.1 Prop的大小写
HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:
1 | Vue.component('blog-post', { |
1 | <!-- 在 HTML 中是 kebab-case 的 --> |
如果你使用字符串模板,那么这个限制就不存在了;
2.2 Prop 类型
可以以字符串数组形式或是以对象形式(对象中可以指定的值类型);
1 | props: ['title', 'likes', 'isPublished'] |
2.3 传递静态或动态 Prop
静态:<blog-post title="My journey with Vue"></blog-post>
动态:使用 v-bind
指令动态赋值,prop可以接受任何类型的值;
1 | <!-- 动态赋予一个变量的值 --> |
不仅可以传字符串,还可以传入数字、布尔值、数组、对象、一个对象的所有属性(使用不带参数的 v-bind (取代 v-bind:prop-name))等等;
2.4 单向数据流
所有的 prop
都使得其父子 prop
之间形成了一个单向下行绑定(父组件传向子组件),每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值,若在子组件中修改props值,Vue 代码会出现警告;
我们一般通过两种方式接收父组件向子组件传递的 prop
属性的值:
- 方法1: prop 用来传递一个初始值,子组件将其作为一个本地的 prop 数据来使用,则定义一个本地的 data 属性并将这个 prop 用作其初始值:
1 | props: ['initialCounter'], |
- 方法2:prop 以一种原始的值传入且需要进行转换,则使用这个 prop 的值来定义一个计算属性:
1 | props: ['size'], |
注意: 在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态;
2.5 Prop 验证:为组件的 prop 指定验证要求
可以为 props
中的值提供一个带有验证需求的对象,而不是一个字符串数组,例如:
1 | Vue.component('my-component', { |
prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告;
类型检查:type
可以是下列原生构造函数中的一个:String Number Boolean Array Object Date Function Symbol
; type
还可以是一个自定义的构造函数,并且通过 instanceof
来进行检查确认;
2.6 非 Prop 的特性
一个非 prop
特性是指传向一个组件,但是该组件并没有相应 prop 定义的特性;
- 替换/合并已有的特性;
- 禁用特性继承,如果你不希望组件的根元素继承特性,你可以设置在组件的选项中设置 :
1 | Vue.component('my-component', { |
3.自定义事件
3.1 事件名
事件名不会存在自动化的大小写转换,而是触发的事件名需要完全匹配
监听这个事件所用的名称(如:子组件通过发布订阅模式向父组件传递数据$emit()方式
);
推荐事件的命名方式为 kebab-case
中划线命名 方式;
3.2 自定义组件的 v-model
组件的 v-model
默认会利用名为 value
的 prop
和名为 input
的事件,但是像单选框、复选框等类型的输入控件的 value 特性用于不同的目的;model 选项
可以用来避免这样的冲突;例:
1 | Vue.component('base-checkbox', { |
1 | <base-checkbox v-model="lovingVue"></base-checkbox> |
这里的 lovingVue
的值将会传入这个名为 checked
的 prop。同时当 <base-checkbox>
触发一个 change
事件并附带一个新的值的时候,这个 lovingVue
的属性将会被更新;
3.3 将原生事件绑定到组件(有点儿难)
在一个组件的根元素上直接监听一个原生事件,使用 v-on
的 .native
修饰符,例:
1 | <base-input v-on:focus.native="onFocus"></base-input> |
当监听一个类似 <input>
的非常特定的元素时,该元素不是根元素,此时父级的 .native 监听器将静默失败,也不会报错,但是 onFocus
处理函数不会如你预期地被调用;不过Vue 提供了一个 $listeners
属性解决这个问题, $listeners
它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:
1 | { |
可以配合 v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 <input>
的你希望它也可以配合 v-model
工作的组件来说,为这些监听器创建一个类似下述 inputListeners
的计算属性通常是非常有用的:
1 | Vue.component('base-input', { |
3.4 .sync 修饰符
对prop进行双向绑定,推荐以 update:myPropName
的模式触发事件取而代之。举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:this.$emit('update:title', newTitle)
;然后父组件可以监听那个事件并根据需要更新一个本地的数据属性;
1 | <text-document |
这种模式提供一个缩写,即 .sync
修饰符:
1 | <!-- 上面代码等价于: --> |
注意:带有 .sync
修饰符的 v-bind
不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’”
是无效的)。
4. 插槽(有点儿难)
4.1 插槽内容
将 <slot>
元素作为承载分发内容的出口。
4.2 具名插槽
<slot>
元素有一个特殊的特性:name
。这个特性可以用来定义额外的插槽;
插槽用法:例如,一个假设的 <base-layout>
组件的模板如下:
1 | <div class="container"> |
- 我们可以在一个父组件的
<template>
元素上使用slot
特性:
1 | <base-layout> |
- 另一种
slot
特性的用法是直接用在一个普通的元素
上:
1 | <base-layout> |
- 上述两个示例渲染出来的 HTML 都将会是:
1
2
3
4
5
6
7
8
9
10
11
12<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
4.3 插槽的默认内容
在 <slot>
标签内部指定默认的内容;(如果父组件为这个插槽提供了内容,则默认的内容会被替换掉。)
4.4 编译作用域
父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
4.5 作用域插槽(不怎么明白)
1 | <ul> |
若希望每个独立的待办项渲染出和 todo.text
不太一样的东西。这也是作用域插槽的用武之地。为了让这个特性成为可能,你需要做的全部事情就是将待办项内容包裹在一个 <slot>
元素上,然后将所有和其上下文相关的数据传递给这个插槽:
1 | <ul> |
当使用 <todo-list>
组件的时候,可以选择为待办项定义一个不一样的 <template>
作为替代方案,并且可以通过 slot-scope
特性从子组件获取数据:
1 | <todo-list v-bind:todos="todos"> |
在 2.5.0+,slot-scope
不再限制在 <template>
元素上使用,而可以用在插槽内的任何元素或组件上。
解构 slot-scope
: 如果一个 JavaScript 表达式在一个函数定义的参数位置有效,那么这个表达式实际上就可以被 slot-scope
接受,可以在支持的环境下 (单文件组件或现代浏览器),在这些表达式中使用 ES2015 解构语法
,例如:
1 | <todo-list v-bind:todos="todos"> |
5. 动态组件和异步组件
5.1 在动态组件上使用 keep-alive
曾经在一个多标签的界面中使用 is
特性来切换不同的组件:<component v-bind:is="currentTabComponent"></component>
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。
解决方法:可以用一个 <keep-alive>
元素将其动态组件包裹起来,将那些标签的组件实例在它们第一次被创建的时候缓存下来;
1 | <!-- 失活的组件将会被缓存!--> |
5.2 异步组件(不明白)
Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
5.2.1 处理加载状态
异步组件工厂函数也可以返回一个如下格式的对象:
1 | const AsyncComponent = () => ({ |
注意:若在 Vue Router
的路由组件中使用上述语法的话,你必须使用 Vue Router 2.4.0+
版本。
6. 处理边界情况(有点儿难)
6.1 访问元素&组件
在绝大多数情况下,我们最好不要
触达另一个组件实例内部或手动操作 DOM
元素。不过也确实在一些情况下做这些事情是合适的。
- 访问根实例:在每个
new Vue
实例的子组件中,其根实例可以通过$root
属性进行访问;所有的子组件都可以将这个实例作为一个全局store
来访问或使用:this.$root.foo // 获取根组件的数据
- 访问父级组件实例:
$parent
属性可以用来从一个子组件访问父组件的实例;它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以prop
的方式传入子组件的方式。 - 访问子组件实例或子元素:可以通过
ref
特性为这个子组件赋予一个ID
引用; - 依赖注入:
provide
选项允许我们指定我们想要提供给后代组件的数据/方法;然后在任何后代组件里,我们都可以使用inject
选项来接收指定的我们想要添加在这个实例上的属性;
1 | provide: function () { |
- 通过
$on(eventName, eventHandler)
侦听一个事件 - 通过
$once(eventName, eventHandler)
一次性侦听一个事件 - 通过
$off(eventName, eventHandler)
停止侦听一个事件
一般不会用到这些,但是当你需要在一个组件实例上手动侦听事件
时,它们是派得上用场的;它们也可以用于代码组织工具。例如,你可能经常看到这种集成一个第三方库的模式:
1 | // 一次性将这个日期选择器附加到一个输入框上 |
这里有两个潜在的问题:
- 它需要在这个组件实例中保存这个
picker
,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。 - 建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。
因而应该通过一个程序化的侦听器解决这两个问题:
1 | mounted: function () { |
使用了这个策略,甚至可以让多个输入框元素同时使用不同的 Pikaday
,每个新的实例都程序化地在后期清理它自己:
1 | mounted: function () { |
注意:如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件,更多程序化侦听器的内容,请查阅实例方法 / 事件相关的 API;
6.3 循环引用
6.3.1 递归组件
组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name
选项来做这件事:
1 | name: 'unique-name-of-my-component' |
当你使用 Vue.component
全局注册一个组件时,这个全局的 ID
会自动设置为该组件的 name
选项:
1 | Vue.component('unique-name-of-my-component', { |
注意:稍有不慎,递归组件就可能导致无限循环,要确保递归调用是条件性的 (例如使用一个最终会得到 false
的 v-if
)。
1 | name: 'stack-overflow', |
6.3.2 组件之间的循环引用
若需要构建一个文件目录树,像访达或资源管理器那样的。你可能有一个 <tree-folder>
组件,模板是这样的:
1 | <p> |
还有一个 <tree-folder-contents>
组件,模板是这样的:
1 | <ul> |
若使用 Vue.component
全局注册组件的时候,可以解决这两个组件的父子组件关系;或者是经过其中一个组件而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点,在例子中,把 <tree-folder>
组件设为了那个点,那个产生悖论的子组件是 <tree-folder-contents>
组件,所以会等到生命周期钩子 beforeCreate
时去注册它:
1 | beforeCreate: function () { |
或者,在本地注册组件的时候,你可以使用 webpack
的异步 import
:
1 | components: { |
6.4 模板定义的替代品
6.4.1 内联模板
当inline-template
这个特殊的特性出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容;
1 | <my-component inline-template> |
inline-template
会让你模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template
选项或 .vue
文件里的一个 <template>
元素来定义模板;
6.4.2 X-Templates
另一个定义模板的方式是在一个 <script>
元素中,并为其带上 text/x-template
的类型,然后通过一个 id
将模板引用过去。例如:
1 | <script type="text/x-template" id="hello-world-template"> |
6.5 控制更新
Vue 的响应式系统始终知道何时进行更新;
6.5.1 强制更新
你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。
然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate
来做这件事
6.5.2 通过 v-once 创建低开销的静态组件
渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once
特性以确保这些内容只计算一次然后缓存起来,就像这样:
1 | Vue.component('terms-of-service', { |