【问题标题】:Increasing performance of v-data-table with custom cells and async data loading通过自定义单元格和异步数据加载提高 v-data-table 的性能
【发布时间】:2020-10-19 05:08:04
【问题描述】:

我正在使用 v-data-table 创建一个页面。此表的某些内容在 mounted 阶段加载,但一列的数据应在渲染整体后通过异步 API 调用在后台逐行加载 桌子。表格行也应该根据 API 调用返回的数据着色。

我已经开发了这个页面,但是遇到了一个问题 - 当表格包含由 item 插槽重新定义的 复合单元格 时(例如,一个带有图标、工具提示或跨度),表格行更新时间显着增加。

根据业务逻辑,页面可能包含大量行,但我不能使用 v-data-table 分页来减少一页的条目数。

问题是 - 我怎样才能更新行(实际上,只是它的颜色和一个单元格值)而尽可能降低性能?

a Codepen with this problem。在这个 Codepen 中完全保留了将数据加载到页面中的方式,但是 API 调用被替换为具有固定超时的 Promise。

Codepen 中仍然存在问题。默认情况下,对 100 个项目的所有请求都在 12-13 秒内通过(页面底部有一个计数器)。当我注释掉最后一个 td 时,它们仅在 7-8 秒内通过。当我注释掉另一个 td (从末尾开始的第二个)时,它们会在 6 秒内通过。当我将项目数增加到 1000 时,行更新时间也会增加。

new Vue({
  el: '#app',
  vuetify: new Vuetify(),
  data () {
    return {
      headers: [
        {
          text: 'Dessert (100g serving)',
          value: 'name',
        },
        { text: 'Second name', value: 'secondName' },
        { text: 'Fat (g)', value: 'fat' },
        { text: 'Carbs (g)', value: 'carbs' },
        { text: 'Protein (g)', value: 'protein' },
        { text: 'Max value', value: 'maxValue' },
        { text: 'Actions', value: 'id' },
      ],
      desserts: [],
      timerStart: null,
      loadingTime: null,
    }
  },
  created() {
    this.generateDesserts();
  },
  mounted() {
    this.countMaxValues(this.desserts).then(() => {
      this.loadingTime = (Date.now() - this.timerStart) / 1000;
    });
  },
  methods: {
    generateDesserts() {
      let dessertNames = [
        'Frozen Yogurt  ',
        'Ice cream sandwich ',
        'Eclair',
        'Cupcake',
        'Gingerbread',
        'Jelly bean',
        'Lollipop',
        'Honeycomb',
        'Donut',
        'KitKat',
        null
      ];
      for (let i = 0; i < 100; i++) {
        let dessert = {
          id: i,
          name: dessertNames[Math.floor(Math.random() * dessertNames.length)],
          secondName: dessertNames[8 + Math.floor(Math.random() * (dessertNames.length - 8))],
          fat: Math.random() * 100,
          carbs: Math.floor(Math.random() * 100),
          protein: Math.random() * 10
        };
        this.desserts.push(dessert);
      }
    },
    async countMaxValues(array) {
      this.timerStart = Date.now();
      for (const item of array) {
        await this.countMaxValue(item).catch(() => {
          //Even when one request throws error we should not stop others
        })
      }
    },
    async countMaxValue(item) {
      await new Promise(resolve => setTimeout(resolve, 50)).then(() => {
        let maxVal = Math.random() * 100;
        item.maxValue = maxVal < 20 ? null : maxVal;
        this.desserts.splice(item.id, 1, item);
      });
    }
  }
})

 
<div id="app">
  <v-app id="inspire">
    <v-data-table
            :headers="headers"
            :items="desserts"
            :footer-props='{
                itemsPerPageOptions: [-1],
                prevIcon: null,
                nextIcon: null,
            }'
    >
        <template v-slot:item="props">
            <tr :style="{
                        background: (props.item.maxValue !== null && (props.item.carbs < props.item.maxValue))
                            ? '#ffcdd2'
                            : (
                                (props.item.maxValue !== null && (props.item.carbs > props.item.maxValue)
                                    ? '#ffee58'
                                    : (
                                        props.item.maxValue === null ? '#ef5350' : 'transparent'
                                    )
                                )
                              )}">
                <td>{{ props.item.name || '—' }}</td>
                <td>{{ props.item.secondName || '—' }}</td>
                <td>{{ props.item.fat }}</td>
                <td>{{ props.item.carbs }}</td>
                <td>{{ props.item.protein }}</td>
                <td>
                    <span>
                        {{ props.item.maxValue || '—' }}
                    </span>
                    <v-btn v-if="props.item.name && props.item.maxValue" icon>
                        <v-icon small>mdi-refresh</v-icon>
                    </v-btn>
                </td>
                <td class="justify-center text-center" style="min-width: 100px">
                    <v-tooltip bottom v-if="props.item.name && props.item.secondName">
                        <template v-slot:activator="{ on }">
                            <v-icon v-on="on"
                                    class="mr-2"
                                    small
                            >
                                format_list_numbered_rtl
                            </v-icon>
                        </template>
                        <span>Some action tooltip</span>
                    </v-tooltip>
                    <v-tooltip bottom v-if="props.item.name && props.item.secondName">
                        <template v-slot:activator="{ on }">
                            <v-icon v-on="on"
                                    class="mr-2"
                                    small
                            >
                                edit
                            </v-icon>
                        </template>
                        <span>Edit action tooltip</span>
                    </v-tooltip>
                    <v-tooltip bottom v-if="props.item.name === 'KitKat'">
                        <template v-slot:activator="{ on }">
                            <v-icon v-on="on"
                                    small
                            >
                                delete
                            </v-icon>
                        </template>
                        <span>Delete action tooltip</span>
                    </v-tooltip>
                </td>
            </tr>
        </template>
    </v-data-table>
    <p>{{ "Page loading time (sec): " + (loadingTime || '...') }}</p>
  </v-app>
</div>

【问题讨论】:

    标签: performance vue.js async-await vuetify.js


    【解决方案1】:

    如果 Vue 包裹在组件中,似乎可以更有效地更新 DOM(对不起,我不知道具体原因)。

    这是您在JSFiddle 中的原始代码。它将使用大约 12-13 秒。

    然后我创建一个组件来包装你的整个tr

    const Tr = {
      props: {
        item: Object
      },
        template: `
        <tr>
          ... // change props.item to item
        </tr>
      `
    }
    
    new Vue({
      el: '#app',
      vuetify: new Vuetify(),
      components: {
        'tr-component': Tr // register Tr component
      },
      ...
    
      async countMaxValue(item) {
        await new Promise(resolve => setTimeout(resolve, 50)).then(() => {
          let maxVal = Math.random() * 100;
          // update entire object instead of one property since we send it as object to Tr
          let newItem = {
            ...item,
            maxValue: maxVal < 20 ? null : maxVal
          }
          this.desserts.splice(newItem.id, 1, newItem);
        });
      }
    })
    

    您的 html 将如下所示:

    <v-data-table
      :headers="headers"
      :items="desserts"
      :footer-props='{
        itemsPerPageOptions: [-1],
        prevIcon: null,
        nextIcon: null,
      }'>
      <template v-slot:item="props">
        <tr-component :item='props.item'/>
      </template>
    </v-data-table>
    

    result 将使用大约 6-7 秒,而更新 DOM 仅需 1-2 秒。

    或者,如果您发现您的函数触发非常快(在您的示例中使用 50 毫秒,我认为这太快了)您可以尝试限制它以减少更新 DOM。

    ...
    methods: {
      async countMaxValue(item) {
        await new Promise(resolve => setTimeout(resolve, 50)).then(() => {
          let maxVal = Math.random() * 100;
          let newItem = {
            ...item,
            maxValue: maxVal < 20 ? null : maxVal
          }
          this.changes.push(newItem) // keep newItem to change later
          this.applyChanges() // try to apply changes if it already schedule it will do nothing
        });
      },
      applyChanges () {
        if (this.timeoutId) return
        this.timeoutId = setTimeout(() => {
          while (this.changes.length) {
            let item = this.changes.pop()
            this.desserts.splice(item.id, 1, item)
          }
          this.timeoutId = null
        }, 1500)
      }
    }
    

    result 将使用大约 5-6 秒,但您可以看到它不会立即更新。

    或者您可以尝试并行调用您的 API,例如 10 个请求,您可以从等待 100 * 50 毫秒减少到大约 10 * 50 毫秒(数学上)。

    希望对你有所帮助。

    【讨论】:

    • 哇,没想到这么解决的办法!我完全可以接受您创建单独组件的第一个小提琴!第二种方法也不错,但是这个 API 以后不会那么灵敏了,而且由于业务逻辑的限制,我不能并行发送 API 调用。非常感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多