Hiểu về WeakRef và FinalizationRegistry

Những tính năng ẩn của ngôn ngữ” Bài viết này đề cập đến một chủ đề rất hẹp, mà hầu hết các nhà phát triển rất hiếm khi gặp trong thực tế (và có thể thậm chí không nhận thức được sự tồn tại của nó).
Chúng tôi khuyên bạn nên bỏ qua chương này nếu bạn mới bắt đầu học JavaScript.

Nhắc lại khái niệm cơ bản của nguyên tắc khả năng truy cập từ chương Garbage collection, chúng ta có thể lưu ý rằng động cơ JavaScript được đảm bảo giữ các giá trị trong bộ nhớ mà có thể truy cập hoặc đang được sử dụng.

Ví dụ:

//  the user variable holds a strong reference to the object
let user = { name: "John" };

// let's overwrite the value of the user variable
user = null;

// the reference is lost and the object will be deleted from memory

Hoặc mã tương tự, nhưng hơi phức tạp hơn với hai tham chiếu mạnh:

//  the user variable holds a strong reference to the object
let user = { name: "John" };

// copied the strong reference to the object into the admin variable
*!*
let admin = user;
*/!*

// let's overwrite the value of the user variable
user = null;

// the object is still reachable through the admin variable

Đối tượng { name: "John" } sẽ chỉ bị xóa khỏi bộ nhớ nếu không còn bất kỳ tham chiếu mạnh nào đến nó (nếu chúng ta cũng ghi đè giá trị của biến admin).
Trong JavaScript, có một khái niệm gọi là WeakRef, hành xử hơi khác trong trường hợp này.

Tham chiếu mạnh, Tham chiếu yếu

Tham chiếu mạnh – là một tham chiếu đến một đối tượng hoặc giá trị, ngăn không cho chúng bị xóa bởi garbage collector. Do đó, giữ đối tượng hoặc giá trị trong bộ nhớ mà nó chỉ đến.
Điều này có nghĩa là đối tượng hoặc giá trị vẫn ở trong bộ nhớ và không bị garbage collector thu gom miễn là còn các tham chiếu mạnh hoạt động đến nó.

Trong JavaScript, các tham chiếu thông thường đến các đối tượng là các tham chiếu mạnh. Ví dụ:

// the user variable holds a strong reference to this object
let user = { name: "John" };

Tham chiếu yếu – là một tham chiếu đến một đối tượng hoặc giá trị, không ngăn chúng bị xóa bởi garbage collector.
Một đối tượng hoặc giá trị có thể bị xóa bởi garbage collector nếu các tham chiếu còn lại duy nhất đến chúng là các tham chiếu yếu.

1. WeakRef

Trước khi chúng ta đi vào chi tiết, đáng lưu ý rằng việc sử dụng chính xác các cấu trúc được thảo luận trong bài viết này yêu cầu suy nghĩ rất cẩn thận, và tốt nhất là nên tránh nếu có thể.

WeakRef- là một đối tượng, chứa một tham chiếu yếu tới một đối tượng khác, gọi làtargethoặcreferent.
Đặc điểm của
WeakReflà nó không ngăn cản garbage collector xóa đối tượng tham chiếu của nó. Nói cách khác, một đối tượngWeakRefkhông giữ cho đối tượngreferentsống.

Bây giờ hãy xem xét biếnusernhư là "referent" và tạo một tham chiếu yếu từ nó tới biếnadmin. Để tạo một tham chiếu yếu, bạn cần sử dụng constructorWeakRef, truyền vào đối tượng mục tiêu (đối tượng mà bạn muốn có tham chiếu yếu).
Trong trường hợp của chúng ta — đây là biến
user:

//  the user variable holds a strong reference to the object
let user = { name: "John" };

//  the admin variable holds a weak reference to the object
*!*
let admin = new WeakRef(user);
*/!*

Sơ đồ dưới đây mô tả hai loại tham chiếu: một tham chiếu mạnh sử dụng biếnuservà một tham chiếu yếu sử dụng biếnadmin:


Sau đó, vào một thời điểm nào đó, chúng ta ngừng sử dụng biến
user- nó bị ghi đè, ra khỏi phạm vi, v.v., trong khi vẫn giữ đối tượngWeakReftrong biếnadmin`:

// let's overwrite the value of the user variable
user = null;

Một tham chiếu yếu tới một đối tượng không đủ để giữ nó sống. Khi các tham chiếu duy nhất còn lại tới đối tượng referent là tham chiếu yếu, garbage collector có thể phá hủy đối tượng này và sử dụng bộ nhớ của nó cho mục đích khác.
Tuy nhiên, cho đến khi đối tượng thực sự bị phá hủy, tham chiếu yếu có thể trả về nó, ngay cả khi không còn tham chiếu mạnh nào tới đối tượng này. Tức là, đối tượng của chúng ta trở thành một loại “mèo Schrödinger” – chúng ta không thể biết chắc liệu nó còn “sống” hay đã “chết”:


Tại điểm này, để lấy đối tượng từ instance WeakRef, chúng ta sẽ sử dụng phương thức deref().
Phương thức deref() trả về đối tượng referent mà WeakRef đang trỏ tới, nếu đối tượng vẫn còn trong bộ nhớ. Nếu đối tượng đã bị xóa bởi garbage collector, thì phương thức deref() sẽ trả về undefined:

let ref = admin.deref();

if (ref) {
  // the object is still accessible: we can perform any manipulations with it
} else {
  // the object has been collected by the garbage collector
}

2. Các trường hợp sử dụng WeakRef

WeakRef thường được sử dụng để tạo các bộ nhớ đệm hoặc mảng liên kết lưu trữ các đối tượng tốn tài nguyên. Điều này cho phép tránh việc ngăn cản những đối tượng này bị garbage collector thu gom chỉ dựa trên sự hiện diện của chúng trong bộ nhớ đệm hoặc mảng liên kết.
Một trong những ví dụ chính – là tình huống khi chúng ta có nhiều đối tượng hình ảnh nhị phân (chẳng hạn, được biểu diễn dưới dạng ArrayBuffer hoặc Blob), và chúng ta muốn liên kết một tên hoặc đường dẫn với từng hình ảnh. Các cấu trúc dữ liệu hiện tại không hoàn toàn phù hợp với các mục đích này:
– Sử dụng Map để tạo các liên kết giữa tên và hình ảnh, hoặc ngược lại, sẽ giữ các đối tượng hình ảnh trong bộ nhớ vì chúng hiện diện trong Map dưới dạng khóa hoặc giá trị.
WeakMap cũng không phù hợp với mục tiêu này: vì các đối tượng được biểu diễn dưới dạng khóa WeakMap sử dụng tham chiếu yếu, và không được bảo vệ khỏi việc bị xóa bởi garbage collector.
Nhưng, trong tình huống này, chúng ta cần một cấu trúc dữ liệu sử dụng tham chiếu yếu trong các giá trị của nó.
Để thực hiện điều này, chúng ta có thể sử dụng một bộ sưu tập Map, mà giá trị của nó là các đối tượng WeakRef tham chiếu đến các đối tượng lớn mà chúng ta cần. Do đó, chúng ta sẽ không giữ các đối tượng lớn và không cần thiết này trong bộ nhớ lâu hơn mức cần thiết.

Ngược lại, đây là cách để lấy đối tượng hình ảnh từ bộ nhớ cache nếu nó vẫn có thể truy cập được. Nếu nó đã bị thu gom bởi garbage collector, chúng ta sẽ tái tạo hoặc tải xuống lại nó.

Theo cách này, ít bộ nhớ hơn được sử dụng trong một số tình huống.

3. Ví dụ №1: sử dụng WeakRef cho bộ nhớ cache

Dưới đây là đoạn mã minh họa kỹ thuật sử dụng WeakRef.

Tóm lại, chúng ta sử dụng một Map với các khóa kiểu chuỗi và các đối tượng WeakRef làm giá trị của chúng. Nếu đối tượng WeakRef chưa bị garbage collector thu gom, chúng ta lấy nó từ bộ nhớ cache. Ngược lại, chúng ta tải xuống lại và đặt nó vào bộ nhớ cache để sử dụng lại nếu cần:

function fetchImg() {
    // abstract function for downloading images...
}

function weakRefCache(fetchImg) { // (1)
    const imgCache = new Map(); // (2)

    return (imgName) => { // (3)
        const cachedImg = imgCache.get(imgName); // (4)

        if (cachedImg?.deref()) { // (5)
            return cachedImg?.deref();
        }

        const newImg = fetchImg(imgName); // (6)
        imgCache.set(imgName, new WeakRef(newImg)); // (7)

        return newImg;
    };
}

const getCachedImg = weakRefCache(fetchImg);

Hãy đi vào chi tiết về những gì đã xảy ra ở đây:
1. weakRefCache – là một hàm bậc cao nhận một hàm khác, fetchImg, làm đối số. Trong ví dụ này, chúng ta có thể bỏ qua mô tả chi tiết về hàm fetchImg, vì nó có thể là bất kỳ logic nào để tải xuống hình ảnh.
2. imgCache – là bộ nhớ cache của các hình ảnh, lưu trữ kết quả đã được lưu vào bộ nhớ cache của hàm fetchImg, dưới dạng các khóa kiểu chuỗi (tên hình ảnh) và các đối tượng WeakRef làm giá trị của chúng.

3.Trả về một hàm vô danh nhận tên hình ảnh làm đối số. Đối số này sẽ được sử dụng làm khóa cho hình ảnh đã được lưu vào bộ nhớ cache.

4.Cố gắng lấy kết quả từ bộ nhớ đệm, sử dụng khóa đã cung cấp (tên hình ảnh).

5. Nếu bộ nhớ đệm chứa giá trị cho khóa đã chỉ định và đối tượng WeakRef chưa bị thu gom bởi bộ thu gom rác, trả về kết quả đã lưu.

6. Nếu không có mục nào trong bộ nhớ đệm với khóa yêu cầu, hoặc phương thức deref() trả về undefined (nghĩa là đối tượng WeakRef đã bị thu gom), hàm fetchImg tải lại hình ảnh.

7. Đưa hình ảnh đã tải vào bộ nhớ đệm dưới dạng đối tượng WeakRef.

Giờ đây, chúng ta có một bộ sưu tập Map, trong đó các khóa là tên hình ảnh dưới dạng chuỗi, và giá trị là các đối tượng WeakRef chứa chính các hình ảnh.

    Kỹ thuật này giúp tránh việc cấp phát một lượng lớn bộ nhớ cho các đối tượng tiêu tốn tài nguyên, mà không còn ai sử dụng nữa.

    Nó cũng tiết kiệm bộ nhớ và thời gian trong trường hợp tái sử dụng các đối tượng đã lưu trong bộ nhớ đệm.

    Đây là một đại diện hình ảnh về cách đoạn mã này trông như thế nào:


    Nhưng, cách triển khai này có nhược điểm của nó: theo thời gian, Map sẽ bị lấp đầy với các chuỗi làm khóa, mà trỏ đến một WeakRef, mà đối tượng tham chiếu của nó đã bị thu gom rác:


    Một cách để xử lý vấn đề này là định kỳ dọn dẹp bộ nhớ đệm và xóa các mục “chết”. Một cách khác là sử dụng các finalizer, mà chúng ta sẽ khám phá tiếp theo.

    4. Ví dụ №2: Sử dụng WeakRef để theo dõi các đối tượng DOM

    Một trường hợp sử dụng khác của WeakRef là theo dõi các đối tượng DOM.
    Hãy tưởng tượng một kịch bản trong đó một số mã hoặc thư viện của bên thứ ba tương tác với các phần tử trên trang của chúng ta miễn là chúng tồn tại trong DOM.

    Ví dụ, có thể là một tiện ích bên ngoài để theo dõi và thông báo về trạng thái của hệ thống (thường được gọi là “logger” – một chương trình gửi các thông điệp thông tin được gọi là “logs”).

    Ví dụ tương tác:

    https://plnkr.co/edit/dRLimhrjNSKQhwhB?p=preview&preview

    Khi nhấn nút “Bắt đầu gửi thông điệp”, trong “cửa sổ hiển thị logs” (một phần tử với lớp .window__body), các thông điệp (logs) bắt đầu xuất hiện.
    Nhưng, ngay khi phần tử này bị xóa khỏi DOM, logger nên ngừng gửi thông điệp. Để tái tạo việc xóa phần tử này, chỉ cần nhấn nút Đóng ở góc trên bên phải.

    Để không làm phức tạp công việc của chúng ta và không thông báo cho mã bên ngoài mỗi khi phần tử DOM của chúng ta có sẵn hay không, chỉ cần tạo một tham chiếu yếu đến nó bằng cách sử dụng WeakRef.

    Khi phần tử bị xóa khỏi DOM, logger sẽ nhận ra điều đó và ngừng gửi thông báo.

    Bây giờ, hãy xem xét mã nguồn một cách chi tiết (tab index.js):

    1. Lấy phần tử DOM của nút “Bắt đầu gửi thông báo”.
    2. Lấy phần tử DOM của nút “Đóng”.
    3. Lấy phần tử DOM của cửa sổ hiển thị nhật ký bằng cách sử dụng constructor new WeakRef(). Bằng cách này, biến windowElementRef giữ một tham chiếu yếu đến phần tử DOM.
    4. Thêm một trình lắng nghe sự kiện vào nút “Bắt đầu gửi thông báo”, chịu trách nhiệm khởi động logger khi được nhấp.
    5. Thêm một trình lắng nghe sự kiện vào nút “Đóng”, chịu trách nhiệm đóng cửa sổ hiển thị nhật ký khi được nhấp.
    6. Sử dụng setInterval để bắt đầu hiển thị một thông báo mới mỗi giây.
    7. Nếu phần tử DOM của cửa sổ hiển thị nhật ký vẫn có thể truy cập và được giữ trong bộ nhớ, tạo và gửi một thông báo mới.
    8. Nếu phương thức deref() trả về undefined, điều đó có nghĩa là phần tử DOM đã bị xóa khỏi bộ nhớ. Trong trường hợp này, logger ngừng hiển thị thông báo và xóa bộ hẹn giờ.
    9. alert, sẽ được gọi sau khi phần tử DOM của cửa sổ hiển thị nhật ký bị xóa khỏi bộ nhớ (tức là sau khi nhấp vào nút “Đóng”). Lưu ý rằng việc xóa khỏi bộ nhớ có thể không xảy ra ngay lập tức, vì nó phụ thuộc vào cơ chế nội bộ của garbage collector.

    Chúng ta không thể điều khiển trực tiếp quá trình này từ mã. Tuy nhiên, bất chấp điều đó, chúng ta vẫn có tùy chọn để buộc garbage collection từ trình duyệt.
    Trong Google Chrome, để thực hiện việc này, bạn cần mở công cụ phát triển (Ctrl + Shift + J trên Windows/Linux hoặc Option + + J trên macOS), chuyển đến tab “Hiệu suất” và nhấp vào biểu tượng thùng rác – “Thu thập rác”:

    Tính năng này được hỗ trợ trong hầu hết các trình duyệt hiện đại. Sau khi thực hiện các thao tác, alert sẽ được kích hoạt ngay lập tức.

    5. FinalizationRegistry

    Bây giờ là lúc để nói về các bộ dọn dẹp (finalizers). Trước khi tiếp tục, hãy làm rõ thuật ngữ:
    Callback dọn dẹp (finalizer) – là một hàm được thực thi khi một đối tượng, đã đăng ký trong FinalizationRegistry, bị xóa khỏi bộ nhớ bởi bộ thu gom rác.
    Mục đích của nó – là cung cấp khả năng thực hiện các thao tác bổ sung liên quan đến đối tượng, sau khi nó đã bị xóa hoàn toàn khỏi bộ nhớ.

    Registry (hoặc FinalizationRegistry) – là một đối tượng đặc biệt trong JavaScript quản lý việc đăng ký và hủy đăng ký các đối tượng và các callback dọn dẹp của chúng.

    Cơ chế này cho phép đăng ký một đối tượng để theo dõi và liên kết một callback dọn dẹp với nó. Về cơ bản, nó là một cấu trúc lưu trữ thông tin về các đối tượng đã đăng ký và các callback dọn dẹp của chúng, và sau đó tự động gọi các callback đó khi các đối tượng bị xóa khỏi bộ nhớ.

    Để tạo một instance của FinalizationRegistry, bạn cần gọi constructor của nó, nhận một tham số duy nhất – callback dọn dẹp (finalizer).

    Cú pháp:

    function cleanupCallback(heldValue) { 
      // cleanup callback code 
    }
    
    const registry = new FinalizationRegistry(cleanupCallback);

    Ở đây:
    cleanupCallback – một callback dọn dẹp sẽ được gọi tự động khi một đối tượng đã đăng ký bị xóa khỏi bộ nhớ.
    heldValue – giá trị được truyền làm đối số cho callback dọn dẹp. Nếu heldValue là một đối tượng, registry giữ một tham chiếu mạnh đến nó.
    registry – một instance của FinalizationRegistry.
    Các phương thức của FinalizationRegistry:
    register(target, heldValue [, unregisterToken]) – được sử dụng để đăng ký các đối tượng trong registry.
    target – đối tượng được đăng ký để theo dõi. Nếu target bị thu gom rác, callback dọn dẹp sẽ được gọi với heldValue làm đối số.
    unregisterToken tùy chọn – một token hủy đăng ký. Nó có thể được truyền để hủy đăng ký một đối tượng trước khi bộ thu gom rác xóa nó. Thông thường, đối tượng target được sử dụng làm unregisterToken, đây là thực hành tiêu chuẩn.

    • unregister(unregisterToken) – phương thức unregister được sử dụng để hủy đăng ký một đối tượng khỏi registry. Nó nhận một tham số – unregisterToken (token hủy đăng ký mà đã được lấy khi đăng ký đối tượng).

    Bây giờ hãy tiếp tục với một ví dụ đơn giản. Chúng ta sẽ sử dụng đối tượng user đã biết và tạo một instance của FinalizationRegistry:

    let user = { name: "John" };
    
    const registry = new FinalizationRegistry((heldValue) => {
      console.log(`${heldValue} has been collected by the garbage collector.`);
    });

    Sau đó, chúng ta sẽ đăng ký đối tượng yêu cầu callback dọn dẹp bằng cách gọi phương thức register:

    registry.register(user, user.name);

    Registry không giữ tham chiếu mạnh đến đối tượng được đăng ký, vì điều này sẽ làm mất mục đích của nó. Nếu registry giữ một tham chiếu mạnh, đối tượng sẽ không bao giờ bị thu gom rác.
    Nếu đối tượng bị xóa bởi bộ thu gom rác, callback dọn dẹp của chúng ta có thể được gọi vào một thời điểm nào đó trong tương lai, với heldValue được truyền cho nó:

    // When the user object is deleted by the garbage collector, the following message will be printed in the console:
    "John has been collected by the garbage collector."

    Cũng có những tình huống mà ngay cả trong các triển khai sử dụng callback dọn dẹp, có khả năng nó sẽ không được gọi.
    Ví dụ:

    • Khi chương trình hoàn toàn kết thúc hoạt động (ví dụ, khi đóng một tab trong trình duyệt).
    • Khi instance của FinalizationRegistry không còn có thể truy cập từ mã JavaScript.
      Nếu đối tượng tạo ra instance của FinalizationRegistry ra khỏi phạm vi hoặc bị xóa, các callback dọn dẹp đã đăng ký trong registry đó cũng có thể không được gọi.

    6. Caching với FinalizationRegistry

    Quay lại ví dụ về cache yếu, chúng ta có thể nhận thấy như sau:
    – Mặc dù các giá trị được bao bọc trong WeakRef đã bị bộ thu gom rác thu thập, vẫn có một vấn đề về “rò rỉ bộ nhớ” dưới dạng các khóa còn lại, mà giá trị của chúng đã bị bộ thu gom rác thu thập.
    Đây là một ví dụ cải tiến về caching sử dụng FinalizationRegistry:

    function fetchImg() {
      // abstract function for downloading images...
    }
    
    function weakRefCache(fetchImg) {
      const imgCache = new Map();
    
      *!*
      const registry = new FinalizationRegistry((imgName) => { // (1)
        const cachedImg = imgCache.get(imgName);
        if (cachedImg && !cachedImg.deref()) imgCache.delete(imgName);
      });
      */!*
    
      return (imgName) => {
        const cachedImg = imgCache.get(imgName);
        
        if (cachedImg?.deref()) {
          return cachedImg?.deref();
        }
    
        const newImg = fetchImg(imgName);
        imgCache.set(imgName, new WeakRef(newImg));
        *!*
        registry.register(newImg, imgName); // (2)
        */!*
    
        return newImg;
      };
    }
    
    const getCachedImg = weakRefCache(fetchImg);

    1. Để quản lý việc dọn dẹp các mục cache “chết”, khi các đối tượng WeakRef liên quan bị bộ thu gom rác thu thập, chúng ta tạo một registry dọn dẹp FinalizationRegistry.
    Điểm quan trọng ở đây là, trong callback dọn dẹp, nên kiểm tra xem mục có bị bộ thu gom rác xóa và không được thêm lại, để không xóa một mục “sống”.

    2. Khi giá trị mới (hình ảnh) được tải xuống và đưa vào cache, chúng ta đăng ký nó trong registry dọn dẹp để theo dõi đối tượng WeakRef.

      Cài đặt này chỉ chứa các cặp khóa/giá trị thực tế hoặc “sống”. Trong trường hợp này, mỗi đối tượng WeakRef được đăng ký trong FinalizationRegistry. Và sau khi các đối tượng được dọn dẹp bởi bộ thu gom rác, callback dọn dẹp sẽ xóa tất cả các giá trị undefined.

      Đây là một đại diện hình ảnh của mã cập nhật:


      Một khía cạnh quan trọng của cài đặt cập nhật là các finalizers cho phép tạo ra các tiến trình song song giữa chương trình chính và các callback dọn dẹp. Trong ngữ cảnh JavaScript, chương trình chính – là mã JavaScript của chúng ta, chạy và thực thi trong ứng dụng hoặc trang web của chúng ta.
      Do đó, từ thời điểm một đối tượng được đánh dấu để xóa bởi bộ thu gom rác cho đến khi callback dọn dẹp thực sự được thực thi, có thể có một khoảng thời gian nhất định. Quan trọng là hiểu rằng trong khoảng thời gian này, chương trình chính có thể thực hiện bất kỳ thay đổi nào đối với đối tượng hoặc thậm chí đưa nó trở lại bộ nhớ.

      Đó là lý do tại sao, trong callback dọn dẹp, chúng ta phải kiểm tra xem một mục có được thêm lại vào cache bởi chương trình chính để tránh xóa các mục “sống”. Tương tự, khi tìm kiếm một khóa trong cache, có thể giá trị đã bị bộ thu gom rác xóa, nhưng callback dọn dẹp vẫn chưa được thực thi.

      Những tình huống như vậy yêu cầu sự chú ý đặc biệt nếu bạn đang làm việc với FinalizationRegistry.

      7. Sử dụng WeakRef và FinalizationRegistry trong thực tế

      Chuyển từ lý thuyết sang thực hành, hãy tưởng tượng một kịch bản thực tế, nơi người dùng đồng bộ hóa ảnh của họ trên thiết bị di động với một dịch vụ đám mây (như iCloud hoặc Google Photos), và muốn xem chúng từ các thiết bị khác. Ngoài chức năng cơ bản của việc xem ảnh, các dịch vụ như vậy cung cấp nhiều tính năng bổ sung, ví dụ:
      – Chỉnh sửa ảnh và hiệu ứng video.
      – Tạo “kỷ niệm” và album.
      – Lắp ghép video từ một loạt các bức ảnh.
      Ở đây, như một ví dụ, chúng ta sẽ sử dụng một triển khai khá nguyên thủy của dịch vụ như vậy. Điểm chính – là để cho thấy một kịch bản có thể xảy ra khi sử dụng WeakRefFinalizationRegistry cùng nhau trong thực tế.
      Đây là những gì nó trông như thế nào:



      Bên trái là một thư viện ảnh đám mây (được hiển thị dưới dạng hình thu nhỏ). Chúng ta có thể chọn những bức ảnh cần thiết và tạo một bức tranh ghép, bằng cách nhấp vào nút “Tạo tranh ghép” ở phía bên phải của trang. Sau đó, bức tranh ghép có thể được tải xuống dưới dạng hình ảnh.
      Để tăng tốc độ tải trang, chúng ta nên tải xuống và hiển thị hình thu nhỏ của ảnh ở chất lượng nén. Tuy nhiên, để tạo tranh ghép từ những bức ảnh đã chọn, hãy tải xuống và sử dụng chúng ở chất lượng toàn bộ kích thước.
      Dưới đây, chúng ta có thể thấy rằng kích thước gốc của các hình thu nhỏ là 240×240 pixel. Kích thước này được chọn có mục đích để tăng tốc độ tải. Hơn nữa, chúng ta không cần ảnh toàn bộ kích thước trong chế độ xem trước.




      Giả sử, chúng ta cần tạo một bức tranh ghép từ 4 bức ảnh: chúng ta chọn chúng và sau đó nhấp vào nút “Tạo tranh ghép”. Ở giai đoạn này, hàm weakRefCache đã biết sẽ kiểm tra xem bức ảnh cần thiết có trong bộ nhớ cache không. Nếu không, nó sẽ tải xuống từ đám mây và lưu vào bộ nhớ cache để sử dụng sau. Điều này xảy ra đối với mỗi bức ảnh đã chọn:

      Chú ý vào đầu ra trong bảng điều khiển, bạn có thể thấy bức ảnh nào đã được tải xuống từ đám mây – điều này được chỉ định bằng FETCHED_IMAGE.
      Vì đây là lần đầu tiên tạo tranh ghép, điều này có nghĩa là ở giai đoạn này, bộ nhớ cache yếu vẫn còn trống và tất cả các bức ảnh đều được tải xuống từ đám mây và lưu vào đó.
      Nhưng, cùng với quá trình tải xuống hình ảnh, cũng có một quá trình dọn dẹp bộ nhớ bởi garbage collector. Điều này có nghĩa là, đối tượng lưu trữ trong bộ nhớ cache, mà chúng ta tham chiếu bằng cách sử dụng một liên kết yếu, sẽ bị garbage collector xóa. Và finalizer của chúng ta sẽ thực thi thành công, do đó xóa khóa mà hình ảnh được lưu trong bộ nhớ cache. Thông báo CLEANED_IMAGE cho chúng ta biết về điều đó:



      Tiếp theo, chúng ta nhận ra rằng chúng ta không thích bức tranh ghép kết quả và quyết định thay đổi một trong các hình ảnh và tạo một cái mới. Để làm điều này, chỉ cần bỏ chọn hình ảnh không cần thiết, chọn một hình ảnh khác và nhấp vào nút “Tạo tranh ghép” lần nữa:



      Nhưng lần này không phải tất cả các hình ảnh đều được tải xuống từ mạng, và một số hình ảnh đã được lấy từ bộ nhớ cache yếu: thông báo CACHED_IMAGE cho chúng ta biết điều đó. Điều này có nghĩa là tại thời điểm tạo tranh ghép, garbage collector vẫn chưa xóa hình ảnh của chúng tôi, và chúng tôi đã lấy nó từ bộ nhớ cache, qua đó giảm số lượng yêu cầu mạng và tăng tốc quá trình tạo tranh ghép:




      Hãy “thử nghiệm” thêm một chút nữa, bằng cách thay thế một trong các hình ảnh và tạo một bức tranh ghép mới:




      Lần này kết quả còn ấn tượng hơn. Trong số 4 hình ảnh đã chọn, 3 hình ảnh được lấy từ bộ nhớ cache yếu, và chỉ có một hình ảnh cần tải xuống từ mạng. Giảm tải mạng khoảng 75%. Ấn tượng phải không?


      Tất nhiên, điều quan trọng là phải nhớ rằng hành vi như vậy không được đảm bảo và phụ thuộc vào cách triển khai và hoạt động cụ thể của garbage collector.
      Dựa trên điều này, một câu hỏi hoàn toàn hợp lý ngay lập tức phát sinh: tại sao chúng ta không sử dụng một bộ nhớ đệm thông thường, nơi chúng ta có thể quản lý các thực thể của nó theo cách của mình, thay vì phụ thuộc vào bộ thu gom rác? Đúng vậy, trong phần lớn các trường hợp, không cần sử dụng WeakRefFinalizationRegistry.

      Ở đây, chúng tôi chỉ đơn giản là trình bày một cách thực hiện thay thế cho chức năng tương tự, sử dụng một phương pháp không đơn giản với các tính năng ngôn ngữ thú vị. Tuy nhiên, chúng ta không thể dựa vào ví dụ này nếu cần một kết quả liên tục và có thể dự đoán được.

      Bạn có thể mở ví dụ này trong sandbox.

      8. Tóm tắt

      WeakRef – được thiết kế để tạo các tham chiếu yếu đến các đối tượng, cho phép chúng bị xóa khỏi bộ nhớ bởi bộ thu gom rác nếu không còn tham chiếu mạnh nào đến chúng. Điều này có lợi cho việc giải quyết tình trạng sử dụng bộ nhớ quá mức và tối ưu hóa việc sử dụng tài nguyên hệ thống trong các ứng dụng.
      FinalizationRegistry – là một công cụ để đăng ký các callback, được thực hiện khi các đối tượng không còn được tham chiếu mạnh, bị tiêu hủy.

      Hy vọng rằng bài viết trên của Cafedev đã giúp bạn nắm bắt được cách sử dụng `WeakRef` và `FinalizationRegistry` trong JavaScript để quản lý bộ nhớ hiệu quả hơn. Những công cụ này không chỉ giúp tối ưu hóa hiệu suất ứng dụng mà còn mở ra cơ hội cho những giải pháp lập trình tiên tiến. Hãy tiếp tục theo dõi Cafedev để cập nhật thêm nhiều kiến thức hữu ích và nâng cao kỹ năng lập trình của bạn!

      Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Javascrypt chi tiết, dễ hiểu từ cơ bản tới nâng cao + Full Bài Tập thực hành nâng cao trình dev

      Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

      Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

      Chào thân ái và quyết thắng!

      Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!