看到標題,你必定要問:react
爲何還要再造一個輪子?
IScroll
很差用嗎?那還有Better-Scroll
啊?api
這兩個庫都不錯,本身平時也用,之因此要作,緣由只有兩個:瀏覽器
ui = f(state)
這兩個庫都是跨平臺的,都是直接操做 dom
的,跨平臺不是很差,確定是好,可是在 React 的世界,要處理狀態的同步,一般都是經過狀態或屬性來控制,雖然能夠用個 React 包裹上面兩個庫,提供 React 版本,可是老是以爲不那麼完美。dom
團隊提供的是PC端面向B端的商業產品,須要有很好的交互體驗函數
產品說:系統的滾動條能不能改改樣式?工具
我說:能夠改,Chrome 能夠改,可是 FireFox 等改不了,oop
產品說:能鼠標 Hover 的時候變大?性能
我說:我試試,Edeg 原本身就支持,Chrome 努努力也行,其餘的好像不行動畫
產品說:你看這裏表格裏面的滾動條,能不能拿到瀏覽器邊上來ui
我說:我靠,這個表格是在內部啊,離瀏覽器邊上隔着幾座山,他是個單獨的組件
產品說:我怎麼拖拽頁面,就讓頁面滾動,不用拖滾動條
我說:移動端觸摸屏能夠,PC 上你能夠用觸摸板
產品說:個人觸摸板咋不起做用
我說:Mac 上能夠,你這 TinkPad 觸摸板好像得安裝個驅動,你能夠用鼠標滾輪
好了,綜上,咱們的產品需求你明白了嗎?下面我先寫一條本身的感悟:
若是你想成長,那麼在面對產品經理的無理需求的時候,你要拒絕,就要作到心安理得的拒絕
事實上,拒絕很容易,老是有理由的,成本也是最低的,可是心裏老是以爲,這個確定能夠實現,惋惜時間成本有點高,bug 這麼多,改不過來,要實現這個,怎麼不得一兩天?一兩天都未必夠,說不許有什麼問題
固然,我仍是是拒絕了產品,集中精力改阻斷性的 bug,而後,趁着週末,好好構思了下這個滾動條該怎麼作,要是答應了產品,最後作不出來,就糗大了,作出來,算是給產品的驚喜,雖然他意識不到這個有多坑……
原生寫法:
<div className="container"
style={{
width: 500,
height: 400,
overflow:'auto'
}}
onScroll={onScroll}
>
<div className="content" ref="content" style={{
width: 1000,
height: 800,
}}>
{content}
</div>
</div>
複製代碼
只須要修改容器的標籤,替換後:
<Scroll className="container"
style={{
width: 500,
height: 400,
overflow:'auto'
}}
onScroll={onScroll}
>
<div className="content" ref="content" style={{
width: 1000,
height: 800,
}}>
{content}
</div>
</Scroll>
複製代碼
onScroll
接口和原生保持一致,不影響原有業務邏輯export interface IScrollEvent {
target: {
scrollLeft: number;
scrollTop: number;
};
}
export interface IScrollProps{
/** * 滾動條距離左側的距離 */
scrollLeft?: number;
/** * 滾動條距離頂部距離 */
scrollTop?: number;
onScroll?: (e: IScrollEvent) => void;
}
複製代碼
scrollLeft
和 scrollTop
屬性修改滾動條位置,同時對象實例也提供 以下屬性,兼容原生 DOM APIexport interface IScroll {
scrollLeft: number;
scrollTop: number;
/** * 滾動到指定位置 */
scrollTo: (left: number, top: number) => void;
/** * 滾動相對距離 */
scrollBy: (left: number, top: number) => void;
/** * 從新計算滾動區域 */
refresh: () => void;
}
複製代碼
可使用 絕對定位
或者 transform
,transform
是首選,由於能夠支持 gpu
加速,處理滾動動畫性能會好一些
固然是監聽 mousedown
,mousemove
,mouseup
,計算鼠標移動的方向,和相對距離,而後肯定元素的位置,移動端要監聽 touchstart
, touchmove
, touchend
事件
查了一下,PC 觸摸板能夠出發 onwheel
事件,就是鼠標滾輪滾動事件,靠這個能夠支持觸摸板,個人 ThinkPad 也能夠支持。
若是隻是用滾動條,能夠不用動畫,可是拖拽的時候,要有個滾屏動畫,好比CSS3裏的 ease-in
ease-out
動畫,能夠用setInterval
或者 requestAnimationFrame
api 來作,能夠先作個勻減速直線運動,其餘的動畫再說
ui = f(state)
範式,頻繁修改 state,從新渲染,是否有性能問題?相比於直接修改 dom ,性能確定是有折扣的,不過在接受的範圍內
上面的問題基本都是能夠解決,沒有阻斷性的問題,下面就是實現:
鼠標拖拽通常使用 onMouseDown
onMouseMove
onMouseUp
事件,大致流程以下:
onMouseDown
事件中記錄鼠標初始位置 pointStart
,爲 document
註冊 mousemove
和 mouseup
事件onMouseMove
事件中鼠標移動,記錄鼠標當前位置 pointEnd,減去 popointEnd
- pointStart
獲得鼠標的偏移量,設置 secrollLeft
,頁面滾動。onMouseUp
事件中,獲取鼠標的 即時速度
,若是速度爲 0
,那麼終止移動,若是速度大於 0
,執行滾動動畫,移除 document
的 mousemove
和 mouseup
事件這裏沒有給滾動區域的根節點加
mousemove
和mouseup
事件,給document
加鼠標的mousemove
和mouseup
事件,由於鼠標的移動區間可能會超過滾動區域,若是超過滾動區域這兩個事件就再也不執行了
鼠標擡起之後,須要知道移動的速度,而後以這個速度作減速運動,因此須要計算 即時速度
,這裏不能用平均速度。
計算 即時速度
須要知道 距離
和 時間
,鼠標點擊之後,經過 setInverval
計時器,每 100ms
記錄鼠標的的位置和時間戳,鼠標擡起之後,終止計算,獲取當前位置和時間,和歷史的位置和時間戳作差,獲得最後 100ms
內的速度,計算以下:
/** * 啓動即時速度計算 */
startCaclRealV = () => {
const me = this;
const t = _REAL_VELOCITY_TIMESPAN;
const timer = setInterval(() => {
if (!me.isDraging) {
clearInterval(timer);
return;
}
if (!me.lastPos) {
me.lastTime = Date.now();
me.lastPos = me.endPoint;
return;
}
me.lastTime = Date.now();
me.lastPos = me.endPoint;
}, t);
return {
destroy() {
clearInterval(timer);
},
}
}
/** * 計算即時速度 */
caclRealV = () => {
const me = this;
if (!me.lastPos) {
return {
realXVelocity:0,
realYVelocity:0
}
}
const time = (Date.now() - me.lastTime) / 1000;
const xdist = Math.abs(me.endPoint.x - me.lastPos.x);
const ydist = Math.abs(me.endPoint.y - me.lastPos.y);
return {
realXVelocity:caclVelocity(xdist, time),
realYVelocity:caclVelocity(ydist, time),
}
}
複製代碼
鼠標擡起之後,以 即時速度
開始作減速運動,這裏能夠利用緩動函數計算位置,設置 scrollLeft
,我這裏使用了勻減速運動,使用 requestAnimationFrame
執行動畫循環,利用 transform: translate3d(0,${indicateTop}px,0)
設置偏移,能夠啓動 pgu 加速,
代碼參考:
import { TDirection, TPoint } from './types'
/** * 動畫執行函數 * @param v 速度 像素/秒 * @param a 減速度 像素/秒平方 * @param onMove 回調函數,返回移動距離 * @param onEnd 回調函數,終止動畫 */
export const animate = (v: number, a: number, onMove: (dist) => boolean, onEnd: () => void): { destroy: () => void } => {
const t = 16;// ms
const start = Date.now();
return loopByFrame(t, () => {
const time = (Date.now() - start) / 1000;
if (time === 0) {
return true;
}
const dist = move(v, a, time);
if (dist === 0) {
return false;
}
return onMove(dist);
}, onEnd);
}
/** * 利用 requestAnimationFrame 執行動畫循環 * @param duration 動畫時間間隔,使用 requestAnimationFrame 不須要設置 * @param onMove 動畫執行函數 * @param onEnd 動畫終止函數 */
export const loopByFrame = (duration = 16, onMove = () => true, onEnd = () => void 0): { destroy: () => void } => {
let animateFrame;
function step(func, end = () => void 0) {
if (!func) {
end();
return;
}
if (!func()) {
destroy();
end();
return;
}
animateFrame = window.requestAnimationFrame(() => {
step(func, end);
});
}
function destroy() {
if (animateFrame) {
window.cancelAnimationFrame(animateFrame);
}
}
step(onMove, onEnd);
return {
destroy,
}
}
/** * 利用 setInterval 執行函數循環 * @param duration 時間間隔 * @param cb 回調函數 * @param onEnd 終止函數 */
export const loopByInterval = (duration = 16, cb = () => true, onEnd = () => void 0): { destroy: () => void } => {
const timer = setInterval(() => {
if (!cb()) {
clearInterval(timer);
onEnd();
}
}, duration);
return {
destroy() {
clearInterval(timer);
onEnd();
},
}
}
/** * 計算以速度 v ,減速度 a,運動 time 時間的距離 * @param v 速度 * @param a 減速度 * @param time 時間 */
export const move = (v: number, a: number, time: number) => {
// 獲取下一時刻速度,若是速度爲 0 終止
const nextV = caclNextVelocity(v, a, time);
if (nextV <= 0) {
return 0;
}
// 計算下一刻的距離
const dist = caclDist(v, time, a);
return dist;
}
/** * 計算滾動方向,暫時只支持橫向滾動 * @param start 起始點 * @param end 終點 */
export const caclDirection = (start: TPoint, end: TPoint): TDirection => {
const xLen = (end.x - start.x);
const yLen = (end.y - start.y);
if (Math.abs(xLen) > Math.abs(yLen)) {
return xLen > 0 ? 'right' : 'left';
} else {
return yLen > 0 ? 'bottom' : 'top';
}
}
/** * 減速直線運動公式,計算距離 * @param v 速度 * @param t 時間 單位秒 * @param a 加速度 */
export const caclDist = (v: number, t: number, a: number) => {
return v * t - (a * t * t) / 2;
}
/** * 計算速度 * @param v0 初始速度 * @param a 加速度 * @param t 時間 */
export const caclNextVelocity = (v0: number, a: number, t: number) => {
return v0 - a * t;
}
/** * 計算速度 * @param dist 距離 單位像素 * @param time 時間 單位秒 */
export const caclVelocity = (dist: number, time: number) => {
if (time <= 0) {
return 0;
}
return dist / time;
}
複製代碼
碰到頁面裏須要多個區域滾動條同步到狀況,可使用這個模式,好比咱們系統中,表頭,表體,工具欄須要進行同步的狀況
滾動條同步原本咱們使用了系統自帶的滾動條的 scrollLeft
屬性進行同步,可是會很卡頓,如今使用了 Scroll
組件,利用 CSS3 的transform
來進行同步,效果好不少。
示例代碼:
import React,{Component} from 'react';
class Demo extends Component{
constructor(props,context){
super(props,context);
const me=this;
me.state={
scrollLeft:0,
scrollTop:0,
}
}
onScroll=(e)=>{
const me=this;
me.setState({
scrollLeft:e.target.scrollLeft,
scrollTop:e.target.scrollTop
});
}
render(){
const me=this;
const {
scrollLeft,
scrollTop
}=me.state;
return (
<Scroll
scrollLeft={scrollLeft}
scrollTop={scrollTop}
className="container"
style={{ width: 500, height: 400,}}
onScroll={me.onScroll}
>
<div className="content" ref="content"
style={{width: 1000,height: 800}}>
</div>
</Scroll>
<Scroll
scrollLeft={scrollLeft}
scrollTop={scrollTop}
className="container"
style={{width: 500,height: 400,}}
onScroll={me.onScroll}
>
<div className="content" ref="content"
style={{width: 1000,height: 800,}}>
</div>
</Scroll>
)
}
}
複製代碼
本文嘗試自行實現滾動條來解決 PC 端各個瀏覽器滾動條行爲不一致的問題,兼容原生的 API,能作到無縫切換,總體實現難度中等,主要實現緩動動畫上須要注意一些,對有些問題是否是有更好的辦法?
即時速度
計算,是否是還有更好的方法?