diff --git a/CHANGELOG.md b/CHANGELOG.md index 61df9d9..36d0ed1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/README.md b/README.md index 1d9b03b..b145b14 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,15 @@ 这是一个上拉加载,下拉刷新的组件。在 Android 和 RN(React Native)里称为 滚动容器(ScrollView)。这个项目是基于CRA(Create React App)搭建的,代码全部采用 TS(TypeScript)编写,核心引擎部分不掺杂框架代码,如果使用React,Vue,Angular,或者原生JS的,可以自己扩展,目前只支持React版本。在设计之初,借鉴了 minirefresh 和 better-scroll 的设计,它们都是很不错的滚动组件。 +## 优化更新 + +最新版本包含以下优化: +- 减少了包体积,移除了不必要的依赖 +- 使用了更高效的动画处理 +- 优化了滚动事件处理,提高了性能 +- 修复了上拉重试功能 +- 改进了错误处理机制 + ## 快速开始 ``` diff --git a/package.json b/package.json index 4fc01f0..38304e5 100644 --- a/package.json +++ b/package.json @@ -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", @@ -25,7 +25,6 @@ "url": "https://github.com/carrollcai/react-fast-scroll/issues" }, "dependencies": { - "core-decorators": "^0.20.0", "lodash": "^4.17.15" }, "scripts": { @@ -33,7 +32,8 @@ "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" @@ -90,5 +90,6 @@ "redux-thunk": "^2.2.0", "typescript": "^3.9.5" }, - "license": "MIT" + "license": "MIT", + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/components/base/core.ts b/src/components/base/core.ts index e1acefb..bc5dfcd 100644 --- a/src/components/base/core.ts +++ b/src/components/base/core.ts @@ -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 @@ -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(); } } } @@ -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; } } @@ -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) { @@ -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; } } diff --git a/src/components/base/decorators.ts b/src/components/base/decorators.ts new file mode 100644 index 0000000..e298fe2 --- /dev/null +++ b/src/components/base/decorators.ts @@ -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; + } + }; +} \ No newline at end of file diff --git a/src/components/scroll.tsx b/src/components/scroll.tsx index 87d251b..1dfca9f 100644 --- a/src/components/scroll.tsx +++ b/src/components/scroll.tsx @@ -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'; @@ -180,21 +180,38 @@ class ScrollView extends Component, IState> { } private async handleRequestError(fn: () => Promise) { + let timeoutId: number; + const errorPromise: Promise = 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() {