高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧。HOC自身不是React API的一部分,是一种基于React的组合特性而形成的设计模式。
简单来说,就是组件作为参数,返回值也是组件的函数,它是纯函数,不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC通过将组件包装在容器组件中来组成新组件。HOC是纯函数,没有副作用。
使用组合的方式,将组件包装在容器上,依赖父子组件的生命周期关系来:
操作props:可以通过属性代理,拦截父组件传递过来的props并进行处理。
//返回一个无状态的函数组件
function HOC(WrappedComponent) {
const newProps = { type: 'HOC' };
return props => <WrappedComponent {...props} {...newProps} />
}
//返回一个有状态的class组件
function HOC(WrappedComponent) {
return class extends React.Component {
render() {
const newProps = { type: 'HOC' };
return <WrappedComponent {...this.props} {...newProps} />
}
}
}
抽象state:通过属性代理无法直接操作原组件的state,可以通过props和cb抽象state。
function HOC(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = { name: '' };
}
onChange = event => {
this.setState({
name: event.target.value,
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onChange
}
};
return <WrappedComponent {...this.props} {...newProps} />
}
};
}
//使用
@HOC
class Example extends Component {
render() {
return <input name='name' {...this.props.name} />
}
}
通过props实现条件渲染:
//通过props来控制是否渲染及传入数据
import * as React from 'react';
function HOC(WrappedComponent) {
return props => (
<div>
{
props.isShow ? (
<WrappedComponent {...props} />
) : <div>暂无数据</div>
}
</div>
);
}
export default HOC;
其他元素wrapper传入的组件:
function withBackgroundColor(WrappedComponent) {
return class extends React.Component {
render() {
return (
<div style={{ backgroundColor: '#ccc' }}>
<WrappedComponent {...this.props} {...newProps} />
</div>
)
}
}
}
使用一个函数接受一个组件作为参数传入,并返回一个继承了该传入组件的类组件,且在返回组件的render()方法中返回super.render()方法。
const HOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
function HOC(WrappedComponent) {
const didMount = WrappedComponent.prototype.componentDidMount;
//继承了传入组件
return class HOC extends WrappedComponent {
async componentDidMount() {
//劫持WrappedComponent组件的生命周期
if(didMount) {
await didMount.apply(this);
}
}
render() {
//使用super调用传入组件的render方法
return super.render();
}
}
}
读取/操作原组件的state:
function HOC(WrappedComponent) {
const didMount = WrappedComponent.prototype.componentDidMount;
//继承了传入组件
return class HOC extends WrappedComponent {
async componentDidMount() {
//劫持WrappedComponent组件的生命周期
if(didMount) {
await didMount.apply(this);
}
//将state中的number值修改成2
this.setState({
number: 2
})
}
render() {
//使用super调用传入组件的render方法
return super.render();
}
}
}
条件渲染:
const HOC = WrappedComponent => {
class extends WrappedComponent {
render() {
if(this.props.isRender) {
return super.render();
}else {
return <div>暂无数据</div>
}
}
}
}
渲染react树:
//修改返回render结果
function HigherOrderComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
const tree = super.render();
const newProps = {};
if(tree && tree.type === 'input') {
newProps.value = 'something here';
}
const props = {
...tree.props,
...newProps
};
const newTree = React.cloneElement(tree, props, tree.props.children);
return tree;
}
};
}
页面复用(属性代理):
//views/PageA.js
import React from 'react';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
class PageA extends React.Component {
state = {
movieList: [],
}
async componentDidMount() {
const movieList = await fetchMovieListByType('comedy');
this.setState({
movieList
})
}
render() {
return <MovieList data={this.state.movieList} emptyTips="暂无喜剧" />
}
}
export default PageA;
//views/PageB.js
import React from 'react';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
class PageB extends React.Component {
state = {
movieList: []
}
async componentDidMount() {
const movieList = await fetchMovieListByType('action');
this.setState({
movieList
})
}
render() {
return <MovieList data={this.state.movieList} emptyTips="暂无动作片" />
}
}
export default PageB;
//上述冗余代码过多,使用HOC复用
import React from 'react';
const withFetchingHOC = (WrappedComponent, fetchingMethod, defaultProps) => {
return class extends React.Component {
async componentDidMount() {
const data = await fetchingMethod();
this.setState({ data });
}
render() {
return (
<WrappedComponent data={this.state.data} {...defaultProps} {...this.props} />
);
}
}
}
//使用:
//views/PageA.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
const defaultProps = {
emptyTips: '暂无喜剧'
}
export default withFetchingHOC(MovieList, fetchMovieListByType('comedy'), defaultProps);
//views/PageB.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
const defaultProps = {
emptyTips: '暂无动作片'
}
export default withFetchingHOC(MovieList, fetchMovieListByType('action'), defaultProps);
权限代理(属性代理):
import React from 'react';
import { whiteListAuth } from '../lib/utils'; // 鉴权⽅法
function AuthWrapper(WrappedComponent) {
return class AuthWrappedComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
permissionDenied: -1,
};
}
async componentDidMount() {
try {
await whiteListAuth();//请求鉴权接口
this.setState({
permissionDenied: 0
})
}catch(err) {
this.setState({
permissionDenied: 1
})
}
}
render() {
if(this.state.permissionDenied === -1) {
return null;//鉴权接口请求未完成
}
if(this.state.permissionDenied) {
return <div>功能即将上线,敬请期待~</div>
}
return <WrappedComponent {...this.props} />
}
}
}
export default AuthWrapper;
组件渲染性能(反向继承):
如何计算一个组件render期间的渲染耗时?
import React from 'react';
// Home 组件
class Home extends React.Component {
render () {
return (<h1>Hello World.</h1>);
}
}
// HOC
function withTiming(WrappedComponent) {
let start, end;
return class extends WrappedComponent {
constructor(props) {
super(props);
start = 0;
end = 0;
}
componentWillMount() {
if(super.componentWillMount) {
super.componentWillMount();
}
start = +Date.now();
}
componentDidMount() {
if(super.componentDidMount) {
super.componentDidMount();
}
end = +Date.now();
console.error(`${WrappedComponent.name}组件渲染时间为${end-start} ms.`);
}
render() {
return super.render();
}
}
}
export default withTiming(Home);
是react16.8以后新增的钩子API。
目的:增加代码的可复用性、逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。
为什么要使用Hooks?
const [number, setNumber] = useState(0);
注意:setState会让组件重新执行render,所以一般需要配合useMemo或useCallback。
const DemoState = props => {
//number为此时state读取值,setNumber为派发更新的函数
const [number, setNumber] = useState(0);
return (
<div>
<span>{number}</span>
<button onClick={() => {
setNumber(number+1);
console.log(number);
}}/>//此处的number打印出来还是0,因为不能够及时改变
</div>
)
}
//当更新函数之后,state的值是不能及时改变的,只有当下一次上下文执行时,state指才会随之改变
const a = 1;
const DemoState = props => {
//useState第一个参数如果是函数,则处理复杂的逻辑,返回值为初始值
let [number, setNumber] = useState(() => {
return a === 1 ? 1:2;
});
return (
<div>
<span>{number}</span>
<button onClick={() => setNumber(number+1)} />
</div>
)
}
//模拟数据交互
function getUserInfo(a) {
return new Promise(resolve => {
setTimeout(() => {
resolve({
name: a,
age: 16
})
}, 500);
})
}
const Demo = ({ a }) => {
const [userMessage, setUserMessage] = useState({});
const [number, setNumber] = useState(0);
const div = useRef();
const handleResize = () => {};
useEffect(() => {
getUserInfo(a).then(res => {
setUserMessage(res);
})
console.log(div.current);//div
window.addEventListener('resize', handleResize);
//只有当props->a和state->number改变时,useEffect副作用函数重新执行。如果此时数组为空,证明函数只有在初始化时执行一次相当于componentDidMount
}, [a, number]);
return (
<div ref={div}>
<span>{userMessage.name}</span>
<span>{userMessage.age}</span>
<div onClick={() => setNumber(1)}>{number}</div>
</div>
)
}
const Demo = ({ a }) => {
const handleResize = () => {}
useEffect(() => {
const timer = setInterval(() => console.log(666), 1000);
window.addEventListener('resize', handleResize);
//此函数用于清除副作用
return function() {
clearInterval(timer);
window.removeEventListener('resize', handleResize);
}
}, [a]);
return (<div></div>);
}
useEffect无法直接使用async await。
//bad
useEffect(async ()=>{
//请求数据
const res = await getUserInfo(payload);
}, [a, number]);
useEffect(() => {
//declare the async data fetching function
const fetchData = async () => {
const data = await fetch('https://xxx.com');
const json = await data.json();
return json;
}
//call the function
const result = fetchData().catch(console.error);
//无效
setData(result);
}, []);
//改进版
useEffect(() => {
const fetchData = async () => {
const data = await fetch('https://xxx.com');
const json = await response.json();
setData(json);
}
//call the function
fetchData().catch(console.error);
}, []);
渲染更新之前的useEffect。
useEffect:组件更新挂载完成 -> 浏览器DOM绘制完成 -> 执行useEffect回调。
useLayoutEffect:组件更新挂载完成 -> 执行useLayoutEffect回调 -> 浏览器DOM绘制完成。
渲染组件:
const DemoUseLayoutEffect = () => {
const target = useRef();
useLayoutEffect(() => {
//需要在DOM绘制之前,移动DOM到制定位置
const { x, y } = getPosition();//获取需要移动的X,y坐标
animate(target.current, {x,y});
}, []);
return (
<div>
<span ref={target} className='animate'></span>
</div>
)
}
用来获取元素、缓存数据;入参可以作为初始值。
const DemoUseRef = () => {
const dom = useRef(null);
const handerSubmit = () => {
console.log(dom.current);
//表单组件 dom 节点
}
return <div>
<div ref={dom}>表单组件</div>
<button onClick={() => handerSumbit()}>提交</button>
</div>
}
//缓存数据,小技巧
//不同于useState,useRef改变值不会使comp re-render
const currentRef = useRef(InitialData);
currentRef.current = newValue;
用来获取父级组件传递过来的context值,这个当前值就是最近的父级组件Provider的value。
从parent comp获取ctx方式:
//用useContext方式
const DemoContext = () => {
const value = useContext(Context);
return <div>my name is {value.name}.</div>
}
//用Context.Consumer方式
const DemoContext1 = () => {
return <Context.Consumer>
{ (value) => <div>my name is {value.name}</div> }
</Context.Consumer>
}
export default () => {
return <div>
<Context.Provider value={{ name: 'aaa' }}>
<DemoContext/>
<DemoContext1/>
</Context.Provider>
</div>
}
入参:
出参:
const DemoUseReducer = () => {
//number为更新后的state值,dispatchNumber为当前的派发函数
const [number, dispatchNumber] = useReducer((state, action) => {
const {payload, name} = action;
//return 值为新的state
switch(name) {
case 'a':
return state + 1;
case 'b':
return state - 1;
case 'c':
return payload;
}
return state;
}, 0);
return <div>
当前值:{number}
{ /* 派发更新 */ }
<button onClick={()=>dispatchNumbner({ name: 'a' })} >增加</button>
<button onClick={()=>dispatchNumbner({ name: 'b' })} >减少</button>
<button onClick={()=>dispatchNumbner({ name: 'c' , payload:666 })}
>赋值</button>
{ /* 把dispatch 和 state 传递给⼦组件 */ }
<MyChildren dispatch={ dispatchNumbner } State={{ number }}/>
</div>
}
业务中经常将 useReducer+useContext 代替Redux。
用来根据useMemo的第二个参数deps(数组)判定是否满足当前的限定条件来决定是否执行第一个cb。
//selectList不更新时,不会重新渲染,减少不必要的循环渲染
useMemo(() => (
<div>
{
selectList.map((i,v) => (
<span className={style.listSpan} key={v}>
{i.parentName}
</span>
))
}
</div>
), [selectList])
// listshow, cacheSelectList 不更新时,不会重新渲染⼦组件
useMemo(() => (
<Modal width={'70%'} visible={listshow} footer={[
<Button key='back'>取消</Button>,
<Button key='submit' type='primary'>确定</Button>
]}>
{ /* 减少了PatentTable组件的渲染 */ }
<PatentTable getList={getList} selectList={selectList} cacheSelectList={cacheSelectList} setCacheSelectList={setCacheSelectList}
/>
</Modal>
), [listshow, cacheSelectList]);
// 减少组件更新导致函数重新声明
const DemoUseMemo = () => {
/* ⽤useMemo 包裹之后的log函数可以避免了每次组件更新再重新声明 ,可以限制上下⽂的执⾏ */
const newLog = useMemo(() => {
const log = () => {
console.log(123);
}
return log;
}, []);
return <div onClick={() => newLog()}></div>
}
// 如果没有加相关的更新条件,是获取不到更新之后的state的值的
const DemoUseMemo = () => {
const newLog = useMemo(() => {
const log = () => {
//点击span之后打印出来的number不是实时更新的number值
console.log(number);
}
return log;
//[]没有number
}, []);
return <div>
<div onClick={() => newLog()}>打印</div>
<span onClick={() => setNumber(number + 1)}>增加</span>
</div>
}
useMemo返回cb的运行结果;useCallback返回cb的函数。
import React, { useState, useCallback } from 'react';
function Button(props) {
const { handleClick, children } = props;
console.log('button -> render');
return (
<button onClick={handleClick}>{children}</button>
)
}
export default function Index() {
const [clickCount, increaseCount] = useState(0);
const handleClick = () => {
console.log('handleClick');
increaseCount(clickCount + 1);
}
return (
<div>
<p>{clickCount}</p>
<MemoizedButton handleClick= {handleClick}>Click</MemoizedButton>
</div>
)
}
//MemoizedButton还是重新渲染了
//Index组件state发生变化,导致组件重新渲染;每次渲染导致重新创建内部函数handleClick,进而导致子组件Button也重新渲染
import React, { useState, useCallback } from 'react';
function Button(props) {
const { handleClick, children } = props;
console.log('button -> render');
return (
<button onClick={handleClick}>{children}</button>
)
}
//使用useMemo返回cb的运算结果
const MemoizedButton = React.memo(Button);
export default function Index() {
const [clickCount, increaseCount] = useState(0);
//使用useCallback返回cb的函数
const handleClick = useCallback(() => {
console.log('handleClick');
increaseCount(clickCount + 1);
}, []);
return (
<div>
<p>{clickCount}</p>
<MemoizedButton handleClick= {handleClick}>Click</MemoizedButton>
</div>
)
}
useEffect中,默认有个共识:useEffect中使用到外部变量,都应该放到第二个数组参数中。
//当props.count和count变化时,上报数据
function Demo(props) {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const [a, setA] = useState('');
useEffect(() => {
monitor(props.count, count, text, a);
}, [props.count, count]);
return (
<div>
<button onClick={() => setCount(count => count+1) }>
click
</button>
<input value={text} onChange={e => setText(e.target.value)} />
<input value={a} onChange={e => setA(e.target.value)} />
</div>
)
}
解决方法:
const someFunc = useCallback(() => {
doSomething();
}, []);
return <ExpensiveComponent func={someFunc} />
const ExpensiveComponent = ({ func }) =>{
return (
<div onClick={func}>hello</div>
)
}
//必须用React.memo包裹子组件,才能避免在参数不变的情况下,不重复渲染,所以一般项目中不建议使用useCallback
const ExpensiveComponent = React.memo(({ func }) => {
return (
<div onClick={func}>hello</div>
)
});
const someFuncA = useCallback((d, g, x, y) => {
doSomething(a,b,c,d,g,x,y);
}, [a,b,c]);
const someFuncB = useCallback(() => {
someFuncA(d, g, x, y);
}, [someFuncA, d, g, x, y]);
useEffect(() => {
someFuncB();
}, [someFuncB]);
//依赖层层传递,最终要找到哪些触发了useEffect执行,所以直接引用即可
const someFuncA = (d,g,x,y) => {
doSomething(a,b,c,d,g,x,y);
};
const someFuncB = () => {
someFuncA(d,g,x,y);
};
useEffect(() => {
someFuncB();
}, [...]);
在deps不变且非简单基础类型运算的情况下建议使用。
//没有使用useMemo
const memoizedValue = computeExpensiveValue(a, b);
//如果没有使用useMemo,computeExpensiveValue会在每一次渲染时执行
//使用useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
//如果使用了useMemo,只有在a和b变化时才会执行一次computeExpensiveValue
const a = 1;
const b = 2;
const c = useMemo(() => a+b, [a,b]);
const c = a+b; //内存消耗少
const SomeComponent = (props) => {
const [source, setSource] = useState([
{type: 'done', value: 1},
{type: 'doing', value: 2},
]);
const [doneSource, setDoneSource] = useState([]);
const [doingState, setDoingSource] = useState([]);
useEffect(() => {
setDoingSource(source.filter(item => item.type === 'doing'));
setDoneSource(source.filter(item => item.type === 'done'));
}, [source]);
return (
<div>...</div>
);
}
//state合并
const SomeComponent = (props) => {
const [source, setSource] = useState([
{ type: 'done', value: 1 },
{ type: 'doing', value: 2 },
]);
const doneSource = useMemo(() => source.filter(item => item.type === 'done'), [source]);
const doingSource = useMemo(() => source.filter(item => item.type === 'doing'), [source]);
return (
<div>...</div>
)
}
//避免props层层传递
function SearchBox({ data }) {
const [searchKey, setSearchKey] = useState(getQuery('key'));
const handleSearchChange = e => {
const key = e.target.value;
setSearchKey(key);
history.push(`/movie-list?key=${key}`);
}
return (
<input value={searchKey} placeholder="Search..." onChange={handleSearchChange} />
);
}
function SearchBox({ data }) {
const searchKey = parse(location.search)?.key;
const handleSearch = e => {
const key = e.target.value;
history.push(`/movie-list?key=${key}`);
}
return (
<input value={searchKey} placeholder="Search..." onChange={handleSearchChange} />
);
}
//url params和state重复了
const [firstName, setFirstName] = useState();
const [lastName, setLastName] = useState();
const [school, setSchool] = useState();
const [age, setAge] = useState();
const [address, setAddress] = useState();
const [weather, setWeather] = useState();
const [room, setRoom] = useState();
const [useInfo, setUserInfo] = useState({
firstName,
lastName,
school,
age,
address
});
const [weather, setWeather] = useState();
const [room, setRoom] = useState();
//更新一个时
setUserInfo(s => ({
...s,
firstName
}));
本质上还是实现一个函数,关键在于实现逻辑。一般实现效果为:
const [a [,b, c...]] = useXXX(arg1 [,arg2, ...]);
import { useEffect } from 'react';
const useTitle = (title) => {
useEffect(() =>{
document.title = title;
}, []);
return ...;
}
export default useTitle;
//使用
const App = () => {
useTitle('newTitle');
return <div>home</div>
}
import { useState } from 'react';
const useUpdate = () => {
const [, setFlag] = useState();
const update = () => {
setFlag(Date.now());
}
return update;
}
export default useUpdate;
//实际使用
const App = (props) => {
const update = useUpdate();
return <div>
{Date.now()}
<div>
<button onClick={update}>update</div>
</div>
</div>
}
import { useState, useEffect } from 'react';
const useScroll = (scrollRef) => {
const [pos, setPos] = useState([0, 0]);
useEffect(() => {
function handleScroll(e) {
setPos([scrollRef.current.scrollLeft, scrollRef.current.scrollTop])
}
scrollRef.current.addEventListener('scroll', handleScroll);
return () => {
scrollRef.current.removeEventListener('scroll', handleScroll);
}
}, []);
return pos;
}
export default useScroll;
//用法
import React, { useRef } from 'react';
import { useScroll } from 'hooks';
const Home = (props) => {
const scrollRef = useRef(null);
const [x, y] = useScroll(scrollRef);
return (
<div ref={scrollRef}>
<div className="innerBox"></div>
</div>
<div>{x}, {y}</div>
)
}
//防抖函数
function debounce(func, ms) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
func.apply(context, args);
}, ms);
}
}
//防抖Hooks
import { useEffect, useRef } from 'react'
const useDebounce = (fn, ms = 30, deps = []) => {
let timeout = useRef();
useEffect(() => {
if (timeout.current) {
clearTimeout(timeout.current);
}
timeout.current = setTimeout(() => {
fn();
}, ms);
}, deps);
const cancel = () => {
clearTimeout(timeout.current);
timeout = null;
}
return [cancel];
}
export default useDebounce;
//使用方法
import { useDebounce } from "hooks";
const Home = (props) => {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [cancel] = useDebounce(() => {
setB(a);
}, 2000, [a]);
const changeIpt = (e) => {
setA(e.target.value);
}
return <div>
<input type="text" onChange={changeIpt}/>
{b} {a}
</div>
}
//节流函数
function throttle(func, ms) {
let previous = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous > ms) {
func.apply(context, args);
previous = now;
}
}
}
//节流Hooks
import { useEffect, useRef, useState } from "react";
const useThrottle = (fn, ms = 30, deps = []) => {
let previous = useRef(0);
let [time, setTime] = useState(ms);
useEffect(() => {
let now = Date.now();
if (now - previous.current > time) {
fn();
previous.current = now;
}
}, deps);
const cancel = () => {
setTime(0);
}
return [cancel];
}
export default useThrottle;
随着项目的增长,代码包也会随之增长,尤其是在引入第三方库的情况下,要避免因体积过大导致加载时间过长。
React16.6中引入了React.lazy和React.Suspense两个API,再配合动态import()语法就可以实现组件代码打包分割和异步加载。
import React, {lazy, Suspense} from 'react';
//lazy和Suspense配套使用,react原生支持代码分割
const About = lazy(() =>import('./About'));
class App extends React.Component {
render() {
return (
<div className="App">
<h1>App</h1>
<Suspense fallback={<div>loading</div>}>
<About/>
</Suspense>
</div>
)
}
}
export default App;
动态import:相对于静态import的import XX from XXX
,动态import指在运行时加载。
import('./test.js').then(test => {
//...
});
//动态import实现了Promise规范,回调函数为返回的模块
错误边界:
React 16中引⼊,部分UI的JS错误不会导致整个应用崩溃。
错误边界是一种React组件,错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误,且会渲染出备用UI而不是崩溃的组件。
//component ErrorBoundary
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: true
};
}
static getDerivedStateFromError(error) {
//更新state使下一次渲染能够显示降级后的UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
//将错误日志上报给服务器
console.log(error, errorInfo);
}
render() {
if(this.state.hasError) {
//自定义降级后的UI并渲染
return <h1>Something went wrong.</h1>
}
return this.props.children;
}
}
export default ErrorBoundary;
//component App
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
class App extends React.Component {
state = { count: 1 };
render() {
const { count } = this.state;
if(count === 3) {
throw new Error('I crashed!');
}
return (
<ErrorBoundary>
<h1>App</h1>
<p>{count}</p>
<button onClick={() =>this.setState({ count: count+1 })}>add</button>
</ErrorBoundary>
)
}
}
export default App;
Suspense组件需要等待异步组件加载完成后再渲染异步组件的内容。
//Component About
const About = lazy(() => new Promise(resolve => {
setTimeout(() => {
resolve({
default: <div>Component content</div>
})
}, 1000);
}));
//Component Suspense
import React from "react";
class Suspense extends React.PureComponent {
//isRender异步组件是否就绪,可以渲染
state = {
isRender: true,
};
componentDidCatch(e) {
this.setState({
isRender: false
});
e.promise.then(() => {
//数据请求后,渲染真实组件
this.setState({
isRender: true,
});
});
}
render() {
const { fallback, children } = this.props;
const { isRender } = this.state;
return isRender ? children : fallback;
}
}
export default Suspense;
//Component lazy
import React, { useEffect } from 'react';
export function lazy(fn) {
const fetcher = {
status: 'pending',
result: null,
promise: null,
};
return function MyComponent() {
const getDataPromise = fn();
fetcher.promise = getDataPromise;
getDataPromise.then(res => {
fetcher.status = 'resolved';
fetcher.result = res.default;
});
useEffect(() => {
if (fetcher.status === 'pending') {
throw fetcher;
}
}, []);
if (fetcher.status === 'resolved') {
return fetcher.result;
}
return null;
}
}
//实现的效果与React支持内容保持一致
import React, { Suspense, lazy } from 'react';
const About = lazy(() => {
import('../About');
});
class App extends React.Component {
render() {
//1.使用React.lazy和import()来引入组件
//2.使用来做异步组件的父组件,并使用fallback来实现组件未加载完成时展示信息
//3.fallback可以传入HTML,也可以自动封装一个统一的提示组件
return (
<div>
<Suspense fallback={<Loading/>}>
<About/>
</Suspense>
</div>
)
}
}
export default ReactComp;
React发布步骤:
主要改动:
将多个状态更新合并成一个重新渲染以取得更好的性能的一种优化方式。
V18以前:默认不批量合并的场景:
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1);//doesn't re-render
setFlag(f => !f);//doesn't re-render
//React will only re-render once at the end(that's batching)
}
return (
<div>
<button onClick={handleClick}>next</button>
<h1 style={{ color: flag ? 'blue' : 'black' }}>
{count}
</h1>
</div>
)
}
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
//React 17 and earlier done not batch these because they run after the event in a callback, not during it.
setCount(c => c + 1);//causes a re-render
setFlag(f => !f);//causes a re-render
});
}
return (
<div>
<button onClick={handleClick}>next</button>
<h1 style={{ color: flag ? "blue":"black"}}>{count}</h1>
</div>
)
}
若不想batching?
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c+1);
});
//React has updated the DOM by now
flushSync(() => {
setFlag(f => !f);
});
//React has updated the DOM by now
}
batching 对hooks及class的影响:
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
//V18前{count: 1, flag: false}
//V18中{count: 0, flag: false},除非使用flushSync
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
//在一些react库中,如react-dom、unstable_batchedUpdates实现类似功能
import { unstable_batchedUpdates } from "react-dom";
unstable_batchedUpdates(() => {
setCount(c => c+1);
setFlag(f => !f);
});
可以让我们的页面在多数据更新里保持响应。这个API通过标记某些更新为transition,来提高用户交互。
实际:可以让我们的页面在展示时时刻保持re-render。
如:我们更新input的value同时用这个value去更新一个有30000个item的list。然而这种多数据更新让页面无法及时响应,也会让用户输入或其他用户交互感觉很慢。
//紧急的更新:展示用户的输入
setInputValue(e.target.value);
//非紧急的更新:展示结果
setContent(e.target.value);
V18前:update的优先级一样。
V18:支持优先级手动设置。
import { startTransition } from 'react'
//urgent: show what was typed
setInputValue(input);
//mark any state updates inside as startTransition
startTransition(() => {
//transition: show the results
setSearchQuery(input);
});
//等同于先执行
setInputValue(e.target.value);
//后执行
setContent(e.target.value);
react中的update:
误区:
//debounce和throttle经常使用
//show what you typed
setInputValue(input);
//show the results
setTimeout(() => {
setSearchQuery(input);
}, 0);
react的SSR(server side render)
V18之前:按序执行。
V18:支持拆解应用为独立单元,不影响其他模块。
正常加载界面:
流式HTML & 选择性hydrate:
<Layout>
<NavBar/>
<Sidebar/>
<RightPane>
<Post/>
<Suspense fallback={<Spinner/>}>//假设HTML加载很慢,分批
<Comments/>
</Suspense>
</RightPane>
</Layout>
//HTML返回过来再加载
<div hidden id="comments">
<!--Comments-->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
//This implementation is slightly simplified
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments');
);
</script>
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
<Suspense fallback={<Spinner/>}
<Comments/>
</Suspnese>
Concurrent Mode(简称CM)
什么是CM和suspense?
在2019年react conf提出了实验性的版本来支持CM和Suspense(可理解为等待代码加载,且指定加载界面)
CM:
可中断渲染(CM):
suspense:以声明的方式来等待任何内容,包括数据。
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails/>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline/>
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
//尝试读取用户信息,尽管该数据可能尚未加载
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
//尝试读取博文信息,尽管该部分数据可能尚未加载
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
)
}
误区:Suspense不是一个数据请求的库,而是一个机制。这个机制是用来给数据请求库向React通信的,说明某个组件正在读取的数据当前仍不可用。
DEMO:
目前fetch data方式:
//在函数组件中
useEffect(() => {
fetchSomething();
}, []);
//在class组件里
componentDidMount() {
fetchSomething();
}
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(u => setUser(u));
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline/>
</>
);
}
function ProfileTimeline() {
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts().then(p => setPosts(p));
}, []);
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
//结果:只有在fetch user后才会fetch post,请求被串行发出
function fetchProfileData() {
return Promise.all([
fetchUser(),
fetchPosts()
]).then(([user, posts]) => {
return {
user, posts
};
});
}
//尽早开始获取数据
const promise = fetchProfileData();
function ProfilePage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
promise.then(data => {
setUser(data.user);
setPosts(data.posts);
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline posts={posts}/>
</>
)
})
}
//子组件不再触发数据请求
function ProfileTimeline({ posts }) {
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
)
}
//fetch完user和post后再render
//这不是一个Promise,而是一个支持Suspense的特殊对象
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails/>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline/>
</Suspense>
</Suspense>
)
}
function ProfileDetails() {
//尝试读取用户信息,尽管信息可能未加载完毕
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
//尝试读取博文信息,尽管信息可能未加载完毕
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
)
}
//一开始fetch data,渲染ProfileDetails和ProfileTimeline
//依次渲染可渲染组件,没有可渲染组件时fallback,渲染h1标签
注意点:
//在渲染之前就开始数据获取
const resource = fetchProfileData();
function ProfileDetails() {
//尝试读取用户信息
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
//若无法保证在初始化时fetch data,而不是组件渲染后fetch data,可根据props获取数据
//开始获取数据越快越好
const initialResource = fetchProfileData(0);
function App() {
const [resource, setResource] = useState(initialResource);
return (
<>
<button onClick={() => {
const nextUserId = getNextId(resource.userId);
//再次获取数据:用户点击时
setResource(fetchProfileData(nextUserId));
}}>next</button>
<ProfilePage resource={resource}/>
</>
)
}
//useEffect race condition
function ProfilePage({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(id).then(u => setUser(u));
}, [id]);
if (user === null) {
return <p>Loading profile...</p>
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline id={id}/>
</>
);
}
function ProfileTimeline({ id }) {
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts(id).then(p => setPosts(p));
}, [id]);
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
)
}
//race condition:快速切换时,某个ProfileTimeline fetch请求延时过高后,旧的response会覆盖新的state
//Suspense:开始获取数据——开始渲染——结束获取数据,获取完数据立马setState
const initialResource = fetchProfileData(0);
function App() {
const [resource, setResource] = useState(initialResource);
return (
<>
<button onClick={() => {
const nextUserId = getNextId(resource.userId);
setResource(fetchProfileData(nextUserId));
}}>next</button>
<ProfilePage resource={resource}/>
</>
)
}
function ProfilePage({ id }) {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource}/>
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource}/>
</Suspense>
</Suspense>
);
}
function ProfileDetails({ resource }) {
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline({ resource }) {
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
//Hooks里setState需要在合理的时间设置
//Suspense里获取完数据立马setState
为什么没有在V18加上CM和Suspense?
// legacy 模式:最常⻅的版本
ReactDOM.render(<App />, rootNode)
// blocking 模式:作为从legacy迁移到cm的版本
ReactDOM.createBlockingRoot(rootNode).render(<App />)
// concurrent 模式:后续CM上stable版本后作为默认⽅式
ReactDOM.createRoot(rootNode).render(<App />)
为什么能够在半天内完成V18的升级?