2023-03-13 10:40:08 +08:00

329 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 5. *Render Props*
*React* 中,代码复用的最基本单位就是组件,但是如果组件中也出现了重复的代码,该怎么做呢?
那么我们需要通过某种方式将代码中公共的部分抽取出来,这些公共的部分就被称之为横切关注点(*Cross-Cutting Concerns*
*React* 中,常见有两种方式来进行横切关注点的抽离:
- 高阶组件(*HOC*
- *Render Props*
*Render Props* 实际上**<u>本身并非什么新语法</u>**,而是指一种在 *React* 组件之间**<u>使用一个值为函数的 *prop* 共享代码</u>**的简单技术。
有关 *Render Props*,咱们主要需要掌握以下 *2* 个点:
- 如何用?
- 何时用?
## 如何使用 *Render Props*
我们首先还是来看一个示例:
```js
// App.jsx
import ChildCom1 from "./components/ChildCom1"
import ChildCom2 from "./components/ChildCom2"
function App() {
return (
<div style={{
display: 'flex',
justifyContent: 'space-between',
width: "850px"
}}>
<ChildCom1 />
<ChildCom2 />
</div>
);
}
export default App;
```
```js
// components/ChildCom1.jsx
import { useState } from 'react';
function ChildCom1() {
const [points, setPoints] = useState({
x: 0,
y: 0
})
function handleMouseMove(e) {
setPoints({
x: e.clientX,
y: e.clientY
})
}
return (
<div style={{
width: '400px',
height: '400px',
backgroundColor: 'red'
}} onMouseMove={handleMouseMove}>
<h1>移动鼠标!</h1>
<p>当前的鼠标位置是 ({points.x}, {points.y})</p>
</div>
);
}
export default ChildCom1;
```
```js
// components/ChildCom2.jsx
import { useState } from 'react';
function ChildCom2() {
const [points, setPoints] = useState({
x: 0,
y: 0
})
function handleMouseMove(e) {
setPoints({
x: e.clientX,
y: e.clientY
})
}
return (
<div style={{
width: '400px',
height: '400px',
backgroundColor: 'grey',
position: 'relative',
overflow: 'hidden'
}} onMouseMove={handleMouseMove}>
<h1>移动鼠标!</h1>
{/* 这里减去 460 是因为要减去左边 div 的宽度 + 两个大 div 之间 50 的间距 */}
<div style={{
width: '15px',
height: '15px',
borderRadius: "50%",
backgroundColor: 'white',
position: 'absolute',
left: points.x - 5 - 460,
top: points.y - 5 - 10,
}}></div>
</div>
);
}
export default ChildCom2;
```
在上面的代码中,*App* 根组件下渲染了两个子组件,这两个子组件一个是显示鼠标的位置,另外一个是根据鼠标位置显示一个追随鼠标移动的小球。
仔细观察代码,你会发现这两个子组件内部的逻辑基本上是一模一样的,只是最终渲染的内容不一样,此时我们就可以使用 *Render Props* 对横切关注点进行一个抽离。
方式也很简单,就是在一个组件中使用一个值为函数的 *prop*,函数的返回值为要渲染的视图。
```js
// /components/MouseMove.jsx
import { useState } from 'react';
function MouseMove(props) {
const [points, setPoints] = useState({
x: 0,
y: 0
})
function handleMouseMove(e) {
setPoints({
x: e.clientX,
y: e.clientY
})
}
return (
props.render ? props.render({ points, handleMouseMove }) : null
);
}
export default MouseMove;
```
在上面的代码中,我们新创建了一个 *MouseMove* 组件,该组件就封装了之前 *ChildCom1**ChildCom2* 组件的公共逻辑。该组件的 *props* 接收一个名为 *render* 的参数,只不过该参数对应的值为一个函数,我们调用时将对应的状态和处理函数传递过去,该函数的调用结果为返回一段视图。
```js
import ChildCom1 from "./components/ChildCom1";
import ChildCom2 from "./components/ChildCom2";
import MouseMove from "./components/MouseMove";
function App() {
return (
<div style={{
display: 'flex',
justifyContent: 'space-between',
width: "850px"
}}>
<MouseMove render={props => <ChildCom1 {...props} />} />
<MouseMove render={props => <ChildCom2 {...props} />} />
</div>
);
}
export default App;
```
接下来在 *App* 根组件中,我们使用 *MouseMove* 组件,该组件上有一个 *render* 属性,对应的值就是函数,函数返回要渲染的组件。
```js
function ChildCom1(props) {
return (
<div style={{
width: '400px',
height: '400px',
backgroundColor: 'red'
}} onMouseMove={props.handleMouseMove}>
<h1>移动鼠标!</h1>
<p>当前的鼠标位置是 ({props.points.x}, {props.points.y})</p>
</div>
);
}
export default ChildCom1;
```
```js
function ChildCom2(props) {
return (
<div style={{
width: '400px',
height: '400px',
backgroundColor: 'grey',
position: 'relative',
overflow: 'hidden'
}} onMouseMove={props.handleMouseMove}>
<h1>移动鼠标!</h1>
{/* 这里减去 460 是因为要减去左边 div 的宽度 + 两个大 div 之间 50 的间距 */}
<div style={{
width: '15px',
height: '15px',
borderRadius: "50%",
backgroundColor: 'white',
position: 'absolute',
left: props.points.x - 5 - 460,
top: props.points.y - 5 - 10,
}}></div>
</div>
);
}
export default ChildCom2;
```
最后就是子组件 *ChildCom1**ChildCom2* 的改写,可以看到这两个子组件就只需要书写要渲染的视图了。公共的逻辑已经被 *MouseMove* 抽取出去了。
另外需要说明的是,虽然这个技巧的名字叫做 *Render Props*,但不是说必须要提供一个名为 *render* 的属性,事实上,**<u>封装公共逻辑的组件(例如上面的 *MouseMove*)只要能够得到要渲染的视图即可</u>**,所以传递的方式可以有多种。
例如:
```js
import ChildCom1 from "./components/ChildCom1";
import ChildCom2 from "./components/ChildCom2";
import MouseMove from "./components/MouseMove";
function App() {
return (
<div style={{
display: 'flex',
justifyContent: 'space-between',
width: "850px"
}}>
<MouseMove>
{(props) => <ChildCom1 {...props} />}
</MouseMove>
<MouseMove>
{props => <ChildCom2 {...props} />}
</MouseMove>
</div>
);
}
export default App;
```
上面使用 *MouseMove* 组件时,并没有传递什么 *render* 属性,而是通过 *props.children* 的形式将要渲染的视图传递到了组件内部。
*MouseMove* 组件内部,就不再是执行 *render* 方法了,而是应该执行 *props.children*,如下:
```js
props.children ? props.children({ points, handleMouseMove }) : null
```
## 何时使用 *Render Props*
知道了 *Render Props* 的使用方法后,接下来我们来看一下何时使用 *Render Props*
同样是作为抽离横切关注点,前面所讲的 *HOC* 也能做到相同的效果。例如我们可以将上面的示例修改为 *HOC* 的方式。
```js
// HOC/withMouseMove.js
import { useState } from "react";
function withMouseMove(Com) {
// 返回一个新组件
return function NewCom() {
const [points, setPoints] = useState({
x: 0,
y: 0,
});
function handleMouseMove(e) {
setPoints({
x: e.clientX,
y: e.clientY,
});
}
const mouseHandle = { points, handleMouseMove };
return <Com {...mouseHandle} />;
};
}
export default withMouseMove;
```
```js
// App.jsx
import ChildCom1 from "./components/ChildCom1";
import ChildCom2 from "./components/ChildCom2";
import withMouseMove from "./HOC/withMouseMove"
const NewChildCom1 = withMouseMove(ChildCom1);
const NewChildCom2 = withMouseMove(ChildCom2);
function App() {
return (
<div style={{
display: 'flex',
justifyContent: 'space-between',
width: "850px"
}}>
<NewChildCom1 />
<NewChildCom2 />
</div>
);
}
export default App;
```
那么自然而然疑问就来了,什么时候使用 *Render Props* ?什么时候使用 *HOC*
一般来讲,**<u>*Render Props* 应用于组件之间功能逻辑完全相同,仅仅是渲染的视图不同</u>**。这个时候我们可以通过 *Render Props* 来指定要渲染的视图是什么。
*HOC* 一般是抽离部分公共逻辑,也就是说组件之间有一部分逻辑相同,但是各自也有自己独有的逻辑,那么这个时候使用 *HOC* 比较合适,可以在原有的组件的基础上做一个增强处理。
---
-*EOF*-