Đừng phớt lờ WeakMap

Là một tính năng JavaScript mạnh mẽ nhưng ít được biết đến trong quản lý bộ nhớ, WeakMap giúp giảm thiểu rủi ro rò rỉ bộ nhớ trong ứng dụng. Một số ví dụ liên quan và các trường hợp nên sử dụng WeakMap sẽ được đề cập trong bài viết này.

Upload image

Cộng đồng JavaScript ngày càng lớn mạnh, đôi khi những tính năng mạnh mẽ và hiệu quả liên quan đến tính bảo mật, hiệu suất của ngôn ngữ, tối ưu hoá trong quản lý bộ nhớ lại thường bị 'ngó lơ' mặc dù chúng mới là yếu tố quyết định đến chất lượng của một ứng dụng phần mềm tốt.

Một trong số các tính năng mình muốn nói đến là WeakMap. Trong bài này, bạn sẽ có thể hiểu rõ hơn về cách hoạt động của WeakMap, sự khác biệt với Map, cũng như ưu, nhược điểm và lựa chọn sử dụng WeakMap trong từng trường hợp.

WeakMap là gì?

WeakMap là một loại cấu trúc dữ liệu được lưu theo dạng khoá-giá trị (key-value) giống như Map. Nhưng với WeakMap, các key phải là kiểu Object, khác so với key trong Map là kiểu dữ liệu tuỳ ý bao gồm cả kiểu nguyên thủy (primitive).

Điểm tiếp theo mà mình nghĩ là yếu tố quan trọng giúp cho WeakMap được biết đến là cách nó quản lý bộ nhớ. WeakMap sẽ không giữ tham chiếu mạnh đến các key, cho phép các Object không còn được sử dụng nữa có thể được thu gom bởi garbage collector, giúp giảm thiểu rủi ro rò rỉ bộ nhớ và tối ưu hoá hiệu suất của ứng dụng.

WeakMap vs. Map trong quản lý bộ nhớ

Từ lâu, việc quản lý bộ nhớ trong JavaScript luôn được thực hiện một cách tự động và âm thầm 'được che đi' ở phía Dev. Điều này đôi khi làm cho Dev không ý thức được liệu việc quản lý bộ nhớ có được tối ưu hóa hay không? Sau đây sẽ là một vài ví dụ liên quan đến quản lý bộ nhớ khi sử dụng Map và WeakMap nhằm giúp ta hiểu rõ hơn về cả hai cấu trúc dữ liệu này.

Các ví dụ

  • Bắt đầu với một ví dụ cơ bản về cơ chế hoạt động của garbage collection. Khi ta khai báo một object rồi sau đó loại bỏ liên kết giữa object đó với tham chiếu, kết quả là object đó sẽ được xoá bỏ khỏi bộ nhớ.
let firstStudent = { name: 'First Student' };

// Loại bỏ liên kết giữa firstStudent với tham chiếu
firstStudent = null;

// Kết quả: firstStudent sẽ được xoá khỏi bộ nhớ
  • Tiếp theo, chúng ta sẽ áp dụng ví dụ trên vào việc thiết lập key trong Map. Khi khai báo key là một object và thiết lập liên kết với tham chiếu, điểm khác biệt ở đây là dù key có bị mất đi liên kết thì nó vẫn còn bên trong Map miễn là Map còn tồn tại. Điều này dẫn đến vấn đề memory leak trong trường hợp có nhiều object vẫn còn được giữ lại trong bộ nhớ dù không còn sử dụng.
let firstStudent = { name: 'First Student' };

let map = new Map();
map.set(firstStudent, 'First Student Description');

firstStudent = null; // Loại bỏ liên kết với tham chiếu ban đầu

// Nhưng firstStudent vẫn còn là key và vẫn tồn tại trong Map
// Bằng việc ta có sử dụng map.keys() để truy vấn ra nó
// -> Không bị xoá khỏi bộ nhớ
  • Cuối cùng, chúng ta xem xét việc sử dụng WeakMap trong trường hợp này. Khi loại bỏ liên kết giữa object với tham chiếu thì mặc dù key vẫn còn tồn tại trong WeakMap nhưng nó vẫn sẽ bị thu gom bởi garbage collector vì không còn bất kì tham chiếu nào đến nó nữa.
let firstStudent = { name: 'First Student' };

let weakMap = new WeakMap();
weakMap.set(firstStudent, 'First Student Description');

firstStudent = null; // Xoá liên kết với tham chiếu ban đầu

// Lúc này firstStudent dù là key của WeakMap và nó cũng sẽ được xoá bỏ khỏi bộ nhớ vì không còn bất kì tham chiếu nào đến nữa

Đây chính là sự khác biệt giữa WeakMap và Map trong việc quản lý bộ nhớ như đã để cập trước đó. Điều quan trọng lúc này là lập trình viên cần hiểu rõ khi nào nên sử dụng WeakMapMap, điều này sẽ được trình bày trong các ý sau.

Lý do dẫn đến việc WeakMap chỉ chấp nhận key là kiểu object cũng phản ánh từ tính chất của WeakMap mà ra, bởi khi thiết lập key trong WeakMap là object thì hệ thống garbage collection có thể dễ dàng nhận thấy key không còn được sử dụng và xoá nó đi nếu như không còn tham chiếu nào liên kết đến nó.

Ngược lại, giả sử khi key là kiểu primitive - một kiểu dữ liệu không có tham chiếu, giá trị được 'nằm' thẳng trên key, dù cho thiết lập key đó bằng null thì hệ thống garbage collection cũng không có căn cứ chính xác nào để xoá key đó ra khỏi bộ nhớ vì có thể key đó vẫn còn được dùng lại.

Các hạn chế

WeakMap có những hạn chế như không hỗ trợ phương thức duyệt như keys(), values(), entries() (khác so với Map). Chính vì thế, ta sẽ không có cách nào để truy vấn tất cả key và value từ nó.

WeakMap chỉ có các tính năng như:

  • weakMap.set(key, value)
  • weakMap.get(key)
  • weakMap.delete(key)
  • weakMap.has(key)

Lý do WeakMap có những hạn chế này xuất phát từ lý do kỹ thuật được ứng dụng trong WeakMap. Vì khi quá trình garbage collection diễn ra thì việc truy vấn tất cả key sẽ không chính xác hoàn toàn do còn phụ thuộc vào cách mà JS engine thực hiện quá trình 'dọn dẹp' trong bộ nhớ. Cụ thể, ta sẽ không biết chính xác việc dọn dẹp diễn ra như thế nào: có thể key sẽ được xoá ngay, key chờ để xoá hay là key đã xoá được một phần (Map ngăn chặn quá trình garbage collection diễn ra cho nên dễ dàng truy vấn được các key vì chúng cố định). Đó có thể là lý do WeakMap không được hỗ trợ tính năng kể trên.

Như vậy với những tính chất đặc biệt của WeakMap, những trường hợp nào nên sử dụng đến tính năng này?

Các trường hợp nên sử dụng

Caching

Trong một ứng dụng web lớn, bạn có thể sử dụng WeakMap để lưu trữ cache tạm thời và sau đó có thể sử dụng lại kết quả đã lưu của cùng một object. Kết quả lưu trong cache có thể dễ dàng được xoá bỏ trong bộ nhớ nếu như key object không được dùng đến nữa.

// Trong file cache.js
let cache = new WeakMap();

// Viết hàm thực hiện lấy dữ liệu đã cache từ server
function fetchDataFromCachedServer(obj) {
    if (!cache.has(obj)) {
        let result = /* lấy dữ liệu từ máy chủ */;

        cache.set(obj, result);
        return result;
    }
    return cache.get(obj);
}

// Trong file main.js
let obj = {/* khởi tạo object */};

let result1 = fetchDataFromCachedServer(obj); // lấy dữ liệu lần 1 với key obj, lưu vào result 1
let result2 = fetchDataFromCachedServer(obj); // nhớ lại kết quả đã lưu từ cache khi truy vấn cùng key obj, lưu vào result 2

// Khi obj không còn dùng đến nữa
obj = null;

// TRƯỜNG HỢP KHI SỬ DỤNG MAP:
// Khi đếm lại kích thước của cache, cụ thể là `cache.size` vẫn là 1 vì key object này sẽ vẫn còn lưu trong cache và làm tiêu tốn bộ nhớ.

// TRƯỜNG HỢP KHI SỬ DỤNG WEAKMAP:
// key object sẽ bị xoá khỏi khỏi bộ nhớ và dữ liệu đã lưu trước cũng bị xoá theo.

Lưu trữ dữ liệu bổ sung

Chúng ta cũng có thể sử dụng WeakMap cho việc lưu trữ thông tin bổ sung. Bài toán cụ thể ở đây có thể áp dụng trong việc lưu trữ thêm thông tin truy cập của người dùng trong một hệ thống nào đó.

// Tạo một WeakMap để lưu trữ metadata cho từng người dùng
const userMetadata = new WeakMap();

// Class User đại diện cho mỗi người dùng trong hệ thống
class User {
    constructor(id, username) {
        this.id = id;
        this.username = username;
        // Gắn metadata mặc định với mỗi người dùng
        userMetadata.set(this, {
            role: 'user', // Quyền mặc định là người dùng ,
            isActive: true // Thời điểm tạo người dùng
            createdAt: new Date() // Trạng thái kích hoạt mặc định
        });
    }

    // Phương thức để lấy metadata của người dùng
    getMetadata() {
        return userMetadata.get(this);
    }

    // Phương thức để cập nhật metadata của người dùng
    updateMetadata(newMetadata) {
        const metadata = userMetadata.get(this);
        userMetadata.set(this, { ...metadata, ...newMetadata });
    }
}

// Tạo một người dùng mới const user1 = new User(1, 'User 1');

// Truy cập và in ra metadata của người dùng
console.log(user1.getMetadata());

// Cập nhật metadata cho người dùng
user1.updateMetadata({ role: 'admin', isActive: false });

// In ra metadata sau khi cập nhật
console.log(user1.getMetadata());

// Khi User không còn được sử dụng đến thì metadata của họ cũng sẽ được loại bỏ theo để tránh rò rỉ bộ nhớ.

Ngoài ra, WeakMap vẫn còn có thể áp dụng trong các trường hợp khác như quản lý đối tượng DOM trong ứng dụng web hay các đối tượng ngắn hạn như Session,...

Kết

Rõ ràng nếu không biết đến WeakMap thì mặc định mình sẽ chỉ dùng Map cho những bài toán liên quan và không quan tâm đến vấn đề quản lý bộ nhớ như mình đã phân tích ở trên. Từ đó có thể thấy việc JavaScript đang dần phát triển ra những tính năng hiệu quả mà chúng ta có thể vô tình 'lơ đi' vì nghĩ mọi thứ đã rất tốt rồi.

WeakMap có những hạn chế nhất định như việc không hỗ trợ các phương thức duyệt như keys(), values(), entries(), nhưng sức mạnh của nó trong việc quản lý bộ nhớ là không thể phủ nhận. Bằng cách sử dụng WeakMap một cách có chọn lọc, chúng ta có thể tối ưu hóa hiệu suất của ứng dụng và tránh được những vấn đề liên quan đến bộ nhớ.

Atekco - Home for Authentic Technical Consultants