深入组件 2
-
插槽
- v-slot Vue 2.6.0 新增,取代了 slot 和 slot-scope。
- 具名插槽和作用域插槽
- Vue 实现了一套内容分发的 API ,将 <slot> 元素作为承载分发内容的出口。
- 如何使用 slot ?看下面这个例子
定义一个 <navigation-link> 的组件如下:
然后使用这个组件:<a v-bind:href="url" class="nav-link" > <slot></slot> </a>
当组件渲染的时候,<slot></slot> 将会被替换为“Your slot”。<navigation-link url=""> Your slot </navigation-link>
- 插槽内可以包含任何模板代码,包括 HTML,甚至其它的组件。
<navigation-link url="/profile"> <!-- 添加一个 Font Awesome 图标 --> <span class="fa fa-user"></span> Your Profile </navigation-link>
<navigation-link url="/profile"> <!-- 添加一个图标的组件 --> <font-awesome-icon name="user"></font-awesome-icon> Your Profile </navigation-link>
- 如果 <navigation-link> 的 template 中没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。看到这,我赶紧回到前面章节测试了一下,果然,没有 <slot> 的普通组件在使用时( HTML 中),标签内包含的内容都不会被渲染出来。
- 小结:插槽,顾名思义,是预留下的接口,将来会填充东西,就像占位符一样。Vue 中的插槽还不挑插入的内容,啥都可以插入,字符串、HTML、其他组件都行。一定要在注册组件时就在 template 中包含 <slot> 表示此处将来会有内容,不然使用组件时没有插槽,你拿胶布粘上去的内容没用。:=)
-
作用域
- 插槽跟模板的其它地方一样可以访问相同的实例 property (也就是相同的“作用域”),即组件的 property?错!是父组件的 property。也就是说把带插槽的组件当做普通 HTML 标签一样使用。
<navigation-link>{{ name }}</navigation-link> // 类似于 <a>{{ name }}</a>
- 作为一条规则,请记住:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
- 怎么理解?
- 插槽跟模板的其它地方一样可以访问相同的实例 property (也就是相同的“作用域”),即组件的 property?错!是父组件的 property。也就是说把带插槽的组件当做普通 HTML 标签一样使用。
-
插槽默认内容
- 教程中称之为“后备内容”。感觉别扭0.0
- 默认内容只会在没有提供任何插槽内容的时候被渲染。
template
使用:<button type="submit"> <slot>Submit</slot> </button>
渲染结果:<submit-button></submit-button>
<button type="submit"> Submit </button>
-
- 当你在一个组件中需要多个插槽时,便要用到具名插槽了。
- <slot> 元素有一个特殊的 attribute:name。这个 attribute 可以用来定义额外的插槽。一个不带 name 的 <slot> 会带有隐含的名字“default”。
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
- 那怎么使用具名插槽?v-slot 指令登场!在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称。
<base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <p>A paragraph for the main content.</p> <p>And another one.</p> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
- 现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。任何没有包裹的内容都是 default 插槽的?即使没有按照顺序写?
- 如果你希望更明确一些,仍然可以在一个 <template> 中包裹默认插槽的内容。
<base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <template v-slot:default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
- 注意 v-slot 只能添加在 <template> 标签上 (只有一种例外情况)。
-
- 只有 <current-user> 组件可以访问到 user 而我们提供的内容是在父级渲染的。为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去:
<span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span>
- 绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
<current-user> <template v-slot:default="slotProps"> {{ slotProps.user.firstName }} </template> </current-user>
- 在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps,但你也可以使用任意你喜欢的名字。
- 注意 slotProps 是包含所有插槽 prop 的对象。
- 独占默认插槽的缩写语法。
- 前面提到 v-slot 只能添加在 <template> 标签上 (只有一种例外情况)。这种情况就是:当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上
<current-user v-slot:default="slotProps"> {{ slotProps.user.firstName }} </current-user>
- 这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽
<current-user v-slot="slotProps"> {{ slotProps.user.firstName }} </current-user>
- 只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法。
- 解构插槽 Prop
- 只有 <current-user> 组件可以访问到 user 而我们提供的内容是在父级渲染的。为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去:
-
动态组件
- 在动态组件上使用 keep-alive
- 在前面我们见过,使用 is attribute 来切换不同组件。但是每次你切换新标签的时候,Vue 都会创建一个新的组件实例。重新创建动态组件的行为有的时候是有用的,但是有的时候你会想保持这些组件的状态(希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来)。为了解决这个问题,我们可以用一个
<keep-alive>
元素将其动态组件包裹起来。<!-- 失活的组件将会被缓存!--> <keep-alive> <component v-bind:is="currentTabComponent"></component> </keep-alive>
- 注意这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。
-
- 在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式(该函数有两个参数,resolve 和 reject,类似 Promise,你需要把组件的配置对象作为 resolve的参数)定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
-
- 访问元素或组件。最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过有时只能这么做。
-
访问根实例。在每个 new Vue 实例的子组件中,其根实例可以通过 $root property 进行访问。
// Vue 根实例 new Vue({ data: { foo: 1 }, computed: { bar: function () { /* ... */ } }, methods: { baz: function () { /* ... */ } } }) // 获取根组件的数据 this.$root.foo // 写入根组件的数据 this.$root.foo = 2 // 访问根组件的计算属性 this.$root.bar // 调用根组件的方法 this.$root.baz()
- 对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 Vuex 来管理应用的状态。
-
访问父组件。和
parent property 可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。
- 在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。
-
访问子组件实例或子元素。尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过
ref
这个 attribute 为子组件赋予一个 ID 引用。
这里教程中将得奇奇怪怪的。。。举的例子只言片语,看不懂,不如给一个较完整的例子。API 中简洁的描述比较容易懂。<base-input ref="usernameInput"></base-input> this.$refs.usernameInput
- ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。
<!-- `vm.$refs.p` will be the DOM node --> <p ref="p">hello</p> <!-- `vm.$refs.child` will be the child component instance --> <child-component ref="child"></child-component>
- 关于 ref 注册时间的重要说明:因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs 也不是响应式的,因此你不应该试图用它在模板中做数据绑定。
- 依赖注入。在访问父级组件实例的时,使用 $parent property 无法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项:provide 和 inject。provide 选项允许我们指定我们想要提供给后代组件的数据/方法。然后在任何后代组件里,我们都可以使用 inject 选项来接收指定的我们想要添加在这个实例上的 property。
- 实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:祖先组件不需要知道哪些后代组件使用它提供的 property;后代组件不需要知道被注入的 property 来自哪里。
-
程序化的事件侦听器
- 注意 Vue 的事件系统不同于浏览器的 EventTarget API。尽管它们工作起来是相似的,但是
on, 和 $off 并不是 dispatchEvent、addEventListener 和 removeEventListener 的别名。
- 注意 Vue 的事件系统不同于浏览器的 EventTarget API。尽管它们工作起来是相似的,但是
-
循环引用
- 递归组件。组件是可以在它们自己的模板中调用自身的。
稍有不慎,递归组件就可能导致无限循环:
类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 false 的 v-if)。name: 'stack-overflow', template: '<div><stack-overflow></stack-overflow></div>'
-
组件之间的循环引用。两个组件 A 和 B。模块系统发现它需要 A,但是首先 A 依赖 B,但是 B 又依赖 A,但是 A 又依赖 B,如此往复。这变成了一个循环,不知道如何不经过其中一个组件而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点,在那里“A 反正是需要 B 的,但是我们不需要先解析 B。”在我们的例子中,把 <tree-folder> 组件设为了那个点。我们知道那个产生悖论的子组件是 <tree-folder-contents> 组件,所以我们会等到生命周期钩子 beforeCreate 时去注册它:
或者,在本地注册组件的时候,你可以使用 webpack 的异步 import:beforeCreate: function () { this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default }
components: { TreeFolderContents: () => import('./tree-folder-contents.vue') }
- 递归组件。组件是可以在它们自己的模板中调用自身的。
-
模板定义的替代品
- 这个翻译有点奇怪哈0.0翻译成“定义模板的其他方式”会好点?
- 内联模板。当 inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。内联模板需要定义在 Vue 所属的 DOM 元素内。
- 不过,inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。
- X-Template。在一个 <script> 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。例如:
<script type="text/x-template" id="hello-world-template"> <p>Hello hello hello</p> </script>
x-template 需要定义在 Vue 所属的 DOM 元素外。Vue.component('hello-world', { template: '#hello-world-template' })
这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。 - 这两种方式好像比较少用?
- 接触到了再回头来看看。
- 补充。今天突然又看到了这种写法(2021年4月29日)。创建一个组件,template 写在 js 中是一件难看难写(没有自动补全自动对齐,懂的都懂)的事,所有便有了将组件模板抽离的方式。
<script type="text/x-template" id="my-component">
和<template id="my-component">
这两种都是将组件模板从 JS 代码抽离到 HTML 中的写法。在以前可谓很实用了?不过今天是2021年了,已经是单文件组件(.vue )的天下了!!!
-
控制更新
- 强制更新。关于强制更新,Vue 提醒你“如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。”你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。
- 如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate 来做这件事。
- 通过 v-once 创建低开销的静态组件。不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。