(一)前言
談及相關性能優化,我們可以想到兩個方面,一個是架構層級,一個是代碼層級。這裏我們說下常規代碼方面,如果加快頁面首次渲染,我們以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的樣式,處理入場動畫。