Vue.js 性能:避免子组件列表更改时父组件重新渲染

时间:2021-04-16 19:42:26

标签: javascript performance vue.js svg vuejs2

在使用 v-for 指令处理列出数千个项目的组件时,我遇到了性能问题:更新某些项目会导致重新渲染父组件。

我们可以举个例子:一个条形图,为客户光标周围的条形着色

m
Vue.component("BarChart", {
  props: ["data", "width"],
  data() {
    return {
      mousePositionX: null
    };
  },
  template: `
<div class="bar-chart">
  <div>Chart rendered: {{ new Date() | time }}</div>
  <svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
    <bar
      v-for="bar in bars"
      :key="bar.id"
      :x="bar.x"
      :y="bar.y"
      :height="bar.height"
      :width="bar.width"
      :show-time="bar.showTime"
      :colored="bar.colored"
    ></bar>
  </svg>
</div>
  `,
  computed: {
    barWidth() {
      return this.width / this.data.length;
    },
    bars() {
      return this.data.map(d => {
        const x = d.id * this.barWidth;
        return {
          id: d.id,
          x: x,
          y: 160 - d.value,
          height: d.value,
          width: this.barWidth,
          showTime: this.barWidth >= 20,
          colored: this.mousePositionX &&
            x >= this.mousePositionX - this.barWidth * 3 &&
            x < this.mousePositionX + this.barWidth * 2
        }
      });
    }
  }
});

Vue.component("Bar", {
  props: ["x", "y", "width", "height", "showTime", "colored"],
  data() {
    return {
      fontSize: 14
    };
  },
  template: `
<g class="bar">
  <rect
    :x="x"
    :y="y"
    :width="width"
    :height="height"
    :fill="colored ? 'red' : 'gray'"
  ></rect>
  <text v-if="showTime" :transform="'translate(' + (x + width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
    {{ new Date() | time }}
  </text>
</g>
`
});

const barCount = 30; // to display the bars time, set barCount <= 30

new Vue({
  el: "#app",
  data() {
    return {
      data: Array.from({
        length: barCount
      }, (v, i) => ({
        id: i,
        value: randomInt(80, 160)
      })),
      width: 795
    }
  }
});
body {
  margin: 0;
}

svg {
  height: 160px;
  background: lightgray;
}

由于显示的时间值,我们可以看到组件重新渲染,只有当相应的组件被渲染时才会更新。

当项目 (Bar) 颜色更新时,只有更新的项目会重新渲染。
但是,这就是问题,即使没有更改任何项目,也会在每次光标移动时重新渲染父级 (BarChart)。

对于具有 30 个柱的条形图,它可能没问题。
但是如果显示大量条形,则重新渲染父组件所花费的时间太大,导致性能损失很大。

以 1500 根柱线为例:

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  Vue.config.devtools = true;
  Vue.config.productionTip = false;
  Vue.filter("time", function(date) {
    return date.toISOString().split('T')[1].slice(0, -1)
  });

  function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }
</script>
<div id="app">
  <bar-chart :data="data" :width="width" />
</div>
Vue.component("BarChart", {
  props: ["data", "width"],
  data() {
    return {
      mousePositionX: null
    };
  },
  template: `
<div class="bar-chart">
  <div>Chart rendered: {{ new Date() | time }}</div>
  <svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
    <bar
      v-for="bar in bars"
      :key="bar.id"
      :x="bar.x"
      :y="bar.y"
      :height="bar.height"
      :width="bar.width"
      :show-time="bar.showTime"
      :colored="bar.colored"
    ></bar>
  </svg>
</div>
  `,
  computed: {
    barWidth() {
      return this.width / this.data.length;
    },
    bars() {
      return this.data.map(d => {
        const x = d.id * this.barWidth;
        return {
          id: d.id,
          x: x,
          y: 160 - d.value,
          height: d.value,
          width: this.barWidth,
          showTime: this.barWidth >= 20,
          colored: this.mousePositionX &&
            x >= this.mousePositionX - this.barWidth * 3 &&
            x < this.mousePositionX + this.barWidth * 2
        }
      });
    }
  }
});

Vue.component("Bar", {
  props: ["x", "y", "width", "height", "showTime", "colored"],
  data() {
    return {
      fontSize: 14
    };
  },
  template: `
<g class="bar">
  <rect
    :x="x"
    :y="y"
    :width="width"
    :height="height"
    :fill="colored ? 'red' : 'gray'"
  ></rect>
  <text v-if="showTime" :transform="'translate(' + (x + width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
    {{ new Date() | time }}
  </text>
</g>
`
});

const barCount = 1500; // to display the bars time, set barCount <= 30

new Vue({
  el: "#app",
  data() {
    return {
      data: Array.from({
        length: barCount
      }, (v, i) => ({
        id: i,
        value: randomInt(80, 160)
      })),
      width: 795
    }
  }
});
body {
  margin: 0;
}

svg {
  height: 160px;
  background: lightgray;
}

对于 1500 个条形图,Vue Devtools 清楚地表明重新渲染父组件所花费的时间太长(约 278 毫秒)并导致性能问题。

enter image description here

那么,有没有办法更新依赖父组件的数据(如光标位置)的子组件,避免父组件不必要的更新?

1 个答案:

答案 0 :(得分:1)

计算属性在 Vue 中非常有用……但并非总是如此。还有一些陷阱....

每次鼠标移动时用一组全新的对象生成新数组就是其中之一。由于新数组整个 BarChart 组件必须重新渲染(并且每 0.X 秒的新数组也不是免费的)。

解决方案是尽量减少数据更改......在这种情况下是观察者。

Vue.component("BarChart", {
  props: ["data", "width"],
  data() {
    return {
      mousePositionX: null,
      bars: []      
    };
  },
  template: `
<div class="bar-chart">
  <div>Chart rendered: {{ new Date() | time }}</div>
  <svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
    <bar
      v-for="bar in bars"
      :key="bar.id"
      :x="bar.x"
      :y="bar.y"
      :height="bar.height"
      :width="bar.width"
      :show-time="bar.showTime"
      :colored="bar.colored"
    ></bar>
  </svg>
</div>
  `,
  computed: {
    barWidth() {
      return this.width / this.data.length;
    },
  },
  watch: {
    data: {
      handler: function() {
        this.bars = this.data.map(d => {
          const x = d.id * this.barWidth;
          return {
            id: d.id,
            x: x,
            y: 160 - d.value,
            height: d.value,
            width: this.barWidth,
            showTime: this.barWidth >= 20,
            colored: false
          }
        });
      },
      immediate: true
    },
    mousePositionX: {
      handler: 'updateBarsColor'
    }
  },
  methods: {
    updateBarsColor(x) {
      this.bars.forEach(bar => {
        bar.colored = x &&
          bar.x >= x - this.barWidth * 3 &&
          bar.x < x + this.barWidth * 2
      })
    }
  }
});

Vue.component("Bar", {
  props: ["x", "y", "width", "height", "showTime", "colored"],
  data() {
    return {
      fontSize: 14
    };
  },
  template: `
<g class="bar">
  <rect
    :x="x"
    :y="y"
    :width="width"
    :height="height"
    :fill="colored ? 'red' : 'gray'"
  ></rect>
  <text v-if="showTime" :transform="'translate(' + (x + width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
    {{ new Date() | time }}
  </text>
</g>
`
});

const barCount = 1500; // to display the bars time, set barCount <= 30

new Vue({
  el: "#app",
  data() {
    return {
      data: Array.from({
        length: barCount
      }, (v, i) => ({
        id: i,
        value: randomInt(80, 160)
      })),
      width: 795
    }
  }
});
body {
  margin: 0;
}

svg {
  height: 160px;
  background: lightgray;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  Vue.config.devtools = true;
  Vue.config.productionTip = false;
  Vue.filter("time", function(date) {
    return date.toISOString().split('T')[1].slice(0, -1)
  });

  function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }
</script>
<div id="app">
  <bar-chart :data="data" :width="width" />
</div>

更新 - 附加问题(来自评论)

<块引用>

好的,它起作用了。但令我惊讶的是 BarChart 仍然重新渲染(您会看到时间在变化)。这不会给性能带来麻烦吗?

经过一番思考,我得出一个结论,BarChart 组件每次似乎都无缘无故地重新渲染的原因是组件将 props 传递给 Bar 子组件的方式。在您的原始(也是我的第一个)示例中,BarChart 将 bar 配置对象“解构”为单独的道具。这样,BarChart 组件依赖于配置对象的每个属性,并且每次更改数组中任何对象的任何属性时都需要重新渲染(为了更新子道具)

解决这个问题的方法是将整个对象传递给 Bar 组件。请参阅我的第二个示例,它甚至更快(BarChart 根本不重新渲染)

Vue.component("BarChart", {
  props: ["data", "width"],
  data() {
    return {
      mousePositionX: null,
      bars: []      
    };
  },
  template: `
<div class="bar-chart">
  <div>Chart rendered: {{ new Date() | time }}</div>
  <svg @mousemove="mousePositionX = $event.x" :style="{width: width}">
    <bar
      v-for="bar in bars"
      :key="bar.id"
      :config="bar"
    ></bar>
  </svg>
</div>
  `,
  computed: {
    barWidth() {
      return this.width / this.data.length;
    },
  },
  watch: {
    data: {
      handler: function() {
        this.bars = this.data.map(d => {
          const x = d.id * this.barWidth;
          return {
            id: d.id,
            x: x,
            y: 160 - d.value,
            height: d.value,
            width: this.barWidth,
            showTime: this.barWidth >= 20,
            colored: false
          }
        });
      },
      immediate: true
    },
    mousePositionX: {
      handler: 'updateBarsColor'
    }
  },
  methods: {
    updateBarsColor(x) {
      this.bars.forEach(bar => {
        bar.colored = x &&
          bar.x >= x - this.barWidth * 3 &&
          bar.x < x + this.barWidth * 2
      })
    }
  }
});

Vue.component("Bar", {
  props: ["config"],
  data() {
    return {
      fontSize: 14
    };
  },
  template: `
<g class="bar">
  <rect
    :x="config.x"
    :y="config.y"
    :width="config.width"
    :height="config.height"
    :fill="config.colored ? 'red' : 'gray'"
  ></rect>
  <text v-if="config.showTime" :transform="'translate(' + (config.x + config.width/2 + fontSize/2) + ',160) rotate(-90)'" :font-size="fontSize" fill="white">
    {{ new Date() | time }}
  </text>
</g>
`
});

const barCount = 1500; // to display the bars time, set barCount <= 30

new Vue({
  el: "#app",
  data() {
    return {
      data: Array.from({
        length: barCount
      }, (v, i) => ({
        id: i,
        value: randomInt(80, 160)
      })),
      width: 795
    }
  }
});
body {
  margin: 0;
}

svg {
  height: 160px;
  background: lightgray;
}
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  Vue.config.devtools = true;
  Vue.config.productionTip = false;
  Vue.filter("time", function(date) {
    return date.toISOString().split('T')[1].slice(0, -1)
  });

  function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }
</script>
<div id="app">
  <bar-chart :data="data" :width="width" />
</div>