深入组件
Provide/Inject
通常,我们需要把数据从父组件向子组件传递时,使用 prop。但对于深度嵌套的组件系统,有时候,深层子组件需要父组件的部分内容,此时,仍然使用 prop机制会非常麻烦。例如,爷爷a 要把数据传递给孙子 c1,使用 prop机制就必须 b2 定义prop,从a获得数据,c1再定义prop,从b2把那个数据再传递一下。
对于如下组件层次结构
Root
└─ TodoList
├─ TodoItem
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
如果要将 todo-list 中 item 的数目直接传递给 todo-list-statistics,按prop机制传递为:todo-list -> todo-list-footer -> todo-list-statistics 。我们使用 Provide/Inject 机制,可以直接让 todo-list 提供依赖项(它不清楚谁依赖这个数据,只是导出),然后 todo-list-statistics 注入依赖项(它不清楚依赖来自谁,只是知道可以导入),注入的依赖项就成为todo-list-statistics的数据属性(注入属性)。我们从前一篇的例子上修改
<div id="app">
<todo-list :title="title">
<template #default="{ index: i, item: todo }">
<span>++ {{ i }} ++ </span>
<span>{{ todo }}</span>
</template>
<template #other="{ date: date }">
<hr>
<p>{{ date }}</p>
</template>
</todo-list>
</div>
<script>
const app = Vue.createApp({
data() {
return {
title: '张三的下班生活'
}
}
})
app.component('todo-list', {
props: ['title'],
data() {
return {
items: ['下班', '洗手', '吃饭', '散步', '睡觉'],
date: '2021-11-11'
}
},
provide: {
user: '李四'
},
template: `
<h2>{{ title }}</h2>
<ul>
<li v-for="(item, index) in items">
<slot :item="item" :index="index"></slot>
</li>
</ul>
<slot :date="date" name="other"></slot>
<todo-list-footer></todo-list-footer>
`
})
app.component('todo-list-footer', {
template: `
<todo-list-statistics></todo-list-statistics>
`
})
app.component('todo-list-statistics', {
inject: ['user'],
created() {
console.log(`Injected property: ${this.user}`)
},
template: `记录人:{{ user }}`
})
app.mount('#app')
在上述代码中,我们让 todo-list Provide 了一个静态字符串数据user,如果我们想 Provider 组件实例的 property,例如 items 的条目数,下述代码不能达到目的
provide: {
user: '李四',
itemsCount: this.items.length // Cannot read properties of undefined (reading 'length')
},
我们必须把 provide 从静态对象转换成返回对象的函数:
provide() {
return {
user: '李四',
itemsCount: this.items.length
}
},
使用则是类似的(当做组件的数据属性)
app.component('todo-list-statistics', {
inject: ['user', 'itemsCount'],
created() {
console.log(`Injected property: ${this.user}、${this.itemsCount}`)
},
template: `共 {{ itemsCount }} 条,记录人:{{ user }}`
})
动态组件与异步组件
在动态组件上使用 keep-alive
在前面的多标签界面实例中,我们了解了动态组件,即用 is attribute 来切换不同的组件:
<component :is="currentTabComponent"></component>
默认来回在这些 tab 页切换时,会反复渲染,对性能有一定影响,因此,我们可能想保持这些组件的状态。另一想保持组件状态的原因是,默认我们在 Posts 页选择了某篇帖子阅读,中途切换到 Archive 页,然后切换回 Posts 页,就回不到刚才阅读的那篇帖子,因为每次切换新标签页,Vue都创建新的 currentTabComponent 实例。要实现上述意图,我们需要一种缓存机制:用一个 <keep-alive> 元素将动态组件包裹起来
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
下面给出简单的实例(可以试验没有 keep-alive 和 有 keep-alive 时,输入 input 的内容是否保持):
<div id="app">
<button v-for="tab in tabs" :key="tab.name"
:class="['tab-button', {active: currentTab === tab.name}]"
@click="currentTab = tab.name">
{{ tab.text }}
</button>
<keep-alive>
<component :is="currentTabComponent" class="tab"></component>
</keep-alive>
</div>
<script>
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: [
{name:'Home', text:'首页'},
{name:'Posts', text:'帖子'},
{name:'Archive', text:'归档'}
]
}
},
computed: {
currentTabComponent() {
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
app.component('tab-home', {
template: `<div class="demo-post">首页内容</div>`
})
app.component('tab-posts', {
template: `<div class="demo-post">帖子……<input type="text" /></div>`
})
app.component('tab-archive', {
template: `<div class="demo-post">(归档)</div>`
})
const vm = app.mount('#app')
</script>
异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了实现这个效果,Vue提供了 defineAsyncComponent 方法实现异步组件。
<div id="app">
<async-example></async-example>
</div>
<script>
const { createApp, defineAsyncComponent } = Vue
const app = createApp({})
const AsyncComp = defineAsyncComponent(() => new Promise(
(resolve, reject) => {
setTimeout(() => {
let data = { template: '<div>这是异步的!</div>' }
resolve(data)
}, 2000)
}
))
app.component('async-example', AsyncComp)
app.mount('#app')
</script>
上面的例子中,我们用延时模拟从服务器加载。defineAsyncComponent 函数的参数是一个返回 Promise 的工厂函数。从服务器检索组件定义后(例子中 data),应调用 Promise 的 resolve 回调 (也可以在失败时调用 reject(reason))。我们从调试插件可以了解到,对于异步组件,有一个 AsyncComponentWrapper 节点,初始它是空的,等到从服务器获得数据后,内部节点(匿名组件)Anonymous Component 就产生了。
使用编译构建工具和ES2015+语法,可以实现动态导入
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
app.component('async-component', AsyncComp)
局部注册的组件也可以动态导入
import { createApp, defineAsyncComponent } from 'vue'
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
模板引用
我们知道,可以通过组件 prop 和 组件事件 实现父级和子组件的通信(父级向子组件传递数据,传递事件回调处理),但有时我们仍然想直接访问子组件。使用 ref attribute 可以为子组件或HTML 元素指定引用ID(带有 ref 属性的模板将变成可引用对象,有点直接访问DOM节点的意思)。下面的例子我们使用模板引用特性让初始光标焦点在第二个输入框组件上
<div id="app">
<base-input ref="usernameInput"></base-input>
<base-input ref="nicknameInput"></base-input>
</div>
<script>
const { createApp } = Vue
const app = createApp({})
app.component('base-input', {
template: `<input ref="input" />`,
methods: {
focusInput() {
this.$refs.input.focus()
}
}
})
const vm = app.mount('#app')
vm.$refs.nicknameInput.focusInput()
</script>
vm实例property $refs 是一个对象,它包括了注册过 ref attribute 的所有DOM元素和组件实例,所有 ref attribute 的值对应该对象的键。$refs 只会在组件渲染完成后生效,并且它本质上就是在直接访问DOM,这和Vue的虚拟DOM精神是违背的,所以,这种机制只是一种弥补机制,应该避免在模板或计算属性中访问 $refs (通常在 mounted() 钩子等使用)
处理边界情况
控制更新
我们知道,Vue是响应式系统,一般它自己知道何时应该更新。但是,某些极端情况,可能需要强制更新(绝大部分时候发生需要强制更新是因为自己设计错误,例如组件创建之后添加 data属性),此时,可以使用组件的实例方法 $forceUpdate 来迫使组件实例重新渲染。
另一种情形则恰恰相反,可能有一个包含很多很多静态内容的组件,你可以向根元素添加 v-once 指令来确保只求值一次并缓存起来,这样可以提高渲染速度(只有在内容的确是静态且渲染速度不够理想时才考虑这种边界处理策略)。
无论强制更新或者单次渲染求值,都是应该尽量避免的,它们存在只是一种弥补机制。