刚结束了这样一个需求:使用tab/panel组件来展示各种类别的产品列表,每个产品列表的顶部是相同的推荐内容业务组件,需要随着列表页滑动而滑动。

由此产生了一个问题:推荐内容业务组件放在组件中什么位置?
-
每个panel下的列表页都放这么一个业务组件?
将导致性能浪费(推荐内容的业务组件自身会查询多个接口),并且切换tab时,由于新建业务组件将导致页面闪烁(第一次切换到该tab);
-
不放在panel下的列表组件中?
那么,业务组件不能随每个列表滑动而滑动。
又因为业务需要,切换tab时,必须重新查询产品列表,这里使用了多tab单panel。(并且由于panel上会有很多状态:各类组合筛选条件、排序等,使用单panel无需进行状态初始化)
每次切换tab时,重建新的panel(panel绑定了key属性,值为tab的名称),导致panel中的推荐内容业务组件也每次重建创建,出现页面闪烁。
思考出路
有没有一种方式,当panel组件重建(销毁)的时候,推荐内容的业务组件没有销毁,可以在重建的panel组件中继续使用。
使用keep-alive可行吗?
- 组件的销毁机制是:当父组件被销毁时,会先销毁子组件。
- 有一个例外,就是被
<keep-alive>
保护的组件(_vnode.data.keepAlive = true
)不会被销毁。 <keep-alive>
组件自身销毁时,所有被其缓存的组件也一并被销毁。<keep-alive>
组件只缓存它包裹的第一个子组件,不能直接缓存子组件下的子孙组件。- 综合,
<keep-alive>
组件若是包裹整个panel组件,则相当于缓存了每个tab对应的panel组件(<keep-alive>
是根据子组件的key值来缓存组件的),这与创建多panel效果是一致的。若是用<keep-alive>
在panel组件内部包裹业务组件,则panel销毁时,<keep-alive>
也会被销毁,导致业务组件被销毁。所以<keep-alive>
无法满足。
使用Modal可行吗?
Modal组件(或传送门portal类似的组件)会与组件所在的环境组件创建父子联系,当父组件销毁时,Modal组件也会被销毁。(dom结构上与环境组件的dom没有父子关系,可以出现在任意位置,所以是传送门)。
自己造轮子
既然决定自己写这么一个组件,来实现父组件销毁而"子组件"不销毁。那么必须要做一些思考:
- 是否能够实现
- 先想好组件的名称
- 组件要做到方便好用
- 控制什么时机销毁(父组件销毁时,没有自动销毁)
能否实现
vue的机制就是父组件销毁前先销毁子组件(除被keep-alive的组件),这个无法绕过去。好在我们只需要dom结构上的父子关系,而非组件结构上的,所以是可行的。
这个组件必须是通过new VueComponent()
的形式手动创建的组件实例,这个的组件实例是脱离原组件环境的,不会自动销毁。
组件名称
最怕起名了!!!父组件销毁时,子组件
还能存活,暂且称它为小强吧,所以取名stubborn
(顽固)。
设计使用方式
首先需要在合适的位置去创建这个推荐内容的业务组件。可以放在与panel平级(或上级)的位置。
xml
<stubborn component="MyComponent" name="myComp" />
- component - 指定需要被顽固化的组件
- name - 给这个组件实例取个名字(可能一个会出现多个
小强
组件)
在panel中使用:
xml
<stubborn show="myComp" />
- show - 指定要展示的
小强
组件的名称
什么时机销毁<stubborn>
顽固化
的组件
xml
<stubborn component="MyComponent" name="myComp" />
在哪个组件中创建了顽固化
的组件,就由当前组件去负责销毁。
代码实现
jsx
import Vue from 'vue';
// 存放顽固化的组件
const components = {};
const nameToComponent = {};
export default {
props: {
name: String,
component: String,
show: String
},
data() {
return {
// 是否需要销毁
needPrune: false
};
},
render() {
const { name, component: compName, show } = this;
// panel中使用
if (show) {
// 仅返回一个标志位,用来插入顽固化组件的dom节点
return <div />;
}
// 不返回内容(vnode),通过组件构造方法创建组件实例
if (name && compName) {
// 没有创建过该组件的构造方法
if (!components[compName]) {
// 取到组件的options
const options = this.$vnode.context.$options.components[compName];
// 创建组件的构造方法
const ctor = Vue.extend(options);
// 一个改造方法,可以创建多个要顽固化的组件
components[compName] = { ctor, instances: {} };
}
const { ctor, instances } = components[compName];
// 指定名称的实例不存在,需要创建
if (!instances[name]) {
instances[name] = new ctor();
// 实例名称 -> 组件名: myComp -> MyComponent
nameToComponent[name] = compName;
// 在当前组件创建,则在当前组件销毁时销毁顽固化组件实例
this.needPrune = true;
}
}
},
mounted() {
// 负责展示的stubborn组件
const { show } = this;
if (!show) return;
const compName = nameToComponent[show];
const { instances } = components[compName];
const inst = instances[show];
// this.$el就是render中返回的标志位div
const { $el } = this;
// 首次挂载
if (!inst._isMounted) {
inst.$mount($el);
} else {
// 组件已挂载过,将挂载的dom移动过来展示
$el.parentNode.insertBefore(inst.$el, $el);
// 删除标志位
$el.remove();
}
},
beforeDestroy() {
// 销毁顽固化组件实例
if (this.needPrune) {
const compName = nameToComponent[this.name];
const { instances } = components[compName];
const inst = instances[this.name];
// 销毁组件实例
inst.$destroy();
// 手动删除dom节点
inst.$el.remove();
// 删除缓存
delete instances[this.name];
delete nameToComponent[this.name];
if (Object.keys(instances).length === 0) {
delete components[compName];
}
}
}
};
由于需求紧急,还有很多实现不到位的地址,欢迎大家给出意见做进一步优化。
TODO
- 当前的实现方法不是特别灵活,不支持给需要
顽固化
的组件传入属性,或绑定事件(后期尝试一下通过propsData)。