প্রতিক্রিয়া জেএস: দ্বি-নির্দেশমূলক অসীম স্ক্রোলিংয়ের মডেলিং


114

ভিন্ন ভিন্ন আইটেমের বৃহত তালিকাতে নেভিগেট করতে আমাদের অ্যাপ্লিকেশন অসীম স্ক্রোলিং ব্যবহার করে uses কয়েকটি রিঙ্কেল রয়েছে:

  • আমাদের ব্যবহারকারীদের 10,000 টি আইটেমের তালিকা থাকা সাধারণ এবং 3k + এর মাধ্যমে স্ক্রোল করা দরকার।
  • এগুলি সমৃদ্ধ আইটেম, সুতরাং ব্রাউজারের কার্যকারিতা অগ্রহণযোগ্য হওয়ার আগে আমরা কেবলমাত্র ডিওএম-তে কয়েক শত রাখতে পারি।
  • আইটেমগুলি বিভিন্ন উচ্চতা হয়।
  • আইটেমগুলিতে চিত্র থাকতে পারে এবং আমরা ব্যবহারকারীকে একটি নির্দিষ্ট তারিখে ঝাঁপ দেওয়ার অনুমতি দিই। এটি জটিল কারণ ব্যবহারকারী আমাদের তালিকার উপরে অবস্থিত একটি বিন্দুতে যেতে পারেন যেখানে ভিউপোর্টের উপরে চিত্রগুলি লোড করা দরকার, যা তারা লোড করার সময় সামগ্রীটিকে নিচে নামিয়ে দেয়। হ্যান্ডেল করতে ব্যর্থ হওয়ার অর্থ এই যে ব্যবহারকারী কোনও তারিখে লাফিয়ে উঠতে পারে তবে তারপরে আগের তারিখে স্থানান্তরিত হতে পারে।

জ্ঞাত, অসম্পূর্ণ সমাধান:

  • ( প্রতিক্রিয়া-অসীম-স্ক্রোল ) - এটি কেবল একটি সরল "আরও লোড যখন আমরা নীচে আঘাত করি" উপাদানটি। এটি কোনও ডিওএমকে চুরি করে না, তাই এটি হাজার হাজার আইটেমের উপরে মারা যাবে।

  • ( প্রতিক্রিয়া সহ স্ক্রোল অবস্থান ) - শীর্ষে সন্নিবেশ করানো বা নীচে সন্নিবেশ করার সময় কীভাবে স্ক্রোল অবস্থানটি সংরক্ষণ এবং পুনরুদ্ধার করা তা দেখায় , তবে উভয়ই একসাথে নয়।

আমি একটি সম্পূর্ণ সমাধানের জন্য কোডটি খুঁজছি না (যদিও এটি দুর্দান্ত হবে)) পরিবর্তে, আমি এই পরিস্থিতির মডেল করার জন্য "প্রতিক্রিয়া উপায়" খুঁজছি। স্ক্রোল পজিশন রাষ্ট্র বা না? তালিকায় আমার অবস্থান ধরে রাখতে আমার কোন রাজ্যে ট্র্যাক করা উচিত? আমার কোন রাজ্যটি রাখা দরকার যাতে আমি যখন রেন্ডার করা হয় তার নীচে বা শীর্ষের কাছে স্ক্রোল করি যখন আমি একটি নতুন রেন্ডার ট্রিগার করি?

উত্তর:


116

এটি একটি অসীম টেবিল এবং একটি অসীম স্ক্রোল দৃশ্যের মিশ্রণ। আমি এর জন্য সবচেয়ে ভাল বিমূর্ততা খুঁজে পেয়েছি:

সংক্ষিপ্ত বিবরণ

এমন একটি <List>উপাদান তৈরি করুন যা সমস্ত বাচ্চাদের অ্যারে নেয় । যেহেতু আমরা এগুলি রেন্ডার করি না, কেবল এগুলি বরাদ্দ করা এবং এগুলি বাতিল করা সত্যই সস্তা। যদি 10 কে বরাদ্দ খুব বেশি হয়, আপনি পরিবর্তে একটি পরিসর নেওয়া এবং ফাংশনগুলি ফিরিয়ে দিতে পারে এমন একটি ফাংশনটি পাস করতে পারেন।

<List>
  {thousandelements.map(function() { return <Element /> })}
</List>

আপনার Listউপাদানটি স্ক্রোলের অবস্থানটি কী তা ট্র্যাক করছে এবং কেবলমাত্র সেই দৃষ্টিতে শিশুদের রেন্ডার করে। এটি প্রারম্ভিক নয় এমন পূর্ববর্তী আইটেমগুলিকে নকল করতে শুরুতে একটি বড় ফাঁকা ডিভ যুক্ত করে।

এখন, আকর্ষণীয় অংশটি হ'ল একবার Elementউপাদানটি রেন্ডার হয়ে গেলে আপনি এর উচ্চতা পরিমাপ করুন এবং এটি আপনার মধ্যে সংরক্ষণ করুন List। এটি আপনাকে স্পেসারের উচ্চতা গণনা করতে দেয় এবং জানতে পারে যে কতগুলি উপাদান প্রদর্শিত হবে।

ভাবমূর্তি

আপনি বলছেন যে যখন চিত্রটি লোড হচ্ছে তারা সবকিছু "লাফিয়ে" নামিয়ে রাখে। এই জন্য সমাধান আপনার চিত্র ট্যাগে চিত্রের আকারসমূহ সেট করা: <img src="..." width="100" height="58" />। এই আকারটি প্রদর্শিত হতে যাচ্ছে তা জানার আগে ব্রাউজারটি এটি ডাউনলোড করার জন্য অপেক্ষা করতে হবে না। এর জন্য কিছু অবকাঠামো প্রয়োজন তবে এটি সত্যই মূল্যবান।

আপনি যদি আকারটি আগেই জানতে না পারেন তবে onloadআপনার চিত্রটিতে শ্রোতাদের যুক্ত করুন এবং এটি লোড হয়ে গেলে এর প্রদর্শিত মাত্রাটি পরিমাপ করুন এবং সঞ্চিত সারির উচ্চতা আপডেট করুন এবং স্ক্রোলের অবস্থানটি ক্ষতিপূরণ দিন।

একটি এলোমেলো উপাদান এ জাম্পিং

আপনি যদি তালিকার এলোমেলো উপাদানটিতে ঝাঁপিয়ে পড়ার প্রয়োজন হয় যা স্ক্রোল অবস্থানের সাথে কিছু কৌশল প্রয়োজন যা আপনি এর মধ্যে থাকা উপাদানগুলির আকার জানেন না। আমি আপনাকে যা করতে পরামর্শ দিচ্ছি তা হ'ল আপনি যে উপাদানটি ইতিমধ্যে গণনা করেছেন তার উচ্চতা গড়ে তুলুন এবং সর্বশেষ জ্ঞাত উচ্চতা + (উপাদানগুলির সংখ্যা * গড়) এর স্ক্রোল অবস্থানে যান।

যেহেতু এটি যথাযথ নয় এটি যখন আপনি সর্বশেষ জ্ঞাত ভাল অবস্থানে ফিরে আসবেন তখন এটি সমস্যার কারণ হতে পারে। যখন কোনও বিরোধ হয়, কেবল ঠিক করার জন্য স্ক্রোলের অবস্থানটি পরিবর্তন করুন। এটি স্ক্রোল বারটি কিছুটা সরিয়ে চলেছে তবে তাকে / তার খুব বেশি প্রভাবিত করা উচিত নয়।

প্রতিক্রিয়া বৈশিষ্ট্য

আপনি সমস্ত রেন্ডারকারী উপাদানগুলির একটি কী সরবরাহ করতে চান যাতে সেগুলি রেন্ডার জুড়ে বজায় থাকে। দুটি কৌশল রয়েছে: (1) কেবলমাত্র এন কী (0, 1, 2, ... n) রয়েছে যেখানে n সর্বাধিক সংখ্যক উপাদানগুলির মধ্যে আপনি তাদের অবস্থানের মডুলো এন প্রদর্শন করতে এবং ব্যবহার করতে পারবেন। (2) উপাদান প্রতি পৃথক কী আছে। যদি সমস্ত উপাদান একই ধরণের কাঠামো ভাগ করে নেয় তবে তাদের ডম নোডগুলি পুনরায় ব্যবহার করতে (1) ব্যবহার করা ভাল। তারা যদি না ব্যবহার করে (2)।

আমার কেবল দুটি প্রতিক্রিয়া স্থিতি থাকবে: প্রথম উপাদানটির সূচক এবং প্রদর্শিত উপাদানের সংখ্যা। বর্তমান স্ক্রোল অবস্থান এবং সমস্ত উপাদানগুলির উচ্চতা সরাসরি সংযুক্ত থাকবে thissetStateআপনি ব্যবহার করার সময় আসলে একটি রেন্ডার করে যা যা কেবল তখনই ঘটে যখন পরিসীমা পরিবর্তন হয়।

আমি এই উত্তরে বর্ণিত কয়েকটি কৌশল ব্যবহার করে অসীম তালিকার একটি উদাহরণ । এটি কিছু কাজ হতে যাচ্ছে তবে প্রতিক্রিয়া একটি অসীম তালিকা বাস্তবায়নের জন্য অবশ্যই একটি ভাল উপায় :)


4
এটি একটি দুর্দান্ত কৌশল। ধন্যবাদ! আমি এটি আমার একটি উপাদান নিয়ে কাজ করতে পেরেছি। তবে, আমার আরও একটি উপাদান রয়েছে যা আমি এটি প্রয়োগ করতে চাই তবে সারিগুলির একটি সুসংগত উচ্চতা নেই। আমি ডিসপ্লেএন্ড / দৃশ্যমান এবং বিভিন্ন উচ্চতার জন্য অ্যাকাউন্ট গণনা করতে আপনার উদাহরণ বাড়ানোর কাজ করছি ... যদি আপনার আরও ভাল ধারণা না থাকে তবে?
মানালং

আমি এটি একটি মোচড় দিয়ে বাস্তবায়ন করেছি এবং একটি ইস্যুতে ছড়িয়ে পড়েছি: আমার জন্য, যে রেকর্ডগুলি আমি পেশ করছি তা কিছুটা জটিল ডিওএম, এবং তাদের # এর কারণে সেগুলি সমস্ত ব্রাউজারে লোড করা বুদ্ধিমানের কাজ নয়, তাই আমি অ্যাসিঙ্ক ফেচ করা সময়ে সময়ে। কিছু কারণে, যখন আমি স্ক্রোল করি এবং অবস্থান খুব দূরে লাফিয়ে যায় (বলুন যে আমি পর্দা এবং পিছনে ফিরে যাই) তখন তালিকা পরিবর্তনটি রাষ্ট্র পরিবর্তন করেও পুনরায় রেন্ডার করে না। কোন ধারনা কেন এই হতে পারে? অন্যথায় দুর্দান্ত উদাহরণ!
নিদ্রাহীন প্রোগ্রামার

1
আপনার জেএসফিডেল বর্তমানে একটি ত্রুটি ছুড়ে ফেলেছে: আনকচড রেফারেন্স এরর: জেনারেট সংজ্ঞায়িত করা হয়নি
মেগলিও

3
আমি একটি আপডেট ফিজল তৈরি করেছি , আমার মনে হয় এটি একইরকম কাজ করা উচিত। কেউ যাচাই করার জন্য যত্ন? @ মেগলিও
একনুদস

1
@ থমাস মডিনিস হাই, আপনি কি 151 এবং 152 লাইন, ডিসপ্লে স্টার্ট এবং ডিসপ্লে
সংক্ষিপ্ত

2

http://adazzle.github.io/react-data-grid/index.html# এ দেখুন একেল-এর মতো বৈশিষ্ট্যযুক্ত এবং অলস লোডিং / অপ্টিমাইজড রেন্ডারিং (কয়েক মিলিয়ন সারি জন্য) সহ একটি শক্তিশালী এবং পারফরম্যান্স ডেটাগ্রিডের মতো সমৃদ্ধ সম্পাদনা বৈশিষ্ট্য (এমআইটি লাইসেন্সযুক্ত)। আমাদের প্রকল্পে এখনও চেষ্টা করা হয়নি তবে খুব শীঘ্রই এটি করা হবে।

এই জাতীয় জিনিসগুলির অনুসন্ধান করার জন্য একটি দুর্দান্ত উত্স হ'ল http://react.rocks/ এই ক্ষেত্রে, একটি ট্যাগ অনুসন্ধান সহায়ক: http://react.rocks/tag/InfiniteScrol


1

ভিন্নজাতীয় আইটেম হাইটের সাথে একক-দিকের অসীম স্ক্রোলিংয়ের মডেলিংয়ের জন্য আমি একই ধরণের চ্যালেঞ্জের মুখোমুখি হয়েছি এবং তাই আমার সমাধান থেকে একটি এনপিএম প্যাকেজ তৈরি করেছে:

https://www.npmjs.com/package/react-variable-height-infinite-scroller

এবং একটি ডেমো: http://tnrich.github.io/react-variable-height-infinite-scroller/

আপনি লজিকের জন্য সোর্স কোডটি পরীক্ষা করে দেখতে পারেন, তবে আমি মূলত উপরের উত্তরে বর্ণিত রেজিস্ট্রেশন @ ভিজেউক্সটি অনুসরণ করেছি। আমি এখনও কোনও নির্দিষ্ট আইটেমটিতে ঝাঁপিয়ে পড়ার বিষয়টি মোকাবেলা করি নি, তবে আমি শীঘ্রই এটি বাস্তবায়নের আশা করছি।

কোডটি বর্তমানে কেমন দেখাচ্ছে তার মূর্খতা এখানে রয়েছে:

var React = require('react');
var areNonNegativeIntegers = require('validate.io-nonnegative-integer-array');

var InfiniteScoller = React.createClass({
  propTypes: {
    averageElementHeight: React.PropTypes.number.isRequired,
    containerHeight: React.PropTypes.number.isRequired,
    preloadRowStart: React.PropTypes.number.isRequired,
    renderRow: React.PropTypes.func.isRequired,
    rowData: React.PropTypes.array.isRequired,
  },

  onEditorScroll: function(event) {
    var infiniteContainer = event.currentTarget;
    var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
    var currentAverageElementHeight = (visibleRowsContainer.getBoundingClientRect().height / this.state.visibleRows.length);
    this.oldRowStart = this.rowStart;
    var newRowStart;
    var distanceFromTopOfVisibleRows = infiniteContainer.getBoundingClientRect().top - visibleRowsContainer.getBoundingClientRect().top;
    var distanceFromBottomOfVisibleRows = visibleRowsContainer.getBoundingClientRect().bottom - infiniteContainer.getBoundingClientRect().bottom;
    var rowsToAdd;
    if (distanceFromTopOfVisibleRows < 0) {
      if (this.rowStart > 0) {
        rowsToAdd = Math.ceil(-1 * distanceFromTopOfVisibleRows / currentAverageElementHeight);
        newRowStart = this.rowStart - rowsToAdd;

        if (newRowStart < 0) {
          newRowStart = 0;
        } 

        this.prepareVisibleRows(newRowStart, this.state.visibleRows.length);
      }
    } else if (distanceFromBottomOfVisibleRows < 0) {
      //scrolling down, so add a row below
      var rowsToGiveOnBottom = this.props.rowData.length - 1 - this.rowEnd;
      if (rowsToGiveOnBottom > 0) {
        rowsToAdd = Math.ceil(-1 * distanceFromBottomOfVisibleRows / currentAverageElementHeight);
        newRowStart = this.rowStart + rowsToAdd;

        if (newRowStart + this.state.visibleRows.length >= this.props.rowData.length) {
          //the new row start is too high, so we instead just append the max rowsToGiveOnBottom to our current preloadRowStart
          newRowStart = this.rowStart + rowsToGiveOnBottom;
        }
        this.prepareVisibleRows(newRowStart, this.state.visibleRows.length);
      }
    } else {
      //we haven't scrolled enough, so do nothing
    }
    this.updateTriggeredByScroll = true;
    //set the averageElementHeight to the currentAverageElementHeight
    // setAverageRowHeight(currentAverageElementHeight);
  },

  componentWillReceiveProps: function(nextProps) {
    var rowStart = this.rowStart;
    var newNumberOfRowsToDisplay = this.state.visibleRows.length;
    this.props.rowData = nextProps.rowData;
    this.prepareVisibleRows(rowStart, newNumberOfRowsToDisplay);
  },

  componentWillUpdate: function() {
    var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
    this.soonToBeRemovedRowElementHeights = 0;
    this.numberOfRowsAddedToTop = 0;
    if (this.updateTriggeredByScroll === true) {
      this.updateTriggeredByScroll = false;
      var rowStartDifference = this.oldRowStart - this.rowStart;
      if (rowStartDifference < 0) {
        // scrolling down
        for (var i = 0; i < -rowStartDifference; i++) {
          var soonToBeRemovedRowElement = visibleRowsContainer.children[i];
          if (soonToBeRemovedRowElement) {
            var height = soonToBeRemovedRowElement.getBoundingClientRect().height;
            this.soonToBeRemovedRowElementHeights += this.props.averageElementHeight - height;
            // this.soonToBeRemovedRowElementHeights.push(soonToBeRemovedRowElement.getBoundingClientRect().height);
          }
        }
      } else if (rowStartDifference > 0) {
        this.numberOfRowsAddedToTop = rowStartDifference;
      }
    }
  },

  componentDidUpdate: function() {
    //strategy: as we scroll, we're losing or gaining rows from the top and replacing them with rows of the "averageRowHeight"
    //thus we need to adjust the scrollTop positioning of the infinite container so that the UI doesn't jump as we 
    //make the replacements
    var infiniteContainer = React.findDOMNode(this.refs.infiniteContainer);
    var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
    var self = this;
    if (this.soonToBeRemovedRowElementHeights) {
      infiniteContainer.scrollTop = infiniteContainer.scrollTop + this.soonToBeRemovedRowElementHeights;
    }
    if (this.numberOfRowsAddedToTop) {
      //we're adding rows to the top, so we're going from 100's to random heights, so we'll calculate the differenece
      //and adjust the infiniteContainer.scrollTop by it
      var adjustmentScroll = 0;

      for (var i = 0; i < this.numberOfRowsAddedToTop; i++) {
        var justAddedElement = visibleRowsContainer.children[i];
        if (justAddedElement) {
          adjustmentScroll += this.props.averageElementHeight - justAddedElement.getBoundingClientRect().height;
          var height = justAddedElement.getBoundingClientRect().height;
        }
      }
      infiniteContainer.scrollTop = infiniteContainer.scrollTop - adjustmentScroll;
    }

    var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
    if (!visibleRowsContainer.childNodes[0]) {
      if (this.props.rowData.length) {
        //we've probably made it here because a bunch of rows have been removed all at once
        //and the visible rows isn't mapping to the row data, so we need to shift the visible rows
        var numberOfRowsToDisplay = this.numberOfRowsToDisplay || 4;
        var newRowStart = this.props.rowData.length - numberOfRowsToDisplay;
        if (!areNonNegativeIntegers([newRowStart])) {
          newRowStart = 0;
        }
        this.prepareVisibleRows(newRowStart , numberOfRowsToDisplay);
        return; //return early because we need to recompute the visible rows
      } else {
        throw new Error('no visible rows!!');
      }
    }
    var adjustInfiniteContainerByThisAmount;

    //check if the visible rows fill up the viewport
    //tnrtodo: maybe put logic in here to reshrink the number of rows to display... maybe...
    if (visibleRowsContainer.getBoundingClientRect().height / 2 <= this.props.containerHeight) {
      //visible rows don't yet fill up the viewport, so we need to add rows
      if (this.rowStart + this.state.visibleRows.length < this.props.rowData.length) {
        //load another row to the bottom
        this.prepareVisibleRows(this.rowStart, this.state.visibleRows.length + 1);
      } else {
        //there aren't more rows that we can load at the bottom so we load more at the top
        if (this.rowStart - 1 > 0) {
          this.prepareVisibleRows(this.rowStart - 1, this.state.visibleRows.length + 1); //don't want to just shift view
        } else if (this.state.visibleRows.length < this.props.rowData.length) {
          this.prepareVisibleRows(0, this.state.visibleRows.length + 1);
        }
      }
    } else if (visibleRowsContainer.getBoundingClientRect().top > infiniteContainer.getBoundingClientRect().top) {
      //scroll to align the tops of the boxes
      adjustInfiniteContainerByThisAmount = visibleRowsContainer.getBoundingClientRect().top - infiniteContainer.getBoundingClientRect().top;
      //   this.adjustmentScroll = true;
      infiniteContainer.scrollTop = infiniteContainer.scrollTop + adjustInfiniteContainerByThisAmount;
    } else if (visibleRowsContainer.getBoundingClientRect().bottom < infiniteContainer.getBoundingClientRect().bottom) {
      //scroll to align the bottoms of the boxes
      adjustInfiniteContainerByThisAmount = visibleRowsContainer.getBoundingClientRect().bottom - infiniteContainer.getBoundingClientRect().bottom;
      //   this.adjustmentScroll = true;
      infiniteContainer.scrollTop = infiniteContainer.scrollTop + adjustInfiniteContainerByThisAmount;
    }
  },

  componentWillMount: function(argument) {
    //this is the only place where we use preloadRowStart
    var newRowStart = 0;
    if (this.props.preloadRowStart < this.props.rowData.length) {
      newRowStart = this.props.preloadRowStart;
    }
    this.prepareVisibleRows(newRowStart, 4);
  },

  componentDidMount: function(argument) {
    //call componentDidUpdate so that the scroll position will be adjusted properly
    //(we may load a random row in the middle of the sequence and not have the infinte container scrolled properly initially, so we scroll to the show the rowContainer)
    this.componentDidUpdate();
  },

  prepareVisibleRows: function(rowStart, newNumberOfRowsToDisplay) { //note, rowEnd is optional
    //setting this property here, but we should try not to use it if possible, it is better to use
    //this.state.visibleRowData.length
    this.numberOfRowsToDisplay = newNumberOfRowsToDisplay;
    var rowData = this.props.rowData;
    if (rowStart + newNumberOfRowsToDisplay > this.props.rowData.length) {
      this.rowEnd = rowData.length - 1;
    } else {
      this.rowEnd = rowStart + newNumberOfRowsToDisplay - 1;
    }
    // var visibleRows = this.state.visibleRowsDataData.slice(rowStart, this.rowEnd + 1);
    // rowData.slice(rowStart, this.rowEnd + 1);
    // setPreloadRowStart(rowStart);
    this.rowStart = rowStart;
    if (!areNonNegativeIntegers([this.rowStart, this.rowEnd])) {
      var e = new Error('Error: row start or end invalid!');
      console.warn('e.trace', e.trace);
      throw e;
    }
    var newVisibleRows = rowData.slice(this.rowStart, this.rowEnd + 1);
    this.setState({
      visibleRows: newVisibleRows
    });
  },
  getVisibleRowsContainerDomNode: function() {
    return this.refs.visibleRowsContainer.getDOMNode();
  },


  render: function() {
    var self = this;
    var rowItems = this.state.visibleRows.map(function(row) {
      return self.props.renderRow(row);
    });

    var rowHeight = this.currentAverageElementHeight ? this.currentAverageElementHeight : this.props.averageElementHeight;
    this.topSpacerHeight = this.rowStart * rowHeight;
    this.bottomSpacerHeight = (this.props.rowData.length - 1 - this.rowEnd) * rowHeight;

    var infiniteContainerStyle = {
      height: this.props.containerHeight,
      overflowY: "scroll",
    };
    return (
      <div
        ref="infiniteContainer"
        className="infiniteContainer"
        style={infiniteContainerStyle}
        onScroll={this.onEditorScroll}
        >
          <div ref="topSpacer" className="topSpacer" style={{height: this.topSpacerHeight}}/>
          <div ref="visibleRowsContainer" className="visibleRowsContainer">
            {rowItems}
          </div>
          <div ref="bottomSpacer" className="bottomSpacer" style={{height: this.bottomSpacerHeight}}/>
      </div>
    );
  }
});

module.exports = InfiniteScoller;
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.