参考资料:
一、高级组件 HOC
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
1、作用
- 代码复用,逻辑抽象
- 渲染劫持
- state的抽象和更改
- props的更改
2、写法
怎么写一个高阶组件?
- 普通方式
- 装饰器
- 多个高阶组件的组合
1、普通写法
//属性代理:操作props
import React, {Component} from 'react';
class Count extends Component {
state = {
number: 0
}
render(){
return (
<div>
{/* 对象会合并,hook不会 */}
{this.state.number}
<button onClick={()=>{this.setState({number: number + 1})}
</div>
)
}
}
function HocNumber (Comp) {
return class Temp extends Component {
render(){
let newProps = {
...this.props,
age: '18'
}
return <Comp {...newProps}/>
}
}
}
Count = HocNumber(Count);
export default Count;
2、装饰器
//定义
import React, {Component} from 'react';
export const decoratorWithNameHeight = (height?: number) => {
//两个return用于获取传参
return (WrappedComponent: any) => {
return class extends Component<any, State> {
public state: State = {
name: ''
}
componentWillMount(){
let username = localStorage.getItem('myName');
this.setState({
name: username
})
}
render(){
return (
<div>
<WrappedComponent name={this.state.name} {...this.props}/>
<p>the height is {height || 0} </p>
</div>
)
}
}
}
}
//使用装饰器的高阶组件
import React, {Component} from 'react';
import {decoratorWithNameHeight} from "../index";
@decoratorWithNameHeight(180)
class UglyWorld extends Component<Props, any>{
render(){
return <div>bye ugly world! my name is {this.props.name}</div>
}
}
export default UglyWorld;
//使用
import {UglyWorld} from "../index"
<UglyWorld/>
3、多个高阶组件的组合
export const decoratorWithWidth = (width?: number) => {
//两个return用于获取传参
return (WrappedComponent: any) => {
return class extends Component<any, any> {
render(){
return (
<div>
<WrappedComponent {...this.props}/>
<p>the width is {width || 0} </p>
</div>
)
}
}
}
}
//使用多个装饰器的高阶组件
import React, {Component} from 'react';
import {decoratorWithNameHeight, decoratorWithWidth} from "../index";
@decoratorWithWidth(100)
@decoratorWithNameHeight(180)
class UglyWorld extends Component<Props, any>{
render(){
return <div>bye ugly world! my name is {this.props.name}</div>
}
}
export default UglyWorld;
//使用:先展示height、再展示width
import {UglyWorld} from "../index"
<UglyWorld/>
3、实现方法及技术作用
高级组件能用来做什么?从技术方面?
实现高阶组件的方法有两种,包括属性代理和反向继承
- 属性代理:继承component,渲染使用原组件
- 操作props
- 操作组件实例
- 抽象state和事件:将子组件的事件和state抽象至高阶组件中,通过props传值使用
- 使用场景:将表单中的数据和事件抽象到高阶组件中,统一处理
- 通过ref使用引用
- 反向继承/劫持:继承传入的组件,从而使用传入组件的属性和方法
- 渲染劫持
- 控制state:可读入传入组件的state对其修改,建议不修改
//属性代理:通过ref使用引用,对子组件DOM进行处理
//1. 父组件使用高阶组件
import React, {Component} from 'react'
import Hoc from './hoc/index'
class App extends Component {
refA = React.creatRef()
componentDidMount(){
//操作子组件的input
this.refA.current.InputRef.current.focus();
}
render(){
return (
<div>
<Hoc name="jian" ref={this.refA}/>
</div>
)
}
}
export default App;
//2.高阶组件使用ref
// app组件传入refA,挂载至Count组件上,this.refA.current可获取到Count组件
// this.refA.current.InputRef.current可获取到input的dom
import React, {Component} from 'react'
class Count extends Component {
state = {
number: 0
}
InputRef = React.createRef()
render(){
return (
<div>
{/* 对象会合并,hook不会 */}
{this.state.number}
<input type="text" ref={this.InputRef}/>
<button onClick={()=>{this.setState({number: number + 1})}
</div>
)
}
}
function HocNumber (Comp) {
class Temp extends Component {
render(){
let newProps = {
...this.props,
age: '18'
}
let { forwardRef } = this.props;
return <Comp {...newProps} ref={forwardRef}/>
}
}
return React.forwardRef((props, ref) => {
return <Temp forwardRef={ref} {...props}/>
})
}
Count = HocNumber(Count);
export default Count;
//属性代理:操作组件实例
export const refHoc = () => {
return (WrappedComponent: any) => {
return class extends Component<any, any>{
ref = null;
componentDidMount(){
console.log(this.ref.state);
}
render(){
return (
<WrappedComponent
{...this.props}
ref={(instance: any)=>{
this.ref = instance;
}}
</WrappedComponent>
)
}
}
}
}
import {refHoc} from "../refHoc";
interface Props {
name?: string;
}
interface State {
weight?: number:
height?: number;
}
@refHoc
class RefDemoComponent extends Component<Props, state>{
state: State = {
weight: 60,
height: 170
}
render(){
return <div>bye ugly world my name is {this.props.name}</div>
}
}
export default RefDemoComponent;
//劫持:劫持方法
import React from 'react';
export function hijackHoc<T extends {new (...args: any[]):any}>(
component: T
){
return class extends component {
//劫持点击事件,做处理
handleClick(){
console.log(this.handleClick)
super.handleClick();
}
render(){
const parent = super.render();
return React.cloneElement(parent, {
onClick: () => this.handleClick()
})
}
}
}
//使用
@hijackHoc
class HijackComponent extends Component<Props, state>{
state: State = {
weight: 60,
height: 170
}
handleClick(){
this.setState({
weight: this.state.weight + 1;
})
}
render(){
return (
<div onClick={()=> this.handleClick()}>
测试按钮 {this.state.weight}
</div>
)
}
}
export default RefDemoComponent;
//渲染劫持
import React, {Component} from 'react';
class Count extends Component {
render(){
return <div>count</div>
}
}
const MyContainer = (WrappedComponent) => {
return class Temp extends WrappedComponent{
render(){
const elementsTree = super.render()
let newProps = {}
if(this.props.isShow){
return super.render()
}else{
return null;
}
}
}
}
Count = MyContainer(Count)
export default Count;
组件命名:Hoc创建的容器组件会与任何其他组件一样,为了方便调试,选择一个显示名称,表示他是HOC的产物,最常见的方式是用HOC保住被包装组件的显示名称,eg:高阶组件名为withSubscription,被包装的组件显示名称为CommentList,显示名称应为WithSubscription(CommentList)。
function withSubscription(WrappedComponent){
class WithSubScription extends React.Component {/* */}
WithSubScription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubScription;
}
function getDisplayName(WrappedComponent){
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
HOC 缺点⼩结:
- 增加了组件嵌套层级,过多时对于渲染的性能有⼀定影响;
- ref 、displayName 等易被忽略,虽然我们不推荐使⽤ ref,但是类似上⾯的需求,尤其是组件库封 装,ref 的转发是必不可少的,在⼀些 Dev tool 中也徒增了 UI ⽆关的组件嵌套;
- 对于已使⽤了 HOC 的业务,需求的扩展有⼀定的难度;
- ⾼阶组件有相似的逻辑时,也会造成执⾏顺序、功能覆盖的⻛险……
4、HOC知名的应用案例
1、react-redux
connect(mapStateToProps, mapDispatchToProps, mergeProps)(App);
// 简化实现等价于:
export function connect(mapStateToProps, mapDispatchToProps) {
return function (WrappedComponent) {
class Connect extends React.Component {
componentDidMount() {
//从context获取store并订阅更新
this.context.store.subscribe(this.forceUpdate.bind(this));
}
render() {
return (<WrappedComponent
// 传⼊该组件的 props,需要由 connect 这个⾼阶组件原样传回原组件
{ ...this.props }
// 根据 mapStateToProps 把 state 挂到 this.props 上
{ ...mapStateToProps(this.context.store.getState()) }
// 根据 mapDispatchToProps 把 dispatch(action) 挂到 this.props 上
{ ...mapDispatchToProps(this.context.store.dispatch) }
/>)
}
}
// 接收 context 的固定写法
Connect.contextTypes = {
store: PropTypes.object
}
return Connect;
}
}
// 因此, App 组件的 props 会被注⼊ action、state 等
2、react-router-dom withRouter
export default withRouter(App); // App 获得了 history,location 等 props
function withRouter(Component) {
const displayName = `withRouter(${Component.displayName || Component.name})`;
const C = props => {
// 如果想要设置被 withRouter 包裹的组件的 ref,这⾥使⽤ wrappedComponentRef
const { wrappedComponentRef, ...remainingProps } = props;
return (
<RouterContext.Consumer>
{context => {
// 将 context 加⼊到 Component 中,注意 ref 的转发,这⾥注⼊了
// RouterContext 中定义的各种 props,其中就包括 history,location 对象
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};
C.displayName = displayName;
C.WrappedComponent = Component;
// 当你给⼀个组件添加⼀个 HOC 时,原来的组件会被⼀个 container 的组件包裹。
// 这意味着新的组件不会有原来组件任何静态⽅法。
// 为了解决这个问题,可以在 return container 之前将 static ⽅法 copy 到 container 上⾯
// ⽤ hoist-non-react-statics 来⾃动复制所有 non-React 的 static methods
return hoistStatics(C, Component);
}
二、Hooks
⾼阶组件允许我们通过套娃的⽅式来增强组件,套娃套多了,维护起来会越来越难。hooks 的诞⽣也顺 带解决了这个问题。因此 hooks 的强⼤能⼒依然是代码逻辑的复⽤,同时也简化了⽣命周期,使得函数式 组件拥有了状态。注意, hooks 只能在函数式组件中使⽤,命名规范为 use 开头,且可以返回组件或任意 类型的数据(也可不返回)。
1、hooks优势
- class的缺点
- 组件间的状态逻辑很难复用
- 组件间如果有state的逻辑是相似的,class模式下基本使用高阶组件来解决。虽然能够解决问题,但是我们需要在组件外部再包一层元素,会导致层级非常冗余。
- 复杂业务的有状态组件会越来越复杂
- 监听和定时器的操作,被分散在多个区域:多个生命周期函数中存在同一业务逻辑,逻辑分散
- this指向问题
class App extends React.Component<any, any>{
constructor(props){
super(props);
this.state = {
num: 1,
title: "Jian"
}
this.handleClick2 = this.handleClick1.bind(this);
}
handleClick1(){
this.setState({
num: this.state.num + 1
})
}
handleClick3 = () => {
this.setState({
num: this.state.num + 1
})
}
render(){
return (
<div>
{/* 考点1: render里bind每次都会返回一个新的函数,造成ChildComponent每次会重新渲染 */}
<ChildComponent onClick={this.handleClick1.bind(this)}></ChildComponent>
<ChildComponent onClick={()=> this.handleClick1()}></ChildComponent>
{/* 考点1解决方法:提取函数 */}
<ChildComponent onClick={this.handleClick2}></ChildComponent>
<ChildComponent onClick={this.handleClick3}></ChildComponent>
</div>
)
}
}
- hooks的优点
- 利用业务逻辑的封装和拆分,可以非常自由的组合各种定义hooks(自己封装的用到的hooks的逻辑)
- 可以在无需修改组件结构的情况下,复用状态逻辑
- 定时器、监听等都被聚合到同一块代码下
2、常用Api
1、useState
import React, {useState, Component} from 'react';
class Temp extends Component{
state = {
count: 0
}
render(){
return (
<div>
{this.state.count}
<button onClick={()=>{
this.setState({
count: this.state.count + 1
})
}}>+</button>
</div>
)
}
}
//hooks写法
const Temp = () => {
const [count, setCount] = useState(0); //初始值为0
return (
<div>
{count}
<button onClick={()=>{setCount(count+1)}}>+</button>
</div>
)
}
//复合值,setValue无法合并对象
const Temp = () => {
const [counter, setValue] = useState(()=>{
return {name: 'jian', age: 18}
});
return (
<div>
{counter.name}{counter.age}
<button onClick={()=>{setValue({...counter, age: counter.age + 1})}}>+</button>
</div>
)
}
2、useEffect
import React, {useState, useEffect} from 'react';
const Temp = () => {
const [number, setNumber] = useState(0);
const [count, setCount] = useState(0);
//Similar to componentDidMount,只执行一次
useEffect(()=>{
console.log('useEffect')
}, [])
//Similar to componentDidMount and componentDidUpdate
useEffect(()=>{
console.log('useEffect')
//返回函数,清楚副作用,componentWillUnmount
return function xx(){
}
})
//Similar to componentDidUpdate,仅在依赖项更新时调用
useEffect(()=>{
console.log('useEffect')
}, [number])
return (
<div>
<h1>{number}</h1>
<h2>{count}</h2>
<button onClick={()=>{setNumber(number+1)}}>number+</button>
<button onClick={()=>{setCount(count+1)}}>number+</button>
</div>
)
}
3、useRef
// 1. 挂载 dom 节点
import React, { useRef, useEffect } from 'react';
export default function UseRef() {
const container = useRef(null);
console.log('container', container); // 第⼀次是拿不到的
useEffect(() => {
console.log('container', container); // current 属性引⽤着虚拟 DOM 节点
}, []);
return (Ref 容器);
}
// 2. 模拟类组件的 this,充当持久化数据对象
export default function UseRef() {
const container = useRef(false);
useEffect(() => {
if (container.current) {
console.log('模拟 componentDidUpdate ,即除了初始化,之后的更新进到这⾥');
} else {
container.current = true; // 初次挂载时⾛这⾥
}
});
return (Ref 容器);
}
4、useCallback
性能优化,缓存方法 (函数)
import React, {useState, useCallback} from 'react'
/*
解析:父组件更改,Child每次在渲染
使用React.memo包裹可优化,对函数组件进行优化
类似:shouldComponentUpdate和PureComponent
*/
const Child = React.memo((getCount) => {
return <div onClick={getCount}>child</div>
})
function Parent(){
const [num, setNum] = useState(0);
const [val, setVal] = useState("");
/*
未使用useCallback包裹时,val和num变时都渲染
使用useCallback包裹时,只有依赖项变动才会渲染
*/
const getCount = useCallback(() => {
console.log(num)
}, [num]}
return (
<div>
<h1>num: {num}</h1>
<h2>val: {val}</h2>
<input type="text" onChange={(ev) => {setVal(ev.target.value)}}/>
<button onClick={()=> {setNum(num + 1)}}>+</button>
<Child getCount={getCount}/>
</div>
)
}
export default Parent;
5、useMemo
缓存计算值,性能优化
import React, { useState } from 'react';
const UseCallbackSub = ({ value, onChange }) => {
console.log('⼦元素发⽣了渲染 value: ', value);
return ;
};
export default function UseCallback() {
const [count, setCount] = useState(0);
const [value, setValue] = useState(0);
// 每次修改 count 时,本组件发⽣渲染⽆可厚⾮,
// 但是⼦组件 UseCallbackSub 也会进⾏不必要的渲染
const onClick = () => {
setCount(count + 1);
};
const onChange = e => {
setValue(e.target.value);
};
return (<>
count 发⽣变化{ count }
</>
);
}
// 通常优化这类情景,可以对⼦组件使⽤ memo 包裹
import React, { useState, useCallback, memo } from 'react';
const UseCallbackSub = memo(({ value, onChange }) => {
console.log('⼦元素发⽣了渲染 value: ', value);
return ;
});
// ⽗组件内:
...
const onChange = useCallback(e => {
setValue(e.target.value);
}, []);
...
// 总结⼀句,useCallback 可以对函数进⾏缓存,保证 onChange 不会随着组件更新⽽改变引⽤,
//⽽ memo 会默认对所有的 props 进⾏对⽐,如果不发⽣变化则不更新组件,避免⽗级引起的⼦级渲染。
//当然,上述⽅式也可以不使⽤useCallback 达到⽬的(组件的更新只取决于 value 的变化),使⽤⾃定义⽐对函数:
const UseCallbackSub = memo(({ value, onChange }) => {
console.log('⼦元素发⽣了渲染 value: ', value);
return ;
}, (prev, next) => prev.value === next.value);
6、useContext
使用context解决父子数据传递问题:Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法
import React, {useState, useContext} from 'react'
let theme = {
black: {
background: "#000",
color: "#fff"
},
pink: {
background: "#eee",
color: "#000"
}
}
const ThemeContext = React.createContext()
//父组件
function App (){
return <ThemeContext.Provider value={theme.black}>
<Theme/>
</ThemeContext.Provider>
}
//子组件
function Theme(){
const ThemeCo = useContext(ThemeContext);
return (
<div style={background: ThemeCo.background, color: ThemeCo.color}>
</div>
)
}
export default App;
7、useReducer
使用reducer保存全局复合数据
import React, {useState, useReducer} from 'react'
const initState = {
name: 'jian',
age: 18
}
function reducer(state, action){
switch(action.type){
case 'increment':
return {...state, age: age + 1}
case 'decrement':
return {...state, age: age - 1}
default:
return state
}
}
function Temp(){
const [state, dispatch] = useReducer(reducer, initState);
return (
<div>
<h1>{state.name}</h1>
<h2>{state.age}</h2>
<button onClick={()=>{dispatch({type: 'increment'})}}>increment</button>
<button onClick={()=>{dispatch({type: 'decrement'})}}>decrement</button>
</div>
)
}
export default Temp;
8、useCustom
使用use开头封装函数,内部使用原hook方法
import React, {useState, useMemo} from 'react'
function useCutDown(init = 60){
const [count, setCount] = useState(init)
useEffect(()=>{
let timer = setInterval(()=> {
if(count < 1){
clearInterval(timer)
return
}
setCount(count -1)
}, 1000)
return ()=>{
clearInterval(timer)
}
}, [])
return count
}
function App(){
let Counter = useCutDown(120);
return <div>{Counter}</div>
}
export default App;
3、自定义hooks实现
⾸次挂载不执⾏,更新时执⾏:
// 上⽂有提到使⽤ useEffect 模拟 componentDidUpdate 的例⼦,如果
// 这个场景⽐较多,我们可以封装起来:
import React, { useRef, useEffect } from 'react';
export default function useUpdated(callback) {
const didUpdate = useRef(false);
useEffect(() => {
if (didUpdate.current) {
callback?.();
} else {
didUpdate.current = true;
// 初次挂载时⾛这⾥
}
});
}
// 分隔线 ---------------------------------------------------
// 使⽤⽅式
import useUpdated from './useUpdated';
export default function UseRef() {
useUpdated(() => {
console.log('模拟 componentDidUpdate ,即除了初始化,之后的更新进到这⾥');
});
return (<button>Ref 容器</button>);
}
参数变化就发起请求,⾃动更新数据源
import { useState, useEffect } from 'react';
const defaultOptions = {}; // 根据实际情况写死⼀些默认值
export default function useRequest(query, { url, method = 'GET' } = {}) {
// 务必保证 query 的变化时有条件的
const [state, setState] = useState({
data: [], error: false, loading: false
});
useEffect(() => {
const opts = { ...defaultOptions, method };
if (method === 'GET') {
opts.params = query;
} else {
opts.body = JSON.stringify(query);
}
setState(state => ({...state, loading: true}));
fetch(url, opts).then(json => json()).then(res => { // 异常处理⾃⼰做⼀下
setState(state => ({
...state,
loading: false,
data: res.data || [],
error: false
}));
}).catch(() => {
setState(state => ({...state, loading: false, error: true }));
});
}, [query]);
return state;
}
// 分隔线 ------------------------------------------------------------
// 使⽤⽅式
import useRequest from './useRequest';
export default function List() {
const [query, setQuery] = useState({});
const { data, error, loading } = useRequest(query, { url: '/list' });
const onChangeQuery = params => setQuery(query => ({ ...query, ...params }));
return <div>
<SearchPanel onChange={onChangeQuery} />
<ul>
{
loading ? 'loading...'
: error ? <Empty description="出错了" />
: data.map(item => <li key={item.id}>{item.name}</li>)
}
</ul>
<Pagination onChange={onChangeQuery} />
</div>
}
三、异步组件
动态导⼊ + Suspense 占位。下⾯的 About 组件将独⽴打包为⼀个⽂件,访问的那⼀刻开始下载,在此 之前并不会占⽤⽹络和系统资源。对于低优先级的任务,尤其是单⻚应⽤的⾸屏展示,异步组件显得⼗分必要。
- 异步模式:请求数据 -> 渲染组件
- 解决问题:优化性能、延迟加载
1、传统模式
渲染组件 -> 请求数据 -> 再渲染组件
function Index(){
const [userInfo, setUserInfo] = React.useState(0);
React.useEffect(()=>{
getUserInfo().then(res => {
setUserInfo(res)
})
}, [])
return <div>
<h1>{userInfo.name}</h1>
</div>
}
//2. 使用Suspense + 异步组件
function AsyncComponent(){
const userInfo = getUserInfo()
return <div>
<h1>{userInfo.name}</h1>
</div>
}
export default function Home(){
return <div>
<React.Suspense fallback={<div>loading...</div>}>
<AsyncComponent/>
</React.Suspense>
</div>
}
//3. 使用Suspense + lazy
//eg:import('./app.js').then()
code splitting和import
2、使用Suspense
//1. 使用Suspense + 异步组件
function AsyncComponent(){
const userInfo = getUserInfo()
return <div>
<h1>{userInfo.name}</h1>
</div>
}
export default function Home(){
return <div>
<React.Suspense fallback={<div>loading...</div>}>
<AsyncComponent/>
</React.Suspense>
</div>
}
//2. 使用Suspense + lazy
//code splitting和import,eg:import('./app.js').then()
const LazyComponent = React.lazy(() => import('./test.js'))
export default function Index(){
return <Suspense fallback={<div>loading...</div>} >
<LazyComponent />
</Suspense>
}
- suspense原理
export class Suspense extends React.Component{
state={ isRender: true }
componentDidCatch(e){
/* 异步请求中,渲染 fallback */
this.setState({ isRender:false })
const { p } = e
Promise.resolve(p).then(()=>{
/* 数据请求后,渲染真实组件 */
this.setState({ isRender:true })
})
}
render(){
const { isRender } = this.state
const { children , fallback } = this.props
return isRender ? children : fallback
}
}
- lazy原理
function lazy(ctor){
return {
$$typeof: REACT_LAZY_TYPE,
_payload:{
_status: -1, //初始化状态
_result: ctor,
},
_init:function(payload){
if(payload._status===-1){ /* 第一次执行会走这里 */
const ctor = payload._result;
const thenable = ctor();
payload._status = Pending;
payload._result = thenable;
thenable.then((moduleObject)=>{
const defaultExport = moduleObject.default;
resolved._status = Resolved; // 1 成功状态
resolved._result = defaultExport;/* defaultExport 为我们动态加载的组件本身 */
})
}
if(payload._status === Resolved){ // 成功状态
return payload._result;
}
else { //第一次会抛出Promise异常给Suspense
throw payload._result;
}
}
}
}
3、实现一个异步组件
import React from 'react';
export default function lazy(loadComponent) {
const Fallback = () => <h1>loading...</h1>;
const [Component, setComponent] = useState(() => Fallback);
useEffect(() => {
loadComponent().then(res => {
setComponent(res.default);
});
}, []);
return ;
}
// 或者使⽤⾼阶函数
export default function lazy(loadComponent) {
return class WrapComponent extends React.Component {
state = {
Component: () =><h1>loading...</h1>;
}
async componentDidMount() {
const { default: Component } = await loadComponent();
this.setState({ Component });
}
render() {
const Component = this.state.Component;
return <Component />;
}
}
}
// 分隔线 -----------------------------------------------------
// 使⽤⽅式
const AsyncAbout = lazy(() => import('./About'));