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 một 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 hiển thị. Trong trình xử lý sự kiện, sau đó, bạn sẽ phải tìm ra những phần tử bị ảnh hưởng bởi thay đổi đó và gọi một quy trình cụ thể để phản ứng một cách 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 đơ máy nếu bạn không xử lý việc nhóm tất cả các lượt đọc và tất cả các lượt ghi.

Điều này thậm chí không bao gồm các trường hợp mà các phần tử thay đổi kích thước mà không cần 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 phần tử 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 phần tử, các phần tử cùng cấp hoặc các phần tử tổ tiên của phần tử đó.

Đây là lý do khiến ResizeObserver trở thành một nguyên tắc cơ bản hữu ích. Thành phần này phản ứng với những thay đổi về kích thước của bất kỳ phần tử nào được quan sát, bất kể nguyên nhân gây ra thay đổi là gì. API 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.

Browser Support

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

Source

API

Tất cả các API có hậu tố Observer mà chúng tôi đề cập ở trên đều có một 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ỗi mục nhập cho một phần tử được quan sát) chứa kích thước 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 đượ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 mà bạn có thể đặt nội dung vào. Đây là hộp đường viền trừ đi khoảng đệm.

Sơ đồ 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ả phương diện của contentRect và khoảng đệm, nhưng nó 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, theo báo cáo của getBoundingClientRect(), 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 trường hợp 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ông tin chi tiết hơn. Mỗi thuộc tính này đều 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 gọi lại.

  • borderBoxSize
  • contentBoxSize
  • devicePixelContentBoxSize

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

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

Khi nào thì báo cáo được gửi?

Thông số kỹ thuật 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 bố cục. Điều này khiến lệnh gọi lại của ResizeObserver trở thành vị trí lý tưởng để thay đổi bố cục 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á thao tác vẽ.

Gotcha

Có thể bạn đang 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 theo dõi 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. Rất may, ResizeObserver có một 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 theo chu kỳ. Các thay đổi sẽ chỉ được xử lý trong cùng một khung hình 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ông nhất được xử lý trong lệnh gọi lại trước đó. Nếu không, các thao tác này sẽ được hoãn lại đến khung hình tiếp theo.

Ứng dụng

Một điều mà ResizeObserver cho phép bạn làm 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ể xác định một cách bắt buộc 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 đây, 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 cần xem xét là cửa sổ trò chuyện. Vấn đề phát sinh trong bố cục cuộc trò chuyện từ trên xuống dưới thông thường là vị trí cuộn. Để tránh gây nhầm lẫn cho người dùng, bạn nên đặt cửa sổ ở cuối cuộc trò chuyện, nơi các tin nhắn mới nhất xuất hiện. Ngoài ra, mọi loại thay đổi bố cục (hãy nghĩ đến việc điện thoại chuyển từ chế độ ngang sang chế độ dọc hoặc ngược lại) đều phải đạt được kết quả tương tự.

ResizeObserver cho phép bạn viết một đoạn mã duy nhất để xử lý cả hai trường hợp. Đổ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 sẽ đổi kích thước phần tử đó (trừ phi bạn đặt overflow: hidden), vì nó cần tạo không gian cho các phần tử mới. Với ý tưởng này, bạn chỉ cần một vài 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á hay 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 thao tác cuộn gắn với 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 bất kỳ loại phần tử tuỳ chỉnh nào đang thực hiện 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, nhờ đó các thành phần con có thể được bố trí lại.

Ảnh hưởng đến chỉ số Lượt tương tác đến nội dung hiển thị tiếp theo (INP)

Lượt tương tác đến nội dung hiển thị tiếp theo (INP) là một chỉ số đo lường khả năng phản hồi tổng thể của một trang đối với hoạt động 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à từ 200 mili giây trở xuống), thì có thể nói rằng trang đó phản hồi một cách đáng tin cậy các hoạt động tương tác của người dùng với trang.

Mặc dù khoảng thời gian cần thiết để các 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ể đóng góp đá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 để nội dung hiển thị tiếp theo của hoạt động tương tác diễn ra. Đây là khoảng thời gian cần thiết để hoàn tất quá trình kết xuất cần thiết nhằm cập nhật giao diện người dùng để phản hồi một lượt tương tác.

Đối với ResizeObserver, điều này rất quan trọng vì lệnh gọi lại mà một thực thể ResizerObserver chạy sẽ xảy ra ngay trước khi hoạt động kết xuất diễn ra. Đây là một thiết kế, vì công việc diễn 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 đối với giao diện người dùng.

Hãy cẩn thận để thực hiện ít công việc kết xuất nhất có thể trong lệnh gọi lại ResizeObserver, vì công việc kết xuất quá mức có thể tạo ra những tình huống mà trình duyệt bị chậm trễ trong việc thực hiện công việc quan trọng. Ví dụ: nếu có bất kỳ hoạt động tương tác nào có một 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 thực hiện những thao tác sau để mang lại 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 công việc tính toán lại kiểu dáng quá mức. Việc tính toán lại kiểu diễn ra ngay trước bố cục và các bộ chọn CSS phức tạp có thể trì hoãn 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 lại bắt buộc.
  • Thời gian cần thiết để cập nhật bố cục của một trang thường tăng lên cùng với số lượng phần tử DOM trên trang. Mặc dù điều này đú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 về cấu trúc của một trang tăng lên.

Kết luận

ResizeObserver có trong tất cả các trình duyệt chính 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ử. Tuy nhiên, hãy thận trọng để không trì hoãn quá trình kết xuất quá nhiều bằng API mạnh mẽ này.