npm install react react-dom @types/react @types/react-dom --save
npm install vite @vitejs/plugin-react-refresh --save-dev
vite.config.js
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
export default defineConfig({
plugins: [reactRefresh()]
})
package.json
{
"scripts": {
"start": "vite"
},
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="root"></div>
<script src="/src/main.jsx" type="module"></script>
</body>
</html>
src\main.jsx
import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
const element = <div>app</div>
render(element, root);
createRoot(root).render(element);
src\main.jsx
import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
+import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
+const element = <OldBatchUpdatePage />
render(element, root);
//createRoot(root).render(element);
src\routes\OldBatchUpdatePage.jsx
import React, { Component } from 'react'
import { unstable_batchedUpdates } from 'react-dom'
class OldBatchUpdatePage extends Component {
state = { number: 0 }
handleCLick = () => {
this.setState({ number: this.state.number + 1 });
console.log("number", this.state.number);
this.setState({ number: this.state.number + 1 });
console.log("number", this.state.number);
setTimeout(() => {
//unstable_batchedUpdates(() => {
this.setState({ number: this.state.number + 1 });
console.log("number", this.state.number);
this.setState({ number: this.state.number + 1 });
console.log("number", this.state.number);
//})
}, 0);
};
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleCLick}>+</button>
</div>
);
}
}
export default OldBatchUpdatePage;
let isBatchingUpdate = false;
let updateQueue = [];
let state = { number: 0 };
function setState(newState) {
if (isBatchingUpdate) {
updateQueue.push(newState);
} else {
state = newState;
}
}
const handleCLick = () => {
setState({ number: state.number + 1 });
console.log("number", state.number);
setState({ number: state.number + 1 });
console.log("number", state.number);
setTimeout(() => {
setState({ number: state.number + 1 });
console.log("number", state.number);
setState({ number: state.number + 1 });
console.log("number", state.number);
}, 0);
};
function batchedUpdates(fn) {
isBatchingUpdate = true;
fn();
isBatchingUpdate = false;
updateQueue.forEach(newState => {
state = newState
});
}
batchedUpdates(handleCLick);
src\main.jsx
import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
+import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
+const element = <NewBatchUpdatePage />
render(element, root);
//createRoot(root).render(element);
src\routes\NewBatchUpdatePage.jsx
import React, { Component } from 'react'
import { flushSync } from 'react-dom'
class NewBatchUpdatePage extends Component {
state = { number: 0 }
handleCLick = () => {
this.setState({ number: this.state.number + 1 });
console.log("number", this.state.number);
this.setState({ number: this.state.number + 1 });
console.log("number", this.state.number);
setTimeout(() => {
flushSync(() => {
this.setState({ number: this.state.number + 1 });
});
console.log("number", this.state.number);
flushSync(() => {
this.setState({ number: this.state.number + 1 });
});
console.log("number", this.state.number);
}, 0);
};
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleCLick}>+</button>
</div>
);
}
}
export default NewBatchUpdatePage
let updateQueue = [];
let lastPriority = -1;
let state = { number: 0 };
const InputPriority = 1;
const NormalPriority = 1;
let lastUpdatePriority;
function setState(newState, priority) {
updateQueue.push(newState);
if (lastUpdatePriority === priority) {
return;
}
lastUpdatePriority = priority;
setTimeout(() => {
updateQueue.forEach(newState => {
state = newState
});
});
}
function flushSync(fn) {
lastUpdatePriority = null;
fn();
}
const handleCLick = () => {
setState({ number: state.number + 1 }, InputPriority);
console.log("number", state.number);
setState({ number: state.number + 1 }, InputPriority);
console.log("number", state.number);
setTimeout(() => {
setState({ number: state.number + 1 }, NormalPriority);
console.log("number", state.number);
setState({ number: state.number + 1 }, NormalPriority);
console.log("number", state.number);
}, 500);
};
handleCLick();
Suspense
让你的组件在渲染之前进行等待,并在等待时显示fallback
的内容Suspense
内的组件子树比组件树的其他部分拥有更低的优先级render
函数中我们可以使用异步请求数据react
会从我们缓存中读取这个缓存promise异常
ErrorBoundary
(错误边界)是一个组件,该组件会捕获到渲染期间(render)子组件发生的错误,并有能力阻止错误继续传播src\main.jsx
import React from 'react'
-import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
+import SuspensePage from './routes/SuspensePage';
+const element = <SuspensePage />
-render(element, root);
createRoot(root).render(element);
src\routes\SuspensePage.jsx
import React, { Component, Suspense } from 'react'
import ErrorBoundary from './components/ErrorBoundary';
import { fetchUser } from '../fakeApi';
import { wrapPromise } from '../utils';
const userPromise = fetchUser('1');
const userResource = wrapPromise(userPromise);
class SuspensePage extends Component {
render() {
return (
<Suspense fallback={<h3>Loading User......</h3>}>
<ErrorBoundary>
<User />
</ErrorBoundary>
</Suspense >
);
}
}
function User() {
const user = userResource.read();
return <div>{user.id}:{user.name}</div>
}
export default SuspensePage;
src\routes\components\ErrorBoundary.jsx
import React from 'react'
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return {
hasError: true,
error
};
}
render() {
if (this.state.hasError) {
return (
<>
{this.state.error.msg}
</>
);
}
return this.props.children;
}
}
export default ErrorBoundary
src\fakeApi.jsx
export function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id, name: `姓名${id}` });
//reject({ msg: '获取数据失败' });
}, 1000 * Number(id));
});
}
src\utils.jsx
export function wrapPromise(promise) {
let status = "pending";
let result;
let suspender = promise.then(
(r) => {
status = "success";
result = r;
},
(e) => {
status = "error";
result = e;
}
);
return {
read() {
if (status === "pending") {
throw suspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
}
};
}
startTransition
主要为了能在大量的任务下也能保持 UI 响应startTransition
可以通过将特定更新标记为过渡
来显着改善用户交互src\main.jsx
import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
import SuspensePage from './routes/SuspensePage';
+import StartTransitionPage from './routes/StartTransitionPage';
+const element = <StartTransitionPage />
//render(element, root);
createRoot(root).render(element);
src\routes\StartTransitionPage.jsx
import React, { startTransition, useEffect, useState } from 'react';
function getSuggestions(keyword) {
let items = new Array(10000).fill(keyword)
return new Promise((resolve) => {
setTimeout(() => {
resolve(items);
}, 1000 * keyword.length);
});
}
function Suggestion(props) {
const [suggestions, setSuggestions] = useState([]);
useEffect(() => {
if (props.keyword && props.keyword.length > 0) {
getSuggestions(props.keyword).then(suggestions => {
startTransition(() => {
setSuggestions(suggestions);
})
})
}
}, [props.keyword]);
useEffect(() => {
console.log(props.keyword);
})
return (
<ul>
{
suggestions.map((item, index) => (<li key={index}>{item}</li>))
}
</ul>
)
}
function StartTransitionPage() {
const [keyword, setKeyword] = useState < string > ("");
const handleChange = (event) => {
setKeyword(event.target.value);
};
return (
<div>
<div>关键字<input value={keyword} onChange={handleChange} /></div>
<Suggestion keyword={keyword} />
</div>
);
}
export default StartTransitionPage
UpdatePriorityPage.js
import React, { startTransition, useEffect } from 'react'
import { flushSync } from 'react-dom';
function UpdatePriorityPage() {
let [state, setState] = React.useState('');
useEffect(() => {
console.log(state);
});
const update = () => {
flushSync(() => startTransition(() => setState(state => state + 'A')));
flushSync(setState(state => state + 'B'));
flushSync(() => startTransition(() => setState(state => state + 'C')));
flushSync(setState(state => state + 'D'));
}
return (
<>
<div>{state}</div>
<button onClick={update}>更新</button>
</>
)
}
export default UpdatePriorityPage;
useDeferredValue
降低其计算的优先级,使得避免整个应用变得卡顿src\main.jsx
import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
import SuspensePage from './routes/SuspensePage';
import StartTransitionPage from './routes/StartTransitionPage';
+import UseDeferredValuePage from './routes/UseDeferredValuePage';
+const element = <UseDeferredValuePage />
createRoot(root).render(element);
src\routes\UseDeferredValuePage.jsx
import React, { useDeferredValue, useEffect, useState } from 'react';
function getSuggestions(keyword) {
let items = new Array(10000).fill(0).map((item, index) => keyword + index);
return Promise.resolve(items);
}
function Suggestion(props) {
const [suggestions, setSuggestions] = useState < Array < string >> ([]);
useEffect(() => {
getSuggestions(props.keyword).then(suggestions => {
- startTransition(() => {
setSuggestions(suggestions);
- })
})
}, [props.keyword]);
return (
<ul>
{
suggestions.map((item) => (<li key={item}>{item}</li>))
}
</ul>
)
}
function StartTransitionPage() {
const [keyword, setKeyword] = useState < string > ("");
+ const deferredText = useDeferredValue(keyword);
const handleChange = (event) => {
setKeyword(event.target.value);
};
return (
<div>
<div>关键字<input value={keyword} onChange={handleChange} /></div>
+ <Suggestion keyword={deferredText} />
</div>
);
}
export default StartTransitionPage
useTransition
允许组件在切换到下一个界面之前等待内容加载,从而避免不必要的加载状态useTransition
返回两个值的数组startTransition
是一个接受回调的函数,我们用它来告诉 React 需要推迟的 stateisPending
是一个布尔值,这是 React 通知我们是否正在等待过渡的完成的方式src\main.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'
const root = document.getElementById('root');
import OldBatchUpdatePage from './routes/OldBatchUpdatePage';
import NewBatchUpdatePage from './routes/NewBatchUpdatePage';
import SuspensePage from './routes/SuspensePage';
import StartTransitionPage from './routes/StartTransitionPage';
import UseDeferredValuePage from './routes/UseDeferredValuePage';
+import UseTransitionPage from './routes/UseTransitionPage';
+const element = <UseTransitionPage />
createRoot(root).render(element);
src\routes\UseTransitionPage.jsx
import React, { Suspense, useState, useTransition } from 'react'
import ErrorBoundary from './components/ErrorBoundary';
import { fetchUser } from '../fakeApi';
import { wrapPromise } from '../utils';
const user1Resource = wrapPromise(fetchUser('1'));
const user5Resource = wrapPromise(fetchUser('5'));
function UseTransitionPage() {
const [resource, setResource] = useState(user1Resource);
const [isPending, startTransition] = useTransition();
return (
<>
<Suspense fallback={<h3>Loading User......</h3>}>
<ErrorBoundary>
<User resource={resource} />
</ErrorBoundary>
</Suspense>
<button onClick={() => {
startTransition(() => {
setResource(user5Resource)
});
}}>user5</button>
<h3>{isPending && <p>isPending...</p>}</h3>
</>
);
}
function User({ resource }) {
const user = resource.read();
return <div>{user.id}:{user.name}</div>
}
export default UseTransitionPage;
CPU-bound
的更新 (例如创建新的 DOM 节点和运行组件中的代码),并发意味着一个更急迫的更新可以中断
已经开始的渲染