控制视图首次渲染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的样式,处理入场动画。

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