How to properly set scrollLeft in two objects usin

2019-06-16 21:36发布

问题:

In my page I have two tables of same width, and them both do horizontal scroll. I need to set both tables to the same position when each one scrolls.

Actually, my code is:

var scrollA = $('#scrollA'),
    scrollB = $('#scrollB');

scrollA.on('scroll', function() {
    scrollB[0].scrollLeft = scrollA[0].scrollLeft;
});

It works. The problem is that there are situations where the loaded data is big enough to slow the browser and the scroll event.

Then, I'm trying to improve the user experience in those situations.


I did this snippet where I try to use the __defineSetter__ function of Object:

var elementA = { scrollLeft: 30 },
    elementB = { scrollLeft: 30 };

elementA.__defineSetter__('scrollLeft', val => elementB.scrollLeft = val);

elementA.scrollLeft += 5;
console.log(elementA.scrollLeft + ' == ' + elementB.scrollLeft);

But as you can see running the snippet, it sets undefined to elementA.scrollLeft and NaN to elementB.scrollLeft.

I'd like some guidance here, thanks for your time.

回答1:

Scroll events can fire at a high rate, so you may get smoother scrolling by throttling the scroll event handler, as trincot said in his answer. You can throttle a function using setTimeout. When moving things on the screen or dealing with other visible effects, though, I prefer requestAnimationFrame.

requestAnimationFrame runs the callback before the next repaint, so the browser can do some optimizations.

This code will get you closer to the solution, it is based on this example from MDN:

var scrollA = $('#scrollA'),
    scrollB = $('#scrollB'),
    running = false;

scrollA.on('scroll', function() {
  // if we already requested an animation frame, do nothing
  if (!running) {
    window.requestAnimationFrame(function() {
      // synchronize table B and table A
      scrollB[0].scrollLeft = scrollA[0].scrollLeft
      // we're done here and can request a new frame
      running = false;
    });
  }

  running = true;
});

You can use the timestamp passed to the callback to further debounce the synchronization, but you may end with a kinda sluggish scroll in the second table if you put too much delay.



回答2:

Your call to __defineSetter__ replaces the property scrollLeft with one with the same name, which means you cannot get to the original property any more. Once you have called __defineSetter__, any reading of the property results in undefined:

console.log(scrollA[0].scrollLeft); // some number, like 10
scrollA[0].__defineSetter__('scrollLeft', val => scrollB[0].scrollLeft = val)
console.log(scrollA[0].scrollLeft); // undefined

Also, the browser does not change the scrollLeft value via an assignment as you would do in JavaScript, so the setter function will not be called during a scroll event. You can verify this by putting a console.log in the setter function: nothing is displayed when you scroll:

scrollA[0].__defineSetter__('scrollLeft', val => console.log(val))

For the above two reasons there is no hope you can capture scrolling changes using __defineSetter__. You need to listen to the scroll event like you did first.

You might get better results if you do the synchronisation asynchronously, and skip some of the updates when you have many scroll events occurring:

var scrollA = $('#scrollA'),
    scrollB = $('#scrollB'),
    tmr = -1;

scrollA.on('scroll', function() {
    clearTimeout(tmr);
    tmr = setTimeout(_ => scrollB[0].scrollLeft = scrollA[0].scrollLeft, 0);
});

Now when the browser generates more than one scroll event instantly, the second one will pre-empty your scheduled update of scrollB[0].scrollLeft: the clearTimeout will prevent that update from happening, and the next one is scheduled instead.

You could play with the delay argument of setTimeout to find the right balance:

Increasing the delay will prevent events from stacking up, and so scrollB will then synchronise in a more responsive way, but fewer updates will be made to scrollB's scrollLeft property, which can make it scroll less fluently.



回答3:

Don't do it. Make it one table. Keep it simple. See example.

<!DOCTYPE html>
<html>
<head>
<style>
table {
    font-family: arial, sans-serif;
    border-collapse: collapse;
    width: 2000px;
}

td, th {
    border: 1px solid #dddddd;
    text-align: left;
    padding: 8px;
}

tr:nth-child(even) {
    background-color: #dddddd;
}
</style>
</head>
<body>

<table>
  <tr>
    <th>Company</th>
    <th>Contact</th>
    <th>Country</th>
  </tr>
  <tr>
    <td>Alfreds Futterkiste</td>
    <td>Maria Anders</td>
    <td>Germany</td>
  </tr>
  <tr>
    <td>Centro comercial Moctezuma</td>
    <td>Francisco Chang</td>
    <td>Mexico</td>
  </tr>
  <tr>
    <th>Ernst Handel</th>
    <th>Roland Mendel</th>
    <th>Austria</th>
  </tr>
  <tr>
    <td>Island Trading</td>
    <td>Helen Bennett</td>
    <td>UK</td>
  </tr>
  <tr>
    <td>Laughing Bacchus Winecellars</td>
    <td>Yoshi Tannamuri</td>
    <td>Canada</td>
  </tr>
  <tr>
    <td>Magazzini Alimentari Riuniti</td>
    <td>Giovanni Rovelli</td>
    <td>Italy</td>
  </tr>
</table>

</body>
</html>