-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3b2755c
Showing
26 changed files
with
13,913 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
|
||
# testing | ||
/coverage | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
.env.local | ||
.env.development.local | ||
.env.test.local | ||
.env.production.local | ||
|
||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
## 前言 | ||
项目中有两个页面有很多Echarts画的图,进去的时候特别卡,使用了滚动加载之后流畅很多,用户体验大幅提升。 | ||
另外滚动加载还有很多其他用途,比如:滚动翻页,无线翻滚,图片出现在视口才请求。。。 | ||
本文的内容 | ||
1. 传统方案实现滚动加载 | ||
2. H5 API IntersectionObserver实现滚动加载 | ||
3. 第二种方案使用Hooks实现 | ||
|
||
## 1. 传统方案 | ||
传统方案实现滚动加载流程: | ||
``` | ||
* 展示Loading... | ||
* 判断该元素是否在视口中,是则展示真正的内容,否则进行下一步 | ||
* 递归获取滚动容器 | ||
* 给滚动容器添加滚动事件 | ||
* 当元素出现在视口中时,开始展示真正的内容,并取消监控事件 | ||
``` | ||
### 1.1 创建容器 | ||
index.js文件直接使用项目初始化的代码, | ||
更改app.js创建容器 | ||
``` javascript | ||
import React from 'react'; | ||
import Scroll from './ScrollLoadSimple'; | ||
|
||
import './App.css'; | ||
|
||
const domNum = 20; | ||
|
||
const App = () => { | ||
return ( | ||
<div className="app"> | ||
{ | ||
Array.from( | ||
{ length: domNum }, | ||
(text, index) => ( | ||
<Scroll text={`第${index + 1}个元素`} /> | ||
) | ||
) | ||
} | ||
</div> | ||
); | ||
} | ||
|
||
export default App; | ||
|
||
``` | ||
### 1.2展示Loading | ||
创建ScrollLoadSimple文件夹,并创建index.js | ||
在滚动加载的组件中我们需要在state中添加一个字段表示正在加载中,不妨使用loading。 | ||
index.js | ||
``` javascript | ||
import React from 'react'; | ||
|
||
class ScrollLoad extends React.Component { | ||
state = { loading: true } | ||
|
||
render() { | ||
const { loading } = this.state; | ||
const { text } = this.props; | ||
return ( | ||
<div className="scrollitem"> | ||
{ | ||
loading ? 'Loading...' : text | ||
} | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
export default ScrollLoad; | ||
``` | ||
style.css | ||
``` css | ||
.scrollitem { | ||
height: 200px; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
|
||
border: 1px solid green; | ||
margin-top: 10px; | ||
} | ||
|
||
.scrollitem:first-child { | ||
margin-top: 0; | ||
} | ||
``` | ||
### 1.3 判断元素是否出现在视口中 | ||
判断元素是否出现在视口中也有两种方案,一种是最基本的方案,使用offsetTop计算body元素到该元素的距离,计算比较繁琐,容易出问题,另一种方案比较简单,使用getboundingclientrect计算元素是否出现在视口中。 | ||
### 1.3.1 基本方案 通过offsetTop计算元素是否出现在视口中 | ||
*这种计算方案极力不推荐,计算繁琐,并且滚动容器嵌套的话还可能有问题。* | ||
首先获取该元素的offsetTop, | ||
然后递归获取父元素的offsetTop,相加之后的和就是视口左上角到该元素的距离, | ||
接着获取滚动容器,通过scrollTop获取滚动高度,滚动容器的条件是scrollHeight > clientHeight。当没有父元素满足该条件时返回null,此时计算滚动高度使用`document.scrollingElement.scrollTop`,在chrome中`document.scrollingElement`是`html`,同时也是`document.documentElement`。 | ||
通过对比屏幕高度+滚动高度与该距离就能得知元素是否出现在视口中。 | ||
这里有个坑就是offsetTop是根据position为relative的祖先元素或body来计算的, | ||
假设A元素的position为relative, | ||
B元素的position不是relative,B元素的父节点是A元素,offsetTop为36, | ||
C节点为B元素的子节点,并且顶部与B元素重合,则C元素的offsetTop也是36, | ||
因此递归获取offsetTop时,只能使用position为relative的祖先元素。 | ||
``` javascript | ||
import React from 'react'; | ||
import './style.css'; | ||
|
||
class ScrollLoad extends React.Component { | ||
state = { loading: true } | ||
ref = React.createRef(); | ||
|
||
componentDidMount() { | ||
const node = this.ref.current; | ||
this.scrollParent = this.getScrollParent(node); | ||
if (this.checkVisible(node)) { | ||
this.setState({ loading: false }); | ||
} | ||
} | ||
|
||
getScrollParent = (node) => { | ||
if (!node || node.parentNode === document.documentElement) { | ||
return null; | ||
} | ||
const parentNode = node.parentNode; | ||
if (parentNode.scrollHeight > parentNode.clientHeight | ||
|| parentNode.scrollWidth > parentNode.clientWidth | ||
) { | ||
return parentNode; | ||
} | ||
return this.getScrollParent(parentNode); | ||
} | ||
|
||
checkVisible = (node) => { | ||
let offsetTop = node.offsetTop; | ||
let offsetLeft = node.offsetLeft; | ||
let parentNode = node.parentNode; | ||
while (parentNode && parentNode !== document.body) { | ||
if (getComputedStyle(parentNode).position === 'relative') { | ||
offsetTop += parentNode.offsetTop; | ||
offsetLeft += parentNode.offsetLeft; | ||
} | ||
parentNode = parentNode.parentNode; | ||
} | ||
// 滚动元素在最外层时,计算scrollTop要使用scrollingElement | ||
const scrollParent = this.scrollParent || document.scrollingElement; | ||
return window.innerHeight + scrollParent.scrollTop > offsetTop | ||
&& window.innerWidth + scrollParent.scrollLeft > offsetLeft; | ||
} | ||
|
||
render() { | ||
const { loading } = this.state; | ||
const { text } = this.props; | ||
return ( | ||
<div className="scrollitem" ref={this.ref}> | ||
{ | ||
loading ? 'Loading...' : text | ||
} | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
export default ScrollLoad; | ||
``` | ||
### 1.3.2 使用getboundingclientrect计算是否出现在视口中 | ||
使用getBoundingClientRect可以获得节点相对于视口的信息。 | ||
|
||
重写`checkVisible`函数 | ||
```javascript | ||
checkVisible = (node) => { | ||
if (node) { | ||
const { top, bottom, left, right } = node.getBoundingClientRect(); | ||
return bottom > 0 | ||
&& top < window.innerHeight | ||
&& left < window.innerWidth | ||
&& right > 0; | ||
} | ||
return false; | ||
} | ||
``` | ||
超级简单。 | ||
|
||
### 1.4 获取滚动容器 | ||
在1.3.1节中`getScrollTop`函数就是为了获取滚动容器的,这里不再赘述。 | ||
### 1.5 添加滚动事件 | ||
当有元素未出现在视口中时,要监听滚动容器的滚动事件, | ||
修改didMount | ||
```javascript | ||
componentDidMount() { | ||
const node = this.ref.current; | ||
this.scrollParent = this.getScrollParent(node); | ||
if (this.checkVisible(node)) { | ||
this.setState({ loading: false }); | ||
} else { | ||
this.addEvent(); | ||
} | ||
} | ||
``` | ||
添加滚动事件,当没有滚动容器时,要在window上添加滚动事件。 | ||
当容器开始滚动时,判断是否出现在视口中,如果出现了,则展示真正的内容,并取消监听事件。 | ||
由于滚动事件触发频率特别高,所以要使用节流函数,这里使用lodash的节流函数。 | ||
下面看代码 | ||
`import throttle from 'lodash/throttle';` | ||
|
||
``` js | ||
onScroll = throttle(() => { | ||
const node = this.ref.current; | ||
if (this.checkVisible(node)) { | ||
this.setState({ loading: false }); | ||
this.cancelEvent(); | ||
} | ||
}, 200) | ||
|
||
addEvent = () => { | ||
// 滚动元素在最外层时,要在window上添加滚动事件 | ||
const scrollParent = this.scrollParent || window; | ||
scrollParent.addEventListener('scroll', this.onScroll); | ||
} | ||
|
||
cancelEvent = () => { | ||
const scrollParent = this.scrollParent || window; | ||
scrollParent.removeEventListener('scroll', this.onScroll); | ||
} | ||
``` | ||
最后,不要忘记了在willUnmount中取消监听 | ||
``` js | ||
componentWillUnmount() { | ||
this.cancelEvent(); | ||
} | ||
``` | ||
|
||
## 2. 使用H5 API IntersectionObserver | ||
### 2.1 如何实现 | ||
直接上代码 | ||
```js | ||
import React from 'react'; | ||
import './style.css'; | ||
|
||
class ScrollLoad extends React.Component { | ||
state = { loading: true } | ||
ref = React.createRef(); | ||
|
||
componentDidMount() { | ||
const node = this.ref.current; | ||
this.observer = new IntersectionObserver((entries, observer) => { | ||
entries.forEach((entry) => { | ||
if (entry.isIntersecting) { | ||
this.setState({ loading: false }); | ||
observer.unobserve(node); | ||
} | ||
}) | ||
}); | ||
this.observer.observe(node); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.observer.disconnect(); | ||
} | ||
|
||
render() { | ||
const { loading } = this.state; | ||
const { text } = this.props; | ||
return ( | ||
<div className="scrollitem" ref={this.ref}> | ||
{ | ||
loading ? 'Loading...' : text | ||
} | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
export default ScrollLoad; | ||
``` | ||
优点:IntersectionObserver优先度极低,消耗性能也就很低,并且不需要做很多判断,代码量少,逻辑简单。 | ||
IntersectionObserver能实现的功能还有很多。 | ||
但是兼容性还有点问题,可以使用polyfill。 | ||
|
||
### 2.2 Hooks版本 |
Oops, something went wrong.