把组件作为参数,并返回(高阶)组件的函数,称为高阶组件
优点 代码复⽤,状态/逻辑抽象可以对 state/event/props 进⾏劫持、 *** 作 使用案例假如有这样的场景,两个查询列表的⻚⾯结构相同,查询条件相同,只是表头包括 *** 作列不⼀样:
显然这两个⻚⾯具有很⾼的复⽤性,不只是 UI 级别的复⽤,逻辑都⼏乎⼀致,这时候,⾼阶组件就派上⽤场了。我们定义查询条件的部分为组件 SearchPanel,
表格组件为 Table(antd design 的 Table ⾃带 底部分⻚区),那么这两个⻚⾯可能是下⾯的代码结构:
⻚⾯ 1,可能是普通⽤户查看⻚
import React, { Component } from 'react';
import request from 'axios';
import { Button, Table } from 'antd';
import SearchPanel from './SearchPanel';
export default class Page1 extends Component {
state = {
query: {
name: '',
id: '',
time: '',
valid: ''
},
dataSource: []
}
columns = [
{dataIndex: 'label', title: '标签'},
{dataIndex: 'action', title: ' *** 作',
render: (_, record) => {
const onOpen = () => window.open(`/xxx/${record.id}`);
return ;
}
}]
onChange = (params) => {
this.setState(query => ({ ...query, ...params }));
request('/api/list', {
method: 'GET',
params
}).then(res => { // 这⾥暂不考虑异常
this.setState({ dataSource: res.data });
});
}
componentDidMount() {
this.onChange(this.state.query);
}
render() {
const { query, dataSource } = this.state;
return (
<>
>
);
}
}
⻚⾯ 2,可能是管理员⻚⾯
import React, { Component } from 'react';
import request from 'axios';
import { Button, Table } from 'antd';
import SearchPanel from './SearchPanel';
export default class Page2 extends Component {
state = {
query: {
name: '',
id: '',
time: '',
valid: ''
},
dataSource: []
}
onEdit = id => {}
onDelete = id => {}
columns = [
{dataIndex: 'name', title: '名称'},
{dataIndex: 'action', title: ' *** 作',
render: (_, record) => {
return <>
>;
}
}]
onChange = (params) => {
this.setState(query => ({ ...query, ...params }));
request('/api/list/admin', {
method: 'GET',
params
}).then(res => { // 这⾥暂不考虑异常
this.setState({ dataSource: res.data });
});
}
componentDidMount() {
this.onChange(this.state.query);
}
render() {
const { query, dataSource } = this.state;
return (
<>
>
);
}
}
可以看到,两份代码除了表格列及其 *** 作外,请求数据的接⼝也分别为 /api/list 和 /api/list/admin,这两份⽂件总计约 100 ⾏代码。我们使⽤⾼阶组件整合相同的逻辑:
Page1 和 Page2 的公共 UI 部分:
import React from 'react';
import { Table } from 'antd';
import SearchPanel from './SearchPanel';
// ⽆状态组件,所以⽤函数实现更简洁
export default function PageCommon({ query, dataSource, onChange, columns }) {
return (
<>
>
);
}
⾼阶组件:
import React, { Component } from 'react';
import request from 'axios';
export default function hoc(WrappedComponent, api) {
return class extends Component {
state = {
query: {
name: '',
id: '',
time: '',
valid: ''
},
dataSource: []
}
onChange = (params) => {
this.setState(query => ({ ...query, ...params }));
request(api, {
method: 'GET',
params
}).then(res => { // 这⾥暂不考虑异常
this.setState({ dataSource: res.data });
});
}
componentDidMount() {
this.onChange(this.state.query);
}
render() {
retrun ;
}
}
}
最终分别得到两个⻚⾯,Page1:
class Page1 extends Component {
columns = [
{dataIndex: 'label', title: '标签'},
{dataIndex: 'action', title: ' *** 作',
render: (_, record) => {
const onOpen = () => window.open(`/xxx/${record.id}`);
return ;
}
}]
render() {
return ;
}
}
export default hoc(Page1, '/api/list');
Page2:
class Page2 extends Component {
onEdit = id => {}
onDelete = id => {}
columns = [
{dataIndex: 'name', title: '名称'},
{dataIndex: 'action', title: ' *** 作',
render: (_, record) => {
return <>
>;
}
}]
render() {
return ;
}
}
export default hoc(Page2, '/api/list/admin');
累计 80 ⾏左右代码,如果再来⼏个相似的⻚⾯,代码量的增加也只限定在特有的业务逻辑上,重点是,我们不需要再重复维护相似的多份逻辑了。从上⾯的例⼦可以看到⾼阶组件具有的能⼒:
向被修饰的组件注⼊额外的状态或⽅法,返回值依然是个组件(react 中,返回类组件或函数式组件均可)。⾃定义注⼊内容可以实现组件功能的增强。当⾼阶组件只有⼀个组件作为参数时,可以嵌套使⽤,例如⼀个基础组件为:
function Base({ list }) {
return
{ list.map(({ id, name }) => { name }) }
}
我想在 Base 组件渲染更新的时候记录⽇志,便于调试:
const insertLog = WrappedComponent => class extends Component {
componentDidUpdate(...args) {
console.log(...args);
}
render() {
return
}
}
const BaseWithLog = insertLog(Base);
我想在 Base 组件渲染报错的时候不要⽩屏,显示⼀个固定的信息并上报问题:
const insertErrorBoundary = WrappedComponent => class extends Component {
state = {
error: false
}
componentDidCatch(error, errorInfo) {
this.setState({ error: true }, () => {
logErrorToMyService(error, errorInfo); // 调⽤已有的接⼝
});
}
render() {
if (this.state.error) {
return ⻚⾯不可⽤,请检测组件{Component.displayName}的逻辑
;
}
return ;
}
}
const BaseWithErrorBoundary = insertErrorBoundary(Base);
我两个功能都要:
// 因为这两个⾼阶组件的参数都是唯⼀的,所以可以不区分顺序地组合
const BaseWithLogAndErrorBoundary = insertErrorBoundary(insertLog(Base));
// 我们希望捕获错误的范围尽量⼤⼀些,所以把 insertErrorBoundary
// 放在最外⾯,最后⼀步再修饰组件。
⾼阶组件的优点想必⼤家已经能⾃⼰总结了,但事物往往具有两⾯性,下⾯我们通过实例讲解⾼阶组件存在或易引发的问题:
import React, { Component, createRef } from 'react';
class Sub extends Component {
input = createRef()
focus = () => { // focus ⽅法执⾏时会让 input 元素聚焦。
this.input.current.focus();
}
render() {
return ;
}
}
class Parent extends Component {
state = {
value: ''
}
input = createRef() // 引⽤⼦组件实例,便于调⽤实例上的⽅法
onFocus = () => {
this.input.current.focus(); // 调⽤⼦组件实例上的⽅法
}
onChange = e => {
this.setState({ value: e.target.value });
}
render() {
return <>
>;
}
}
由于需求变更,我想对 Sub 组价进⾏增强,⽐如⽤上述 insertLog 进⾏⽇志输出:
// ...Sub 定义略
import insertLog from './insertLog';
class Parent extends Component {
...
render() {
const SubWithLog = insertLog(Sub);
return <>
>;
}
}
输⼊⽂本看看出现了什么问题?问题就是⼀旦输⼊⼀个字符,输⼊框就失去焦点了。⾼阶组件在render 函数内频繁调⽤,意味着 SubWithLog 始终是新返回的组件,我们将失去 Sub 组件的状态!
fix tips:
const SubWithLog = insertLog(Sub);
// 放到 render 以外,可以使组件外,也可以是Parent组件的实例上
class Parent extends Component {
render() {
return <>
>
}
}
// 或者
class Parent extends Component {
SubWithLog = insertLog(Sub)
render() {
const SubWithLog = this.SubWithLog;
return <>
>
}
}
然后点击“点击聚焦”按钮,报错了:
仅仅是加了个⾼阶组件就报错了!看这⾥的注释:
当使⽤ SubWithLog 时,实例已经不是 Sub 的了,那是什么呢?断点:
破案了,Parent 内,this.input.current 引⽤的实例不是 Sub 的,⽽是 insertLog 返回的那个匿名class 的实例。根本原因是,react pops 中,
key 和 ref 是两个特殊的 property,不会被转发到下层组件,⼀旦⾼阶组件没有处理好 ref 的转发,被修饰的组件就会失去与上层组件的联系。
fix tips:
import { forwardRef } from 'react';
const insertLog = WrappedComponent => {
class Log extends Component {
componentDidUpdate(...args) {
console.log(...args);
}
render() {
const { forwardedRef, ...props } = this.props
return
}
}
return forwardRef((props, ref) => );
}
其原理是,函数式组件除了 props 参数外,还⽀持第⼆个参数 ref(如果有传递 ref 的话),我们将 ref命名为 forwardedRef ——即当做⼀个 props 继续往后传递(13⾏),
并且在第 10 ⾏再次转换为 ref 挂载到⽬标组件上。最终,聚焦事件正常⼯作了。
HOC 缺点⼩结:
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 (
)
}
}
// 接收 context 的固定写法
Connect.contextTypes = {
store: PropTypes.object
}
return Connect;
}
}
// 因此, App 组件的 props 会被注⼊ action、state 等
react-router-dom withRouter
export default withRouter(App); // App 获得了 history,location 等 props
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 (
)
}
}
// 接收 context 的固定写法
Connect.contextTypes = {
store: PropTypes.object
}
return Connect;
}
}
// 因此, App 组件的 props 会被注⼊ action、state 等
function withRouter(Component) {
const displayName = `withRouter(${Component.displayName || Component.name})`;
const C = props => {
// 如果想要设置被 withRouter 包裹的组件的 ref,这⾥使⽤ wrappedComponentRef
const { wrappedComponentRef, ...remainingProps } = props;
return (
{context => {
// 将 context 加⼊到 Component 中,注意 ref 的转发,这⾥注⼊了
// RouterContext 中定义的各种 props,其中就包括 history,location 对象
return (
);
}}
);
};
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);
}
2. react hooks(v16.8新加的特性)
Hook 是一个特殊的函数,它可以让你“钩入” React 的特性
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中) 增加hook的原因clas有一些问题,比如react认为class不好理解,是学习react的一道屏障,开发者可能会在class中使用一些会使react的优化措施无效的方案,还有class给它目前使用的工具带来了
一些问题。而hook可以保证我们在即使不使用class也能使用尽可能多的react特性
返回一个 state,以及更新 state 的函数,在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同
import { useState } from 'React';
const [count, setCount] = useState(0);
useEffect
useEffect可以传入两个参数,第一个参数是函数effect(副作用),第二个是可选的监听状态参数,为一个数组形式,如果第二个参数不传的话表示每次渲染完之后
调用effect(类似于componentDidMount和componentDidUpdate),第二个参数如果传入空数组则表示只在首次渲染完之后调用effect,如果数组中包含了状态,
则会在每次渲染完之后判断这些状态有没有发生改变,只在发生了改变才会调用effect。effect函数也可以返回一个函数,我们可以在这个函数中定义一些清除 *** 作,
这个函数会在下一次执行该effect函数之前调用
import { useEffect } from 'React';
useEffect(() => {
console.log(`mount + update: ${count}`); // 只要本组件有更新,就会执⾏
});
useEffect(() => {
console.log(`mount: ${count}`); // 只在本组件第⼀次加载才会执⾏
}, []);
useEffect(() => {
console.log(`mount + update count: ${count}`); // 只要 count 发⽣变化,就执⾏
}, [count]);
question: 为什么effect的清除阶段需要在每一次effect都执行,而不在组件卸载的时候只执行一次呢
正如官方给的订阅好友状态的例子中所示,如果只是在didMount订阅和willUnmount取消订阅的话,当页面更新的时候props中传入的朋友ID可以发生了变化,如果在最后
的willUnmount取消订阅的时候采用了错误的朋友ID就可能会发生内存泄露和崩溃的问题。可以通过在didUpdate中取消订阅后重新用最新的朋友ID重新订阅
question: 我们可以在单个组件中使用多个state hook和effect hook, react是如何确定那个state对应哪个useState呢
靠的是hook调用的顺序,只要hook的每次调用顺序在每次渲染的时候都是相同的,它就能正常工作,react就能正确的将内部的state和对应的hook进行关联,这也是我们
为什么要遵循上面两条规则的原因。React团队发布了一个名为 eslint-plugin-react-hooks
的 ESLint 插件来强制执行这两条规则
useRef等效于类组件的createRef
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 ();
}
3. 异步组件
示例(异步组件+展位)
异步组件其实就是采用const Xxx = lazy(() => import('相对路径'))
的方式引入组件
在组件没有加载出来时候采用占位则使用的是Suspense组件
import { Suspense, lazy } from 'react';
const About = lazy(() => import('./About'));
export default function App() {
return
;
}
实现一个异步组件lazy
import React from 'react';
export default function lazy(loadComponent) {
const Fallback = () => loading...;
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: () => loading...
}
async componentDidMount() {
const { default: Component } = await loadComponent();
this.setState({ Component });
}
render() {
const Component = this.state.Component;
return ;
}
}
}
// 分隔线 -----------------------------------------------------
// 使⽤⽅式
const AsyncAbout = lazy(() => import('./About'));
..
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)