本文章主要是源自实际项目开发项目的总结,一些思考是参考了看过的文章,做了一篇总结,demo是跑过的,可以放心食用。
目录
一.组件通信
1.父组件向子组件通信
2.子组件向父组件通信
3.跨级组件通信
4.兄弟组件通信
5.无嵌套关系的组件通信
二、避免重复渲染
1.隔离独立渲染的子组件
2.与渲染无关的变量不用state来管理数据
3.批量更新state、合并state
4.class组件中使用shouldComponentUpdate
5.绑定事件尽量不使用箭头函数
三、代码优化
1.大量的props
2.不兼容的props
3.props经过处理变为state
4.使用枚举管理状态
5.自定义Hook
四、其他优化
1.使用React.Fragment减少额外标签
2.避免使用内联样式属性
3.优化条件渲染
五、懒加载
1.懒加载React组件、第三方依赖组件
2.不用React.lazy懒加载
1)函数组件:父组件useRef()创建一个ref,通过ref属性附加到子组件上,子组件用forwardRef来获取传递给它的ref,配合useImperativeHandle使用可以自定义暴露给父组件的方法
// 父组件
import { useRef } from "react";
const childRef = useRef();
useEffect(() => {
childRef.current.childFunc();
}, []);
return (
);
// 子组件
import { forwardRef, useImperativeHandle } from "react";
const Child = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
childFunc() {
return logChild();
},
}));
const logChild = () => {
console.log("---child-----");
};
return child;
});
export default Child;
2)class组件:子组件通过调用父组件的props方法,将子组件本身暴露出去,父组件可直接使用子组件内的方法,不想暴露给父组件的方法可以用static关键字定义静态方法
// 父组件
export default class Parent extends React.Component {
onClick = () => {
this.childRef.childFunc();
};
render() {
return (
{
this.childRef = ref;
}}
/>
);
}
}
// 子组件
class Child extends React.Component {
componentDidMount() {
this.props.onRef(this);
}
static myFunc = () => {
console.log('本方法不暴露给父组件');
};
childFunc = () => {
console.log("---child-----");
};
render() {
return child;
}
}
export default Child;
父组件向子组件传递props方法,子组件主动调用该方法,将需要的信息作为参数传递到父组件的作用域中
// 父组件
{
this.setState({
selectedValue
})
}}
/>
// 子组件
onSelect = (value) => {
this.props.onSelect(value);
}
使用Context共享组件树上能够全局访问的数据。注:Child1为Child的子组件
1)React.createContext(defaultValue)创建Context对象,订阅了Context对象的组件会从组件树中找到离自身最近的Provider中读取到当前的context,Context对象会返回Provider组件,允许Consumer消费组件订阅context的变化
// context/AppContext.js
import { createContext } from "react";
export default createContext({
name: "",
changeName: () => {},
});
2)函数组件:子组件使用useContext(),接收一个context对象,返回该context的当前值
// 根组件
import { useState } from "react";
import Child from "./Child";
import Context from "./context/AppContext";
const Parent = () => {
const [name, setName] = useState("defaultName");
const changeName = (name) => {
setName(name);
};
return (
);
};
export default Parent;
// 子组件Child1
import { useContext } from "react";
import Context from "./context/AppContext";
const Child1 = () => {
const context = useContext(Context);
return context.changeName("Child")}>{context.name};
};
export default Child1;
3)class组件:子组件contextType属性会被重新赋值为一个由 React.createContext() 创建的 Context 对象
// 根组件
import Child from "./Child";
import Context from "./context/AppContext";
export default class Parent extends React.Component {
state = {
name: "defaultName",
};
changeName = (name) => {
this.setState({
name,
});
};
render() {
return (
);
}
}
// 子组件Child1
import { Component } from "react";
import Context from "./context/AppContext";
class Child1 extends Component {
static contextType = Context;
render() {
return (
this.context.changeName("Child")}>
{this.context.name}
// 16.x版本后可写成,不需要定义contextType
{({ name, changeName }) => {
return changeName("Child")}>{name};
}}
);
}
}
export default Child1;
利用useReducer和context实现,可模仿一个简单的redux,useReducer传入reducer和defaultState,返回当前state和dispatch,通过context传给各个子组件,让子组件共享state和修改state
// types.js
export const EXAMPLE_TEST = "EXAMPLE_TEST";
// reducer.js
import * as Types from "./types";
export const defaultState = {
count: 0,
};
export default (state, action) => {
switch (action.type) {
case Types.EXAMPLE_TEST:
return {
...state,
count: action.count,
};
default: {
return state;
}
}
};
// action.js
import * as Types from "./types";
export const onChangeCount = (count) => ({
type: Types.EXAMPLE_TEST,
count: count + 1,
});
// 根组件
import React, { useReducer } from "react";
import Context from "./context";
import reducer, { defaultState } from "./reducer";
import Child from "./Child";
import Child1 from "./Child1";
function ReducerCom() {
const [state, dispatch] = useReducer(reducer, defaultState);
return (
);
}
export default ReducerCom;
// Child.js
import React, { useEffect, useContext } from "react";
import { onChangeCount } from "./action";
import Context from "./context";
const Child = () => {
const context = useContext(Context);
useEffect(() => {
// 监听变化
console.log("变化执行啦");
}, [context.state.count]);
return (
);
};
export default Child;
// Child1.js
import React, { useContext } from "react";
import Context from "./context";
const Child1 = () => {
const context = useContext(Context);
return (
{context.state.count}
);
};
export default Child1;
// EventEmitter.js
class EventEmitter {
constructor() {
this.subscribers = {};
}
on(type, fn) {
if (!this.subscribers[type]) {
this.subscribers[type] = [];
}
this.subscribers[type].push(fn);
}
off(type, fn) {
let listeners = this.subscribers[type];
if (!listeners || !listeners.length) return;
this.subscribers[type] = listeners.filter((v) => v !== fn);
}
emit(type, ...args) {
let listeners = this.subscribers[type];
if (!listeners || !listeners.length) return;
listeners.forEach((fn) => fn(...args));
}
}
export default new EventEmitter();
// 组件1
const Child = () => {
const [name, setName] = useState("defaultName");
useEffect(() => {
EventEmitter.on("changeName", (name) => {
setName(name);
});
return EventEmitter.off();
}, []);
return {name};
};
// 组件2
const Child1 = () => {
const changeChildName = () => {
EventEmitter.emit("changeName", "change name from child2");
};
return ;
};
用React.memo隔离组件形成独立的渲染单元,避免父组件重新渲染造成子组件也重新渲染,可以用于不依赖于父组件状态渲染的子组件,函数组件和class也可以用useMemo和pureComponent实现。
export default React.memo(Child);
1)函数组件:useMemo的第二个参数是依赖项数组,某个依赖项改变时才会重新渲染子组件。
{useMemo(
() => (
),
[]
)}
2)class组件:用React.pureComponent
export default class Child extends React.PureComponent {}
触发this.setState或者useState,只要state改变就会触发渲染,与在render中是否引用无关,可以直接把数据绑定在this上,或者使用useRef做数据缓存。
1)函数组件
const App = ({name}) => {
const nameRef = useRef('defaultName');
useEffect(() => {
nameRef.current = name;
})
return hello world!;
}
2)class组件
class App extends React.Component{
name = 'defaultName';
componentDidMount() {
this.name = this.props.name;
}
render () {
return hello world!;
}
}
class组件如果在一个函数中setState了三次,会触发三次setState,但是不会渲染三次,因为react会合并成一次做批量更新,但是在异步函数中会多次渲染,批量更新失效。
demo如下:
const Parent = () => {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const handleClick = () => {
setA(a + 1);
setB(b + 1);
setC(c + 1);
};
console.log("----render----");
return (
{a}---{b}---{c}
);
};
export default Parent;
效果如下:点击三次,才会render三次
如果是异步的更改state
const handleClick = () => {
setTimeout(() => {
setA(a + 1);
setB(b + 1);
setC(c + 1);
}, 0);
};
效果如下:点击一次,就会render三次,说明没有合并更新
解决方案:
1)手动批量更新可以用react-dom中的unstable_batchedUpdates,三次更新就会合并成一次
import { unstable_batchedUpdates } from 'react-dom';
const handleClick = () => {
setTimeout(() => {
unstable_batchedUpdates(() => {
setA(a + 1);
setB(b + 1);
setC(c + 1);
});
}, 0);
};
2)合并state:用一个setState改变多个state,或者一个useState保存多个state
使用shouldComponentUpdate控制组件是否需要重新渲染
shouldComponentUpdate(nextProps, nextState) {
if(nextState.id != this.state.id ) {
return true;
}
return false;
}
使用箭头函数每次渲染都会创建一个新的时间处理器,子组件每次都会被渲染。
1)函数组件:子组件使用React.memo配合父组件用useCallback包裹props方法,实现父组件渲染不影响子组件的渲染
// 如果用箭头函数绑定事件父组件还是会影响子组件的渲染
handleClick(value)} />
const handleClick = useCallback((value) => {
console.log(value);
}, []);
2)class组件:不用箭头函数,子组件用React.memo包裹即可
如果需要将大量的props传递到一个组件中,那么可以思考以下几点:
1)该组件是否做了多件事,一个组件应该只做一件事,将该组件拆分成多个小组件是否会更合理;
2)组件是否可以被合成,如果组件中有很多不相干的逻辑,就可以考虑拆分再重新组合;
3)是否传递了很多配置有关的props,比如带分页配置的表格组件,可以将多个配置的props合成一个options,可以更好的控制组件选项,也更规范。
避免组件之间传递不兼容的props,例如有一个组件功能是把输入的小写英文都转变成大写,过了一段时间,想将它用于电话号码的处理,虽然都是用的input元素,但是明显电话号码的处理用不上之前的功能,并且毫无关联,这时候也可以分割组件明确职责,如果有共享的逻辑可以放到hooks中
一般常规做法是,子组件内创建一个state,当props的值改变时再改变state,但是如果只是基于props通过计算得到新的state可以用useMemo来代替useState。
const Child = ({ count }) => {
const formatCount = (value) => {
return value + 10;
};
const formattedCount = useMemo(() => {
return formatCount(count);
}, [count]);
//const [formattedCount, setFormattedCount] = useState(count);
//useEffect(() => {
// setFormattedCount(formatCount(count));
//}, [count]);
return {formattedCount};
};
export default Child;
在编写组件时,很容易用很多个布尔值来表示组件当前的状态,比如isLoading、isFinished等等,虽然技术可行,但是很难推断组件当前处于什么状态,不容易维护,可以用一个枚举的状态来表示。
function Component() {
const [isLoading, setIsLoading] = useState(false)
const [isFinished, setIsFinished] = useState(false)
const [hasError, setHasError] = useState(false)
const fetchSomething = () => {
setIsLoading(true)
fetch(url)
.then(() => {
setIsLoading(false)
setIsFinished(true)
})
.catch(() => {
setHasError(true)
})
}
if (isLoading) return
if (hasError) return
if (isFinished) return
return
}
// 改造后
function Component() {
const [status, setStatus] = useState('loading')
const fetchSomething = () => {
setStatus('loading')
fetch(url)
.then(() => {
setStatus('finished')
})
.catch(() => {
setStatus('error')
})
}
if (status === 'loading') return
if (status === 'error') return
if (status === 'finished') return
return
}
1)场景一:通用查询表格
const [list, setList] = useState([]);
const [loading, setLoading] = useState(false);
const [paginator, setPaginator] = useState({
page: 1,
pageSize: 20,
});
const fetchList = (fetchStatus) => {
setLoading(true);
getList({
pageSize: paginator.pageSize,
pageNo: paginator.page,
name,
}).then((res) => {
setList(res.data);
setLoading(false);
}).catch(() => {
setLoading(false);
});
};
const handlePageSelect = (page, pageSize) => {
setPaginator({ ...paginator, page, pageSize });
};
useEffect(() => {
fetchList();
}, [paginator.page, paginator.pageSize]);
自定义组件 useTable.js
输入:查询列表的接口、【除了分页器之外的参数】、【处理列表数据的函数】、【请求接口完毕的回调】
输出:list、loading、getData函数、分页的配置项
import { useState, useEffect } from 'react';
const useTable = (getListData, extraParam = {}, handleData, callback) => {
const [paginator, setPaginator] = useState({
recordCount: 0,
pageSize: 20,
pageNo: 1,
});
const [list, setList] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
getData();
}, [paginator.pageNo, paginator.pageSize]);
const onSelect = (pageNo, pageSize) => {
if (!isNaN(pageNo)) {
const paginator = { ...paginator, pageSize, pageNo };
setPaginator(paginator);
}
};
// 调用接口,获取数据
const getData = (pageNoParam, newParam) => {
// 搜索条件改变查询的时候,当前页数重置为1
const pageNo = !isNaN(pageNoParam) ? pageNoParam : paginator.pageNo;
const paginatorExtra = { pageNo, pageSize: paginator.pageSize };
setLoading(true);
const params = newParam || extraParam;
getListData &&
getListData({ ...params, ...paginatorExtra })
.then((res = { data: [] }) => {
const data = res.data;
const recordCount = res.total || res.count;
let list = [];
if (handleData) {
list = handleData(data);
} else {
list = data.map((item) => {
// 默认以id为key
return { key: item.id, ...item };
});
}
// 因为使用这个hook的地方可以手动调用getData,所以分页器在这里赋值
setPaginator({ ...paginatorExtra, recordCount });
setList(list);
setLoading(false);
callback && callback(list);
})
.catch((err) => {
setLoading(false);
});
};
return {
list,
loading,
getData,
tableProps: {
prev: true,
next: true,
first: true,
last: true,
recordCount: paginator.recordCount,
pageSize: paginator.pageSize,
activePage: paginator.pageNo,
pageNo: paginator.pageNo,
onSelect,
},
};
};
export default useTable;
使用useTable()
const { list, tableProps, loading, getData } = useTable(
getList,
{ teamId, productName, status },
(data) => {
return data.map((item) => {
return { ...item, id: item.uuid };
});
}
);
2)场景二:封装useInterval代替setInterval
为什么不直接使用setInterval?
如下面的例子,在组件加载时定义一个定时器,卸载组件时也清空定时器,但是useEffect只会执行一次,setInterval中拿到的始终是第一次渲染时拿到的count为1,所以界面上始终上显示的是2。
为了解决这个问题,把useEffect的第二个参数改成[count],这样就会每次拿到最新的count,但是每次count更改,定时器就会不停的新增和移除。
funtion Counter() {
const [count, setCount] = useState(1);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 此时的count始终是1
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return {count};
};
解决办法1:函数式更新,useState 中的set方法可接收函数,该函数将接收之前的state,返回一个更新后的值。这样定时器每次拿到的是最新的值。
setCount((count) => count + 1);
解决办法2:用useRef将定时器函数提取出来,每次定时器触发的时候,都能获取到最新的count。
const myRef = useRef(null);
myRef.current = () => {
setCount(count + 1);
};
useEffect(() => {
const id = setInterval(() => {
myRef.current();
}, 1000);
return () => clearInterval(id);
}, []);
定义useInterval.js
import { useEffect, useRef } from 'react';
const useInterval = (callback, delay) => {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback);
useEffect(() => {
let id;
function tick() {
savedCallback.current(() => {
clearInterval(id);
});
}
if (!isNaN(delay)) {
id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
export default useInterval;
使用useInterval.js
useInterval((clear) => {
setCount(count + 1);
}, 1000);
每个组件都必须要唯一一个父标签,如果该标签只是为了当父标签,没有其他额外的用途,则可以用片段fragment包裹子元素,节省渲染器渲染额外的元素的工作量。
添加的内联样式是js对象不是真正的样式,需要花费更多的时间转换为等效的css样式属性,才会应用样式。
安装和卸载 React 组件是昂贵的操作,所以用条件渲染减少安装和卸载组件。执行不同的if else语句切换渲染的组件,没有更改的部分不需要用条件控制,不必要每次改变state的时候都卸载并重新安装
React16.6版本中,新增了React.lazy函数,可以动态加载React组件,配合webpack的code splitting,当用import()时,webpack监测到这个语法会自动进行代码分割,只有当组件被加载,对应的资源才会导入;Suspense组件可以指定在js加载完成之前的loading。适合路由懒加载、Tab切换、单个资源很大、第三方依赖组件很大的场景。
import React, { Suspense, useState } from "react";
const App = () => {
const [showChild, setShowChild] = useState(false);
const Child = React.lazy(() => import("./Child"));
return (
{showChild && (
loading... }>
)}
效果:2s后加载2.chunk.js
我们也可以指定这个js的名字
const Child = React.lazy(() => import(/* webpackChunkName: "child" */"./Child"));
因为网络问题或者组件内部错误导致资源加载失败时,可能会导致页面白屏,可以用Error Boundaries组件来优雅降级。
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.log(error, errorInfo);
}
render() {
if (this.state.hasError) {
return 资源加载失败,请稍后重试
;
}
return this.props.children;
}
}
// App组件
{showChild && (
loading...
import React, { useState, Component } from "react";
// 异步按需加载component
const asyncComponent = (getComponent) => {
return class AsyncComponent extends Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentDidMount() {
if (!this.state.Component) {
getComponent().then(({ default: Component }) => {
AsyncComponent.Component = Component;
this.setState({ Component });
});
}
}
render() {
const { Component } = this.state;
if (Component) {
return ;
}
return loading...;
}
};
};
const App = () => {
const [showChild, setShowChild] = useState(false);
const Child = asyncComponent(() =>
import(/* webpackChunkName: "child" */ "./Child")
);
return (
{showChild && }
);
};