1.目标
- 高阶组件的用法及封装
- Hooks详解
- 异步组件
- React 18 新特性
知识要点
2.1 高阶组件用法及封装
高阶组件,组件作为参数,返回值也是组件的函数,纯函数,不会修改传入的组件,也不会使用继承来复制其行为,hoc通过组件包装在容器组件中来组成新的组件
- 抽取重复代码,实现组件复⽤:相同功能组件复⽤
- 条件渲染,控制组件的渲染逻辑(渲染劫持):权限控制。
- 捕获/劫持被处理组件的⽣命周期,常⻅场景:组件渲染性能追踪、⽇志打点。
2.1.1 属性代理
- 返回stateless 组件
- 返回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,可以通过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} />;
}
}
export default Example;
2.1.2反向继承
const HOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
通过继承,使用supper, this调用父组件的state,ref, render, 生命周期方法
-
允许HOC通过this访问到原组件,可以直接读取和操作原组件的state/ref等;
-
可以通过super.render()获取传⼊组件的render,可以有选择的渲染劫持; 劫持原组件⽣命周期⽅法
-
读取/操作原组件的state
-
条件渲染 修改react树
-
修改react树
function HOC(WrappedComponent){ const didMount = WrappedComponent.prototype.componentDidMount; // 继承了传⼊组件 return class HOC extends WrappedComponent { async componentDidMount(){ // 劫持 WrappedComponent 组件的⽣命周期 if (didMount) { didMount.apply(this); } // 将 state 中的 number 值修改成 2 this.setState({ number: 2 }); } render(){ //使⽤ super 调⽤传⼊组件的 render ⽅法 if (this.props.isRender) { return super.render(); } else { return <div>暂⽆数据</div>; } return super.render(); } } } 修改react树
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;
}
};
}
2.2 hooks详解
Hooks是react16.8以后新增的钩⼦API; ⽬的:增加代码的可复⽤性,逻辑性,弥补⽆状态组件没有⽣命周期,没有数据管理状态state的缺 陷。
- 开发友好,可扩展性强,抽离公共的⽅法或组件,Hook 使你在⽆需修改组件结构的情况下复⽤状 态逻辑;
- 函数式编程,将组件中相互关联的部分根据业务逻辑拆分成更⼩的函数;
- class更多作为语法糖,没有稳定的提案,且在开发过程中会出现不必要的优化点,Hooks⽆需学习 复杂的函数式或响应式编程技术;
useState
- ⽀持stateless组件有⾃⼰的state;
- ⼊参:具体值或⼀个函数
- 返回值:数组,第⼀项是state值,第⼆项负责派发数据更新,组件渲染
- setState会让组件重新执⾏
import {useState} from 'react';
export const DemoUseState = (props) => {
let [number, setNumber] = useState(1);
return (
<div>
<span>{number}</span>
<button onClick={() => {
setNumber(number + 1);
console.log(number);
}}>click me
</button>
</div>
);
};
useEffect
- 不限制条件,组件每次更新都会触发useEffect --> componentDidUpdate 与 componentwillreceiveprops
- useEffect 第⼀个参数为处理事件,第⼆个参数接收数组,为限定条件,当数组变化时触发事件, 为[]只在组件初始化时触发;
- useEffect第⼀个参数接收返回函数,⽤以消除某些副作⽤ --> componentWillUnmount
- useEffect⽆法直接使⽤async await
- 渲染组件 useEffect 闪动
import {
useEffect,
useRef,
useState,
} from 'react';
import {getUserInfo} from './apiUtil';
export const DemoUseEffect = ({a}) => {
const [user, setUser] = useState({});
const [number, setNumber] = useState(0);
const div = useRef();
const handleResize = () => {
};
// /* useEffect使⽤ ,这⾥如果不加限制 ,会是函数重复执⾏,陷⼊死循环*/
useEffect(() => {
getUserInfo(a, number).then(res => {
setUser(res);
});
console.log(div.current);
window.addEventListener('resize', handleResize);
}, [
a,
number,
]);
/* 只有当props->a和state->number改变的时候 ,useEffect副作⽤函数重新执⾏ ,如
果此时数组为空[],证明函数只有在初始化的时候执⾏⼀次相当于componentDidMount */
return (
<div ref={div}>
<span>{user.name}</span>
<span>{user.age}</span>
<div onClick={() => setNumber(number + 1)}>{number}</div>
</div>
);
};
// 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()
// make sure to catch any error
.catch(console.error);;
}, [])
useEffect: 组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执⾏useEffect回调 ;
useLayoutEffect
- 渲染组件卡顿
useLayoutEffect : 组件更新挂载完成 -> 执⾏useLayoutEffect回调-> 浏览器dom 绘制完成;
export 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>
)
}
useRef
⽤来获取元素(当前节点的element)、缓存数据; ⼊参可以作为初始值
//获取元素
import {useRef} from 'react';
export const DemoUseRef = () => {
const dom = useRef(null);
const handerSubmit = () => {
/* dom 节点*/
console.log(dom);
};
return (<div ref={dom}>
<p>表单组件</p>
<button onClick={() => handerSubmit()}>提交</button>
</div>);
};
useContext
⽤来获取⽗级组件传递过来的context值,这个当前值就是最近的⽗级组件 Provider 的value; 从parent comp获取ctx⽅式; 1. useContext(Context); 2. 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>
}
useReducer(fn,state)
入参:fn(state,action)
state:初始化值
返回:更新后的值state
派发更新的dispatch函数,执⾏dispatch会导致组件re-render
import {useReducer} from 'react';
export const DemoUseReducer = () => {
//number 更新后的state值,dispatchNumber为当前的派发函数
const [number, dispatchNumber] = useReducer((state, action) => {
//提取dspatchNumber传进来的值
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={() => dispatchNumber({name: 'a'})}>add</button>
<button onClick={() => dispatchNumber({name: 'b'})}>sub</button>
<button onClick={() => dispatchNumber({
name: 'c',
payload: 66,
})}>sub
</button>
</div>);
};
userMemo
⽤来根据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 {useMemo} = require('react');
const {useState} = require('react');
export const DemoUseMemo = () => {
/* ⽤useMemo 包裹之后的log函数可以避免了每次组件更新再重新声明 ,可以限制上下⽂的
执⾏ */
const [number, setNumber] = useState(0);
const newLog = useMemo(() => {
const log = () => {
/* 点击span之后 打印出来的number 不是实时更新的number值 */
console.log(number);
};
return log;
/* [] 没有 number */
}, [number]);
return <div>
<div onClick={() => newLog()}>打印</div>
<span onClick={() => setNumber(number + 1)}>增加</span>
</div>;
};
useCallback
useMemo返回cb的运⾏结果; useCallback返回cb的函数;
// 1. 由于是pureComponent每⼀次都会重新⽣成新的props函数,这时候就会触发⼦组件的更
新,使⽤React.memo
// 2. useCallback ,必须配合 react.memo pureComponent ,否则不但不会提升性能,
还有可能降低性能
/* ⽤react.memo */
const DemoChildren = React.memo((props)=> {
/* 只有初始化的时候打印了 ⼦组件更新 */
console.log('⼦组件更新')
useEffect(()=>{
props.getInfo('⼦组件')
},[])
return <div>⼦组件</div>
})
const DemoUseCallback=({ id })=>{
const [number, setNumber] = useState(1)
/* 此时usecallback的第⼀参数 (sonName)=>{ console.log(sonName) }
经过处理赋值给 getInfo */
const getInfo = useCallback((sonName)=>{
console.log(sonName)
},[id])
return <div>
{/* 点击按钮触发⽗组件更新 ,但是⼦组件没有更新 */}
<button onClick={ ()=>setNumber(number+1) } >增加</button>
<DemoChildren getInfo={getInfo} />
</div>
}
2.3 Hooks 实战注意
useEffect
- 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>
)
}
deps参数不能缓解闭包问题
- 只有变化时,需要重新执⾏ useEffect 的变量,才要放到 deps 中。⽽不是 useEffect ⽤到的变量都放 到 deps 中。
- 在有延迟调⽤场景时,可以通过 ref 来解决闭包问题。
// 当进⼊⻚⾯ 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 变化,但对之前的引⽤已经⽆能为⼒了
尽量不要⽤useCallback
- useCallback ⼤部分场景没有提升性能
- useCallback让代码可读性变差
useMemo建议适当使⽤
// 没有使⽤ 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; // 内存消耗少
useState的正确使⽤姿势
- 能⽤其他状态计算出来就不⽤单独声明状态。⼀个 state 必须不能通过其它 state/props 直接计算 出来,否则就不⽤定义 state
- 保证数据源唯⼀,在项⽬中同⼀个数据,保证只存储在⼀个地⽅
- useState 适当合并
2.4⾃定义Hooks
⾃定义Hooks本质上还是实现⼀个函数,关键在于实现逻辑
const [ a, [b, c...] ] = useXXX(arg1,[arg2, ...])
防抖
//防抖
import {
useEffect,
useRef,
useState,
} 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 const DebounceInput = (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:{b}, a:{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;
}
}
}
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]
}
setTitle
import { useEffect } from 'react'
const useTitle = (title) => {
useEffect(() => {
document.title = title
}, [])
return
}
export default useTitle
const App = () => {
useTitle('new title')
return <div>home</div>
}
update hook
import {useState} from 'react';
const useUpdate = () => {
const [, setFlag] = useState();
const update = () => {
setFlag(Date.now());
};
return update;
};
// 实际使⽤
export const DemoUseUpdate = (props) => {
// ...
const update = useUpdate();
return <div>
{Date.now()}
<div>
<button onClick={update}>update</button>
</div>
</div>;
};
useScroll hooks
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>
}
3.异步组件
- 在引⼊第三⽅的库的情况下,要避免因体积过⼤导致加 载时间过⻓。
- 引⼊了 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;
动态impourt
- 相对于静态import的
import XX from XXX,动态import指在运⾏时加载 - 实现Promise规范
import('./test.js').then(test => {
// ...
});
错误边界
错误边界是⼀种 React 组件,错误边界在 渲染期间、⽣命周期⽅法和整个组件树的构造函数 中捕获错 误,且会渲染出备⽤UI⽽不是崩溃的组件。
import {useEffect} from 'react';
import * as React from 'react';
const {useState} = require('react');
class DemoErrorBoundary extends React.Component {
constructor (props) {
super(props);
this.state = {hasError: false};
}
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 const DemoUseErrorBoundary = () => {
const [count, setCount] = useState(1);
useEffect(() => {
if (count === 3) {
throw new Error('I crashed!');
}
}, [count]);
return (
<DemoErrorBoundary>
<h1>App</h1>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add
</button>
</DemoErrorBoundary>
);
};
// comp App
export class DemoUseErrorBoundary1 extends React.Component {
state = {
count: 1,
};
render () {
const {count} = this.state;
if (count === 3) {
throw new Error('I crashed!');
}
return (
<DemoErrorBoundary>
<h1>App</h1>
<p>{count}</p>
<button onClick={() => this.setState({
count: count + 1,
})}>add
</button>
</DemoErrorBoundary>
);
}
}
⼿写异步组件
- 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;
18特性
- Automatic batching(⾃动批量更新) 2.
- startTransition
- ⽀持 React.lazy 的SSR架构
- Concurrent Mode (并发渲染、可选)