1.高阶组件
高阶组件是React中用于复用组件逻辑的一种高级技巧。
简单的说,就是组件作为参数,返回值也是组件的函数,它是纯函数,不会修改传入的组件,也不会使用继承复制其行为。HO通过将组件包装在容器组件中来组成新组件。
1.1 使用HOC的原因
1.抽取重复代码,实现组件复用:相同功能组件复用
2.条件渲染,控制组件的渲染逻辑(渲染劫持):权限控制。
3.捕获/劫持被处理组件的生命周期,常见场景:组件渲染性能追踪、日志打点
1.2 HOC实现方式
1.2.1 属性代理
使用组合的方式,将组件包装在容器上,依赖父子组件的生命周期关系来
1.返回stateless的函数组件
2.返回class组件
- 操作props
// 可以通过属性代理,拦截⽗组件传递过来的porps并进⾏处理。
// 返回⼀个⽆状态的函数组件
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: '',
};
this.onChange = this.onChange.bind(this);
}
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>
);
}
};
}
1.2.2 反向继承
使⽤⼀个函数接受⼀个组件作为参数传⼊,并返回⼀个继承了该传⼊组件的类组件,且在返回组件的 render() ⽅法中返回 super.render() ⽅法
const HOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
- 允许HOC通过this访问到原组件,可以直接读取和操作原组件的state/ref等;
- 可以通过super.render()获取传⼊组件的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(){
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 newTree;
}
};
}
1.2.3 属性代理和反向继承
属性代理: 从‘组合’角度出发,有利于从外部操作wrappedComp,可以操作props,或者在wrappedComp外加一些拦截器(如条件渲染等)
反向继承:从‘继承’角度出发,从内部操作wrappedComp,可以操作组件内部的state,生命周期和render。
2.Hooks详解
Hooks是react16.8以后新增的钩子API;
目的:增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。
为什么要使用?
1.开发友好,可拓展性强。抽离公共的方法或组件,Hook使你在无需修改组件结构的情况下复用状态逻辑
2.函数式编程,将组件中相关联的部分根据业务逻辑拆分成更小的函数
3.class更多作为语法糖,没有稳定的提案,且在开发过程中会出现不必要的优化点,Hooks⽆需学习 复杂的函数式或响应式编程技术
2.1 常见的Hooks
2.1.1 useState
2.1.2 useEffect
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
1.当组件init、dom render完成、操纵dom、请求数据(如componentDidMount)等;
2. 不限制条件,组件每次更新都会触发useEffect --> componentDidUpdate 与 componentwillreceiveprops;
3. useEffect 第⼀个参数为处理事件,第⼆个参数接收数组,为限定条件,当数组变化时触发事件, 为[]只在组件初始化时触发;
4. useEffect第⼀个参数接收返回函数,⽤以消除某些副作⽤ --> componentWillUnmount
2.1.3 useLayoutEffect
作⽤渲染更新之前的 useEffect
useEffect: 组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执⾏useEffect回调 ;
useLayoutEffect : 组件更新挂载完成 -> 执⾏useLayoutEffect回调-> 浏览器dom 绘制完成;
渲染组件 1. useEffect:闪动; 2. useLayoutEffect:卡顿;
const DemoUseLayoutEffect = () => {
const target = useRef()
useLayoutEffect(() => {
/*我们需要在dom绘制之前,移动dom到制定位置*/
const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
animate(target.current,{ x,y })
}, []);
return (
<div >
<span ref={ target } className="animate"></span>
</div>
)
}
2.1.4 useRef
⽤来获取元素、缓存数据; ⼊参可以作为初始值
// 获取元素
const DemoUseRef = ()=>{
const dom= useRef(null)
const handerSubmit = ()=>{
/* <div >表单组件</div> dom 节点 */
console.log(dom.current)
}
return <div>
{/* ref 标记当前dom节点 */}
<div ref={dom} >表单组件</div>
<button onClick={()=>handerSubmit()} >提交</button>
</div>
}
// 缓存数据
// 不同于useState,useRef改变值不会使comp re-render
const currenRef = useRef(InitialData)
currenRef.current = newValue
2.1.5 useContext
⽤来获取⽗级组件传递过来的context值,这个当前值就是最近的⽗级组件 Provider 的value;
从parent comp获取ctx⽅式;
- useContext(Context)
- Context.Consumer
/* ⽤useContext⽅式 */
const DemoContext = ()=> {
const value = useContext(Context);
/* my name is aaa */
return <div> my name is { value.name }</div>
}
/* ⽤Context.Consumer ⽅式 */
const DemoContext1 = ()=>{
return <Context.Consumer>
{/* my name is aaa */}
{ (value)=> <div> my name is { value.name }</div> }
</Context.Consumer>
}
export default ()=>{
return <div>
<Context.Provider value={{ name:'aaa' }} >
<DemoContext />
<DemoContext1 />
</Context.Provider>
</div>
}
2.1.6 useReducer
类似redux
入参:
- 第⼀个为函数,可以视为reducer,包括state 和 action,返回值为根据action的不同⽽改变后的 state
- 第⼆个为state的初始值
出参:
1.第⼀个更新后的state值
3. 第⼆个是派发更新的dispatch函数;执⾏dispatch会导致组件re-render(另⼀个是useState)
const DemoUseReducer = ()=>{
/* number为更新后的state值, dispatchNumbner 为当前的派发函数 */
const [ number , dispatchNumbner ] = 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
2.1.7 useMemo
用来根据useMemo的第二个参数deps(数组)判定是否满足当前的限定条件来确定是否执行第一个cb
// selectList 不更新时,不会重新渲染,减少不必要的循环渲染
useMemo(() => (
<div>{
selectList.map((i, v) => (
<span
className={style.listSpan}
key={v} >
{i.patentName}
</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 [ number ,setNumber ] = useState(0)
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>
}
2.1.8 useCallback
useMemo返回cb的运行结果
useCallback返回cb的函数
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
import React, { useState, useCallback } from 'react'
function Button(props) {
const { handleClick, children } = props;
console.log('Button -> render');
return (
<button onClick={handleClick}>{children}</button>
)
}
const MemoizedButton = React.memo(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>
)
}
const MemoizedButton = React.memo(Button);
export default function Index() {
const [clickCount, increaseCount] = useState(0);
// 这⾥使⽤了`useCallback`
const handleClick = useCallback(() => {
console.log('handleClick');
increaseCount(clickCount + 1);
}, [])
return (
<div>
<p>{clickCount}</p>
<MemoizedButton handleClick=
{handleClick}>Click</MemoizedButton>
</div>
)
}
2.2 Hooks实战
2.2.1 所有依赖项都必须放在依赖数组中吗?
// 当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>
)
}
如果把text 和 a 也引⼊deps中,当text 和 a改变时,也触发了函数执行
Solution:
-
不要使用 eslint-plugin-react-hooks 插件,或者可以选择性忽略该插件的警告;
-
只有⼀种情况,需要把变量放到 deps 数组中,那就是当该变量变化时,需要触发 useEffect 函数
执⾏。⽽不是因为 useEffect 中⽤到了这个变量!
2.2.2 deps参数不能缓解闭包问题
闭包问题和 useEffect 的 deps 参数没有太⼤关系
// 当进⼊⻚⾯ 3s 后,输出当前最新的 count
// Example 1
function Demo() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log(count)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
return (
<button
onClick={() => setCount(count => count + 1)}
>
click
</button>
)
}
// 输出0
// Example 2
useEffect(() => {
const timer = setTimeout(() => {
console.log(count)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [count])
// 输出1
// Example 3
const [count, setCount] = useState(0);
// 通过 ref 来记录最新的 count
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setTimeout(() => {
console.log(countRef.current)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
什么情况下会存在闭包
// 正常情况下,不会存在
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const c = a + b;
useEffect(()=>{
console.log(a, b, c)
}, [a]);
useEffect(()=>{
console.log(a, b, c)
}, [b]);
useEffect(()=>{
console.log(a, b, c)
}, [c]);
// 在延迟调⽤下,会存在闭包
// 1. 使⽤ setTimeout、setInterval、Promise.then 等
// 2. useEffect 的卸载函数
const getUsername = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('John');
}, 3000);
})
}
function Demo() {
const [count, setCount] = useState(0);
// setTimeout 会造成闭包问题
useEffect(() => {
const timer = setTimeout(() => {
console.log(count);
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
// setInterval 会造成闭包问题
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 3000);
return () => {
clearInterval(timer);
}
}, [])
// Promise.then 会造成闭包问题
useEffect(() => {
getUsername().then(() => {
console.log(count);
});
}, [])
// useEffect 卸载函数会造成闭包问题
useEffect(() => {
return () => {
console.log(count);
}
}, []);
return (
<button
onClick={() => setCount(count => count + 1)}
>
click
</button>
)
}
// 都返回0
// 组件初始化,此时 count = 0
// 执⾏ useEffect,此时 useEffect 的函数执⾏,JS 引⽤链记录了对 count=0 的引⽤关
系
// 点击 button,count 变化,但对之前的引⽤已经⽆能为⼒了
Solution:
只有变化时,需要重新执⾏ useEffect 的变量,才要放到 deps 中。⽽不是 useEffect ⽤到的变量都放
到 deps 中。
在有延迟调⽤场景时,可以通过 ref 来解决闭包问题。
2.2.3 尽量不要⽤useCallback
- useCallback ⼤部分场景没有提升性能
- useCallback让代码可读性变差
Example 1
const someFunc = useCallback(()=> {
doSomething();
}, []);
return <ExpensiveComponent func={someFunc} />
const ExpensiveComponent = ({ func }) => {
return (
<div onClick={func}>
hello
</div>
)
}
// 必须⽤React.memo wrapper 住⼦组件,才能避免在参数不变的情况下,不重复渲染
// 所以⼀般项⽬中不建议使⽤useCallback
const ExpensiveComponent = React.memo(({ func }) => {
return (
<div onClick={func}>
hello
</div>
)
}
// Example 2
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();
}, [...]);
2.2.4 useMemo建议适当使⽤
在deps不变,且⾮简单的基础类型运算的情况下建议使⽤
// 没有使⽤ useMemo
const memoizedValue = computeExpensiveValue(a, b);
// 使⽤ useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// 如果没有使⽤ useMemo,computeExpensiveValue 会在每⼀次渲染的时候执⾏;
// 如果使⽤了 useMemo,只有在 a 和 b 变化时,才会执⾏⼀次
computeExpensiveValue。
const a = 1;
const b = 2;
const c = useMemo(()=> a + b, [a, b]);
const c = a + b; // 内存消耗少
2.2.5 useState的正确使用姿势
- 能⽤其他状态计算出来就不⽤单独声明状态。⼀个 state 必须不能通过其它 state/props 直接计算 出来,否则就不⽤定义 state
- 保证数据源唯⼀,在项⽬中同⼀个数据,保证只存储在⼀个地⽅
- useState 适当合并
// Example 1
const SomeComponent = (props) => {
const [source, setSource] = useState([
{type: 'done', value: 1},
{type: 'doing', value: 2},
])
const [doneSource, setDoneSource] = useState([])
const [doingSource, setDoingSource] = useState([])
useEffect(() => {
setDoingSource(source.filter(item => item.type === 'doing'))
setDoneSource(source.filter(item => item.type === 'done'))
}, [source])
return (
<div>
.....
</div>
)
}
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层层传递,在CR中很难看清楚
// Example 2
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(localtion.search)?.key;
const handleSearchChange = e => {
const key = e.target.value;
history.push(`/movie-list?key=${key}`);
}
return (
<input
value={searchKey}
placeholder="Search..."
onChange={handleSearchChange}
/>
);
}
// url params 和 state重复了
// Example 3
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 [userInfo, setUserInfo] = useState({
firstName,
lastName,
school,
age,
address
});
const [weather, setWeather] = useState();
const [room, setRoom] = useState();
// 更新⼀个时
setUserInfo(s=> ({
...s,
2.3 自定义Hooks
注意:⾃定义Hooks本质上还是实现⼀个函数,关键在于实现逻辑
⼀般实现效果如:
const [ a, [b, c...] ] = useXXX(arg1,[arg2, ...])
例1: setTitle hook
import { useEffect } from 'react'
const useTitle = (title) => {
useEffect(() => {
document.title = title
}, [])
return
}
export default useTitle
const App = () => {
useTitle('new title')
return <div>home</div>
}
例2: updata hook
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</button></div>
</div>
}
例3: useScroll hook
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, false)
return () => {
scrollRef.current.removeEventListener('scroll', handleScroll,
false)
}
}, [])
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>
<div ref={scrollRef}>
<div className="innerBox"></div>
</div>
<div>{ x }, { y }</div>
</div>
}
2.3.1 防抖hook
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>
}
2.3.2 节流hook
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
3.异步组件
随着项⽬的增⻓,代码包也会随之增⻓,尤其是在引⼊第三⽅的库的情况下,要避免因体积过⼤导致加
载时间过⻓。
React16.6中,引⼊了 React.lazy 和 React.Suspense 两个API,再配合动态 import() 语法就可以实现
组件代码打包分割和异步加载。
传统模式:渲染组件-> 请求数据 -> 再渲染组件
异步模式:请求数据-> 渲染组件;
// demo
import React, { lazy, Suspense } from 'react';
// lazy 和 Suspense 配套使⽤,react原⽣⽀持代码分割
const About = lazy(() => import(/* webpackChunkName: "about"
*/'./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;
3.1 手写异步组件
Suspense组件需要等待异步组件加载完成再渲染异步组件的内容。
- lazy wrapper住异步组件,React第⼀次加载组件的时候,异步组件会发起请求,并且抛出异常,终
⽌渲染; - Suspense⾥有componentDidCatch⽣命周期函数,异步组件抛出异常会触发这个函数,然后改变
状态使其渲染fallback参数传⼊的组件; - 异步组件的请求成功返回之后,Suspense组件再次改变状态使其渲染正常⼦组件(即异步组件);
/ comp About
const About = lazy(() => new Promise(resolve => {
setTimeout(() => {
resolve({
default: <div>component content</div>
})
}, 1000)
}))
// comp 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
// comp 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, {Suspese, lazy} from 'react'
const About= lazy(() => { import('../About') });
class App extends React.Component {
render() {
/**
* 1. 使⽤ React.Lazy 和 import() 来引⼊组件
* 2. 使⽤<React.Suspense></React.Suspense>来做异步组件的⽗组件,并使⽤
fallback 来实现组件未加载完成时展示信息
* 3. fallback 可以传⼊html,也可以⾃⾏封装⼀个统⼀的提示组件
*/
return (
<div>
<Suspense
fallback={
<Loading />
}
>
<About />
</Suspense>
</div>
)
}
}
export default ReactComp;