从零开始学习React:了解组件的三大属性

从零开始学习React:了解当前React常用技术,编写第一个React组件
从零开始学习React:了解组件的三大属性

上一节我们简单介绍了React的相关技术栈,以及如何在一个 html 文件中使用 React,创建一个React 组件并渲染到Html 中。

本节我们来介绍在 React 中 一个组件比较重要的三大属性

组件三大属性

就如我们上一节中介绍的一个最简单的函数式组件是这样的:

1
const H2 = ({ title }) => <h2>{title}</h2>;

这种写法是比较简洁的 ES6 写法,他等同于下面的这种写法:

1
2
3
function H2(props) {
return <h2>{props.title}</h2>;
}

该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。

1. Props

React 除了可以使用原生的 html 标签,也可以使用用户自定义的组件:

1
const title = <H2 title="这是一个自定义组件" />

当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。

当我们有多个值需要传递给组件时,在使用组件时,就需要多写几个属性(attributes),例如:

1
const title = <H2 title="这是一个自定义组件" subTitle="xxx" author="xxxx" />

这样传递似乎有点麻烦,我们还可以使用一个语法糖来简单的传递多个键值对的 attributes,:例如:

1
2
const props = {title="这是一个自定义组件", subTitle="xxx", author="xxxx"}
const title = <H2 {...props} />

注意这并不等同于拷贝对象的 const a = {...b} ,而是 React 的一个语法糖。

props 是只读属性,所以一个函数式组件如果只通过 props 进行渲染,就是一个“纯函数组件”,React 非常灵活,但它也有一个严格的规则:所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

当然这种情况很苛刻,在业务中我们很少会使用纯函数组件,因为 UI 往往是动态的,而实现动态就需要使用 State。

2. State

state 也就是状态,在业务中,大多数情况我们的组件都是有状态的动态组件。例如一个计数器,可以通过 + 增加计数,通过 - 减少计数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qPcNtreU-1653208014869)(/Users/houwenjun/Library/Application Support/typora-user-images/image-20220522120159451.png)]

这是一个非常简单的示例,在 React 中,我们可以这样实现:

1
2
3
4
5
6
7
8
9
10
const Counter = () => {
const [count, setCount] = React.useState(0)
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
)
}

useState 是一个hook函数可以让我们非常方便的使用组件的 State,该函数接受一个值作为初始状态,返回值是一个元组,第一个值为 state 状态,第二个值是一个函数,用于更新状态,这有与 ComposeUI 中的 remember 函数是一样的,虽然是函数式组件,但是通过 hook,让函数式组件拥有了状态,并可以保持状态。

在 JS 中如果函数的返回值是 元组 类型,我们是可以随意的对其中的成员进行命名的,但是我们一般推荐在使用 useState 这个hook时,使用 const [state,setState] = useState() 这种命名格式。

不要直接修改 State

行文至此我们需要介绍一个重要的 React 思想就是:不可变,在 React 哲学中我们不应该直接的去改变一个对象的属性,当需要时,我们应该创建一个新的不同的对象来作为替代。这是 React 中一个重要的思想,很多 API 的设计都是基于此原则。

state 也是一样,当我们需要修改一个组件的对象时,我们不可以直接操作 state 对象本身,而是需要通过由 useState hook 暴露的的set 函数,传递一个新的值作为新的状态。当 set 函数传入的新状态与原有状态进行对比后,发现需要更新渲染组件,就会触发 React 对组件的渲染。

1
2
3
4
//错误的写法
state.name = 'xxxx';
//正确的写法:
setState({...state,name:'xxxx'})

State 的更新可能是异步的

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

因为 this.propsthis.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

set 函数有两种方式可以更新状态:

  1. 直接传入新值 setCount(123)
  2. 传入一个函数 setCount(oldState => oldState +1 )

状态提升

上面的介绍我们可以了解到,每个函数组件都可以通过 useState 来获得状态,但有时我们需要在多个组件内共享某一个状态。

还有我们之前写的计数器作为例子,我们有一个新的组件<IsOdd>,这个组件用于显示计数器中的数值是否是奇数。我们需要将计数器中的数值状态共享给判断组件,这就要用到状态提升

状态提升并不是什么复杂的概念,简而言之就是将几个组件需要共享的状态,从某一个组件,提升到这几个需要共享状态的父组件中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const App = () => {
//状态从某个组件提升到了父组件,子组件通过props来读取状态、修改状态
const [count, setCount] = React.useState(0)
return (
<div>
<h1>状态提升</h1>
<Counter count={count} setCount={setCount} />
<IsOdd count={count} />
</div>
)
}
//计数器组件不在维护自己状态,而只通过props接受参数,作为自己组件的成员
const Counter = ({ count, setCount }) => {
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
)
}

const IsOdd = ({ count }) => {
return (
<div>
<h3>{count % 2 === 0 ? '偶数' : '奇数'}</h3>
</div>
)
}
// 渲染App
ReactDOM.render(<App />, document.getElementById("root"));

单向数据流

不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。

组件可以选择把它的 state 作为 props 向下传递到它的子组件中,子组件会在其 props 中接收参数 count,但是组件本身无法知道它是来自于 App 的 state,还是手动输入的值。

这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。

如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。

3. Refs

相较于前两个概念,refs 并没有那么重要,在需要使用到 refs 的场景一般也都有替换的方案,只有少数场景是必须使用 refs 的。

在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。

下面是几个适合使用 refs 的情况:

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。

避免使用 refs 来做任何可以通过声明式实现来完成的事情

你可能首先会想到使用 refs 在你的 app 中“让事情发生”。如果是这种情况,请花一点时间,认真再考虑一下 state 属性应该被安排在哪个组件层中。通常你会想明白,让更高的组件层级拥有这个 state,是更恰当的。查看 状态提升 以获取更多有关示例。

在组件内使用 Refs

1
2
3
4
5
6
7
8
9
10
11
12
const Input = () => {
const inputRef = React.useRef()
const handleClick = () => {
alert(inputRef.current.value)
}
return (
<div>
<input ref={inputRef} type="text" placeholder="请在此输入:" />
<button onClick={handleClick} >alert</button>
</div>
)
}

在函数式组件中我们使用 useRef 创建一个 ref 容器,通过将 ref 指定给某个 DOM 元素,来完成ref的使用。就像我们在上面说的,这种情况其实完全可以通过“受控组件”来实现,ref 并非唯一解决方案。

访问 Refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

1
const node = this.myRef.current;

ref 的值根据节点的类型而有所不同:

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能直接在函数组件上使用 ref 属性,因为他们没有实例。

Refs 转发

上面我们说了:你不能直接在函数组件上使用 ref 属性,也就是说,我们无法简单的将 ref 作为 props 来传递给函数式组件,如果你不信邪,非要将ref 作为 props 传递,你会收到如下错误:

1
2
3
4
5
6
7
8
Warning: Input: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://fb.me/react-special-props)

react-dom.development.js:500 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `App`.
in Input (created by App)
in div (created by App)
in App

但是某些特殊情况下,我们需要在函数组件中使用 ref,这就需要用到 Refs 转发。通过 React.forwardRef 函数,我们可以轻松的将父组件创建的的 ref 容器,转发给子组件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const FormItem = ({ label, name, children }) => {
const inputRef = React.useRef()
const handleClick = () => {
console.log({
[name]: inputRef.current.value
})
}
return (
<div style={{ display: 'flex' }}>
<label style={{ marginRight: '10px' }}>{label}:</label>
{React.cloneElement(children, {
ref: inputRef
})}
<button onClick={handleClick} >alert</button>
</div>
)
}
//使用 React.forwardRef 函数转发Refs
const Input = React.forwardRef((props, ref) => {
return (
<div>
<input ref={ref} type="text" placeholder="请在此输入:" />
</div>
)
})

const App = () => {
const [count, setCount] = React.useState(0)
const ref = React.useRef()
return (
<div>
<h1>Refs转发</h1>
<FormItem label="用户名" name="username">
<Input />
</FormItem>
</div>
)
}

ReactDOM.render(<App />, document.getElementById("root"));

通过子组件的转发,父组件轻松拿到了子组件的 DOM 元素,继而可以实现了父组件对子组件的控制,例如 focus、blur 等操作。