本章概要
- 递归组件
- 异步更新队列
- Teleport
10.11.2 递归组件
组件可以在自己的模板中递归调用自身,但这需要使用 name 选项为组件指定一个内部调用的名称。
当调用 Vue.createApp({}).component({})全局注册组件时,这个全局的 ID 会自动设置为该组件的name选项。
递归组件和程序语言中的递归函数调用一样,都需要有一个条件结束递归,否则就会导致无限循环。
例如,可以通过 v-if 指令(表达式计算为假时)结束递归。
以下是一个分类树状显示例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app">
<category-component :list="categories"></category-component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const CategoryComponent = {
name: 'catComp',
props: {
list: {
type: Array
}
},
template: `
<ul>
<!-- 如果list为空,表示没有子分类了,结束递归 -->
<template v-if="list">
<li v-for="cat in list">
{{cat.name}}
<catComp :list="cat.children"/>
</li>
</template>
</ul>
`
}
const app = Vue.createApp({
data() {
return {
categories: [
{
name: '程序设计',
children: [
{
name: 'Java',
children: [
{ name: 'Java SE' },
{ name: 'Java EE' }
]
},
{
name: 'C++'
}
]
},
{
name: '前端框架',
children: [
{ name: 'Vue.js' },
{ name: 'React' }
]
}]
}
},
components: {
CategoryComponent
}
}).mount('#app');
</script>
</body>
</html>
渲染结果如下:
10.11.3 异步更新队列
代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app">
<my-component></my-component>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({});
app.component('my-component', {
data() {
return {
message: '无限恐怖'
}
},
methods: {
change() {
this.message = '无限曙光';
console.log(this.$refs.msg.textContent);
}
},
template: `
<div>
<p ref="msg">{{ message }}</p>
<button @click="change">修改内容</button>
</div>`
})
app.mount('#app');
</script>
</body>
</html>
代码很简单,当点击“修改内容”按钮时,修改组件 message 数据属性的值,然后在 Console 窗口中输出组件模板中 p 元素的文本内容。
按理说,p 元素的内容就是 message 属性的值,修改了 message 属性的值,在 change() 方法中理应输出修改后的值,但实际上输出的是“无限恐怖”。
这是因为 Vue 在数据变化需要更新 DOM 时并不是同步执行,而是异步执行的。每当侦听到数据更改时,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个观察者被多次触发,只会将其放入队列中一次。Vue在缓冲时会去除重复数据,这样可以避免不必要的计算 和 DOM 操作。
然后,在下一个时间循环tick中,Vue 刷新队列并执行实际的工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate ,如果执行环境不支持,则会采用 setTimeout(fn,0)代替。
对于本例,当在change() 方法中修改 message 属性值的时候,该组件不会立即重新渲染。当队列刷新时,组件会在下一个 tick 中更新。多数情况下,不需要关心这个过程,但是如果想在数据更改后立即访问更新后的 DOM ,这时就需要用到 Vue.nextTick(callback) 方法,传递给 Vue.nextTick() 方法的回调函数会在 DOM 更新后完成被调用。
修改上述代码,如下:
change() {
this.message = '无限曙光';
Vue.nextTick(() => console.log(this.$refs.msg.textContent))
}
使用浏览器在此访问页面,单击“修改内容”按钮,在 Console 窗口中的输出为 “无限曙光”。
除了使用全局的 Vue.nextTick() 方法外,在组件内部还可以使用实例的 nextTick() 方法,这样在回调函数中的 this 会自动绑定到当前组件实例上,而不是像上面的代码需要使用箭头函数来绑定 this 到组件实例。
change() {
this.message = '无限曙光';
this.$nextTick(function () {
console.log(this.$refs.msg.textContent);
})
}
10.11.4 Teleport
Vue 可以通过将 UI 和相关行为封装到组件中构建 UI ,组件之间可以嵌套,从而构成一个 UI 数。
然而,有事组件模板的一部分在逻辑上属于该组件,但从技术角度来看,应该将模板的这一部分移到 DOM 中的其它地方,位于 Vue 应用程序实例之外。
一个常见的场景是创建一个包含全屏模态的组件。在大多数情况下,模态的逻辑都存在于组件中的,但是我们会发现,模态的定位很难通过 CSS 来解决,我们不得不考虑对组件进行拆分。
Vue 3.0 官网给出了一个例子,有如下的 HTML 结构:
<body>
<div id="app" style="position: relative;">
<h3>Tooltips with Vue 3 Teleport</h3>
<div>
<modal-button></modal-button>
</div>
</div>
</body>
modal-button 组件在嵌套很深的 div 元素中渲染。modal-button 组件代码如下:
const app = Vue.createApp({});
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<div v-if="modalOpen" class="modal">
<div>
I am a modal !
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
`,
data() {
return {
modalOpen: false
}
}
})
运行结果如下:
modal-button 组件有一个 button 元素触发模态的打开,以及一个具有 .modal 样式类的 div 元素,它包含模态的内容和一个用于自我关闭的按钮。
.modal 样式类使用了一个样式表属性“position:absolute”,当 modal-button 组件在上面的 HTML 结构中渲染时,会发现由于模态在嵌套很深的 div 中渲染,样式属性 position:absolute 将相对于父级 div 元素应用。为了解决这个问题,Vue 3.0 给出了一个内置组件 teleport ,该组件允许控制在 DOM 中的哪个节点下渲染 HTML 片段。
teleport 组件有两个 prop ,如下:
- to:字符串类型,必须的 prop 。其值必须是有效的查询选择器或 HTML 的元素名(如果在浏览器的环境中使用)。teleport 组件的内容将被移动到指定的目标元素中。
- disabled:布尔类型,可选的 prop 。disabled 可以用于禁用 teleport 组件的功能,这意味着它的插槽内容将不会被移动到任何位置,而是在周围父组件中指定 teleport 的地方渲染。
修改 modal-button 组件的代码,使用 teleport 来告诉 Vue “将这个 HTML 传送到 body 标签下”。代码所示如下:
const app = Vue.createApp({});
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
现在,当单机“Open full screen modal! (With teleport!)”按钮,Vue 会正确地将模态的内容在body标签下渲染。运行结果如下:
如果 teleport 的内容中包含了 Vue 组件,那么该组件在逻辑上仍是 teleport 父组件的子组件。代码如下:
const app = Vue.createApp({
template: `
<h1>Root instance</h1>
<parent-component></parent-component>
`
});
app.component('parent-component', {
template: `
<h2>This is a parent component</h2>
<teleport to='#endofbody'>
<child-component name="John"/>
</teleport>
`
})
app.component('child-component', {
props:['name'],
template: `
<div>hello,{{ name }}</div>
`
})
app.mount('#app');
不管 child-component 组件在什么位置渲染,它仍是 parent-component 组件的子组件,并且从父组件接收 name prop 。这意味着来自父组件的注入将按预期工作,并且子组件将嵌套在 Vue Devtools 中父组件之下,而不是放在实际内容移动到的位置。
一个常见的用例场景是一个可重用的 Modal 组件,其中可能同时有多个活动实例。对于这种情况,多个 teleport 组件可以将他们的内容挂载到同一个目标元素下。挂载顺序将是一个简单的追加,代码如下:
<teleport to="#modals">
<div>A</div>
</teleport>
<teleport to="modals">
<div>B</div>
</teleport>
<!-- 结果 -->
<div id="modals">
<div>A</div>
<div>B</div>
</div>