组件(Component)是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素, Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。
之前说过,我们可以通过以下方式创建一个 Vue 实例:
new Vue({ el: '#some-element', // 选项 })
要注册一个全局组件,你可以使用 Vue.component(tagName, options)。 例如:
Vue.component('my-component', { // 选项 })
对于自定义标签名,Vue.js 不强制要求遵循 [W3C规则](https://www.w3.org/TR/custom-elements/#concepts) (小写,并且包含一个短杠),尽管遵循这个规则比较好。
组件在注册之后,便可以在父实例的模块中以自定义元素 的形式使用。要确保在初始化根实例 之前 注册了组件:
// 注册 Vue.component('my-component', { template: 'A custom component!' }) // 创建根实例 new Vue({ el: '#example' })
渲染为:
A custom component!
不必在全局注册每个组件。通过使用组件实例选项注册,可以使组件仅在另一个实例/组件的作用域中可用:
var Child = { template: 'A custom component!' } new Vue({ // https://www.mshxw.com/file/tupian/20210206/.. components: { // 将只在父模板可用 'my-component': Child } })
这种封装也适用于其它可注册的 Vue 功能,如指令。
当使用 DOM 作为模版时(例如,将 el 选项挂载到一个已存在的元素上), 你会受到 HTML 的一些限制,因为 Vue 只有在浏览器解析和标准化 HTML 后才能获取模版内容。尤其像这些元素
在自定义组件中使用这些受限制的元素时会导致一些问题,例如:
自定义组件 被认为是无效的内容,因此在渲染的时候会导致错误。变通的方案是使用特殊的 is 属性:
应当注意,如果您使用来自以下来源之一的字符串模板,这些限制将不适用:
由于这三个组件共享了同一个 data , 因此增加一个 counter 会影响所有组件!我们可以通过为每个组件返回新的 data 对象来解决这个问题: data: function () { return { counter: 0 } } 现在每个 counter 都有它自己内部的状态了: 构成组件 组件意味着协同工作,通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B 。它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。 在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。看看它们是怎么工作的。 Prop 使用 Prop 传递数据 组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。可以使用 props 把数据传给子组件。 prop 是父组件用来传递数据的一个自定义属性。子组件需要显式地用 props 选项声明 “prop”: Vue.component('child', { // 声明 props props: ['message'], // 就像 data 一样,prop 可以用在模板内 // 同样也可以在 vm 实例中像 “this.message” 这样使用 template: '{{ message }}' }) 然后向它传入一个普通字符串: 结果: camelCase vs. kebab-case HTML 特性不区分大小写。当使用非字符串模版时,prop的名字形式会从 camelCase 转为 kebab-case(短横线隔开): Vue.component('child', { // camelCase in Javascript props: ['myMessage'], template: '{{ myMessage }}' }) 再次说明,如果你使用字符串模版,不用在意这些限制。 动态 Prop 类似于用 v-bind 绑定 HTML 特性到一个表达式,也可以用 v-bind 动态绑定 props 的值到父组件的数据中。每当父组件的数据变化时,该变化也会传导给子组件: 使用 v-bind 的缩写语法通常更简单: 结果: 字面量语法 vs 动态语法 初学者常犯的一个错误是使用字面量语法传递数值: 因为它是一个字面 prop ,它的值以字符串 "1" 而不是以实际的数字传下去。如果想传递一个实际的 Javascript 数字,需要使用 v-bind ,从而让它的值被当作 Javascript 表达式计算: 单向数据流 prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。 另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。 通常有两种改变 prop 的情况: prop 作为初始值传入,子组件之后只是将它的初始值作为本地数据的初始值使用; prop 作为需要被转变的原始值传入。 更确切的说这两种情况是: 定义一个局部 data 属性,并将 prop 的初始值作为局部数据的初始值。 props: ['initialCounter'], data: function () { return { counter: this.initialCounter } } 定义一个 computed 属性,此属性从 prop 的值计算得出。 props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } } 注意在 Javascript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它**会影响**父组件的状态。 Prop 验证 组件可以为 props 指定验证要求。如果未指定验证要求,Vue 会发出警告。当组件给其他人使用时这很有用。 prop 是一个对象而不是字符串数组时,它包含验证要求: Vue.component('example', { props: { // 基础类型检测 (`null` 意思是任何类型都可以) propA: Number, // 多种类型 propB: [String, Number], // 必传且是字符串 propC: { type: String, required: true }, // 数字,有默认值 propD: { type: Number, default: 100 }, // 数组/对象的默认值应当由一个工厂函数返回 propE: { type: Object, default: function () { return { message: 'hello' } } }, // 自定义验证函数 propF: { validator: function (value) { return value > 10 } } } }) type 可以是下面原生构造器:
由于这三个组件共享了同一个 data , 因此增加一个 counter 会影响所有组件!我们可以通过为每个组件返回新的 data 对象来解决这个问题:
data: function () { return { counter: 0 } }
现在每个 counter 都有它自己内部的状态了:
组件意味着协同工作,通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B 。它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。
在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。看看它们是怎么工作的。
组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。可以使用 props 把数据传给子组件。
prop 是父组件用来传递数据的一个自定义属性。子组件需要显式地用 props 选项声明 “prop”:
Vue.component('child', { // 声明 props props: ['message'], // 就像 data 一样,prop 可以用在模板内 // 同样也可以在 vm 实例中像 “this.message” 这样使用 template: '{{ message }}' })
然后向它传入一个普通字符串:
结果:
HTML 特性不区分大小写。当使用非字符串模版时,prop的名字形式会从 camelCase 转为 kebab-case(短横线隔开):
Vue.component('child', { // camelCase in Javascript props: ['myMessage'], template: '{{ myMessage }}' })
再次说明,如果你使用字符串模版,不用在意这些限制。
类似于用 v-bind 绑定 HTML 特性到一个表达式,也可以用 v-bind 动态绑定 props 的值到父组件的数据中。每当父组件的数据变化时,该变化也会传导给子组件:
使用 v-bind 的缩写语法通常更简单:
初学者常犯的一个错误是使用字面量语法传递数值:
因为它是一个字面 prop ,它的值以字符串 "1" 而不是以实际的数字传下去。如果想传递一个实际的 Javascript 数字,需要使用 v-bind ,从而让它的值被当作 Javascript 表达式计算:
prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。
通常有两种改变 prop 的情况:
prop 作为初始值传入,子组件之后只是将它的初始值作为本地数据的初始值使用;
prop 作为需要被转变的原始值传入。
更确切的说这两种情况是:
定义一个局部 data 属性,并将 prop 的初始值作为局部数据的初始值。 props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
定义一个局部 data 属性,并将 prop 的初始值作为局部数据的初始值。
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
定义一个 computed 属性,此属性从 prop 的值计算得出。 props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
定义一个 computed 属性,此属性从 prop 的值计算得出。
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
注意在 Javascript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它**会影响**父组件的状态。
组件可以为 props 指定验证要求。如果未指定验证要求,Vue 会发出警告。当组件给其他人使用时这很有用。
prop 是一个对象而不是字符串数组时,它包含验证要求:
Vue.component('example', { props: { // 基础类型检测 (`null` 意思是任何类型都可以) propA: Number, // 多种类型 propB: [String, Number], // 必传且是字符串 propC: { type: String, required: true }, // 数字,有默认值 propD: { type: Number, default: 100 }, // 数组/对象的默认值应当由一个工厂函数返回 propE: { type: Object, default: function () { return { message: 'hello' } } }, // 自定义验证函数 propF: { validator: function (value) { return value > 10 } } } })
type 可以是下面原生构造器:
String
Number
Boolean
Function
Object
Array
type 也可以是一个自定义构造器,使用 instanceof 检测。
当 prop 验证失败了, Vue 将拒绝在子组件上设置此值,如果使用的是开发版本会抛出一条警告。
我们知道,父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,应该怎样做?那就是自定义事件!
每个 Vue 实例都实现了事件接口(Events interface),即:
使用 $on(eventName) 监听事件
使用 $emit(eventName) 触发事件
Vue的事件系统分离自浏览器的[EventTarget API](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)。尽管它们的运行类似,但是`$on` 和 `$emit` __不是__`addEventListener` 和 `dispatchEvent` 的别名。
另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。
下面是一个例子:
{{ total }}
Vue.component('button-counter', { template: '{{ counter }}', data: function () { return { counter: 0 } }, methods: { increment: function () { this.counter += 1 this.$emit('increment') } }, }) new Vue({ el: '#counter-event-example', data: { total: 0 }, methods: { incrementTotal: function () { this.total += 1 } } })
在本例中,子组件已经和它外部完全解耦了。它所做的只是触发一个父组件关心的内部事件。
有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 .native 修饰 v-on 。例如:
自定义事件也可以用来创建自定义的表单输入组件,使用 v-model 来进行数据双向绑定。牢记:
仅仅是一个语法糖:
所以在组件中使用时,它相当于下面的简写:
所以要让组件的 v-model 生效,它必须:
接受一个 value 属性
在有新的 value 时触发 input 事件
一个非常简单的货币输入:
Vue.component('currency-input', { template: ' $ ', props: ['value'], methods: { // 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制 updatevalue: function (value) { var formattedValue = value // 删除两侧的空格符 .trim() // 保留 2 小数位 .slice(0, value.indexOf('.') + 3) // 如果值不统一,手动覆盖以保持一致 if (formattedValue !== value) { this.$refs.input.value = formattedValue } // 通过 input 事件发出数值 this.$emit('input', Number(formattedValue)) } } })
上面的实现方式太过理想化了。 比如,用户甚至可以输入多个小数点或句号 - 哦哦!因此我们需要一个更有意义的例子,下面是一个更加完善的货币过滤器:
这个接口不仅仅可以用来连接组件内部的表单输入,也很容易集成你自己创造的输入类型。想象一下:
有时候非父子关系的组件也需要通信。在简单的场景下,使用一个空的 Vue 实例作为中央事件总线:
var bus = new Vue()
// 触发组件 A 中的事件 bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件 bus.$on('id-selected', function (id) { // https://www.mshxw.com/file/tupian/20210206/.. })
在更多复杂的情况下,你应该考虑使用专门的 状态管理模式.
在使用组件时,常常要像这样组合它们:
注意两点:
组件不知道它的挂载点会有什么内容。挂载点的内容是由的父组件决定的。
组件很可能有它自己的模版。
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发 (或 "transclusion" 如果你熟悉 Angular)。Vue.js 实现了一个内容分发 API ,参照了当前 Web 组件规范草案,使用特殊的 元素作为原始内容的插槽。
在深入内容分发 API 之前,我们先明确内容的编译作用域。假定模板为:
{{ message }}
message 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:
父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。
一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:
假定 someChildProperty 是子组件的属性,上例不会如预期那样工作。父组件模板不应该知道子组件的状态。
如果要绑定子组件内的指令到一个组件的根节点,应当在它的模板内这么做:
Vue.component('child-component', { // 有效,因为是在正确的作用域内 template: 'Child', data: function () { return { someChildProperty: true } } })
类似地,分发内容是在父组件作用域内编译。
除非子组件模板包含至少一个 插口,否则父组件的内容将会被丢弃。当子组件模板只有一个没有属性的 slot 时,父组件整个内容片段将插入到 slot 所在的 DOM 位置,并替换掉 slot 标签本身。
最初在 标签中的任何内容都被视为备用内容。备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。
假定 my-component 组件有下面模板:
我是子组件的标题 只有在没有要分发的内容时才会显示。
父组件模版:
我是父组件的标题 这是一些初始内容 这是更多的初始内容
这是一些初始内容
这是更多的初始内容
渲染结果:
我是父组件的标题 我是子组件的标题 这是一些初始内容 这是更多的初始内容
元素可以用一个特殊的属性 name 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。
仍然可以有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot ,这些找不到匹配的内容片段将被抛弃。
例如,假定我们有一个 app-layout 组件,它的模板为:
这里可能是一个页面标题 主要内容的一个段落。 另一个主要段落。 这里有一些联系信息
主要内容的一个段落。
另一个主要段落。
这里有一些联系信息
渲染结果为:
这里可能是一个页面标题 主要内容的一个段落。 另一个主要段落。
在组合组件时,内容分发 API 是非常有用的机制。
2.1.0 新增
作用域插槽是一种特殊类型的插槽,用作使用一个(能够传递数据到)可重用模板替换已渲染元素。
在子组件中,只需将数据传递到插槽,就像你将 prop 传递给组件一样:
在父级中,具有特殊属性 scope 的 元素,表示它是作用域插槽的模板。scope 的值对应一个临时变量名,此变量接收从子组件中传递的 prop 对象:
hello from parent {{ props.text }}
如果我们渲染以上结果,得到的输出会是:
hello from parent hello from child
作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项:
{{ props.text }}
列表组件的模板:
多个组件可以使用同一个挂载点,然后动态地在它们之间切换。使用保留的 元素,动态地绑定到它的 is 特性:
var vm = new Vue({ el: '#example', data: { currentView: 'home' }, components: { home: { }, posts: { }, archive: { } } })
也可以直接绑定到组件对象上:
var Home = { template: 'Welcome home!' } var vm = new Vue({ el: '#example', data: { currentView: Home } })
Welcome home!
如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数:
在API 参考查看更多 的细节。
在编写组件时,记住是否要复用组件有好处。一次性组件跟其它组件紧密耦合没关系,但是可复用组件应当定义一个清晰的公开接口。
Vue 组件的 API 来自三部分 - props, events 和 slots :
Props 允许外部环境传递数据给组件
Events 允许组件触发外部环境的副作用
Slots 允许外部环境将额外的内容组合在组件中。
使用 v-bind 和 v-on 的简写语法,模板的缩进清楚且简洁:
Hello!
尽管有 props 和 events ,但是有时仍然需要在 Javascript 中直接访问子组件。为此可以使用 ref 为子组件指定一个索引 ID 。例如:
var parent = new Vue({ el: '#parent' }) // 访问子组件 var child = parent.$refs.profile
当 ref 和 v-for 一起使用时, ref 是一个数组或对象,包含相应的子组件。
`$refs` 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案——应当避免在模版或计算属性中使用 `$refs` 。
在大型应用中,我们可能需要将应用拆分为多个小模块,按需从服务器下载。为了让事情更简单, Vue.js 允许将组件定义为一个工厂函数,动态地解析组件的定义。Vue.js 只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。例如:
Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // Pass the component definition to the resolve callback resolve({ template: 'I am async!' }) }, 1000) })
工厂函数接收一个 resolve 回调,在收到从服务器下载的组件定义时调用。也可以调用 reject(reason) 指示加载失败。这里 setTimeout 只是为了演示。怎么获取组件完全由你决定。推荐配合使用 :Webpack 的代码分割功能:
Vue.component('async-webpack-example', function (resolve) { // 这个特殊的 require 语法告诉 webpack // 自动将编译后的代码分割成不同的块, // 这些块将通过 Ajax 请求自动下载。 require(['./my-async-component'], resolve) })
你可以使用 Webpack 2 + ES2015 的语法返回一个 Promise resolve 函数:
Vue.component( 'async-webpack-example', () => System.import('./my-async-component') )
如果你是 Browserify 用户,可能就无法使用异步组件了,它的作者已经[表明](https://github.com/substack/node-browserify/issues/58#issuecomment-21978224) Browserify 是不支持异步加载的。如果这个功能对你很重要,请使用 Webpack。
当注册组件(或者 props)时,可以使用 kebab-case ,camelCase ,或 TitleCase 。Vue 不关心这个。
// 在组件定义中 components: { // 使用 kebab-case 形式注册 'kebab-cased-component': { }, // register using camelCase 'camelCasedComponent': { }, // register using TitleCase 'TitleCasedComponent': { } }
在 HTML 模版中,请使用 kebab-case 形式:
当使用字符串模式时,可以不受 HTML 的 case-insensitive 限制。这意味实际上在模版中,你可以使用 camelCase 、 TitleCase 或者 kebab-case 来引用:
如果组件未经 slot 元素传递内容,你甚至可以在组件名后使用 / 使其自闭合:
当然,这只在字符串模版中有效。因为自闭的自定义元素是无效的 HTML ,浏览器原生的解析器也无法识别它。
组件在它的模板内可以递归地调用自己,不过,只有当它有 name 选项时才可以:
name: 'unique-name-of-my-component'
当你利用Vue.component全局注册了一个组件, 全局的ID作为组件的 name 选项,被自动设置.
Vue.component('unique-name-of-my-component', { // https://www.mshxw.com/file/tupian/20210206/.. })
如果你不谨慎, 递归组件可能导致死循环:
name: 'stack-overflow', template: ''
上面组件会导致一个错误 “max stack size exceeded” ,所以要确保递归调用有终止条件 (比如递归调用时使用 v-if 并让他最终返回 false )。
Let's say you're building a file directory tree, like in Finder or File Explorer. You might have a tree-folder component with this template:
{{ folder.name }}
Then a tree-folder-contents component with this template:
When you look closely, you'll see that these components will actually be each other's descendent and ancestor in the render tree - a paradox! When registering components globally with Vue.component, this paradox is resolved for you automatically. If that's you, you can stop reading here.
However, if you're requiring/importing components using a module system, e.g. via Webpack or Browserify, you'll get an error:
Failed to mount component: template or render function not defined.
To explain what's happening, I'll call our components A and B. The module system sees that it needs A, but first A needs B, but B needs A, but A needs B, etc, etc. It's stuck in a loop, not knowing how to fully resolve either component without first resolving the other. To fix this, we need to give the module system a point at which it can say, "A needs B eventually, but there's no need to resolve B first."
In our case, I'll make that point the tree-folder component. We know the child that creates the paradox is the tree-folder-contents component, so we'll wait until the beforeCreate lifecycle hook to register it:
beforeCreate: function () { this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue') }
Problem solved!
如果子组件有 inline-template 特性,组件将把它的内容当作它的模板,而不是把它当作分发内容。这让模板更灵活。
These are compiled as the component's own template. Not parent's transclusion content.
These are compiled as the component's own template.
Not parent's transclusion content.
但是 inline-template 让模板的作用域难以理解。最佳实践是使用 template 选项在组件内定义模板或者在 .vue 文件中使用 template 元素。
另一种定义模版的方式是在 Javascript 标签里使用 text/x-template 类型,并且指定一个id。例如:
Vue.component('hello-world', { template: '#hello-world-template' })
这在有很多模版或者小的应用中有用,否则应该避免使用,因为它将模版和组件的其他定义隔离了。
尽管在 Vue 中渲染 HTML 很快,不过当组件中包含大量静态内容时,可以考虑使用 v-once 将渲染结果缓存起来,就像这样:
Vue.component('terms-of-service', { template: ' Terms of Service https://www.mshxw.com/file/tupian/20210206/.. a lot of static content https://www.mshxw.com/file/tupian/20210206/.. ' })
原文:http://vuejs.org/guide/components.html
上一篇 Vue 表单控件绑定 · Vue.js教程
下一篇 ES6 前言 - ES6文档
版权所有 (c)2021-2022 MSHXW.COM
ICP备案号:晋ICP备2021003244-6号