(一)前言
谈及相关性能优化,我们可以想到两个方面,一个是架构层级,一个是代码层级。这里我们说下常规代码方面,如果加快页面首次渲染,我们以React.js为例子,其实常规谈及最多就是减少重复渲染,但是如果我们在做一个页面,会有很长视图,用户首次渲染是看不到视图之外的页面的,这时候如果在首次打开页面,就直接进行渲染,那么当后续加载的视图足够大,越容易产生性能瓶颈,或许在web看不出,但是就h5体验方面,对这方面影响很大
(二)实现过程
首先,我们来拆解需求,
- 绑定当前事件监听
- 判断当前元素是否在容器内,然后更新state,并为其增加入场动画(如果需要)
- 更具state,操作元素显示隐藏。
可能发生的问题:
- 绑定scroll,回调函数,需要用throttle(节流)函数
- 如果初次加载,页面保存了滚动条的位置,那么就需要在组件componentDidMount生命勾子,初始化加载一次
- 绑定在isAnimated字段,如果为false时候,直接返回null,会让内容区域,多个高度同时失去,导致,多个元素一起出现。(这里有两个方法,一种是对于首页需求,很多图片采用visibility进行隐藏,二种是对于普通页面可以给容器一个最小高度,防止塌陷问题)
- 如果内容区域还有切换动画,很可能会于入场动画冲突,那么我们的解决方案是,在更新完isAnimated,打开一个一次定时器,在动画完成后,移除样式文件
- 你回发现这部分代码,在每个组件都是通用的,但是我们需要生命勾子来实现绑定
现在我们来实现代码
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的样式,处理入场动画。