Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
## 1.0.5

`2025-05-22`

- 性能优化:
- 使用 requestAnimationFrame 优化动画处理
- 优化滚动事件处理,提高滚动性能
- 改进错误处理机制
- 实现上拉重试功能

- 依赖优化:
- 移除 core-decorators 依赖,使用自定义 autobind 装饰器
- 优化 lodash 导入,只导入需要的函数
- 添加优化的构建脚本

- 代码质量:
- 清理注释和未使用的代码
- 改进代码可读性和可维护性

## 1.0.4

`2020-07-08`

修复一些小问题

## 1.0.3

`2020-07-07`
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

这是一个上拉加载,下拉刷新的组件。在 Android 和 RN(React Native)里称为 滚动容器(ScrollView)。这个项目是基于CRA(Create React App)搭建的,代码全部采用 TS(TypeScript)编写,核心引擎部分不掺杂框架代码,如果使用React,Vue,Angular,或者原生JS的,可以自己扩展,目前只支持React版本。在设计之初,借鉴了 minirefresh 和 better-scroll 的设计,它们都是很不错的滚动组件。

## 优化更新

最新版本包含以下优化:
- 减少了包体积,移除了不必要的依赖
- 使用了更高效的动画处理
- 优化了滚动事件处理,提高了性能
- 修复了上拉重试功能
- 改进了错误处理机制

## 快速开始

```
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-fast-scroll",
"version": "1.0.4",
"version": "1.0.5",
"description": "a pull-up and pull-down react component, you can also customize to other framework, like vue, angular",
"keywords": [
"react scroll",
Expand All @@ -25,15 +25,15 @@
"url": "https://github.com/carrollcai/react-fast-scroll/issues"
},
"dependencies": {
"core-decorators": "^0.20.0",
"lodash": "^4.17.15"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "jest",
"eject": "react-scripts eject",
"release": "NODE_ENV=production babel src/components --copy-files --extensions .js,.jsx,.ts,.tsx --out-dir lib"
"release": "NODE_ENV=production babel src/components --copy-files --extensions .js,.jsx,.ts,.tsx --out-dir lib",
"release:optimized": "NODE_ENV=production BABEL_ENV=production babel src/components --copy-files --extensions .js,.jsx,.ts,.tsx --out-dir lib --no-comments"
},
"eslintConfig": {
"extends": "react-app"
Expand Down Expand Up @@ -90,5 +90,6 @@
"redux-thunk": "^2.2.0",
"typescript": "^3.9.5"
},
"license": "MIT"
"license": "MIT",
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
151 changes: 85 additions & 66 deletions src/components/base/core.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@

import { autobind } from 'core-decorators';
import { autobind } from './decorators';
import { DefaultOptions, PER_SECOND, Event, Events } from './const';
import { IPartialOptions, IEvents, IDimension, IOptions, HTMLAttribute, IEventType, IContainer } from './interface';
import { getDocumentValue } from './utils';
import Scroll from './scroll';
import { throttle, merge } from 'lodash';
import throttle from 'lodash/throttle';
import merge from 'lodash/merge';

/**
* Todo
Expand Down Expand Up @@ -215,14 +216,11 @@ class Core {
// 触发了下拉刷新或者上拉加载更多,即退出
if (this.isPullingUp || this.isPullingDown || this.executingScrollTo || direction < 0) return;

// if (!this.options.up.enable && !this.isFinishUp && scrollHeight > 0) {
if (!this.isFinishUp && scrollHeight > 0) {
const toBottom = scrollHeight - clientHeight - scrollTop;
if (toBottom <= this.options.up.offset) {
// 满足上拉加载
if (!this.isPullingUp && !this.isFinishUp) {
this.pullUp();
}
// 满足上拉加载条件
if (toBottom <= this.options.up.offset && !this.isPullingUp) {
this.pullUp();
}
}
}
Expand All @@ -237,62 +235,67 @@ class Core {
private touchmove(e: TouchEvent) {
this.events[Event.TOUCHMOVE]?.(e);

// if (this.startTop !== null && this.startTop <= 0 && !this.isPullingDown && !this.options.down.enable) {
if (this.startTop !== null && this.startTop <= 0 && !this.isPullingDown) {
const curX = this.getTouchPosition(e, 'X');
const curY = this.getTouchPosition(e, 'Y');

// 手指滑出屏幕触发刷新
if (curY > this.documentClientHeight) {
return this.touchend(e);
}

if (!this.preY) this.preY = curY;

const diff = curY - this.preY;
// Check if we're at the top and not already pulling down
if (this.startTop === null || this.startTop > 0 || this.isPullingDown) {
return;
}

const curX = this.getTouchPosition(e, 'X');
const curY = this.getTouchPosition(e, 'Y');

this.preY = curY;
// If finger moved out of screen, end touch
if (curY > this.documentClientHeight) {
return this.touchend(e);
}

const moveY = curY - this.startY;
const moveX = curX - this.startX;
if (!this.preY) this.preY = curY;

if (this.options.isLockX && !this.isHorizontal) {
this.isHorizontal = Math.abs(moveX) > Math.abs(moveY);
}
const diff = curY - this.preY;
this.preY = curY;

if (this.isHorizontal) {
return e.preventDefault();
}
const moveY = curY - this.startY;
const moveX = curX - this.startX;

if (this.isBouncing) return;
// Determine if this is a horizontal scroll
if (this.options.isLockX && !this.isHorizontal) {
this.isHorizontal = Math.abs(moveX) > Math.abs(moveY);
}

if (moveY > 0) {
this.isMoveDown = true;
// If horizontal scroll, prevent default and exit
if (this.isHorizontal) {
e.preventDefault();
return;
}

// 阻止浏览器的默认滚动事件,因为这时候只需要执行动画即可
e.preventDefault();
// If already bouncing, exit
if (this.isBouncing) return;

const { offset, dampRateBegin, dampRate } = this.options.down;
let rate = 1;
if (this.pullingDownHeight < offset) {
rate = dampRateBegin;
} else {
rate = dampRate;
}
// Handle pull down
if (moveY > 0) {
this.isMoveDown = true;

// 添加阻尼系数
if (diff >= 0) {
this.pullingDownHeight += diff * rate;
} else {
this.pullingDownHeight = Math.max(0, this.pullingDownHeight + (diff * rate));
}
// Prevent browser's default scroll since we're handling it
e.preventDefault();

this.events[Event.PULLING_DOWN]?.(this.pullingDownHeight);
const { offset, dampRateBegin, dampRate } = this.options.down;

// Apply damping rate based on pull distance
const rate = this.pullingDownHeight < offset ? dampRateBegin : dampRate;

this.translateContentDom(this.pullingDownHeight);
// Calculate new pull height with damping
if (diff >= 0) {
this.pullingDownHeight += diff * rate;
} else {
this.isBouncing = true;
this.pullingDownHeight = Math.max(0, this.pullingDownHeight + (diff * rate));
}

// Trigger pulling down event
this.events[Event.PULLING_DOWN]?.(this.pullingDownHeight);

// Update the DOM
this.translateContentDom(this.pullingDownHeight);
} else {
this.isBouncing = true;
}
}

Expand Down Expand Up @@ -355,22 +358,38 @@ class Core {

// 改变wrap的位置(css动画)
const wrap = this.content;

wrap.style.webkitTransitionDuration = `${duration}ms`;
wrap.style.transitionDuration = `${duration}ms`;
wrap.style.webkitTransform = `translate(0px, ${y}px) translateZ(0px)`;
wrap.style.transform = `translate(0px, ${y}px) translateZ(0px)`;

if (duration === 0) {
// For immediate changes, apply directly
wrap.style.webkitTransitionDuration = '0ms';
wrap.style.transitionDuration = '0ms';
wrap.style.webkitTransform = `translate3d(0px, ${y}px, 0px)`;
wrap.style.transform = `translate3d(0px, ${y}px, 0px)`;
return;
}

// For animated changes, use requestAnimationFrame for smoother animation
requestAnimationFrame(() => {
wrap.style.webkitTransitionDuration = `${duration}ms`;
wrap.style.transitionDuration = `${duration}ms`;
wrap.style.webkitTransform = `translate3d(0px, ${y}px, 0px)`;
wrap.style.transform = `translate3d(0px, ${y}px, 0px)`;
});
}

private loadFullScreen() {
// && wrapper.scrollHeight - options.loadingHeight <= getClientHeightByDom(wrapper) scrollHeight是网页内容高度(最小值是clientHeight),需要减去loading的高度50
if (
// !this.options.up.isLock
this.loadFullCnt <= this.options.up.loadFull.loadCount
&& this.getElementValue('scrollTop') === 0 // 避免无法计算高度时无限加载
&& this.getElementValue('scrollHeight') > 0
&& this.getElementValue('scrollHeight') <= this.getElementValue('clientHeight')
) {
// Check if content is smaller than container and we need to load more
const scrollTop = this.getElementValue('scrollTop');
const scrollHeight = this.getElementValue('scrollHeight');
const clientHeight = this.getElementValue('clientHeight');

const shouldLoadMore =
this.loadFullCnt <= this.options.up.loadFull.loadCount &&
scrollTop === 0 &&
scrollHeight > 0 &&
scrollHeight <= clientHeight;

if (shouldLoadMore) {
clearTimeout(this.loadFullTimer);
this.loadFullTimer = window.setTimeout(() => {
if (this.loadFullCnt < this.options.up.loadFull.loadCount) {
Expand All @@ -381,8 +400,8 @@ class Core {
this.endPullUp(true);
}
}, this.options.up.loadFull.time);
} else {
if (this.loadFullCnt) this.loadFullCnt = 0;
} else if (this.loadFullCnt) {
this.loadFullCnt = 0;
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/components/base/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* A lightweight autobind decorator that replaces core-decorators
*/
export function autobind(_target: any, _key: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value;

return {
configurable: true,
enumerable: false,
get() {
if (this === _target.prototype || this.hasOwnProperty(_key)) {
return originalMethod;
}

const boundFn = originalMethod.bind(this);
Object.defineProperty(this, _key, {
configurable: true,
writable: true,
enumerable: false,
value: boundFn
});

return boundFn;
}
};
}
35 changes: 26 additions & 9 deletions src/components/scroll.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { autobind } from 'core-decorators';
import { autobind } from './base/decorators';
import { Scroll, Event, IPartialOptions } from './base/index';
import Loading from './loading';
import './scroll.css';
Expand Down Expand Up @@ -180,21 +180,38 @@ class ScrollView extends Component<Partial<IProps & IPartialOptions>, IState> {
}

private async handleRequestError(fn: () => Promise<boolean>) {
let timeoutId: number;

const errorPromise: Promise<boolean> = new Promise((resolve, reject) => {
clearTimeout(this.requestErrorTimer);
this.requestErrorTimer = window.setTimeout(() => {
timeoutId = window.setTimeout(() => {
Scroll.info('request callback error');
reject(Error('error'));
reject(new Error('Request timeout'));
}, this.props.requestErrorTime);
});
const res = await Promise.race([fn(), errorPromise]);
clearTimeout(this.requestErrorTimer);
return res;

try {
// Use Promise.race to either get the result or timeout
const res = await Promise.race([fn(), errorPromise]);
clearTimeout(timeoutId);
return res;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}

// Todo 上拉失败之后发起重试
// Retry after pull up failure
private pullUpRetry() {

if (this.state.isPullUpError) {
this.setState({
isPullUpError: false,
isPullingUp: false
});

// Reset the pull up state and try again
(this.scroll as Scroll).resetPullUp();
this.pullUp(true);
}
}

private renderPullDown() {
Expand Down