perf: optimization BackTop and Anchor components scrollTo func

This commit is contained in:
shao 2019-07-24 22:56:20 +08:00
parent 90a65d1e57
commit ccef30885a
7 changed files with 164 additions and 87 deletions

View File

@ -0,0 +1,43 @@
import scrollTo from '../scrollTo';
import { sleep } from '../../../tests/utils';
describe('Test ScrollTo function', () => {
it('test scrollTo', async () => {
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((x, y) => {
const w = window;
w.scrollY = y;
w.pageYOffset = y;
});
scrollTo(0, 1000);
await sleep(1000);
expect(window.pageYOffset).toBe(1000);
scrollToSpy.mockRestore();
});
it('test unknow easing funciton', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
scrollTo(0, 0, {
ease: 'ffff',
});
expect(warnSpy).toHaveBeenCalledWith('Unkonw easing funciton in scrollTo');
warnSpy.mockRestore();
});
it('test callback - option', async () => {
const cbMock = jest.fn();
scrollTo(0, 1000, {
callback: cbMock,
});
await sleep(1000);
expect(cbMock).toHaveBeenCalledTimes(1);
});
it('test getContainer - option', async () => {
const div = document.createElement('div');
scrollTo(0, 1000, {
getContainer: () => div,
});
await sleep(1000);
expect(div.scrollTop).toBe(1000);
});
});

17
components/_util/eases.ts Normal file
View File

@ -0,0 +1,17 @@
const Eases = {
easeInOutCubic: (t: number, b: number, c: number, d: number) => {
const cc = c - b;
t /= d / 2;
if (t < 1) {
return (cc / 2) * t * t * t + b;
}
return (cc / 2) * ((t -= 2) * t * t + 2) + b;
},
easeOutCubic: (t: number, b: number, c: number, d: number) => {
const cc = c - b;
t /= d - 1;
return cc * (t * t * t + 1) + b;
},
};
export default Eases;

View File

@ -0,0 +1,53 @@
import raf from 'raf';
import getScroll from './getScroll';
import Eases from './eases';
type easeType = keyof typeof Eases;
interface ScrollToOptions {
/** Scroll container, deault as window */
getContainer?: () => HTMLElement | Window;
/** Scroll end callback */
callback?: () => any;
/** Animation duration, default as 450 */
duration?: number;
/** easing function name, default as easeInOutCubic */
ease?: easeType;
}
// TODO: support x
export default function scrollTo(x: number, y: number, options: ScrollToOptions = {}) {
const {
getContainer = () => window,
callback,
duration = 450,
ease = 'easeInOutCubic',
} = options;
const container = getContainer();
const scrollTop = getScroll(container, true);
const startTime = Date.now();
let easeFunc = Eases[ease];
if (!easeFunc) {
console.warn('Unkonw easing funciton in scrollTo');
easeFunc = Eases.easeInOutCubic;
}
const frameFunc = () => {
const timestamp = Date.now();
const time = timestamp - startTime;
const nextScrollTop = easeFunc(time > duration ? duration : time, scrollTop, y, duration);
if (container === window) {
window.scrollTo(x, nextScrollTop);
} else {
(container as HTMLElement).scrollTop = nextScrollTop;
}
if (time < duration) {
raf(frameFunc);
} else {
if (typeof callback === 'function') callback();
}
};
raf(frameFunc);
}

View File

@ -7,6 +7,7 @@ import raf from 'raf';
import Affix from '../affix';
import AnchorLink from './AnchorLink';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import scrollTo from '../_util/scrollTo';
import getScroll from '../_util/getScroll';
function getDefaultContainer() {
@ -35,54 +36,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
return rect.top;
}
function easeInOutCubic(t: number, b: number, c: number, d: number) {
const cc = c - b;
t /= d / 2;
if (t < 1) {
return (cc / 2) * t * t * t + b;
}
const r = (cc / 2) * ((t -= 2) * t * t + 2) + b;
// fix return value more than target value
return cc > 0 ? (r > c ? c : r) : r < c ? c : r;
}
const sharpMatcherRegx = /#([^#]+)$/;
function scrollTo(
href: string,
targetOffset = 0,
getContainer: () => AnchorContainer,
callback = () => {},
) {
const container = getContainer();
const scrollTop = getScroll(container, true);
const sharpLinkMatch = sharpMatcherRegx.exec(href);
if (!sharpLinkMatch) {
return;
}
const targetElement = document.getElementById(sharpLinkMatch[1]);
if (!targetElement) {
return;
}
const eleOffsetTop = getOffsetTop(targetElement, container);
const targetScrollTop = scrollTop + eleOffsetTop - targetOffset;
const startTime = Date.now();
const frameFunc = () => {
const timestamp = Date.now();
const time = timestamp - startTime;
const nextScrollTop = easeInOutCubic(time, scrollTop, targetScrollTop, 450);
if (container === window) {
window.scrollTo(window.pageXOffset, nextScrollTop);
} else {
(container as HTMLElement).scrollTop = nextScrollTop;
}
if (time < 450) {
raf(frameFunc);
} else {
callback();
}
};
raf(frameFunc);
}
type Section = {
link: string;
@ -208,7 +162,35 @@ export default class Anchor extends React.Component<AnchorProps, AnchorState> {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
}
};
handleScrollTo = (link: string) => {
const { offsetTop, getContainer, targetOffset } = this.props as AnchorDefaultProps;
this.setState({ activeLink: link });
const container = getContainer();
const scrollTop = getScroll(container, true);
const sharpLinkMatch = sharpMatcherRegx.exec(link);
if (!sharpLinkMatch) {
return;
}
const targetElement = document.getElementById(sharpLinkMatch[1]);
if (!targetElement) {
return;
}
const eleOffsetTop = getOffsetTop(targetElement, container);
let y = scrollTop + eleOffsetTop;
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
this.animating = true;
scrollTo(window.pageXOffset, y, {
callback: () => {
this.animating = false;
},
getContainer,
});
};
getCurrentAnchor(offsetTop = 0, bounds = 5): string {
const { getCurrentAnchor } = this.props;
@ -267,15 +249,6 @@ export default class Anchor extends React.Component<AnchorProps, AnchorState> {
}
};
handleScrollTo = (link: string) => {
const { offsetTop, getContainer, targetOffset } = this.props as AnchorDefaultProps;
this.animating = true;
this.setState({ activeLink: link });
scrollTo(link, targetOffset !== undefined ? targetOffset : offsetTop, getContainer, () => {
this.animating = false;
});
};
updateInk = () => {
if (typeof document === 'undefined') {
return;

View File

@ -1,16 +1,23 @@
import React from 'react';
import { mount } from 'enzyme';
import { sleep } from '../../../tests/utils';
import BackTop from '..';
describe('BackTop', () => {
it('should scroll to top after click it', async () => {
const wrapper = mount(<BackTop visibilityHeight={-1} />);
document.documentElement.scrollTop = 400;
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((x, y) => {
const w = window;
w.scrollY = y;
w.pageYOffset = y;
});
window.scrollTo(0, 400);
// trigger scroll manually
wrapper.instance().handleScroll();
await new Promise(resolve => setTimeout(resolve, 0));
await sleep();
wrapper.find('.ant-back-top').simulate('click');
await new Promise(resolve => setTimeout(resolve, 1000));
expect(Math.abs(Math.round(document.documentElement.scrollTop))).toBe(0);
await sleep(1000);
expect(window.pageYOffset).toBe(0);
scrollToSpy.mockRestore();
});
});

View File

@ -3,20 +3,9 @@ import Animate from 'rc-animate';
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import classNames from 'classnames';
import omit from 'omit.js';
import raf from 'raf';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import getScroll from '../_util/getScroll';
const easeInOutCubic = (t: number, b: number, c: number, d: number) => {
const cc = c - b;
t /= d / 2;
if (t < 1) {
return (cc / 2) * t * t * t + b;
}
return (cc / 2) * ((t -= 2) * t * t + 2) + b;
};
function noop() {}
import scrollTo from '../_util/scrollTo';
function getDefaultTarget() {
return window;
@ -79,20 +68,13 @@ export default class BackTop extends React.Component<BackTopProps, any> {
};
scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => {
const scrollTop = this.getCurrentScrollTop();
const startTime = Date.now();
const frameFunc = () => {
const timestamp = Date.now();
const time = timestamp - startTime;
this.setScrollTop(easeInOutCubic(time, scrollTop, 0, 450));
if (time < 450) {
raf(frameFunc);
} else {
this.setScrollTop(0);
}
};
raf(frameFunc);
(this.props.onClick || noop)(e);
const { target = getDefaultTarget } = this.props;
scrollTo(window.pageXOffset, 0, {
getContainer: target,
});
if (typeof this.props.onClick === 'function') {
this.props.onClick(e);
}
};
handleScroll = () => {

View File

@ -8,3 +8,5 @@ export function setMockDate(dateString = '2017-09-18T03:30:07.795') {
export function resetMockDate() {
MockDate.reset();
}
export const sleep = (timeout = 0) => new Promise(resolve => setTimeout(resolve, timeout));