Skip to content

Commit 306bcf9

Browse files
committed
feat: suspense
1 parent 18d2504 commit 306bcf9

23 files changed

+884
-60
lines changed

demos/suspense-use/Cpn.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useState, use, useEffect } from 'react';
2+
3+
const delay = (t) =>
4+
new Promise((r) => {
5+
setTimeout(r, t);
6+
});
7+
8+
const cachePool: any[] = [];
9+
10+
function fetchData(id, timeout) {
11+
const cache = cachePool[id];
12+
if (cache) {
13+
return cache;
14+
}
15+
return (cachePool[id] = delay(timeout).then(() => {
16+
return { data: Math.random().toFixed(2) * 100 };
17+
}));
18+
}
19+
20+
export function Cpn({ id, timeout }) {
21+
const [num, updateNum] = useState(0);
22+
const { data } = use(fetchData(id, timeout));
23+
24+
if (num !== 0 && num % 5 === 0) {
25+
cachePool[id] = null;
26+
}
27+
28+
useEffect(() => {
29+
console.log('effect create');
30+
return () => console.log('effect destroy');
31+
}, []);
32+
33+
return (
34+
<ul onClick={() => updateNum(num + 1)}>
35+
<li>ID: {id}</li>
36+
<li>随机数: {data}</li>
37+
<li>状态: {num}</li>
38+
</ul>
39+
);
40+
}

demos/suspense-use/index.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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>Suspense</title>
9+
</head>
10+
11+
<body>
12+
<div id="root"></div>
13+
<script type="module" src="main.tsx"></script>
14+
</body>
15+
16+
</html>

demos/suspense-use/main.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Fragment, Suspense, useState } from 'react';
2+
import ReactDOM from 'react-dom/client';
3+
import { Cpn } from './Cpn';
4+
5+
// 简单例子 + 没有Suspense catch的情况
6+
function App() {
7+
return (
8+
<Suspense fallback={<div>loading...</div>}>
9+
<Cpn id={0} timeout={1000} />
10+
</Suspense>
11+
// <Cpn id={0} timeout={1000} />
12+
);
13+
}
14+
15+
// 嵌套Suspense
16+
// function App() {
17+
// return (
18+
// <Suspense fallback={<div>外层...</div>}>
19+
// <Cpn id={0} timeout={1000} />
20+
// <Suspense fallback={<div>内层...</div>}>
21+
// <Cpn id={1} timeout={3000} />
22+
// </Suspense>
23+
// </Suspense>
24+
// );
25+
// }
26+
27+
// 缓存快速失效
28+
// function App() {
29+
// const [num, setNum] = useState(0);
30+
// return (
31+
// <div>
32+
// <button onClick={() => setNum(num + 1)}>change id: {num}</button>
33+
// <Suspense fallback={<div>loading...</div>}>
34+
// <Cpn id={num} timeout={2000} />
35+
// </Suspense>
36+
// </div>
37+
// );
38+
// }
39+
40+
ReactDOM.createRoot(document.getElementById('root')).render(<App />);

demos/suspense-use/vite-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "index.js",
66
"scripts": {
77
"build:dev": "rm -rf dist && rollup --config scripts/rollup/dev.config.js",
8-
"demo": "vite serve demos/context --config scripts/vite/vite.config.js --force",
8+
"demo": "vite serve demos/suspense-use --config scripts/vite/vite.config.js --force",
99
"lint": "eslint --ext .ts,.jsx,.tsx --fix --quiet ./packages",
1010
"test": "jest --config scripts/jest/jest.config.js"
1111
},

packages/react-dom/src/hostConfig.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,21 @@ export const scheduleMicroTask =
6868
: typeof Promise === 'function'
6969
? (callback: (...args: any) => void) => Promise.resolve(null).then(callback)
7070
: setTimeout;
71+
72+
export function hideInstance(instance: Instance) {
73+
const style = (instance as HTMLElement).style;
74+
style.setProperty('display', 'none', 'important');
75+
}
76+
77+
export function unhideInstance(instance: Instance) {
78+
const style = (instance as HTMLElement).style;
79+
style.display = '';
80+
}
81+
82+
export function hideTextInstance(textInstance: TextInstance) {
83+
textInstance.nodeValue = '';
84+
}
85+
86+
export function unhideTextInstance(textInstance: TextInstance, text: string) {
87+
textInstance.nodeValue = text;
88+
}

packages/react-reconciler/src/beginWork.ts

Lines changed: 182 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { ReactElementType } from 'shared/ReactTypes';
22
import { mountChildFibers, reconcileChildFibers } from './childFibers';
3-
import { FiberNode } from './fiber';
3+
import {
4+
FiberNode,
5+
createFiberFromFragment,
6+
createWorkInProgress,
7+
createFiberFromOffscreen,
8+
OffscreenProps
9+
} from './fiber';
410
import { renderWithHooks } from './fiberHooks';
511
import { Lane } from './fiberLanes';
612
import { processUpdateQueue, UpdateQueue } from './updateQueue';
@@ -10,10 +16,19 @@ import {
1016
FunctionComponent,
1117
HostComponent,
1218
HostRoot,
13-
HostText
19+
HostText,
20+
OffscreenComponent,
21+
SuspenseComponent
1422
} from './workTags';
15-
import { Ref } from './fiberFlags';
23+
import {
24+
Ref,
25+
NoFlags,
26+
DidCapture,
27+
Placement,
28+
ChildDeletion
29+
} from './fiberFlags';
1630
import { pushProvider } from './fiberContext';
31+
import { pushSuspenseHandler } from './suspenseContext';
1732

1833
// 递归中的递阶段
1934
export const beginWork = (wip: FiberNode, renderLane: Lane) => {
@@ -31,6 +46,10 @@ export const beginWork = (wip: FiberNode, renderLane: Lane) => {
3146
return updateFragment(wip);
3247
case ContextProvider:
3348
return updateContextProvider(wip);
49+
case SuspenseComponent:
50+
return updateSuspenseComponent(wip);
51+
case OffscreenComponent:
52+
return updateOffscreenComponent(wip);
3453
default:
3554
if (__DEV__) {
3655
console.warn('beginWork未实现的类型');
@@ -72,6 +91,12 @@ function updateHostRoot(wip: FiberNode, renderLane: Lane) {
7291
const { memoizedState } = processUpdateQueue(baseState, pending, renderLane);
7392
wip.memoizedState = memoizedState;
7493

94+
const current = wip.alternate;
95+
// 考虑RootDidNotComplete的情况,需要复用memoizedState
96+
if (current !== null) {
97+
current.memoizedState = memoizedState;
98+
}
99+
75100
const nextChildren = wip.memoizedState;
76101
reconcileChildren(wip, nextChildren);
77102
return wip.child;
@@ -107,3 +132,157 @@ function markRef(current: FiberNode | null, workInProgress: FiberNode) {
107132
workInProgress.flags |= Ref;
108133
}
109134
}
135+
136+
function updateOffscreenComponent(workInProgress: FiberNode) {
137+
const nextProps = workInProgress.pendingProps;
138+
const nextChildren = nextProps.children;
139+
reconcileChildren(workInProgress, nextChildren);
140+
return workInProgress.child;
141+
}
142+
143+
function updateSuspenseComponent(workInProgress: FiberNode) {
144+
const current = workInProgress.alternate;
145+
const nextProps = workInProgress.pendingProps;
146+
147+
let showFallback = false;
148+
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
149+
150+
if (didSuspend) {
151+
showFallback = true;
152+
workInProgress.flags &= ~DidCapture;
153+
}
154+
const nextPrimaryChildren = nextProps.children;
155+
const nextFallbackChildren = nextProps.fallback;
156+
pushSuspenseHandler(workInProgress);
157+
158+
if (current === null) {
159+
if (showFallback) {
160+
return mountSuspenseFallbackChildren(
161+
workInProgress,
162+
nextPrimaryChildren,
163+
nextFallbackChildren
164+
);
165+
} else {
166+
return mountSuspensePrimaryChildren(workInProgress, nextPrimaryChildren);
167+
}
168+
} else {
169+
if (showFallback) {
170+
return updateSuspenseFallbackChildren(
171+
workInProgress,
172+
nextPrimaryChildren,
173+
nextFallbackChildren
174+
);
175+
} else {
176+
return updateSuspensePrimaryChildren(workInProgress, nextPrimaryChildren);
177+
}
178+
}
179+
}
180+
181+
function mountSuspensePrimaryChildren(
182+
workInProgress: FiberNode,
183+
primaryChildren: any
184+
) {
185+
const primaryChildProps: OffscreenProps = {
186+
mode: 'visible',
187+
children: primaryChildren
188+
};
189+
const primaryChildFragment = createFiberFromOffscreen(primaryChildProps);
190+
workInProgress.child = primaryChildFragment;
191+
primaryChildFragment.return = workInProgress;
192+
return primaryChildFragment;
193+
}
194+
195+
function mountSuspenseFallbackChildren(
196+
workInProgress: FiberNode,
197+
primaryChildren: any,
198+
fallbackChildren: any
199+
) {
200+
const primaryChildProps: OffscreenProps = {
201+
mode: 'hidden',
202+
children: primaryChildren
203+
};
204+
const primaryChildFragment = createFiberFromOffscreen(primaryChildProps);
205+
const fallbackChildFragment = createFiberFromFragment(fallbackChildren, null);
206+
// 父组件Suspense已经mount,所以需要fallback标记Placement
207+
fallbackChildFragment.flags |= Placement;
208+
209+
primaryChildFragment.return = workInProgress;
210+
fallbackChildFragment.return = workInProgress;
211+
primaryChildFragment.sibling = fallbackChildFragment;
212+
workInProgress.child = primaryChildFragment;
213+
214+
return fallbackChildFragment;
215+
}
216+
217+
function updateSuspensePrimaryChildren(
218+
workInProgress: FiberNode,
219+
primaryChildren: any
220+
) {
221+
const current = workInProgress.alternate as FiberNode;
222+
const currentPrimaryChildFragment = current.child as FiberNode;
223+
const currentFallbackChildFragment: FiberNode | null =
224+
currentPrimaryChildFragment.sibling;
225+
226+
const primaryChildProps: OffscreenProps = {
227+
mode: 'visible',
228+
children: primaryChildren
229+
};
230+
231+
const primaryChildFragment = createWorkInProgress(
232+
currentPrimaryChildFragment,
233+
primaryChildProps
234+
);
235+
primaryChildFragment.return = workInProgress;
236+
primaryChildFragment.sibling = null;
237+
workInProgress.child = primaryChildFragment;
238+
239+
if (currentFallbackChildFragment !== null) {
240+
const deletions = workInProgress.deletions;
241+
if (deletions === null) {
242+
workInProgress.deletions = [currentFallbackChildFragment];
243+
workInProgress.flags |= ChildDeletion;
244+
} else {
245+
deletions.push(currentFallbackChildFragment);
246+
}
247+
}
248+
249+
return primaryChildFragment;
250+
}
251+
252+
function updateSuspenseFallbackChildren(
253+
workInProgress: FiberNode,
254+
primaryChildren: any,
255+
fallbackChildren: any
256+
) {
257+
const current = workInProgress.alternate as FiberNode;
258+
const currentPrimaryChildFragment = current.child as FiberNode;
259+
const currentFallbackChildFragment: FiberNode | null =
260+
currentPrimaryChildFragment.sibling;
261+
262+
const primaryChildProps: OffscreenProps = {
263+
mode: 'hidden',
264+
children: primaryChildren
265+
};
266+
const primaryChildFragment = createWorkInProgress(
267+
currentPrimaryChildFragment,
268+
primaryChildProps
269+
);
270+
let fallbackChildFragment;
271+
272+
if (currentFallbackChildFragment !== null) {
273+
// 可以复用
274+
fallbackChildFragment = createWorkInProgress(
275+
currentFallbackChildFragment,
276+
fallbackChildren
277+
);
278+
} else {
279+
fallbackChildFragment = createFiberFromFragment(fallbackChildren, null);
280+
fallbackChildFragment.flags |= Placement;
281+
}
282+
fallbackChildFragment.return = workInProgress;
283+
primaryChildFragment.return = workInProgress;
284+
primaryChildFragment.sibling = fallbackChildFragment;
285+
workInProgress.child = primaryChildFragment;
286+
287+
return fallbackChildFragment;
288+
}

0 commit comments

Comments
 (0)