$emit有时也并无那么好用

背景

在项目中,自定义了一个组件,在点击子组件时,触发选中事件,并经过$emit,将子组件的数据传递给父组件,另外有一个全选按钮来触发子组件的所有选中。原本按照设想,子组件的事情交给子组件处理,在修改子组件的状态时经过$emit来进行数据交互,可是在数据量超出必定程度时,$emit很影响性能。html

其实这就是在组件开发中,如何处理onSelect以及selectionChange性能优化

组件Demo

下面几段代码是父子组件的简单定义,在子组件中经过监听data.isSelect的变化,来触发选中事件,设想是在使用组件的过程当中,依旧能够经过设置data.isSelect字段,来更新组件状态,以及触发选中。dom

父组件主要是监听子组件的事件,组装selection,而且将子组件的事件继续向外传递。性能

父组件template测试

<template>
  <div>
    <button v-model="checkedAll" @click="onSelectAll">全选</button>
    <children v-for="item of children" @onSelect="onSelect" :data="item" ></children>
  </div>
</template>
复制代码

父组件script大数据

export default {
  data() {
    return {
      checkedAll: false,
      children: [],
      selection: [],
    };
  },
  methods: {
    onSelectAll() {
      this.children.forEach((child) => {
        child.isSelect = true;
      });
    },
    onSelect(child, selectStatus) {
      if (selectStatus) {
        this.selection.push(child);
      } else {
        this.selection = this.selection.filter(item => item != child);
      }
      this.$emit('onSelect', child, selectStatus);
      this.$emit('selectionChange', this.selection);
    },
  },
}
复制代码

子组件负责数据的展现,以及选中事件的处理,在 demo 中父组件只有一个,可是在实际项目中,有两个不一样类型的父组件使用,也是将共同逻辑抽出的一个良好的例子。优化

子组件templatethis

<template>
  <div @click="onSelect">
    <span>{{ data.isSelect ? 'checked' : 'unchecked' }}</span>
    <span>{{ data.name }}</span>
  </div>
</template>
复制代码

子组件scriptspa

export default {
  props: ['data'],
  methods: {
    onSelect() {
      this.data.isSelect = !this.data.isSelect;
    },
  },
  watch: {
    'data.isSelect': function() {
      this.$emit('onSelect', this.data, this.data.isSelect);
    },
  },
}
复制代码

冲突及处理

这个设计原本没有问题,可是在全选时,若是数据较多,如 2000+,全交给watch来处理$emit事件,每次$emit只有几百ms,可是整个加起来一共会耗时几分钟。设计

没有解决办法时,选择参考优秀的开源项目,在el-table的源码当中,toggleRowSelection的源码以下,经过控制参数emitChange来控制事件的触发与否,既能保证相同逻辑的复用,也能控制$emit所带来的性能影响。虽然不知道 Element-UI 本意是否是控制性能,可是在实际测试中,对于大数据量的表格,选中事件的响应效果仍是很理想的,因此借鉴这个思路,在组件代码中增长控制事件相关的代码。

toggleRowSelection(row, selected, emitChange = true) {
  const changed = toggleRowStatus(this.states.selection, row, selected);
  if (changed) {
    const newSelection = (this.states.selection || []).slice();
    // 调用 API 修改选中值,不触发 select 事件
    if (emitChange) {
      this.table.$emit('select', newSelection, row);
    }
    this.table.$emit('selection-change', newSelection);
  }
},
复制代码

增长事件控制

因为父组件并非调用子组件的 API 修改子组件的状态,选择在子组件propsdata中增长字段来控制,修改后的代码以下:

父组件 onSelectAll

onSelectAll() {
  this.children.forEach((child) => {
    child.updateByComponent = true;
    child.isSelect = true;
    this.$nextTick(() => {
      child.updateByComponent = false;
    });
  });
}
复制代码

子组件 watch

watch: {
  'data.isSelect': function() {
    if (this.data.updateByComponent) return;
    this.$emit('onSelect', this.data, this.data.isSelect);
  },
}
复制代码

实际效果

修改后的代码,将原来的响应时间由4分多钟,下降到了400毫秒,带来的实际体验仍是很好的。在一样的数据量状况下,选中效果基本上是及时生效。

总结

这是项目中的一次性能优化的反思,也是一次开源项目源码的阅读过程,虽然组件是我本身编写的,坑是本身挖的,可是此次经历也是本身在组件封装上的一个成长。若是你有什么好的建议,欢迎在评论中提出。


后续

在编写了最小 demo 之后,全选的响应在秒级,可是在项目中,更新isSelect只涉及单个dom的更新,不知道为何会产生这样的影响。要了解透的话,得花时间一点一点的去尝试了。

最小 demo 地址

相关文章
相关标签/搜索