Vuejs: Hạn chế số lần thực thi hàm bằng Debounce và Throttle

Nếu bạn đang tìm kiếm một phương pháp để giảm thiểu số lần thực thi hàm không cần thiết nhằm cải thiện performance cho website thì bài viết này có thể sẽ giải quyết vấn đề đó cho bạn.

Việc gọi hàm khi một biến thay đổi hay một sự kiện xảy ra đã trở nên rất quen thuộc trong Vuejs. Tuy nhiên, một số loại sự kiện trong DOM sẽ xảy ra rất nhiều lần trong khoảng thời gian ngắn như scrolling, resizing hay input change. Và việc thực thi các lệnh mỗi khi các sự kiện đó xảy ra không phải là một phương pháp tốt.

Lấy một ví dụ đơn giản như việc tạo ra một khung tìm kiếm sẽ tự động hiện kết quả khi người dùng nhập từ khoá. Việc gọi API về phía máy chủ mỗi lần người dùng gõ kí tự có thể sẽ gây ra những vấn đề liên quan đến performance.

Và hai cơ chế debouncing và throttling đã được tạo ra để khống chế số lần thực thi hàm ở mức hợp lý.

Thế nào là debouncing và throttling?

Debouncing là một phương pháp để gom nhiều lần gọi hàm liên tiếp lại thành một lần gọi duy nhất. Có nghĩa là khi một hàm được gọi bằng cơ chế debouncing, việc thực thi hàm được trì hoãn trong một khoảng thời gian cụ thể, và nếu có một hàm khác được gọi trong thời gian chờ đó, thời gian chờ được lặp lại và chỉ có hàm cuối cùng là được thực thi.

Throttling là một cơ chế đảm bảo hàm chỉ được thực thi nhiều nhất một lần trong khoảng thời gian nhất định. Một khi hàm được gọi, các lần gọi hàm phía sau sẽ bị bỏ qua cho đến khi hết thời gian chờ.

Mặc dù có thể tự tạo ra hai cơ chế trên, bạn không cần phải code vì hai hàm debounce và throttle đã được cung cấp trong hai bộ thư thư viện UnderscoreLodash.

Sau đây là cách mà debounce và throttle được áp dụng với watchers và event handlers trong Vuejs. Vì cả hai hàm đều có cùng một tham số nên trong demo sẽ chỉ áp dụng debounce, các bạn có thể thay debounce bằng throttle tuỳ mục đích sử dụng.

Áp dụng trong watcher

Bắt đầu với một component đơn giản, bạn nhập kí tự vào khung input, sau đó kết quả sẽ được cập nhật ở dưới và log ra console mỗi khi sự kiện input change diễn ra:

<template>
  <div>
    <input v-model="value" type="text" />
    <p>{{ value }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      value: "",
    };
  },
  watch: {
    value(newValue, oldValue) {
      console.log("Value changed: ", newValue);
    }
  }
};
</script>

Trong ví dụ trên chỉ là một hàm log được gọi, tuy nhiên, trong thực tế nó có thể là một lần gọi API, và bạn sẽ không muốn việc gọi hàm xảy ra quá nhiều.

Hãy áp dụng cơ chế debouncing cho đoạn code phía trên:

<template>
  <div>
    <input v-model="value" type="text" />
    <p>{{ value }}</p>
  </div>
</template>
<script>
import debounce from "lodash.debounce";
export default {
  data() {
    return {
      value: "",
    };
  },
  watch: {
    value(...args) {
      this.debouncedWatch(...args);
    },
  },
  created() {
    this.debouncedWatch = debounce((newValue, oldValue) => {
      console.log('New value:', newValue);
    }, 500);
  },
  beforeUnmount() {
    this.debouncedWatch.cancel();
  },
};
</script>

Nếu bạn chạy thử component trên, bạn sẽ thấy dòng console.log chỉ được gọi sau 500ms từ khi chúng ta ngưng nhập dữ liệu vào input. Thật là ‘ờ mây zing gút chóp’ phải không?

Việc áp dụng debouncing vào component trên gồm ba bước đơn giản:

  1. Tại created() hook của component, khởi tạo debounceWatch.
  2. Bên trong watch callback, gọi debounceWatch với các tham số cần thiết.
  3. Cuối cùng là huỷ debounceWatch tại beforeUnmount() hook của component khi component không được sử dụng nữa.

Áp dụng trong event handler

Việc áp dụng debouncing cho event handler trong Vuejs cũng không khác nhiều lắm đối với watcher.

Vẫn sẽ là đoạn ví dụ trên, nhưng chúng ta sẽ dùng hàm handler cho sự kiện thay vì dùng watch:

<template>
  <input v-on:input="handler" type="text" />
</template>
<script>
export default {
  methods: {
    handler(event) {
      console.log('New value:', event.target.value);
    }
  }
};
</script>

Và sau khi áp dụng debouncing:

<template>
  <input v-on:input="debouncedHandler" type="text" />
</template>
<script>
import debounce from "lodash.debounce";
export default {
  created() {
    this.debouncedHandler = debounce(event => {
      console.log('New value:', event.target.value);
    }, 500);
  },
  beforeUnmount() {
    this.debouncedHandler.cancel();
  }
};
</script>

Thay đổi duy nhất chính là debounchHandler được gọi trực tiếp khi sự kiện xảy ra. và chúng ta vẫn đạt được mục đích mong muốn.

Kết luận

Việc áp dụng hai cơ chế debouncing và throttling trong Vuejs rất đơn giản và có thể được tóm gọn như sau.

Tạo một debouncedCallback ở created hook:

// ...
  created() {
    this.debouncedCallback = debounce((...args) => {
      // The debounced callback
    }, 500);
  },
// ...

Sau đó gọi debouncedCallback ở watcher:

// ...
  watch: {
    value(...args) {
      this.debouncedCallback(...args);
    },
  },
// ...

Hoặc set event handler:

<template>
  <input v-on:input="debouncedHandler" type="text" />
</template>

Bạn có thể thắc mắc rằng tại sao phải khởi tạo debounce ở created() hook mà không phải là một hàm trực tiếp trong methods như thế này?

// ...
  methods: {
    // Why not?
    debouncedHandler: debounce(function () { ... }}, 500)
  }
// ...

Vấn đề ở đây là nếu component này xuất hiện nhiều hơn một lần trong một trang thì tất cả các instances của component đều sử dụng chung một hàm debounced handler duy nhất. Và điều đó có thể sẽ dẫn tới những trục trặc mà chúng ta không mong muốn xuất hiện.

Trên đây là cách áp dụng debouncing và throttling trong Vuejs, mong rằng bài viết này sẽ giúp giải quyết được vấn đề mà project của bạn đang gặp phải.

Tham khảo Dmitri Pavluti

Atekco - Home for Authentic Technical Consultants