Skip to content

Commit e075472

Browse files
committed
feat: useTransition
1 parent ac2759e commit e075472

File tree

15 files changed

+205
-32
lines changed

15 files changed

+205
-32
lines changed

demos/transition/AboutTab.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function AboutTab() {
2+
return <p>我是卡颂,这是我的个人页</p>;
3+
}

demos/transition/ContactTab.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default function ContactTab() {
2+
return (
3+
<>
4+
<p>你可以通过如下方式联系我:</p>
5+
<ul>
6+
<li>B站:魔术师卡颂</li>
7+
<li>微信:kasong555</li>
8+
</ul>
9+
</>
10+
);
11+
}

demos/transition/PostsTab.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const PostsTab = function PostsTab() {
2+
const items = [];
3+
for (let i = 0; i < 500; i++) {
4+
items.push(<SlowPost key={i} index={i} />);
5+
}
6+
return <ul className="items">{items}</ul>;
7+
};
8+
9+
function SlowPost({ index }) {
10+
const startTime = performance.now();
11+
while (performance.now() - startTime < 4) {}
12+
13+
return <li className="item">博文 #{index + 1}</li>;
14+
}
15+
16+
export default PostsTab;

demos/transition/TabButton.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useTransition } from 'react';
2+
3+
export default function TabButton({ children, isActive, onClick }) {
4+
if (isActive) {
5+
return <b>{children}</b>;
6+
}
7+
return (
8+
<button
9+
onClick={() => {
10+
onClick();
11+
}}
12+
>
13+
{children}
14+
</button>
15+
);
16+
}

demos/transition/index.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>Vite + React + TS</title>
9+
</head>
10+
11+
<body>
12+
<div id="root"></div>
13+
<!-- <script type="module" src="main.ts"></script> -->
14+
<script type="module" src="main.tsx"></script>
15+
</body>
16+
17+
</html>

demos/transition/main.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import ReactDOM from 'react-dom';
2+
3+
import { useState, useTransition } from 'react';
4+
import TabButton from './TabButton';
5+
import AboutTab from './AboutTab';
6+
import PostsTab from './PostsTab';
7+
import ContactTab from './ContactTab';
8+
import './style.css';
9+
10+
function App() {
11+
const [isPending, startTransition] = useTransition();
12+
const [tab, setTab] = useState('about');
13+
console.log('hello');
14+
function selectTab(nextTab) {
15+
startTransition(() => {
16+
setTab(nextTab);
17+
});
18+
}
19+
20+
return (
21+
<>
22+
<TabButton isActive={tab === 'about'} onClick={() => selectTab('about')}>
23+
首页
24+
</TabButton>
25+
<TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')}>
26+
博客 (render慢)
27+
</TabButton>
28+
<TabButton
29+
isActive={tab === 'contact'}
30+
onClick={() => selectTab('contact')}
31+
>
32+
联系我
33+
</TabButton>
34+
<hr />
35+
{tab === 'about' && <AboutTab />}
36+
{tab === 'posts' && <PostsTab />}
37+
{tab === 'contact' && <ContactTab />}
38+
</>
39+
);
40+
}
41+
42+
const root = ReactDOM.createRoot(document.querySelector('#root'));
43+
44+
root.render(<App />);

demos/transition/style.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
button {
2+
margin: 0 5px;
3+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"lint": "eslint --ext .js,.ts,.jsx,.tsx --fix --quiet ./packages",
88
"build:dev": "rimraf dist && rollup --bundleConfigAsCjs --config scripts/rollup/dev.config.js ",
9-
"demo": "vite serve demos/test-fc --config scripts/vite/vite.config.js --force",
9+
"demo": "vite serve demos/transition --config scripts/vite/vite.config.js --force",
1010
"test": "jest --config scripts/jest/jest.config.js"
1111
},
1212
"keywords": [],

packages/react-reconciler/src/fiberHooks.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useState } from 'react';
21
import { Dispatch } from 'react/src/currentDispatcher';
32
import { Dispatcher } from 'react/src/currentDispatcher';
3+
import currentBatchConfig from 'react/src/currentBatchConfig';
44
import internals from 'shared/internals';
55
import { Action } from 'shared/ReactTypes';
66
import { FiberNode } from './fiber';
@@ -80,12 +80,14 @@ export function renderWithHooks(wip: FiberNode, lane: Lane) {
8080

8181
const HooksDispatcherOnMount: Dispatcher = {
8282
useState: mountState,
83-
useEffect: mountEffect
83+
useEffect: mountEffect,
84+
useTransition: mountTransition
8485
};
8586

8687
const HooksDispatcherOnUpdate: Dispatcher = {
8788
useState: updateState,
88-
useEffect: updateEffect
89+
useEffect: updateEffect,
90+
useTransition: updateTransition
8991
};
9092

9193
function mountEffect(create: EffectCallback | void, deps: EffectDeps | void) {
@@ -215,17 +217,17 @@ function updateState<State>(): [State, Dispatch<State>] {
215217
// 保存在current中
216218
current.baseQueue = pending;
217219
queue.shared.pending = null;
220+
}
218221

219-
if (baseQueue !== null) {
220-
const {
221-
memoizedState,
222-
baseQueue: newBaseQueue,
223-
baseState: newBaseState
224-
} = processUpdateQueue(baseState, baseQueue, renderLane);
225-
hook.memoizedState = memoizedState;
226-
hook.baseState = newBaseState;
227-
hook.baseQueue = newBaseQueue;
228-
}
222+
if (baseQueue !== null) {
223+
const {
224+
memoizedState,
225+
baseQueue: newBaseQueue,
226+
baseState: newBaseState
227+
} = processUpdateQueue(baseState, baseQueue, renderLane);
228+
hook.memoizedState = memoizedState;
229+
hook.baseState = newBaseState;
230+
hook.baseQueue = newBaseQueue;
229231
}
230232

231233
return [hook.memoizedState, queue.dispatch as Dispatch<State>];
@@ -295,13 +297,40 @@ function mountState<State>(
295297
const queue = createUpdateQueue<State>();
296298
hook.updateQueue = queue;
297299
hook.memoizedState = memoizedState;
300+
hook.baseState = memoizedState;
298301

299302
// @ts-ignore
300303
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
301304
queue.dispatch = dispatch;
302305
return [memoizedState, dispatch];
303306
}
304307

308+
function mountTransition(): [boolean, (callback: () => void) => void] {
309+
const [isPending, setPending] = mountState(false);
310+
const hook = mountWorkInProgresHook();
311+
const start = startTransition.bind(null, setPending);
312+
hook.memoizedState = start;
313+
return [isPending, start];
314+
}
315+
316+
function updateTransition(): [boolean, (callback: () => void) => void] {
317+
const [isPending] = updateState();
318+
const hook = updateWorkInProgresHook();
319+
const start = hook.memoizedState;
320+
return [isPending as boolean, start];
321+
}
322+
323+
function startTransition(setPending: Dispatch<boolean>, callback: () => void) {
324+
setPending(true);
325+
const prevTransition = currentBatchConfig.transition;
326+
currentBatchConfig.transition = 1;
327+
328+
callback();
329+
setPending(false);
330+
331+
currentBatchConfig.transition = prevTransition;
332+
}
333+
305334
function dispatchSetState<State>(
306335
fiber: FiberNode,
307336
updateQueue: UpdateQueue<State>,

packages/react-reconciler/src/fiberLanes.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ReactCurrentBatchConfig from 'react/src/currentBatchConfig';
12
import {
23
unstable_getCurrentPriorityLevel,
34
unstable_IdlePriority,
@@ -10,18 +11,24 @@ import { FiberRootNode } from './fiber';
1011
export type Lane = number;
1112
export type Lanes = number;
1213

13-
export const SyncLane = 0b0001;
14-
export const NoLane = 0b0000;
15-
export const NoLanes = 0b0000;
16-
export const InputContinuousLane = 0b0010;
17-
export const DefaultLane = 0b0100;
18-
export const IdleLane = 0b1000;
14+
export const SyncLane = 0b00001;
15+
export const NoLane = 0b00000;
16+
export const NoLanes = 0b00000;
17+
export const InputContinuousLane = 0b00010;
18+
export const DefaultLane = 0b00100;
19+
export const TransitionLane = 0b01000;
20+
export const IdleLane = 0b10000;
1921

2022
export function mergeLanes(laneA: Lane, laneB: Lane): Lanes {
2123
return laneA | laneB;
2224
}
2325

2426
export function requestUpdateLane() {
27+
const isTransition = ReactCurrentBatchConfig.transition !== null;
28+
if (isTransition) {
29+
return TransitionLane;
30+
}
31+
2532
// 从上下文环境中获取Scheduler优先级
2633
const currentSchedulerPriority = unstable_getCurrentPriorityLevel();
2734
const lane = schedulerPriorityToLane(currentSchedulerPriority);

packages/react-reconciler/src/fiberReconciler.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { Container } from 'hostConfig';
2+
import {
3+
unstable_ImmediatePriority,
4+
unstable_runWithPriority
5+
} from 'scheduler';
26
import { ReactElementType } from 'shared/ReactTypes';
37
import { FiberNode, FiberRootNode } from './fiber';
48
import { requestUpdateLane } from './fiberLanes';
@@ -22,13 +26,15 @@ export function updateContainer(
2226
element: ReactElementType | null,
2327
root: FiberRootNode
2428
) {
25-
const hostRootFiber = root.current;
26-
const lane = requestUpdateLane();
27-
const update = createUpdate<ReactElementType | null>(element, lane);
28-
enqueueUpdate(
29-
hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>,
30-
update
31-
);
32-
scheduleUpdateOnFiber(hostRootFiber, lane);
29+
unstable_runWithPriority(unstable_ImmediatePriority, () => {
30+
const hostRootFiber = root.current;
31+
const lane = requestUpdateLane();
32+
const update = createUpdate<ReactElementType | null>(element, lane);
33+
enqueueUpdate(
34+
hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>,
35+
update
36+
);
37+
scheduleUpdateOnFiber(hostRootFiber, lane);
38+
});
3339
return element;
3440
}

packages/react-reconciler/src/workLoop.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,15 @@ function ensureRootIsScheduled(root: FiberRootNode) {
8383
}
8484
let newCallbackNode = null;
8585

86+
if (__DEV__) {
87+
console.log(
88+
`在${updateLane === SyncLane ? '微' : '宏'}任务中调度,优先级:`,
89+
updateLane
90+
);
91+
}
92+
8693
if (updateLane === SyncLane) {
8794
// 同步优先级 用微任务调度
88-
if (__DEV__) {
89-
console.log('在微任务中调度,优先级:', updateLane);
90-
}
9195
// [performSyncWorkOnRoot, performSyncWorkOnRoot, performSyncWorkOnRoot]
9296
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root, updateLane));
9397
scheduleMicroTask(flushSyncCallbacks);

packages/react/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Dispatcher, resolveDispatcher } from './src/currentDispatcher';
22
import currentDispatcher from './src/currentDispatcher';
3+
import currentBatchConfig from './src/currentBatchConfig';
34
import {
45
createElement as createElementFn,
56
isValidElement as isValidElementFn
@@ -17,9 +18,15 @@ export const useEffect: Dispatcher['useEffect'] = (create, deps) => {
1718
return dispatcher.useEffect(create, deps);
1819
};
1920

21+
export const useTransition: Dispatcher['useTransition'] = () => {
22+
const dispatcher = resolveDispatcher();
23+
return dispatcher.useTransition();
24+
};
25+
2026
// 内部数据共享层
2127
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
22-
currentDispatcher
28+
currentDispatcher,
29+
currentBatchConfig
2330
};
2431

2532
export const version = '0.0.0';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
interface BatchConfig {
2+
transition: number | null;
3+
}
4+
5+
const ReactCurrentBatchConfig: BatchConfig = {
6+
transition: null
7+
};
8+
9+
export default ReactCurrentBatchConfig;

packages/react/src/currentDispatcher.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Action } from 'shared/ReactTypes';
33
export interface Dispatcher {
44
useState: <T>(initialState: (() => T) | T) => [T, Dispatch<T>];
55
useEffect: (callback: () => void | void, deps: any[] | void) => void;
6+
useTransition: () => [boolean, (callback: () => void) => void];
67
}
78

89
export type Dispatch<State> = (action: Action<State>) => void;

0 commit comments

Comments
 (0)