[源碼閱讀]通過react-infinite-scroller理解滾動加載要點

react-infinite-scroller就是一個組件,主要邏輯就是addEventListener綁定scroll事件。

看它的源碼主要意義不在知道如何使用它,而是知道以後處理滾動加載要注意的東西。

此處跳到總結

初識

參數:

// 渲染出來的DOM元素name
element: 'div',
// 是否能繼續滾動渲染
hasMore: false,
// 是否在訂閱事件的時候執行事件
initialLoad: true,
// 表示當前翻頁的值(每渲染一次遞增)
pageStart: 0,
// 傳遞ref,返回此組件渲染的 DOM
ref: null,
// 觸發渲染的距離
threshold: 250,
// 是否在window上綁定和處理距離
useWindow: true,
// 是否反向滾動,即到頂端後渲染
isReverse: false,
// 是否使用捕獲模式
useCapture: false,
// 渲染前的loading組件
loader: null,
// 自定義滾動組件的父元素
getScrollParent: null,

深入

componentDidMount

componentDidMount() {
  this.pageLoaded = this.props.pageStart;
  this.attachScrollListener();
}

執行attachScrollListener


attachScrollListener

attachScrollListener() {
  const parentElement = this.getParentElement(this.scrollComponent);
  
  if (!this.props.hasMore || !parentElement) {
    return;
  }

  let scrollEl = window;
  if (this.props.useWindow === false) {
    scrollEl = parentElement;
  }
  scrollEl.addEventListener(
    'mousewheel',
    this.mousewheelListener,
    this.props.useCapture,
  );
  scrollEl.addEventListener(
    'scroll',
    this.scrollListener,
    this.props.useCapture,
  );
  scrollEl.addEventListener(
    'resize',
    this.scrollListener,
    this.props.useCapture,
  );
  
  if (this.props.initialLoad) {
    this.scrollListener();
  }
}

此處通過getParentElement獲取父組件(用戶自定義父組件或者當前dom的parentNode)

然後綁定了3個事件,分別是scroll,resize,mousewheel

前2種都綁定scrollListenermousewheel是一個非標準事件,是不建議在生產模式中使用的。

那麼這裏爲什麼要使用呢?


mousewheel解決chrome的等待bug

此處的mousewheel事件是爲了處理chrome瀏覽器的一個特性(不知道是否是一種bug)。

stackoverflow:Chrome的滾動等待問題

上面這個問題主要描述,當在使用滾輪加載,而且加載會觸發ajax請求的時候,當滾輪到達底部,會出現一個漫長而且無任何動作的等待(長達2-3s)。

window.addEventListener("mousewheel", (e) => {
    if (e.deltaY === 1) {
        e.preventDefault()
    }
})

以上綁定可以消除這個"bug"。

個人並沒有遇到過這種情況,不知道是否有遇到過可以說說解決方案。

getParentElement

getParentElement(el) {
  const scrollParent =
    this.props.getScrollParent && this.props.getScrollParent();
  if (scrollParent != null) {
    return scrollParent;
  }
  return el && el.parentNode;
}

上面用到了getParentElement,很好理解,使用用戶自定義的父組件,或者當前組件DOM.parentNode


scrollListener

scrollListener() {
  const el = this.scrollComponent;
  const scrollEl = window;
  const parentNode = this.getParentElement(el);

  let offset;
  // 使用window的情況
  if (this.props.useWindow) {
    const doc = document.documentElement || document.body.parentNode || document.body;
    const scrollTop = scrollEl.pageYOffset !== undefined
        ? scrollEl.pageYOffset
        : doc.scrollTop;
    // isReverse指 滾動到頂端,load新組件
    if (this.props.isReverse) {
      // 相反模式獲取到頂端距離
      offset = scrollTop;
    } else {
      // 正常模式則獲取到底端距離
      offset = this.calculateOffset(el, scrollTop);
    }
    // 不使用window的情況
  } else if (this.props.isReverse) {
    // 相反模式組件到頂端的距離
    offset = parentNode.scrollTop;
  } else {
    // 正常模式組件到底端的距離
    offset = el.scrollHeight - parentNode.scrollTop - parentNode.clientHeight;
  }

  // 此處應該要判斷確保滾動組件正常顯示
  if (
    offset < Number(this.props.threshold) &&
    (el && el.offsetParent !== null)
  ) {
    // 卸載事件
    this.detachScrollListener();
    // 卸載事件後再執行 loadMore
    if (typeof this.props.loadMore === 'function') {
      this.props.loadMore((this.pageLoaded += 1));
    }
  }
}

組件核心。

幾個學習/複習點

  1. offsetParent

    offsetParent返回一個指向最近的包含該元素的定位元素.

    offsetParent很有用,因爲計算offsetTopoffsetLeft都是相對於offsetParent邊界的。

    ele.offsetParent爲 null 的3種情況:

    • ele 爲body
    • ele 的positionfixed
    • ele 的displaynone

      此組件中offsetParent處理了2種情況

      1. useWindow的情況下(即事件綁定在window,滾動作用在body)

        通過遞歸獲取offsetParent到達頂端的高度(offsetTop)。

        calculateTopPosition(el) {
         if (!el) {
           return 0;   
         }
         return el.offsetTop + this.calculateTopPosition(el.offsetParent);   
        }
      2. 通過判斷offsetParent不爲null的情況,確保滾動組件正常顯示

          if (
            offset < Number(this.props.threshold) &&
            (el && el.offsetParent !== null)
          ) {/* ... */ }
  2. scrollHeightclientHeight

    在無滾動的情況下,scrollHeightclientHeight相等,都爲height+padding*2

    在有滾動的情況下,scrollHeight表示實際內容高度,clientHeight表示視口高度。

  3. 每次執行loadMore前卸載事件。

    確保不會重複(過多)執行loadMore,因爲先卸載事件再執行loadMore,可以確保在執行過程中,scroll事件是無效的,然後再每次componentDidUpdate的時候重新綁定事件。

render

render() {
  // 獲取porps
  const renderProps = this.filterProps(this.props);
  const {
    children,
    element,
    hasMore,
    initialLoad,
    isReverse,
    loader,
    loadMore,
    pageStart,
    ref,
    threshold,
    useCapture,
    useWindow,
    getScrollParent,
    ...props
  } = renderProps;

  // 定義一個ref
  // 能將當前組件的DOM傳出去
  props.ref = node => {
    this.scrollComponent = node;
    // 執行父組件傳來的ref(如果有)
    if (ref) {
      ref(node);
    }
  };

  const childrenArray = [children];
  // 執行loader
  if (hasMore) {
    if (loader) {
      isReverse ? childrenArray.unshift(loader) : childrenArray.push(loader);
    } else if (this.defaultLoader) {
      isReverse
        ? childrenArray.unshift(this.defaultLoader)
        : childrenArray.push(this.defaultLoader);
    }
  }
  // ref 傳遞給 'div'元素
  return React.createElement(element, props, childrenArray);
}

這裏一個小亮點就是,在react中,this.props是不允許修改的。

這裏使用瞭解構

getScrollParent,
...props
} = renderProps;

這裏解構相當於Object.assign,定義了一個新的object,便可以添加屬性了,並且this.props不會受到影響。

總結

react-infinite-scroller邏輯比較簡單。

一些注意/學習/複習點:

  • Chrome的一個滾動加載請求的bug。本文位置
  • offsetParent的一些實際用法。本文位置
  • 通過不斷訂閱和取消事件綁定讓滾動執行函數不會頻繁觸發。本文位置
  • scrollHeightclientHeight區別。本文位置

此庫建議使用在自定義的一些組件上並且不那麼複雜的邏輯上。

用在第三方庫可以會無法獲取正確的父組件,而通過document.getElementBy..傳入。

面對稍微複雜的邏輯,

例如,一個搜索組件,訂閱onChange事件並且呈現內容,搜索"a",對呈現內容滾動加載了3次,再添加搜索詞"b",這時候"ab"的內容呈現是在3次之後。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章