Thuật toán cuộn - cải thiện tìm nạp và hiển thị dữ liệu


8

Tôi muốn đặt ra một phần của một vấn đề lý thuyết.

Giả sử rằng tôi có một cuộn vô hạn, đã triển khai một cái gì đó như được mô tả ở đây: https://medium.com/frontend-jTHERs/how-virtual-infinite-scrolling-works-239f7ee5aa58 . Không có gì lạ mắt với nó, đủ để nói rằng đó là một bảng dữ liệu, nói NxN, và người dùng có thể cuộn xuống và sang phải, giống như một bảng tính và nó sẽ chỉ hiển thị dữ liệu trong chế độ xem hiện tại cộng với một xử lý.

Bây giờ, chúng ta cũng nói rằng phải mất khoảng 10ms để "tìm nạp và hiển thị" dữ liệu trong chế độ xem đó, với một chức năng như:

get_data(start_col, end_col, start_row, end_row);

Điều này tải ngay lập tức khi nhấp vào một nơi nào đó trong thanh cuộn hoặc thực hiện một 'cuộn nhẹ' để hiển thị dữ liệu cần thiết. Tuy nhiên, chúng ta cũng giả sử rằng với mỗi 'sự kiện tìm nạp chưa hoàn thành', sẽ mất gấp đôi thời gian để hiển thị dữ liệu xem cần thiết (do bộ nhớ, gc và một số thứ khác). Vì vậy, nếu tôi cuộn từ trái sang phải theo kiểu cố tình chậm, tôi có thể tạo ra hơn 100 sự kiện cuộn sẽ kích hoạt việc tải dữ liệu - lúc đầu không có độ trễ đáng chú ý. Quá trình tìm nạp xảy ra trong vòng dưới 10ms, nhưng chẳng mấy chốc, nó bắt đầu mất 20ms và sau đó là 40ms, và bây giờ chúng ta có một cái gì đó giống như độ trễ đáng chú ý, cho đến khi nó đạt đến một giây để tải dữ liệu cần thiết. Ngoài ra, chúng tôi không thể sử dụng một cái gì đó như gỡ lỗi / trì hoãn,

Những cân nhắc nào tôi cần phải tính đến và thuật toán mẫu sẽ trông như thế nào để thực hiện điều này? Dưới đây là một ví dụ về tương tác người dùng mà tôi muốn có trên dữ liệu, giả sử bảng tính 10000 x 10000 (mặc dù Excel có thể tải tất cả dữ liệu cùng một lúc) - https://gyazo.com/0772f941f43f9d14f884b7afeac9f414 .


Không bao giờ có nhiều hơn một yêu cầu trong chuyến bay? Khi người dùng cuộn chỉ gửi yêu cầu nếu không có yêu cầu chờ xử lý. Khi bạn nhận được phản hồi cho yêu cầu đang chờ xử lý, nếu cuộn thay đổi kể từ thời điểm bạn gửi yêu cầu cuối cùng, hãy gửi yêu cầu mới.
ybungalobill

Tôi tự hỏi tại sao bạn không chấp nhận câu trả lời đã được đưa ra. Bạn có thể làm rõ lý do tại sao, và những gì bạn đang hy vọng là một câu trả lời?
trincot

@trincot - vâng, đó là một câu trả lời tuyệt vời đồng ý. Ai đó đã chỉnh sửa bài đăng gốc của tôi (xem các chỉnh sửa) trong đó tôi nói "Tôi sẽ trao tiền thưởng vì đây là câu hỏi lý thuyết ..."
samuelbrody1249

1
Điều đó không thực sự trả lời câu hỏi của tôi ...
trincot

1
Một chiến lược khác đáng để xem xét là đệm dữ liệu bảng dựa trên hướng cuộn. Ví dụ: nếu người dùng đang cuộn xuống, thì không chỉ tìm nạp những gì trong chế độ xem mà còn tìm nạp thêm 25-50 hàng nữa để dự đoán người dùng tiếp tục cuộn xuống. Ngoài ra (và tôi nghĩ rằng Yosef ám chỉ điều này) trước khi chế độ xem dữ liệu của bạn tiêu thụ dữ liệu được đệm, đệm thêm dữ liệu (để bạn luôn có 25-50 hàng được đệm) trong khi người dùng đang cuộn. Dữ liệu bổ sung này có thể sẽ thêm ít chi phí đã tham gia vào chuyến đi khứ hồi ...
Jon Trent

Câu trả lời:


3

Tôi nghĩ bạn không nên gửi yêu cầu tại bất kỳ sự kiện cuộn nào. chỉ khi cuộn này, người dùng đến cuối cuộn.

if(e.target.scrollHeight - e.target.offsetHeight === 0) {
    // the element reach the end of vertical scroll
}
if(e.target.scrollWidth - e.target.offsetWidth === 0) {
   // the element reach the end of horizontal scroll
}

Bạn cũng có thể chỉ định chiều rộng sẽ được xác định là đủ gần để tìm nạp dữ liệu mới (ei e.target.scrollHeight - e.target.offsetHeight <= 150)


1

Lý thuyết và thực hành: Trong lý thuyết không có sự khác biệt giữa lý thuyết và thực hành, nhưng trong thực tế thì có.

  • Lý thuyết: mọi thứ đều rõ ràng, nhưng không có gì hoạt động;
  • Thực hành: mọi thứ đều hoạt động, nhưng không có gì rõ ràng;
  • Đôi khi lý thuyết đáp ứng thực tiễn: không có gì hoạt động và không có gì rõ ràng.

Đôi khi cách tiếp cận tốt nhất là một nguyên mẫu, và thấy vấn đề thú vị, tôi đã dành một ít thời gian để nấu một cái, mặc dù là một nguyên mẫu, nó thừa nhận có nhiều mụn cóc ...

Nói tóm lại, giải pháp đơn giản nhất để hạn chế tồn đọng các lần tìm nạp dữ liệu dường như chỉ đơn giản là thiết lập sự thay đổi của một người nghèo trong thói quen thực hiện việc tìm nạp. (Trong ví dụ mã bên dưới, hàm tìm nạp mô phỏng là simulateFetchOfData.) Mutex liên quan đến việc thiết lập một biến ngoài phạm vi hàm sao cho nếu falsetìm nạp được mở để sử dụng và nếu truequá trình tìm nạp hiện đang diễn ra.

Đó là, khi người dùng điều chỉnh thanh trượt ngang hoặc dọc để bắt đầu tìm nạp dữ liệu, chức năng tìm nạp dữ liệu trước tiên sẽ kiểm tra xem biến toàn cục mutexcó đúng không (nghĩa là quá trình tìm nạp đã được thực hiện) và nếu vậy, chỉ cần thoát . Nếu mutexkhông đúng, thì nó được đặt mutexthành đúng và sau đó tiếp tục thực hiện tìm nạp. Và tất nhiên, ở cuối chức năng tìm nạp, mutexđược đặt thành false, sao cho sự kiện đầu vào của người dùng tiếp theo sau đó sẽ đi qua phía trước kiểm tra mutex và thực hiện một lần tìm nạp khác ...

Một vài lưu ý về nguyên mẫu.

  • Trong simulateFetchOfDatachức năng, có chế độ ngủ (100) được cấu hình là một Lời hứa mô phỏng độ trễ trong việc truy xuất dữ liệu. Điều này được kẹp với một số đăng nhập vào giao diện điều khiển. Nếu bạn loại bỏ kiểm tra mutex, bạn sẽ thấy với giao diện điều khiển mở trong khi di chuyển các thanh trượt, nhiều trường hợp simulateFetchOfDatađược khởi tạo và hồi hộp chờ trong khi ngủ (nghĩa là tìm nạp dữ liệu mô phỏng) để giải quyết, trong khi với kiểm tra mutex tại chỗ, chỉ có một trường hợp được bắt đầu tại một thời điểm.
  • Thời gian ngủ có thể được điều chỉnh để mô phỏng độ trễ của mạng hoặc cơ sở dữ liệu lớn hơn, do đó bạn có thể cảm nhận được trải nghiệm người dùng. Ví dụ: các mạng tôi đang trải nghiệm độ trễ 90ms cho các giao dịch trên khắp lục địa Hoa Kỳ.
  • Một điều đáng chú ý khác là khi hoàn tất quá trình tìm nạp và sau khi đặt lại mutexthành false, kiểm tra được thực hiện để xác định xem các giá trị cuộn ngang và dọc có thẳng hàng hay không. Nếu không, một lần tìm nạp khác được bắt đầu. Điều này đảm bảo rằng mặc dù một số sự kiện cuộn có thể không kích hoạt do quá trình tìm nạp đang bận, nhưng tối thiểu các giá trị cuộn cuối cùng được xử lý bằng cách kích hoạt một lần tìm nạp cuối cùng.
  • Dữ liệu ô mô phỏng chỉ đơn giản là một giá trị chuỗi của số hàng-dash-cột. Ví dụ: "555-333" biểu thị hàng 555 cột 333.
  • Một mảng thưa có tên bufferđược sử dụng để giữ dữ liệu "tìm nạp". Kiểm tra nó trong bảng điều khiển sẽ cho thấy nhiều mục "trống x XXXX". Các simulateFetchOfDatachức năng được thiết lập như vậy nếu dữ liệu đã được tổ chức tại bufferthì không "lấy" được thực hiện.

(Để xem nguyên mẫu, chỉ cần sao chép và dán toàn bộ mã vào tệp văn bản mới, đổi tên thành ".html" và mở trong trình duyệt. EDIT: Đã được thử nghiệm trên Chrome và Edge.)

<html><head>

<script>

function initialize() {

  window.rowCount = 10000;
  window.colCount = 5000;

  window.buffer = [];

  window.rowHeight = Array( rowCount ).fill( 25 );  // 20px high rows 
  window.colWidth = Array( colCount ).fill( 70 );  // 70px wide columns 

  var cellAreaCells = { row: 0, col: 0, height: 0, width: 0 };

  window.contentGridCss = [ ...document.styleSheets[ 0 ].rules ].find( rule => rule.selectorText === '.content-grid' );

  window.cellArea = document.getElementById( 'cells' );

  // Horizontal slider will indicate the left most column.
  window.hslider = document.getElementById( 'hslider' );
  hslider.min = 0;
  hslider.max = colCount;
  hslider.oninput = ( event ) => {
    updateCells();
  }

  // Vertical slider will indicate the top most row.
  window.vslider = document.getElementById( 'vslider' );
  vslider.max = 0;
  vslider.min = -rowCount;
  vslider.oninput = ( event ) => {
    updateCells();
  }

  function updateCells() {
    // Force a recalc of the cell height and width...
    simulateFetchOfData( cellArea, cellAreaCells, { row: -parseInt( vslider.value ), col: parseInt( hslider.value ) } );
  }

  window.mutex = false;
  window.lastSkippedRange = null;

  window.addEventListener( 'resize', () => {
    //cellAreaCells.height = 0;
    //cellAreaCells.width = 0;
    cellArea.innerHTML = '';
    contentGridCss.style[ "grid-template-rows" ] = "0px";
    contentGridCss.style[ "grid-template-columns" ] = "0px";

    window.initCellAreaSize = { height: document.getElementById( 'cellContainer' ).clientHeight, width: document.getElementById( 'cellContainer' ).clientWidth };
    updateCells();
  } );
  window.dispatchEvent( new Event( 'resize' ) );

}

function sleep( ms ) {
  return new Promise(resolve => setTimeout( resolve, ms ));
}

async function simulateFetchOfData( cellArea, curRange, newRange ) {

  //
  // Global var "mutex" is true if this routine is underway.
  // If so, subsequent calls from the sliders will be ignored
  // until the current process is complete.  Also, if the process
  // is underway, capture the last skipped call so that when the
  // current finishes, we can ensure that the cells align with the
  // settled scroll values.
  //
  if ( window.mutex ) {
    lastSkippedRange = newRange;
    return;
  }
  window.mutex = true;
  //
  // The cellArea width and height in pixels will tell us how much
  // room we have to fill.
  //
  // row and col is target top/left cell in the cellArea...
  //

  newRange.height = 0;
  let rowPixelTotal = 0;
  while ( newRange.row + newRange.height < rowCount && rowPixelTotal < initCellAreaSize.height ) {
    rowPixelTotal += rowHeight[ newRange.row + newRange.height ];
    newRange.height++;
  }

  newRange.width = 0;
  let colPixelTotal = 0;
  while ( newRange.col + newRange.width < colCount && colPixelTotal < initCellAreaSize.width ) {
    colPixelTotal += colWidth[ newRange.col + newRange.width ];
    newRange.width++;
  }

  //
  // Now the range to acquire is newRange. First, check if this data 
  // is already available, and if not, fetch the data.
  //

  function isFilled( buffer, range ) {
    for ( let r = range.row; r < range.row + range.height; r++ ) {
      for ( let c = range.col; c < range.col + range.width; c++ ) {
        if ( buffer[ r ] == null || buffer[ r ][ c ] == null) {
          return false;
        }
      }
    }
    return true;
  }

  if ( !isFilled( buffer, newRange ) ) {
    // fetch data!
    for ( let r = newRange.row; r < newRange.row + newRange.height; r++ ) {  
      buffer[ r ] = [];
      for ( let c = newRange.col; c < newRange.col + newRange.width; c++ ) {
        buffer[ r ][ c ] = `${r}-${c} data`;
      }
    }
    console.log( 'Before sleep' );
    await sleep(100);
    console.log( 'After sleep' );
  }

  //
  // Now that we have the data, let's load it into the cellArea.
  //

  gridRowSpec = '';
  for ( let r = newRange.row; r < newRange.row + newRange.height; r++ ) {
    gridRowSpec += rowHeight[ r ] + 'px ';
  }

  gridColumnSpec = '';
  for ( let c = newRange.col; c < newRange.col + newRange.width; c++ ) {
    gridColumnSpec += colWidth[ c ] + 'px ';
  }

  contentGridCss.style[ "grid-template-rows" ] = gridRowSpec;
  contentGridCss.style[ "grid-template-columns" ] = gridColumnSpec;

  cellArea.innerHTML = '';

  for ( let r = newRange.row; r < newRange.row + newRange.height; r++ ) {  
    for ( let c = newRange.col; c < newRange.col + newRange.width; c++ ) {
      let div = document.createElement( 'DIV' );
      div.innerText = buffer[ r ][ c ];
      cellArea.appendChild( div );
    }
  }

  //
  // Let's update the reference to the current range viewed and clear the mutex.
  //
  curRange = newRange;

  window.mutex = false;

  //
  // One final step.  Check to see if the last skipped call to perform an update
  // matches with the current scroll bars.  If not, let's align the cells with the
  // scroll values.
  //
  if ( lastSkippedRange ) {
    if ( !( lastSkippedRange.row === newRange.row && lastSkippedRange.col === newRange.col ) ) {
      lastSkippedRange = null;
      hslider.dispatchEvent( new Event( 'input' ) );
    } else {
      lastSkippedRange = null;
    }
  }
}

</script>

<style>

/*

".range-slider" adapted from... https://codepen.io/ATC-test/pen/myPNqW

See https://www.w3schools.com/howto/howto_js_rangeslider.asp for alternatives.

*/

.range-slider-horizontal {
  width: 100%;
  height: 20px;
}

.range-slider-vertical {
  width: 20px;
  height: 100%;
  writing-mode: bt-lr; /* IE */
  -webkit-appearance: slider-vertical;
}

/* grid container... see https://www.w3schools.com/css/css_grid.asp */

.grid-container {

  display: grid;
  width: 95%;
  height: 95%;

  padding: 0px;
  grid-gap: 2px;
  grid-template-areas:
    topLeft column  topRight
    row     cells   vslider
    botLeft hslider botRight;
  grid-template-columns: 50px 95% 27px;
  grid-template-rows: 20px 95% 27px;
}

.grid-container > div {
  border: 1px solid black;
}

.grid-topLeft {
  grid-area: topLeft;
}

.grid-column {
  grid-area: column;
}

.grid-topRight {
  grid-area: topRight;
}

.grid-row {
  grid-area: row;
}

.grid-cells {
  grid-area: cells;
}

.grid-vslider {
  grid-area: vslider;
}

.grid-botLeft {
  grid-area: botLeft;
}

.grid-hslider {
  grid-area: hslider;
}

.grid-botRight {
  grid-area: botRight;
}

/* Adapted from... https://medium.com/evodeck/responsive-data-tables-with-css-grid-3c58ecf04723 */

.content-grid {
  display: grid;
  overflow: hidden;
  grid-template-rows: 0px;  /* Set later by simulateFetchOfData */
  grid-template-columns: 0px;  /* Set later by simulateFetchOfData */
  border-top: 1px solid black;
  border-right: 1px solid black;
}

.content-grid > div {
  overflow: hidden;
  white-space: nowrap;
  border-left: 1px solid black;
  border-bottom: 1px solid black;  
}
</style>


</head><body onload='initialize()'>

<div class='grid-container'>
  <div class='topLeft'> TL </div>
  <div class='column' id='columns'> column </div>
  <div class='topRight'> TR </div>
  <div class='row' id = 'rows'> row </div>
  <div class='cells' id='cellContainer'>
    <div class='content-grid' id='cells'>
      Cells...
    </div>
  </div>
  <div class='vslider'> <input id="vslider" type="range" class="range-slider-vertical" step="1" value="0" min="0" max="0"> </div>
  <div class='botLeft'> BL </div>
  <div class='hslider'> <input id="hslider" type="range" class="range-slider-horizontal" step="1" value="0" min="0" max="0"> </div>
  <div class='botRight'> BR </div>
</div>

</body></html>

Một lần nữa, đây là một nguyên mẫu để chứng minh một phương tiện để hạn chế tồn đọng các cuộc gọi dữ liệu không cần thiết. Nếu điều này được tái cấu trúc cho mục đích sản xuất, nhiều khu vực sẽ yêu cầu giải quyết, bao gồm: 1) giảm việc sử dụng không gian biến toàn cầu; 2) thêm nhãn hàng và cột; 3) thêm các nút vào thanh trượt để cuộn các hàng hoặc cột riêng lẻ; 4) có thể đệm dữ liệu liên quan, nếu cần tính toán dữ liệu; 5) v.v ...


cảm ơn vì câu trả lời tuyệt vời này và dành thời gian cho câu trả lời này
samuelbrody1249

0

Có một số điều có thể được thực hiện. Tôi thấy nó là một trình xen kẽ hai cấp được đặt giữa thủ tục yêu cầu dữ liệu và sự kiện cuộn người dùng.

1. Trì hoãn xử lý sự kiện cuộn

Bạn đã đúng, gỡ lỗi không phải là bạn của chúng tôi trong các vấn đề liên quan đến cuộn. Nhưng có một cách đúng đắn để giảm số lượng các vụ cháy.

Sử dụng phiên bản điều chỉnh của trình xử lý sự kiện cuộn sẽ được gọi nhiều nhất một lần cho mỗi khoảng thời gian cố định. Bạn có thể sử dụng van tiết lưu hoặc thực hiện phiên bản riêng [ 1 ], [ 2 ], [ 3 ]. Đặt 40 - 100 ms làm giá trị khoảng. Bạn cũng sẽ cần phải đặt trailingtùy chọn để sự kiện cuộn cuối cùng được xử lý bất kể khoảng thời gian hẹn giờ.

2. Luồng dữ liệu thông minh

Khi trình xử lý sự kiện cuộn được gọi, quy trình yêu cầu dữ liệu sẽ được bắt đầu. Như bạn đã đề cập, thực hiện nó mỗi khi một sự kiện cuộn xảy ra (ngay cả khi chúng ta đã hoàn thành việc điều chỉnh) có thể gây ra thời gian trễ. Có thể có một số chiến lược phổ biến: 1) không yêu cầu dữ liệu nếu có một yêu cầu đang chờ xử lý khác; 2) yêu cầu dữ liệu không quá một lần trong một khoảng thời gian; 3) hủy yêu cầu chờ xử lý trước đó.

Cách tiếp cận thứ nhất và thứ hai không hơn gì việc gỡ rối và điều chỉnh ở cấp luồng dữ liệu. Việc gỡ lỗi có thể được thực hiện với những nỗ lực tối thiểu chỉ với một điều kiện trước khi bắt đầu yêu cầu + một yêu cầu bổ sung cuối cùng. Nhưng tôi tin rằng van tiết lưu là hình thức phù hợp hơn theo quan điểm của UX. Ở đây bạn sẽ cần cung cấp một số logic và đừng quên trailingtùy chọn vì nó nên có trong trò chơi.

Cách tiếp cận cuối cùng (hủy yêu cầu) cũng thân thiện với UX nhưng ít cẩn thận hơn so với phương pháp điều chỉnh. Dù sao thì bạn cũng bắt đầu yêu cầu nhưng vứt bỏ kết quả của nó nếu yêu cầu khác đã được bắt đầu sau yêu cầu này. Bạn cũng có thể cố gắng hủy bỏ yêu cầu nếu bạn đang sử dụng fetch.

Theo tôi, lựa chọn tốt nhất sẽ là kết hợp các chiến lược (2) và (3), vì vậy bạn chỉ yêu cầu dữ liệu nếu một khoảng thời gian cố định đã trôi qua kể từ khi bắt đầu yêu cầu trước VÀ bạn hủy yêu cầu nếu một yêu cầu khác được bắt đầu sau .


0

Không có thuật toán cụ thể nào trả lời câu hỏi này, nhưng để không bị tích tụ độ trễ, bạn cần đảm bảo hai điều:

1. Không rò rỉ bộ nhớ

Hãy chắc chắn rằng không có gì trong ứng dụng của bạn tạo ra các phiên bản mới của các đối tượng, lớp, mảng, v.v. Bộ nhớ sẽ giống nhau sau khi cuộn khoảng 10 giây như trong 60 giây, v.v. Bạn có thể phân bổ trước cấu trúc dữ liệu nếu bạn cần (bao gồm các mảng), và sau đó sử dụng lại chúng:

2. Sử dụng lại cấu trúc dữ liệu liên tục

Điều này là phổ biến trong các trang cuộn vô hạn. Trong một bộ sưu tập hình ảnh cuộn vô hạn hiển thị tối đa 30 hình ảnh trên màn hình cùng một lúc, thực tế có thể chỉ có 30-40 <img>yếu tố được tạo. Sau đó chúng được sử dụng và tái sử dụng khi cuộn người dùng, do đó không cần tạo các phần tử HTML mới (hoặc bị hủy và do đó được thu gom rác). Thay vào đó, những hình ảnh này nhận được URL nguồn mới và vị trí mới và người dùng có thể tiếp tục cuộn, nhưng (không biết đến chúng) họ luôn thấy các thành phần DOM giống nhau lặp đi lặp lại.

Nếu bạn đang sử dụng canvas, bạn sẽ không sử dụng các thành phần DOM để hiển thị dữ liệu này, nhưng lý thuyết thì giống nhau, đó chỉ là cấu trúc dữ liệu là của riêng bạn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.