Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 用RAF实现setTimeout #1527

Merged
merged 5 commits into from
Mar 30, 2022
Merged
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
1 change: 1 addition & 0 deletions config/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const menus = [
'useInterval',
'useRafInterval',
'useTimeout',
'useRafTimeout',
'useLockFn',
'useUpdate',
],
Expand Down
2 changes: 2 additions & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import useWebSocket from './useWebSocket';
import useWhyDidYouUpdate from './useWhyDidYouUpdate';
import { createUpdateEffect } from './createUpdateEffect';
import useRafInterval from './useRafInterval';
import useRafTimeout from './useRafTimeout';

export {
useRequest,
Expand Down Expand Up @@ -148,4 +149,5 @@ export {
useFocusWithin,
createUpdateEffect,
useRafInterval,
useRafTimeout,
};
2 changes: 1 addition & 1 deletion packages/hooks/src/useRafInterval/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Please note that the following two cases are likely to be inapplicable, and `use
- the time interval is less than `16ms`
- want to execute the timer when page is not rendering;

> `requestAnimationFrame` will automatically downgrade to `setInterval` in node enviraonment
> `requestAnimationFrame` will automatically downgrade to `setInterval` in node environment

## Examples

Expand Down
39 changes: 39 additions & 0 deletions packages/hooks/src/useRafTimeout/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { renderHook } from '@testing-library/react-hooks';
import useRafTimeout from '../index';

interface ParamsObj {
fn: (...arg: any) => any;
delay: number | undefined;
}

const setUp = ({ fn, delay }: ParamsObj) => renderHook(() => useRafTimeout(fn, delay));

const FRAME_TIME = 16.7;
describe('useRafTimeout', () => {
beforeAll(() => {
jest.useFakeTimers('modern');
});
afterAll(() => {
jest.restoreAllMocks();
});
it('should be defined', () => {
expect(useRafTimeout).toBeDefined();
});

it('timeout should work', () => {
const callback = jest.fn();
setUp({ fn: callback, delay: FRAME_TIME });
expect(callback).not.toBeCalled();
jest.advanceTimersByTime(FRAME_TIME * 2.5);
expect(callback).toHaveBeenCalledTimes(1);
});

it('timeout should stop when delay is undefined', () => {
const delay: number | undefined = undefined;
const callback = jest.fn();
setUp({ fn: callback, delay });
expect(callback).not.toBeCalled();
jest.advanceTimersByTime(FRAME_TIME * 1.5);
expect(callback).not.toBeCalled();
});
});
37 changes: 37 additions & 0 deletions packages/hooks/src/useRafTimeout/__tests__/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { renderHook } from '@testing-library/react-hooks';
import useRafTimeout from '../index';

interface ParamsObj {
fn: (...arg: any) => any;
delay: number | undefined;
}

const setUp = ({ fn, delay }: ParamsObj) => renderHook(() => useRafTimeout(fn, delay));

const FRAME_TIME = 16.7;
describe('useRafTimeout', () => {
beforeAll(() => {
jest.useFakeTimers('modern');
});
afterAll(() => {
jest.restoreAllMocks();
});
it('should downgrade to setTimeout when requstAnimationFrame is undefined', () => {
const _requestAnimationFrame = global.requestAnimationFrame;
const _cancelAnimationFrame = global.cancelAnimationFrame;

// @ts-ignore
delete global.requestAnimationFrame;
// @ts-ignore
delete global.cancelAnimationFrame;

const callback = jest.fn();
setUp({ fn: callback, delay: FRAME_TIME });
expect(callback).not.toBeCalled();
jest.advanceTimersByTime(FRAME_TIME * 1.5);
expect(callback).toHaveBeenCalledTimes(1);

global.requestAnimationFrame = _requestAnimationFrame;
global.cancelAnimationFrame = _cancelAnimationFrame;
});
});
20 changes: 20 additions & 0 deletions packages/hooks/src/useRafTimeout/demo/demo1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* title: Basic usage
* desc: Execute after 2000ms.
*
* title.zh-CN: 基础用法
* desc.zh-CN: 在 2000ms 后执行。
*/

import React, { useState } from 'react';
import { useRafTimeout } from 'ahooks';

export default () => {
const [count, setCount] = useState(0);

useRafTimeout(() => {
setCount(count + 1);
}, 2000);

return <div>count: {count}</div>;
};
44 changes: 44 additions & 0 deletions packages/hooks/src/useRafTimeout/demo/demo2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* title: Advanced usage
* desc: Modify the delay to realize the timer timeout change and pause.
*
* title.zh-CN: 进阶使用
* desc.zh-CN: 动态修改 delay 以实现定时器间隔变化与暂停。
*/

import React, { useState } from 'react';
import { useRafTimeout } from 'ahooks';

export default () => {
const [count, setCount] = useState(0);
const [delay, setDelay] = useState<number | undefined>(1000);

useRafTimeout(() => {
setCount(count + 1);
}, delay);

return (
<div>
<p> count: {count} </p>
<p style={{ marginTop: 16 }}> Delay: {delay} </p>
<button onClick={() => setDelay((t) => (!!t ? t + 1000 : 1000))} style={{ marginRight: 8 }}>
Delay + 1000
</button>
<button
style={{ marginRight: 8 }}
onClick={() => {
setDelay(1000);
}}
>
reset Delay
</button>
<button
onClick={() => {
setDelay(undefined);
}}
>
clear
</button>
</div>
);
};
36 changes: 36 additions & 0 deletions packages/hooks/src/useRafTimeout/index.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
nav:
path: /hooks
---

# useRafTimeout

A hook implements with `requestAnimationFrame` for better performance. The API is consistent with `useTimeout`.

> `requestAnimationFrame` will automatically downgrade to `setTimeout` in node environment

## Examples

### Default usage

<code src="./demo/demo1.tsx" />

### Advanced usage

<code src="./demo/demo2.tsx" />

## API

```typescript
useRafTimeout(
fn: () => void,
delay?: number | undefined,
);
```

### Params

| Property | Description | Type |
|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|
| fn | The function to be executed every `delay` milliseconds. | `() => void` |
| delay | The time in milliseconds, the timer should delay in between executions of the specified function. The timer will be cancelled if delay is set to `undefined`. | `number` \| `undefined` |
59 changes: 59 additions & 0 deletions packages/hooks/src/useRafTimeout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect } from 'react';
import useLatest from '../useLatest';

interface Handle {
id: number | NodeJS.Timeout;
}

const setRafTimeout = function (callback: () => void, delay: number = 16.7): Handle {
if (typeof requestAnimationFrame === typeof undefined) {
return {
id: setTimeout(callback, delay),
};
}
const handle: Handle = {
id: 0,
};

const now = Date.now;
let startTime = now();
let endTime = startTime;

const loop = () => {
handle.id = requestAnimationFrame(loop);
endTime = now();
if (endTime - startTime >= delay) {
callback();
clearRafTimeout(handle);
}
};
handle.id = requestAnimationFrame(loop);
return handle;
};

function cancelAnimationFrameIsNotDefined(t: any): t is NodeJS.Timer {
return typeof cancelAnimationFrame === typeof undefined;
}

const clearRafTimeout = function (handle: Handle) {
if (cancelAnimationFrameIsNotDefined(handle.id)) {
return clearTimeout(handle.id);
}
cancelAnimationFrame(handle.id);
};

function useRafTimeout(fn: () => void, delay: number | undefined) {
const fnRef = useLatest(fn);

useEffect(() => {
if (typeof delay !== 'number' || delay < 0) return;
const timer = setRafTimeout(() => {
fnRef.current();
}, delay);
return () => {
clearRafTimeout(timer);
};
}, [delay]);
}

export default useRafTimeout;
36 changes: 36 additions & 0 deletions packages/hooks/src/useRafTimeout/index.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
nav:
path: /hooks
---

# useRafTimeout

用 `requestAnimationFrame` 模拟实现 `setTimeout`,API 和 `useTimeout` 保持一致

> Node 环境下 `requestAnimationFrame` 会自动降级到 `setTimeout`

## 代码演示

### 基础用法

<code src="./demo/demo1.tsx" />

### 进阶使用

<code src="./demo/demo2.tsx" />

## API

```typescript
useRafTimeout(
fn: () => void,
delay?: number | undefined,
);
```

### Params

| 参数 | 说明 | 类型 |
|---------|---------------------------------------------|-------------------------|
| fn | 要定时调用的函数 | `() => void` |
| delay | 间隔时间,当取值 `undefined` 时会停止计时器 | `number` \| `undefined` |