vue中父组件销毁时,保留子组件

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

由此产生了一个问题:推荐内容业务组件放在组件中什么位置?

  1. 每个panel下的列表页都放这么一个业务组件?

    将导致性能浪费(推荐内容的业务组件自身会查询多个接口),并且切换tab时,由于新建业务组件将导致页面闪烁(第一次切换到该tab);

  2. 不放在panel下的列表组件中?

    那么,业务组件不能随每个列表滑动而滑动。

又因为业务需要,切换tab时,必须重新查询产品列表,这里使用了多tab单panel。(并且由于panel上会有很多状态:各类组合筛选条件、排序等,使用单panel无需进行状态初始化)

每次切换tab时,重建新的panel(panel绑定了key属性,值为tab的名称),导致panel中的推荐内容业务组件也每次重建创建,出现页面闪烁。

思考出路

有没有一种方式,当panel组件重建(销毁)的时候,推荐内容的业务组件没有销毁,可以在重建的panel组件中继续使用。

使用keep-alive可行吗?

  1. 组件的销毁机制是:当父组件被销毁时,会先销毁子组件。
  2. 有一个例外,就是被<keep-alive>保护的组件(_vnode.data.keepAlive = true)不会被销毁。
  3. <keep-alive>组件自身销毁时,所有被其缓存的组件也一并被销毁。
  4. <keep-alive>组件只缓存它包裹的第一个子组件,不能直接缓存子组件下的子孙组件。
  5. 综合,<keep-alive>组件若是包裹整个panel组件,则相当于缓存了每个tab对应的panel组件(<keep-alive>是根据子组件的key值来缓存组件的),这与创建多panel效果是一致的。若是用<keep-alive>在panel组件内部包裹业务组件,则panel销毁时,<keep-alive>也会被销毁,导致业务组件被销毁。所以<keep-alive>无法满足。

使用Modal可行吗?

Modal组件(或传送门portal类似的组件)会与组件所在的环境组件创建父子联系,当父组件销毁时,Modal组件也会被销毁。(dom结构上与环境组件的dom没有父子关系,可以出现在任意位置,所以是传送门)。

自己造轮子

既然决定自己写这么一个组件,来实现父组件销毁而"子组件"不销毁。那么必须要做一些思考:

  1. 是否能够实现
  2. 先想好组件的名称
  3. 组件要做到方便好用
  4. 控制什么时机销毁(父组件销毁时,没有自动销毁)

能否实现

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

  1. 当前的实现方法不是特别灵活,不支持给需要顽固化的组件传入属性,或绑定事件(后期尝试一下通过propsData)。
相关推荐
wordbaby2 分钟前
TanStack Router 基于文件的路由
前端
wordbaby7 分钟前
TanStack Router 路由概念
前端
wordbaby9 分钟前
TanStack Router 路由匹配
前端
cc蒲公英10 分钟前
vue nextTick和setTimeout区别
前端·javascript·vue.js
程序员刘禹锡15 分钟前
Html中常用的块标签!!!12.16日
前端·html
我血条子呢25 分钟前
【CSS】类似渐变色弯曲border
前端·css
DanyHope26 分钟前
LeetCode 两数之和:从 O (n²) 到 O (n),空间换时间的经典实践
前端·javascript·算法·leetcode·职场和发展
hgz071027 分钟前
企业级多项目部署与Tomcat运维实战
前端·firefox
用户18878710698427 分钟前
基于vant3的搜索选择组件
前端
zhoumeina9927 分钟前
懒加载图片
前端·javascript·vue.js