$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 地址

相關文章
相關標籤/搜索