ResizeObserver: giống như document.onresize cho các phần tử

ResizeObserver cho bạn biết khi kích thước của một phần tử thay đổi.

Trước ResizeObserver, bạn phải đính kèm trình nghe vào sự kiện resize của tài liệu để nhận thông báo về mọi thay đổi về kích thước của khung nhìn. Trong trình xử lý sự kiện, bạn sẽ phải tìm ra những phần tử nào đã bị thay đổi đó ảnh hưởng và gọi một quy trình cụ thể để phản ứng thích hợp. Nếu cần kích thước mới của một phần tử sau khi đổi kích thước, bạn phải gọi getBoundingClientRect() hoặc getComputedStyle(). Điều này có thể gây ra tình trạng bố cục bị xáo trộn nếu bạn không xử lý việc phân lô tất cả lượt đọc và tất cả lượt ghi.

Điều này thậm chí không bao gồm các trường hợp các phần tử thay đổi kích thước mà không thay đổi kích thước cửa sổ chính. Ví dụ: việc thêm thành phần con mới, đặt kiểu display của một thành phần thành none hoặc các thao tác tương tự có thể thay đổi kích thước của một thành phần, các thành phần con hoặc thành phần mẹ.

Đây là lý do ResizeObserver là một hàm nguyên gốc hữu ích. Nó phản ứng với các thay đổi về kích thước của mọi phần tử được quan sát, bất kể nguyên nhân gây ra thay đổi. Phương thức này cũng cung cấp quyền truy cập vào kích thước mới của các phần tử được quan sát.

Hỗ trợ trình duyệt

  • Chrome: 64.
  • Edge: 79.
  • Firefox: 69.
  • Safari: 13.1.

Nguồn

API

Tất cả các API có hậu tố Observer mà chúng ta đã đề cập ở trên đều có thiết kế API đơn giản. ResizeObserver cũng không ngoại lệ. Bạn tạo một đối tượng ResizeObserver và truyền một lệnh gọi lại đến hàm khởi tạo. Lệnh gọi lại được truyền một mảng các đối tượng ResizeObserverEntry (một mục nhập cho mỗi phần tử được quan sát) chứa các phương diện mới cho phần tử đó.

var ro = new ResizeObserver(entries => {
  for (let entry of entries) {
    const cr = entry.contentRect;

    console.log('Element:', entry.target);
    console.log(`Element size: ${cr.width}px x ${cr.height}px`);
    console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
  }
});

// Observe one or multiple elements
ro.observe(someElement);

Một số thông tin chi tiết

Nội dung nào đang được báo cáo?

Nhìn chung, ResizeObserverEntry sẽ báo cáo hộp nội dung của một phần tử thông qua một thuộc tính có tên là contentRect. Thuộc tính này sẽ trả về một đối tượng DOMRectReadOnly. Hộp nội dung là hộp có thể chứa nội dung. Đây là hộp đường viền trừ đi khoảng đệm.

Sơ đồ về mô hình hộp CSS.

Điều quan trọng cần lưu ý là mặc dù ResizeObserver báo cáo cả kích thước của contentRect và khoảng đệm, nhưng chỉ theo dõi contentRect. Đừng nhầm lẫn contentRect với hộp giới hạn của phần tử. Hộp giới hạn, như được getBoundingClientRect() báo cáo, là hộp chứa toàn bộ phần tử và các phần tử con của phần tử đó. SVG là một ngoại lệ đối với quy tắc này, trong đó ResizeObserver sẽ báo cáo kích thước của hộp giới hạn.

Kể từ Chrome 84, ResizeObserverEntry có 3 thuộc tính mới để cung cấp thêm thông tin chi tiết. Mỗi thuộc tính trong số này trả về một đối tượng ResizeObserverSize chứa một thuộc tính blockSize và một thuộc tính inlineSize. Thông tin này là về phần tử được quan sát tại thời điểm lệnh gọi lại được gọi.

  • borderBoxSize
  • contentBoxSize
  • devicePixelContentBoxSize

Tất cả các mục này đều trả về mảng chỉ có thể đọc vì trong tương lai, chúng tôi hy vọng rằng các mục này có thể hỗ trợ các phần tử có nhiều mảnh, xảy ra trong các trường hợp nhiều cột. Hiện tại, các mảng này sẽ chỉ chứa một phần tử.

Khả năng hỗ trợ nền tảng cho các thuộc tính này còn hạn chế, nhưng Firefox đã hỗ trợ hai thuộc tính đầu tiên.

Khi nào sự cố được báo cáo?

Quy cách này quy định rằng ResizeObserver phải xử lý tất cả các sự kiện đổi kích thước trước khi vẽ và sau khi bố cục. Điều này khiến lệnh gọi lại của ResizeObserver trở thành nơi lý tưởng để thực hiện các thay đổi đối với bố cục của trang. Vì quá trình xử lý ResizeObserver diễn ra giữa bố cục và vẽ, nên việc này sẽ chỉ vô hiệu hoá bố cục chứ không vô hiệu hoá bản vẽ.

Tiếng Đúng

Bạn có thể tự hỏi điều gì sẽ xảy ra nếu tôi thay đổi kích thước của một phần tử được quan sát bên trong lệnh gọi lại thành ResizeObserver? Câu trả lời là: bạn sẽ kích hoạt ngay một lệnh gọi khác đến lệnh gọi lại. May mắn là ResizeObserver có cơ chế để tránh các vòng lặp gọi lại vô hạn và các phần phụ thuộc tuần hoàn. Các thay đổi sẽ chỉ được xử lý trong cùng một khung nếu phần tử được đổi kích thước nằm sâu hơn trong cây DOM so với phần tử nằm trên cùng được xử lý trong lệnh gọi lại trước đó. Nếu không, các lượt chuyển đổi sẽ bị trì hoãn sang khung hình tiếp theo.

Ứng dụng

ResizeObserver cho phép bạn làm một việc là triển khai các truy vấn nội dung nghe nhìn theo từng phần tử. Bằng cách quan sát các phần tử, bạn có thể bắt buộc phải xác định các điểm ngắt thiết kế và thay đổi kiểu của một phần tử. Trong ví dụ sau, hộp thứ hai sẽ thay đổi bán kính đường viền theo chiều rộng của hộp.

const ro = new ResizeObserver(entries => {
  for (let entry of entries) {
    entry.target.style.borderRadius =
        Math.max(0, 250 - entry.contentRect.width) + 'px';
  }
});
// Only observe the second box
ro.observe(document.querySelector('.box:nth-child(2)'));

Một ví dụ thú vị khác mà bạn có thể xem xét là cửa sổ trò chuyện. Vấn đề phát sinh trong bố cục cuộc trò chuyện thông thường từ trên xuống là vị trí cuộn. Để tránh gây nhầm lẫn cho người dùng, bạn nên cố định cửa sổ ở cuối cuộc trò chuyện, nơi xuất hiện các tin nhắn mới nhất. Ngoài ra, mọi thay đổi về bố cục (nghĩ rằng điện thoại chuyển từ ngang sang dọc hoặc ngược lại) đều phải đạt được như nhau.

ResizeObserver cho phép bạn viết một đoạn mã duy nhất để xử lý cả hai trường hợp. Việc đổi kích thước cửa sổ là một sự kiện mà ResizeObserver có thể ghi lại theo định nghĩa, nhưng việc gọi appendChild() cũng đổi kích thước phần tử đó (trừ khi bạn đặt overflow: hidden), vì cần tạo không gian cho các phần tử mới. Do đó, bạn chỉ cần rất ít dòng để đạt được hiệu ứng mong muốn:

const ro = new ResizeObserver(entries => {
  document.scrollingElement.scrollTop =
    document.scrollingElement.scrollHeight;
});

// Observe the scrollingElement for when the window gets resized
ro.observe(document.scrollingElement);

// Observe the timeline to process new messages
ro.observe(timeline);

Khá gọn gàng phải không?

Từ đây, tôi có thể thêm mã để xử lý trường hợp người dùng đã cuộn lên theo cách thủ công và muốn cuộn dính vào thông báo đó khi có thông báo mới.

Một trường hợp sử dụng khác là cho mọi loại thành phần tuỳ chỉnh đang tạo bố cục riêng. Cho đến ResizeObserver, không có cách nào đáng tin cậy để nhận thông báo khi kích thước của thành phần này thay đổi để có thể bố trí lại các thành phần con.

Ảnh hưởng đến lượt tương tác với nội dung hiển thị tiếp theo (INP)

Lượt tương tác với nội dung hiển thị tiếp theo (INP) là chỉ số đo lường khả năng phản hồi tổng thể của một trang đối với các lượt tương tác của người dùng. Nếu INP của một trang nằm trong ngưỡng "tốt" (tức là 200 mili giây trở xuống), thì có thể nói rằng trang đó phản hồi đáng tin cậy với các lượt tương tác của người dùng.

Mặc dù khoảng thời gian cần thiết để lệnh gọi lại sự kiện chạy nhằm phản hồi một lượt tương tác của người dùng có thể góp phần đáng kể vào tổng độ trễ của một lượt tương tác, nhưng đó không phải là khía cạnh duy nhất của INP cần xem xét. INP cũng xem xét khoảng thời gian cần thiết để lần hiển thị tiếp theo của tương tác xảy ra. Đây là khoảng thời gian cần thiết để công việc kết xuất cập nhật giao diện người dùng nhằm phản hồi một lượt tương tác hoàn tất.

Trong trường hợp liên quan đến ResizeObserver, việc này rất quan trọng vì lệnh gọi lại mà thực thể ResizerObserver chạy xảy ra ngay trước để kết xuất công việc. Điều này là do thiết kế, vì công việc xảy ra trong lệnh gọi lại phải được tính đến, vì kết quả của công việc đó rất có thể sẽ yêu cầu thay đổi giao diện người dùng.

Cẩn thận thực hiện ít thao tác kết xuất theo yêu cầu trong lệnh gọi lại ResizeObserver, vì quá trình kết xuất quá mức có thể xảy ra tình huống trình duyệt bị chậm trễ khi thực hiện các công việc quan trọng. Ví dụ: nếu bất kỳ lượt tương tác nào có lệnh gọi lại khiến lệnh gọi lại ResizeObserver chạy, hãy đảm bảo bạn đang làm như sau để tạo điều kiện cho trải nghiệm mượt mà nhất có thể:

  • Đảm bảo bộ chọn CSS của bạn đơn giản nhất có thể để tránh phải tính toán lại kiểu quá nhiều. Việc tính toán lại kiểu xảy ra ngay trước bố cục và bộ chọn CSS phức tạp có thể làm chậm các thao tác bố cục.
  • Tránh thực hiện bất kỳ thao tác nào trong lệnh gọi lại ResizeObserver có thể kích hoạt luồng dữ liệu buộc lại.
  • Thời gian cần thiết để cập nhật bố cục của trang thường tăng lên theo số lượng phần tử DOM trên trang. Mặc dù điều này luôn đúng cho dù các trang có sử dụng ResizeObserver hay không, nhưng công việc được thực hiện trong lệnh gọi lại ResizeObserver có thể trở nên đáng kể khi độ phức tạp của cấu trúc trang tăng lên.

Kết luận

ResizeObserver có sẵn trong tất cả trình duyệt lớn và cung cấp một cách hiệu quả để theo dõi việc đổi kích thước phần tử ở cấp phần tử. Bạn chỉ cần thận trọng để không trì hoãn việc kết xuất quá nhiều với API mạnh mẽ này.