Skip to content

Commit

Permalink
feat: suspense
Browse files Browse the repository at this point in the history
  • Loading branch information
BetaSu committed Jul 23, 2023
1 parent 18d2504 commit 306bcf9
Show file tree
Hide file tree
Showing 23 changed files with 884 additions and 60 deletions.
40 changes: 40 additions & 0 deletions demos/suspense-use/Cpn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState, use, useEffect } from 'react';

const delay = (t) =>
new Promise((r) => {
setTimeout(r, t);
});

const cachePool: any[] = [];

function fetchData(id, timeout) {
const cache = cachePool[id];
if (cache) {
return cache;
}
return (cachePool[id] = delay(timeout).then(() => {
return { data: Math.random().toFixed(2) * 100 };
}));
}

export function Cpn({ id, timeout }) {
const [num, updateNum] = useState(0);
const { data } = use(fetchData(id, timeout));

if (num !== 0 && num % 5 === 0) {
cachePool[id] = null;
}

useEffect(() => {
console.log('effect create');
return () => console.log('effect destroy');
}, []);

return (
<ul onClick={() => updateNum(num + 1)}>
<li>ID: {id}</li>
<li>随机数: {data}</li>
<li>状态: {num}</li>
</ul>
);
}
16 changes: 16 additions & 0 deletions demos/suspense-use/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Suspense</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="main.tsx"></script>
</body>

</html>
40 changes: 40 additions & 0 deletions demos/suspense-use/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Fragment, Suspense, useState } from 'react';
import ReactDOM from 'react-dom/client';
import { Cpn } from './Cpn';

// 简单例子 + 没有Suspense catch的情况
function App() {
return (
<Suspense fallback={<div>loading...</div>}>
<Cpn id={0} timeout={1000} />
</Suspense>
// <Cpn id={0} timeout={1000} />
);
}

// 嵌套Suspense
// function App() {
// return (
// <Suspense fallback={<div>外层...</div>}>
// <Cpn id={0} timeout={1000} />
// <Suspense fallback={<div>内层...</div>}>
// <Cpn id={1} timeout={3000} />
// </Suspense>
// </Suspense>
// );
// }

// 缓存快速失效
// function App() {
// const [num, setNum] = useState(0);
// return (
// <div>
// <button onClick={() => setNum(num + 1)}>change id: {num}</button>
// <Suspense fallback={<div>loading...</div>}>
// <Cpn id={num} timeout={2000} />
// </Suspense>
// </div>
// );
// }

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
1 change: 1 addition & 0 deletions demos/suspense-use/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"build:dev": "rm -rf dist && rollup --config scripts/rollup/dev.config.js",
"demo": "vite serve demos/context --config scripts/vite/vite.config.js --force",
"demo": "vite serve demos/suspense-use --config scripts/vite/vite.config.js --force",
"lint": "eslint --ext .ts,.jsx,.tsx --fix --quiet ./packages",
"test": "jest --config scripts/jest/jest.config.js"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/react-dom/src/hostConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,21 @@ export const scheduleMicroTask =
: typeof Promise === 'function'
? (callback: (...args: any) => void) => Promise.resolve(null).then(callback)
: setTimeout;

export function hideInstance(instance: Instance) {
const style = (instance as HTMLElement).style;
style.setProperty('display', 'none', 'important');
}

export function unhideInstance(instance: Instance) {
const style = (instance as HTMLElement).style;
style.display = '';
}

export function hideTextInstance(textInstance: TextInstance) {
textInstance.nodeValue = '';
}

export function unhideTextInstance(textInstance: TextInstance, text: string) {
textInstance.nodeValue = text;
}
185 changes: 182 additions & 3 deletions packages/react-reconciler/src/beginWork.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ReactElementType } from 'shared/ReactTypes';
import { mountChildFibers, reconcileChildFibers } from './childFibers';
import { FiberNode } from './fiber';
import {
FiberNode,
createFiberFromFragment,
createWorkInProgress,
createFiberFromOffscreen,
OffscreenProps
} from './fiber';
import { renderWithHooks } from './fiberHooks';
import { Lane } from './fiberLanes';
import { processUpdateQueue, UpdateQueue } from './updateQueue';
Expand All @@ -10,10 +16,19 @@ import {
FunctionComponent,
HostComponent,
HostRoot,
HostText
HostText,
OffscreenComponent,
SuspenseComponent
} from './workTags';
import { Ref } from './fiberFlags';
import {
Ref,
NoFlags,
DidCapture,
Placement,
ChildDeletion
} from './fiberFlags';
import { pushProvider } from './fiberContext';
import { pushSuspenseHandler } from './suspenseContext';

// 递归中的递阶段
export const beginWork = (wip: FiberNode, renderLane: Lane) => {
Expand All @@ -31,6 +46,10 @@ export const beginWork = (wip: FiberNode, renderLane: Lane) => {
return updateFragment(wip);
case ContextProvider:
return updateContextProvider(wip);
case SuspenseComponent:
return updateSuspenseComponent(wip);
case OffscreenComponent:
return updateOffscreenComponent(wip);
default:
if (__DEV__) {
console.warn('beginWork未实现的类型');
Expand Down Expand Up @@ -72,6 +91,12 @@ function updateHostRoot(wip: FiberNode, renderLane: Lane) {
const { memoizedState } = processUpdateQueue(baseState, pending, renderLane);
wip.memoizedState = memoizedState;

const current = wip.alternate;
// 考虑RootDidNotComplete的情况,需要复用memoizedState
if (current !== null) {
current.memoizedState = memoizedState;
}

const nextChildren = wip.memoizedState;
reconcileChildren(wip, nextChildren);
return wip.child;
Expand Down Expand Up @@ -107,3 +132,157 @@ function markRef(current: FiberNode | null, workInProgress: FiberNode) {
workInProgress.flags |= Ref;
}
}

function updateOffscreenComponent(workInProgress: FiberNode) {
const nextProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
reconcileChildren(workInProgress, nextChildren);
return workInProgress.child;
}

function updateSuspenseComponent(workInProgress: FiberNode) {
const current = workInProgress.alternate;
const nextProps = workInProgress.pendingProps;

let showFallback = false;
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;

if (didSuspend) {
showFallback = true;
workInProgress.flags &= ~DidCapture;
}
const nextPrimaryChildren = nextProps.children;
const nextFallbackChildren = nextProps.fallback;
pushSuspenseHandler(workInProgress);

if (current === null) {
if (showFallback) {
return mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren
);
} else {
return mountSuspensePrimaryChildren(workInProgress, nextPrimaryChildren);
}
} else {
if (showFallback) {
return updateSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren
);
} else {
return updateSuspensePrimaryChildren(workInProgress, nextPrimaryChildren);
}
}
}

function mountSuspensePrimaryChildren(
workInProgress: FiberNode,
primaryChildren: any
) {
const primaryChildProps: OffscreenProps = {
mode: 'visible',
children: primaryChildren
};
const primaryChildFragment = createFiberFromOffscreen(primaryChildProps);
workInProgress.child = primaryChildFragment;
primaryChildFragment.return = workInProgress;
return primaryChildFragment;
}

function mountSuspenseFallbackChildren(
workInProgress: FiberNode,
primaryChildren: any,
fallbackChildren: any
) {
const primaryChildProps: OffscreenProps = {
mode: 'hidden',
children: primaryChildren
};
const primaryChildFragment = createFiberFromOffscreen(primaryChildProps);
const fallbackChildFragment = createFiberFromFragment(fallbackChildren, null);
// 父组件Suspense已经mount,所以需要fallback标记Placement
fallbackChildFragment.flags |= Placement;

primaryChildFragment.return = workInProgress;
fallbackChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;

return fallbackChildFragment;
}

function updateSuspensePrimaryChildren(
workInProgress: FiberNode,
primaryChildren: any
) {
const current = workInProgress.alternate as FiberNode;
const currentPrimaryChildFragment = current.child as FiberNode;
const currentFallbackChildFragment: FiberNode | null =
currentPrimaryChildFragment.sibling;

const primaryChildProps: OffscreenProps = {
mode: 'visible',
children: primaryChildren
};

const primaryChildFragment = createWorkInProgress(
currentPrimaryChildFragment,
primaryChildProps
);
primaryChildFragment.return = workInProgress;
primaryChildFragment.sibling = null;
workInProgress.child = primaryChildFragment;

if (currentFallbackChildFragment !== null) {
const deletions = workInProgress.deletions;
if (deletions === null) {
workInProgress.deletions = [currentFallbackChildFragment];
workInProgress.flags |= ChildDeletion;
} else {
deletions.push(currentFallbackChildFragment);
}
}

return primaryChildFragment;
}

function updateSuspenseFallbackChildren(
workInProgress: FiberNode,
primaryChildren: any,
fallbackChildren: any
) {
const current = workInProgress.alternate as FiberNode;
const currentPrimaryChildFragment = current.child as FiberNode;
const currentFallbackChildFragment: FiberNode | null =
currentPrimaryChildFragment.sibling;

const primaryChildProps: OffscreenProps = {
mode: 'hidden',
children: primaryChildren
};
const primaryChildFragment = createWorkInProgress(
currentPrimaryChildFragment,
primaryChildProps
);
let fallbackChildFragment;

if (currentFallbackChildFragment !== null) {
// 可以复用
fallbackChildFragment = createWorkInProgress(
currentFallbackChildFragment,
fallbackChildren
);
} else {
fallbackChildFragment = createFiberFromFragment(fallbackChildren, null);
fallbackChildFragment.flags |= Placement;
}
fallbackChildFragment.return = workInProgress;
primaryChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;

return fallbackChildFragment;
}
Loading

0 comments on commit 306bcf9

Please sign in to comment.