目录
1. 组件复用的说明
问题 1:如果两个组件中的部分功能相似或相同,但 UI 结构不同,该如何优化相似的功能?复用
问题 2:复用什么?1. state 状态 2. 操作 state 状态的方法。也就是:组件状态逻辑复用
在 Hooks 之前,组件的状态逻辑复用经历了:mixins(混入)、HOCs(高阶组件)、render-props 等模式
注意:这几种方式不是新的 API,而是利用 React 自身特点的编码技巧,演化而成的固定模式(写法)
通过一个鼠标位置的案例,来演示组件的状态逻辑复用。
2. mixins 混入(已废弃)
存在的问题:
-
Mixins 引入了隐式依赖关系,组件中的方法和数据的来源不明确,不利于维护
-
Mixins 导致名称冲突
3. 高阶组件
概述
高阶组件(HOC,High-Order Component)作用:通过增强组件的能力,来实现组件状态逻辑复用
采用 包装(装饰)模式 ,比如,
-
手机:获取保护功能
-
手机壳 :提供保护功能
高阶组件就相当于手机壳,通过包装组件,增强组件功能
基本使用
高阶组件是一个函数,接收要包装的组件,返回增强后的组件
高阶组件的命名约定以 with
开头,比如:withMouse、withRouter 等
原理:高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过 prop 将复用的状态传递给被包装组件
// 高阶组件内部创建的类组件:
const withMouse = (BaseComponent) => {
class Wrapper extends React.Component {
state = {
x: 0,
y: 0,
};
render() {
// const { x, y } = this.state
return <BaseComponent x={x} y={y} {...wrapper组件状态数据} />;
}
}
return Wrapper;
};
// 参数:需要增强的组件
// 返回值:增强后的组件
const HOCComponent = withMouse(被包装的组件);
示例:
假设 withMouse 高阶组件,可以拿到鼠标位置状态,而 Cat 组件需要使用鼠标位置状态,就可以通过高阶组件来复用鼠标位置相关的状态逻辑代码了:
// withMouse 高阶组件:提供鼠标位置相关的状态逻辑
const CatWithMouse = withMouse(Cat);
// 复用1:
// 使用高阶组件包装后,组件内部就可以通过 props 来获取鼠标位置
const Cat = (props) => {
// props => { x: 1, y: 1 }
return (
<img
src={catImg}
style={{
position: 'absolute',
top: props.y,
left: props.x,
}}
alt=""
/>
);
};
// 渲染时,要渲染增强后返回的组件
<CatWithMouse />;
// 复用2:
// 使用高阶组件包装后,组件内部就可以通过 props 来获取鼠标位置
const PositionWithMouse = withMouse(Position);
const Position = ({ x, y }) => {
return (
<div>
鼠标当前位置:(x: {x}, y: {y})
</div>
);
};
<PositionWithMouse />;
封装 withMouse 高阶组件
// 创建高阶组件
const withMouse = (BaseComponent) => {
// 该类组件用来提供状态逻辑
return class Wrapper extends React.Component {
state = {
x: 0,
y: 0,
};
handleMousemove = (e) => {
const { pageX, pageY } = e;
this.setState({
x: pageX,
y: pageY,
});
};
componentDidMount() {
window.addEventListener('mousemove', this.handleMousemove);
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMousemove);
}
render() {
const { x, y } = this.state;
return <BaseComponent x={x} y={y} />;
}
};
};
高阶组件的注意点
1.推荐设置 displayName 属性
- 作用: 用来在 React 浏览器开发者工具(插件)中展示名称
const withMouse = () => {
class Wrapper ... {}
// 设置 displayName
Wrapper.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
return Wrapper
}
2.推荐传递 props
- 如果没有传递,那么,props 会丢失
- 最终的 props 传递路径: 增强后的组件(CatWithMouse) -> 高阶组件函数中的 Mouse -> 被包装组件(Cat)
const withMouse = (WrappedComponent) => {
class Mouse ... {
render() {
// {...this.props} 就表示将接收到 props 传递给被包装组件
return <WrappedComponent {...this.props} />
}
}
return Mouse
}
const CatWithMouse = withMouse(Cat)
// 注意: 如果不传递 props,那么,name属性就丢了
<CatWithMouse name="rose" />
4. render-props 模式
基本使用
将要复用的状态逻辑代码封装到一个组件中,通过一个值为函数的 prop 对外暴露数据,实现状态逻辑复用
比如,要通过 render-props 实现鼠标位置的状态逻辑复用:
- 创建一个类组件,提供鼠标位置相关的状态逻辑代码
- 调用传给该组件的 render 属性,将状态通过参数暴露出去
- 通过 render 属性的返回值指定要渲染的结构内容
class Mouse extends React.Component {
// … 省略state和操作state的方法
render() {
return this.props.render(this.state);
}
}
<Mouse
render={(mouse) => (
<p>
鼠标当前位置 {mouse.x},{mouse.y}
</p>
)}
/>
children 代替 render 属性
-
注意:并不是该模式叫 render props 就必须使用名为 render 的 prop,实际上可以使用任意名称的 prop
-
把 prop 是一个函数并且告诉组件要渲染什么内容的技术叫做:render props 模式
<Mouse>
{({ x, y }) => (
<p>
鼠标的位置是 {x},{y}
</p>
)}
</Mouse>;
// 组件内部:
this.props.children(this.state);
比如,Context 中的 Consumer 组件就是 render-props 的使用模式
// Context 中的用法:
<Consumer>{(data) => <span>data参数表示接收到的数据 -- {data}</span>}</Consumer>
5. React Hooks 状态逻辑复用
// 创建自定义 hook,实现鼠标位置状态逻辑复用
const useMouse = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const onMouseMove = (e) => {
const { pageX, pageY } = e;
setPosition({
x: pageX,
y: pageY,
});
};
window.addEventListener('mousemove', onMouseMove);
return () => window.removeEventListener('mousemove', onMouseMove);
}, []);
return position;
};
// 复用:
const Cat = () => {
const { x, y } = useMouse();
return (
<img
src={catImg}
style={{
position: 'absolute',
top: y,
left: x,
}}
alt=""
/>
);
};
const Position = () => {
const { x, y } = useMouse();
return (
<div>
鼠标当前位置:(x: {x}, y: {y})
</div>
);
};
6. 为什么要有 Hooks
从以下两个角度来看下 Hooks 出现之前(React v16.8 以前)React 存在的问题
两个角度: 1 组件的状态逻辑复用 2 class 组件自身的问题
-
组件的状态逻辑复用:
- 在 Hooks 之前,组件的状态逻辑复用经历了:mixins(混入)、HOCs(高阶组件)、render-props 等模式
- (早已废弃)mixins 的问题:1 数据来源不清晰 2 命名冲突
- HOCs、render-props 的问题:重构组件结构,导致组件形成 JSX 嵌套地狱问题,这两种模式都不是官方实现。所以,React 急需一种官方的方式,来解决状态逻辑复用导致的其他问题。
-
class 组件自身的问题:
- 选择:函数组件和 class 组件之间的区别以及使用哪种组件更合适
- 需要理解 class 中的 this 是如何工作的
- 相互关联且需要对照修改的代码被拆分到不同生命周期函数中
- 相比于函数组件来说,不利于代码压缩和优化,也不利于 TS 的类型推导
正是由于 React 原来存在的这些问题,才有了 Hooks 来解决这些问题。
7. 性能优化
React 自身提供了一些与性能优化相关的 API,按照 class 或 hooks,分为两类:
- class
PureComponent
shouldComponentUpdate
- hooks
React.memo
useMemo
useCallback
注:对于性能优化来说,一定要避免过早优化,也就是在没有出现性能问题前,可以不用进行优化。因为只要是优化,就会有成本,所以,优化时一定要确保成本小于优化带来的收获。
React 对性能的理解:只要用户不感觉卡顿,那就是没有性能问题。
8. 优化的方向
React 组件的特性:只要父组件更新,子组件都会无条件的更新
注意,此处的 更新
指的是:子组件中的代码重新执行一遍,进行一次 diff,最终只把变化后的内容更新到浏览器中
但是,某些情况下,子组件是没有必要更新的,比如:
- 子组件是一个展示组件,仅仅用来展示一段静态的 JSX 结构
- 子组件没有从父组件中接收任何 props
- 父组件传递给子组件的 props 引用变了,但是,本质内容没变(比如,父组件给子组件传递了一个回调函数)
此时,就可以通过上述 API 来进行优化。
9. React.memo
React.memo
是一个高阶组件,用来包裹 函数组件 来阻止函数组件不必要的更新
用法:
const Child2 = React.memo(() => {
console.log('Child2 re-render');
return <div>Child2</div>;
});
// 通过 React.memo 避免不必要的组件更新
const Child2 = React.memo(
() => {
console.log('Child2 re-render');
return <div>Child2</div>;
},
// 如果需要自己来对比 props ,就需要传入第二个参数
// 如果返回 false,表示更新前后的两次 props 发生了改变,此时,组件会重新渲染
// 如果返回 true,表示更新前后的两次 props 没有改变,此时,组件不会重新渲染
// (prevProps, nextProps) => {
// console.log(prevProps, nextProps)
// return false
// }
);
说明:React.memo
会对比组件更新前后两次接收到的 props 是否相同,
- 如果相同,就阻止组件重新渲染
- 如果不同,才会重新渲染组件
注意:函数组件每次更新时,会重新执行组件中的所有代码,也就是每次都会重新创建该组件中声明的函数、对象等等
浅对比的说明
注意:React.memo
对比 props 的方式是:浅对比
,也就是之比较值或引用是否相同
比如:
// 对于简单类型来说,比较的是:值是否相同
1 === 1 // true -> 组件不会重新渲染
true === false // false -> 组件会重新渲染
// 对于引用类型来说,比较的是:引用是否相同
[1, 2] === [1, 2] // false -> 组件会重新渲染
{} === {} // false -> 组件会重新渲染
这样,就导致了一个问题:对于引用类型的 props来说,即使更新前后的 prop 值相同,但是引用不同,还是会导致组件重新渲染。
因此,针对于这种 props 是引用类型的情况,需要使用 useCallback
或 useMemo
来处理。
10. useCallback
作用:缓存(记忆)一个函数,该函数只在依赖项变化时才会重新创建新函数(改变)
一般都会配合 React.memo 高阶组件来使用
用法:
const fn = () => {
console.log('fn 执行了')
}
// 第一个参数:要缓存的函数
// 第二个参数:依赖项,类似于 useEffect 的第二个参数
const memoFn = useCallback(fn, [])
<Child2 fn={memoFn} />
11. useMemo
作用:缓存(记忆)一个对象(非函数),该对象只在依赖项变化时才会改变
一般都会配合 React.memo 高阶组件来使用
用法:
const memoObj = useMemo(() => {
return {
name: '豆豆'
}
}, [])
<Child2 obj={memoObj} />
- 使用 useMemo 模拟 useCallback 的功能:
// 使用 useMemo 来模拟 useCallback
const memoFn = useMemo(() => {
return () => {
console.log('fn 执行了', count)
}
}, [count])
<Child2 fn={memoFn} />
- useMemo 的回调函数代码只会在依赖项改变时重新执行,因此,除了缓存对象之外,还可以避免昂贵计算,提升性能
- 如果一个数据,需要经过昂贵的大量计算得到,此时,也可以使用 useMemo 来缓存数据,避免重复计算提升性能
const memoObj = useMemo(() => {
// 模拟昂贵计算:
// 创建长度为 1000 的数组
const nums = new Array(1000).fill(0).map((item, index) => index)
return {
name: '豆豆'
}
}, [])
<Child2 obj={memoObj} />
12. class 组件优化
对于 class 组件来说,PureComponent
的作用相当于 React.memo
;而 shouldComponentUpdate
可以自定义 props 之间的对比规则
PureComponent
- 浅对比shouldComponentUpdate
- 注意:
shouldComponentUpdate
不能与PureComponent
一起使用
- 注意: