控制視圖首次渲染DOM數量

(一)前言
談及相關性能優化,我們可以想到兩個方面,一個是架構層級,一個是代碼層級。這裏我們說下常規代碼方面,如果加快頁面首次渲染,我們以React.js爲例子,其實常規談及最多就是減少重複渲染,但是如果我們在做一個頁面,會有很長視圖,用戶首次渲染是看不到視圖之外的頁面的,這時候如果在首次打開頁面,就直接進行渲染,那麼當後續加載的視圖足夠大,越容易產生性能瓶頸,或許在web看不出,但是就h5體驗方面,對這方面影響很大

(二)實現過程
首先,我們來拆解需求,

  1. 綁定當前事件監聽
  2. 判斷當前元素是否在容器內,然後更新state,併爲其增加入場動畫(如果需要)
  3. 更具state,操作元素顯示隱藏。

可能發生的問題:

  1. 綁定scroll,回調函數,需要用throttle(節流)函數
  2. 如果初次加載,頁面保存了滾動條的位置,那麼就需要在組件componentDidMount生命勾子,初始化加載一次
  3. 綁定在isAnimated字段,如果爲false時候,直接返回null,會讓內容區域,多個高度同時失去,導致,多個元素一起出現。(這裏有兩個方法,一種是對於首頁需求,很多圖片採用visibility進行隱藏,二種是對於普通頁面可以給容器一個最小高度,防止塌陷問題)
  4. 如果內容區域還有切換動畫,很可能會於入場動畫衝突,那麼我們的解決方案是,在更新完isAnimated,打開一個一次定時器,在動畫完成後,移除樣式文件
  5. 你回發現這部分代碼,在每個組件都是通用的,但是我們需要生命勾子來實現綁定

現在我們來實現代碼

import * as React from 'react';
import { connect } from 'react-redux';
import { throttle } from 'lodash';

interface WithScrollVisiblePropsType {
  [random: string]: any;
}

// 判斷元素是否在可視區
function isInViewPort(el) {
  if (!el) {
    return {};
  }
  const viewPortHeight =
    window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
  const top = el.getBoundingClientRect() && el.getBoundingClientRect().top;
  return {
    needAnimate: top > 0,
    isVisible: top <= viewPortHeight - 100 && top >= 100 - viewPortHeight,
  };
}

/** 參數
 *  {node} Component 容器組件 isRequire
 *  {string} scrollClassName 當前滾動的className
 *  {object} classList 樣式列表 isRequire ['className']
 *  {string} animateClassName 動畫名稱 'fadeInUp'
 *  {number} duration 動畫持續時間 800 默認進場800秒 -> ./common/animated.scss $animated-duration
 * */
function WithScrollVisible(
  Component,
  { scrollClassName = '', classList, animateClassName = 'fadeInUp', duration = 800 },
) {
  @connect(state => ({ state: state.home, all: state }))
  class WrapperComponent extends React.Component<WithScrollVisiblePropsType> {
    public state = {
      isAnimated: false,
    };

    private getCurrScrollElem = () => {
      if (scrollClassName) {
        return document.querySelector(scrollClassName) || window;
      }
      return window;
    };

    public componentDidMount() {
      // 初始化的時候判斷一次是否本身在可視範圍內
      this.handleScroll();
      this.getCurrScrollElem().addEventListener('scroll', this.handleScrollThrottle);
    }

    public componentWillUnmount() {
      this.removeEventListener();
    }

    private removeEventListener = () => {
      this.getCurrScrollElem().removeEventListener('scroll', this.handleScrollThrottle);
    };

    // 進場完成後0.1s後,移除進場className,避免和切換動畫衝突
    private removeClassName = element => {
      setTimeout(() => {
        try {
          if (!element.classList) {
            return;
          }
          element.classList.remove('animated', animateClassName);
        } catch (e) {
          // 不處理
        }
      }, duration + 100);
    };

    private handleScroll = () => {
      const element = document.querySelector(classList[0]) as Element;
      const { needAnimate, isVisible } = isInViewPort(element);
      if (isVisible && !this.state.isAnimated) {
        this.removeEventListener();
        this.setState({ isAnimated: true }, () => {
          if (needAnimate) {
            element.classList.add('animated', animateClassName);
            this.removeClassName(element);
          }
          if (classList.length <= 1) {
            return;
          }
          classList.slice(1).forEach(item => {
            const ele = document.querySelector(item);
            if (!ele) {
              return;
            }
            if (needAnimate) {
              ele.classList.add('animated', animateClassName);
              this.removeClassName(ele);
            }
          });
        });
      }
    };
    private handleScrollThrottle = throttle(this.handleScroll, 100);

    private getVisibilityStyle = () => {
      return this.state.isAnimated ? { visibility: 'visible' } : { visibility: 'hidden' };
    };

    public render() {
      const {
        state: { isAnimated },
      } = this;
      const componentProps = {
        ...this.props,
        isAnimated,
        visibilityStyle: this.getVisibilityStyle(),
      };
      return <Component {...componentProps} />;
    }
  }
  return WrapperComponent;
}

export default WithScrollVisible;

上面代碼中,我們通過綁定scroll, 然後isInViewPort方法判斷當前元素是否在可見區域,最後爲元素加上animated 和 fadeInUp的樣式,處理入場動畫。

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