列表(List
), 键(Key
)
回顾一下在javascript
中如何转换列表:在数组中使用map()
函数对numbers
数组中的每个元素依次执操作
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled) // 2, 4, 6, 8, 10
React 基本借鉴了以上写法,只不过将数组替换成了元素列表
多组件渲染
可以创建元素集合,并用一对大括号 {}
在 JSX 中直接将其引用即可
下面,我们用 JavaScript 的 map()
函数将 numbers
数组循环处理。对于每一项,我们返回一个 元素。最终,我们将结果元素数组分配给
listItems
:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
{number}
)
再把整个 listItems
数组包含到一个 元素,并渲染到DOM
ReactDOM.render(
{listItems}
,
document.getElementById('root')
)
基本列表组件
通常情况下,我们会在一个组件中渲染列表而不是直接放到root
上。重构一下上例
function NumberList(props) {
const numbers = props.number;
const listItems = numbers.map((number) =>
{number}
)
return (
{listItems}
)
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
,
document.getElementById('root')
)
当运行上述代码的时候,将会受到一个警告:a key should be provided for list items
,要求应该为元素提供一个键(注:min版本react无提示)。要去掉这个警告也简单,只需要在listItem
的每个li
中增加key
属性即可,增加后的每个如下
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
{number}
);
return (
{listItems}
);
}
当创建元素列表时,“key” 是一个你需要包含的特殊字符串属性,那为什么要包含呢?
键(Keys)
键Keys
帮助React标识那个项被修改、添加或者移除了。数组中的每一个元素都应该有一个唯一不变的键来标识。
挑选key
最好的办法是使用一个在它的同辈元素中不重复的表示字符串。多数情况下可以使用数据中的IDs
来作为Keys
。但是还是会遇到没有id
字段的数据,这种情况你可以使用数据项的索引值
cosnt todoItems = todos.map((todo, index) =>
// 数据项没有IDs时使用该办法
{todo.text}
)
如果列表项可能被重新排序,这种用法存在一定的性能问题,React会产生时间复杂度为O(n^3)的算法执行。因此优先使用数据项本身的字段内容来设置键
使用 Keys 提取组件
Keys
只有在数组的上下文中存在意义。例如,如果你提取了一个ListItem
组件,应该把key
放置在数组处理的
元素中,而不能放在ListItem
组件自身的根元素上。
以下的用法就是错误的
function ListItem(props) {
const value = props.value;
return (
// 错误!不需要再这里指定 key
{value}
)
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 错误!key 应该在这里指定:
);
return (
{listItems}
);
}
应该写成如下
function ListItem(props) {
// 正确!这里不需要指定 key :
return {props.value} ;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 正确!key 应该在这里被指定
);
return (
{listItems}
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
,
document.getElementById('root')
);
keys 在同辈元素中必须唯一
在数组中使用的 keys 必须在它们的同辈之间唯一。然而它们并不需要全局唯一。我们可以在操作两个不同数组的时候使用相同的 keys :
function Blog(props) {
const sidebar = (
{props.posts.map((post) =>
-
{post.title}
)}
);
const content = props.posts.map((post) =>
{post.title}
{post.content}
);
return (
{sidebar}
{content}
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
,
document.getElementById('root')
);
【注意】键是一个内部映射,他不会作为props
传递给组件内部,如果你需要在组件中使用到这个值,可以自定义一个属性名将该值传入到props
中,如下例中我们定义了一个id
属性传入给props
.
const content = posts.map((post) =>
);
在这个例子中,我们能读取props.id
,但是读取不了props.key
直接在JSX中使用map()
在上例中我们先声明了一个listItem
然后在jsx中引用,然而我们也能在JSX
中直接引用,称之为 内联map()
function NumberList(props) {
const numbers = props.numbers;
return (
{ numbers.map((number) =>
)}
);
}
至于选用哪种风格编写,只要遵循代码清晰易读原则即可。
refs
refs:标识组件内元素(相当于document.getElementById,获取DOM元素)
DOM操作官方说明
要求:自定义组件,并完成以下功能
// 1.定义组件
class MyComponent extends React.Components {
// 1.1 强制绑定(给自定义方法的this强制绑定为组件对象)
constructor(props) {
super(props)
this.showInput = this.showInput.bind(this)
this.showOutBlur = this.showOutBlur.bind(this)
}
// 1.2 自定义方法
showInput() {
const input = this.refs.input
alert(input.value)
}
showOutBlur(event) {
alert(event.target.value)
}
// 1.3 渲染
render() {
return (
)
}
}
获取表单数据
1、非受控组件
ref={input => this.nameInput=input},前一个input是形参,可以自定义,和最后的input一致;nameInput也是自定义
2、受控组件
在组件中:
在方法中:
handleChange(event) {
const psw = event.target.value
this.setState({ psw: psw })
}
完整代码:
/
HTML 表单元素与 React 中的其他 DOM 元素有所不同,因为表单元素自然地保留了一些内部状态。例如,这个纯 HTML 表单接受一个单独的 name:
该表单和 HTML 表单的默认行为一致,当用户提交此表单时浏览器会打开一个新页面。如果你希望 React 中保持这个行为,也可以工作。但是多数情况下,用一个处理表单提交并访问用户输入到表单中的数据的 JavaScript 函数也很方便。实现这一点的标准方法是使用一种称为“受控组件(controlled components
)”的技术。
受控组件(Controlled Components)
在 HTML 中,表单元素如
,
和
表单元素通常保持自己的状态,并根据用户输入进行更新。而在 React 中,可变状态一般保存在组件的 state(状态) 属性中,并且只能通过 setState()
更新。
通过使 React 的 state 成为 “单一数据源原则” 来结合这两个形式。然后渲染表单的 React 组件也可以控制用户输入之后的行为。这种形式,其值由 React 控制的输入表单元素称为“受控组件”。
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
}
handleChange(event) {
this.setState({value:event.target.value})
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
)
}
}
设置表单元素的value
属性之后,其显示值将由this.state.value
决定,以满足react
状态的同一个数据理念。每次键盘敲击之后会执行handleChange
方法以便更新React状态,显示只也将随着用户的输入而改变。
由于value
属性设置在我们的表单元素上,显示的值总是this.state.value
,以满足state
状态的同意数据理念。由于 handleChange
在每次敲击键盘时运行,以更新React state
,显示的值将更新为用户的输入
对于受控组件来说,每一次state
的变化都会伴有相关联的处理函数。这使得可以直接修改或验证用户的输入。比如,我们希望强制name
的输入都是大写字母,可以如下实现
handleChange(event) {
this.setState({value: event.target.value.toUpperCase()});
}
textarea标签
在 HTML 中,
元素通过它的子节点定义了它的文本值:
在 React 中,
的赋值使用 value
属性替代。这样一来,表单中
的书写方式接近于单行文本输入框 :
class EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 'Please write an esay about your favorite DOM element.'
}
}
// ...
render() {
return (
)
}
}
注意,this.state.value
在构造函数中初始化,所以这些文本一开始就出现在文本域中。
select 标签
在 HTML 中,
创建了一个下拉列表用法如下
html利用selected
默认选中,但在React中,不使用selected
,而是给
标签中增加一个value
属性,这使得受控组件使用更加方便,因为你只需要更新一处变量即可。
class FlavorForm extends React.Component {
// ...
render() {
return (
);
}
}
总的来说,这使
,
和
都以类似的方式工作 —— 它们都接受一个 value
属性可以用来实现一个受控组件。
多选select
使用多选select
时,需要给select
标签增加value
属性,同时给value
属性赋值一个数组
# 利用e.target
合并多个输入元素的处理事件
当您需要处理多个受控的 input 元素时,您可以为每个元素添加一个 name 属性,并且让处理函数根据 event.target.name 的值来选择要做什么。
class Reservation extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({ [name]: value });
}
render() {
return (
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
注意这里使用ES6计算的属性名称语法来更新与给定输入名称相对应的 state
(状态) 键的办法
this.setState({ [name]: value })
var partialState = {};
partialState[name] = value;
this.setState(partialState);
由于 setState()
自动将部分状态合并到当前状态,所以我们只需要调用更改的部分即可。
受控 Input 组件的 null 值
在 受控组件上指定值 prop 可防止用户更改输入,除非您希望如此。 如果你已经指定了一个 value
,但是输入仍然是可编辑的,你可能会意外地把 value
设置为undefined
或 null
。
以下代码演示了这一点。 (输入首先被锁定,但在短暂的延迟后可以编辑。)
ReactDOM.render(, mountNode);
setTimeout(function() {
ReactDOM.render(, mountNode);
}, 1000);
九、状态提升 (Lifting State Up)
通常情况下,同一个数据的变化需要几个不同的组件来反映。我们建议提升共享的状态到它们最近的祖先组件中。为了更好的理解,从一个案例来分析
温度计算器
在本案例中,我们采用自下而上的方式来创建一个温度计算器,用来计算在一个给定温度下水是否会沸腾(水温是否高于100C)
(1)创建一个 BoilingVerdict
组件,用来判水是否会沸腾并打印
function BiolingVerdict(props) {
if (props.celsius >= 100) {
return The water would boil.
}
return The water would not boil.
}
(2)有了判断温度的组件之后,我们需要一个Calculator
组件,他需要包含一个
提供我们输入文图,并在this.state.temperature
中保存值。另外,以上BoilingVerdict
组件将会获取到该输入值并进行判断
class Caculator extends React.Component {
constructor(props) {
super(props);
this.state = { temperature: '' };
}
handleChange(e) {
this.setState({ temperature: e.target.value });
}
render() {
const temperature = this.state.temperature;
return (
)
}
}
(3)现在我们实现了基础的父子组件通信功能,假设我们有这样的需求:除了一个设施文图的输入之外,还需要有一个华氏温度输入,并且要求两者保持自动同步
我们从Calculator
中提取出TemperatureInput
,然后增加新的scale
属性,值可能是c
或f
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
}
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.state = { temperature: 200 }
}
handleChange(e) {
this.setState({ temperature: e.target.value });
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
)
}
}
抽出TemperatureInput
之后,Calculator
组件如下
class Calculator extends React.Component {
render() {
return (
);
}
}
现在有了两个输入框,但这两个组件是独立存在的,不会互相影响,也就是说,输入其中一个温度另一个并不会改变,与需求不符
我们不能再Calculator
中显示BoilingVerdict
, Calcultor
不知道当前的温度,因为它是在TemperatureInput
中隐藏的, 因此我们需要编写转换函数
(4)编写转换函数
我们先来实现两个函数在摄氏度和华氏度之间转换
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
functin toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
接下来,编写函数用来接收一个字符串temperature
和一个转化器函数作为参数,并返回一个字符串,这个函数在两个输入之间进行相互转换。为了健壮性,对于无效的temperature
值,返回一个空字符串,输出保留三位小数
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
其中,convert
取值为 toCelsius
或toFahrenheit
状态提升
目前,两个 TempetureInput 组件都将其值保留在本地状态中,但是我们希望这两个输入时相互同步的。但我们更新摄氏温度输入时,华氏温度输入应该反映并自动更新,反之亦然。
在React 中,共享state(状态)是通过将其移动到需要的的组件的最接近的共同祖先组件来实现的,这被称之为状态提升(Lifting State Up)。我们将从TemperatureInput
中移除相关状态本地状态,并将其移动到Calculator
中
如果Calculator
拥有共享状态,那么他将成为两个输入当前温度的单一数据源
。他可以指示他们具有彼此一致的值 。由于两个TemperatureInput
的组件的props
来自于同一个父级Calculator
组件,连个输入将始终保持同步
让我们来一步步实现这个过程
(1)将值挪出组件,用props
传入
render() {
// const temperature = this.state.temperature;
const temperature = this.props.temperature;
}
我们知道,props
是只读的,因此我们不能根据子组件调用this.setState()
来改变它。这个问题,在React中通常使用 受控的方式来解决。就像DOM
一样接收一个value
和onChange
prop
, 所以可以定制Temperature
接受来自其腹肌 Calculator
的 temperature
和 onTemperatureChange
:
thandleChange(e) {
// 之前是:this.setState({ temperature: e.target.value });
this.props.onTemperatureChange(e.target.value);
}
请注意,之定义组件中的 templature
或 onTemperatureChange
prop
(属性)名称没有特殊的含义。我们可以命名为任何其他名字,就像命名他们为value
和onChange
。是一个和常见的惯例
onTemperatureChange prop
和 temperature prop
一起由父级的Calculator
组件提供,他将通过修改自己的本地state
来处理数据变更,从而通过新值重新渲染两个输入。现在我们的代码如下
import React from 'react';
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
}
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
console.log(this.props); // {scale: "c", temperature: "", onTemperatureChange: ƒ}
}
handleChange(e) {
// 触发父组件onTemperatureChange
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
)
}
}
我们将当前输入的 temperature
和scale
存储在本地的state
中,这是我们冲输入“提升”的state(状态)
,他将作为连个输入的“单一数据源”。为了渲染这两个输入,我们需要知道的所有数据的最小表示,如摄氏度输入37,这时Calculator
组件状态将是:
{
temperature: '37',
scale: 'c'
}
我们确实可以存储两个输入框(摄氏度和华氏度)的值,但事实证明是不必要的。我们只要存储最近更改的输入框的值,以及他们所表示的度量衡(scale
)就足够了。然后推断出另一个值。这也是我们实现两个输入框保持同步的途径
import React from 'react';
import TemperatureInput from './TemperatureInput';
import BiolingVerdict from './BoilingVerdict';
class Calculator extends React.Component {
constructor() {
super();
this.state = {
temperature: '',
scale: 'c'
}
}
/**
* 摄氏度和华氏度之间转换
* temperature: input值
* convert: 单位
*/
tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
// 两个函数在摄氏度和华氏度 start
toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
// 两个函数在摄氏度和华氏度 end
// 获取input值并改变状态 start
handleCelsiusChange(temperature) {
this.setState({ scale: 'c', temperature });
}
handleFahrenheitChange(temperature) {
this.setState({ scale: 'f', temperature })
}
// 获取input值并改变状态 end
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? this.tryConvert(temperature, this.toCelsius) : temperature;
const fahrenheit = scale === 'c' ? this.tryConvert(temperature, this.toFahrenheit) : temperature;
return (
{/* onTemperatureChange 传给子组件 */}
)
}
}
现在,无论你编辑哪个输入框,Calculator
中的 this.state.temperature
和 this.state.scale
都会更新。其中一个输入框获取值,所以任何用户输入都被保留,并且另一个输入总是基于它重新计算值。
让我们回顾一下编辑输入时会发生什么:
- React 调用在 DOM
上的 onChange
指定的函数。在我们的例子中,这是 TemperatureInput
组件中的 handleChange
方法。
TemperatureInput
组件中的handleChange
方法使用 新的期望值 调用 this.props.onTemperatureChange()
。TemperatureInput
组件中的props(属性)
,包括 onTemperatureChange
,由其父组件 Calculator
提供。
- 当它预先呈现时,
Calculator
指定了摄氏 TemperatureInput
的 onTemperatureChange
是 Calculator
的 handleCelsiusChange
方法,并且华氏 TemperatureInput
的 onTemperatureChange
是 Calculator
的 handleFahrenheitChange
方法。因此,会根据我们编辑的输入框,分别调用这两个 Calculator
方法。
- 在这些方法中,
Calculator
组件要求 React 通过使用 新的输入值 和 刚刚编辑的输入框的当前度量衡 来调用 this.setState()
来重新渲染自身
- React 调用
Calculator
组件的 render
方法来了解 UI 外观应该是什么样子。基于当前温度和激活的度量衡来重新计算两个输入框的值。这里进行温度转换
- React 使用
Calculator
指定的新 props
(属性) 调用各个 TemperatureInput
组件的 render
方法。 它了解 UI 外观应该是什么样子
- React DOM 更新 DOM 以匹配期望的输入值。我们刚刚编辑的输入框接收当前值,另一个输入框更新为转换后的温度。
^状态提升经验总结
在一个 React 应用中,对于任何可变的数据都应该遵循“单一数据源”原则,通常情况下,state
首先被添加到需要它进行渲染的组件,然后如果其他的组件也需要它,你可以提升状态到他们最近的祖先组件。你应该依赖从上到下的数据流向,而不是试图在不同的组件中同步状态。
提升状态相对于双向绑定方法需要写更多的"模板"代码,但是有个好处,他可以更方便的找到和隔离bugs
。由于热河state
(状态)都"存活"若干个组件中,而且可以分别对其独立修改,所以发生错误的可能性大大减少。另外,你可以实现任何定制的逻辑来拒绝或者转换用户输入。
如果某个东西可以从props(属性)
或者state(状态)
得到,那么他可能不应该在state
中。例如我们只保存最后编辑的temperature
和scale
,而不是保存celsiusValue
和fahrenheitValue
。另一个输入框的值总是在render()
中计算得到。这是我们对其进行清除和四舍五入到其他字段的同事不会丢失其精度
当你看到UI中的错误,你可以使用React开发者工具来检查props
,并向上遍历树,知道找到负责更新状态的组件,这是你可以跟踪到bug
的源头:Monitoring State in React DevTools
十、组合 VS 继承
组合 Composition
vs 继承Inheritance
React 拥有一个强大的组合模型,建议使用组合而不是继承以实现代码的重用
接下来同样从案例触发来考虑几个问题,新手一般会用继承,然后这里推荐使用组合
组合
一些组件在设计前无法或者自己要使用什么子组件,尤其是在 Sidebar
和 Dialog
等通用的 “容器” 中比较常见
这种组件建议使用特殊的children prop
来直接传递子元素到他们的输出中:
function FancyBorder(props) {
return (
// children 表示来自父组件中的子元素
{props.children}
)
}
这允许其他组件通过嵌套JSX传递任意子组件给他们,比如在父组件中有h1
和p
子元素
function WelcomeDialog() {
return (
Welcome
Thank you for your visitiong
)
}
在
JSX 标签中的任何内容被传递到FancyBorder
组件中,作为一个 children prop(属性)
。由于 FancyBorder
渲染{props.children}
到一个 中,传递的元素会呈现在最终的输出中。
这是一种简单的用法,这种案例并不常见,有时候我们需要在一个组件中有多个“占位符”,这种情况下,你可以使用自定义的prop
属性,而不是children
:
function Contacts() {
return ;
}
function Chat() {
return ;
}
function SplitPane(props) {
return (
{props.left}
{props.right}
)
}
function App() {
return (
}
right={
}
/>
)
}
如
和
等 React
元素本质上也是对象,所以可以将其像其他数据一样作为 props(属性) 传递使用。
特例
有时候,我们考虑组件作为其它组件的“特殊情况”。例如,我们可能说一个 WelcomeDialog
是 Dialog
的一个特殊用例。
在React中,也可以使用组合来实现,一个偏“特殊”的组件渲染出一个偏“通用”的组件,通过 props(属性)
配置它:
function FancyBorder(props) {
return (
{props.children}
);
}
function Dialog(props) {
return (
{props.title}
{props.message}
)
}
function WelcomeDialog() {
return (
这对于类定义的组件组合也同样适用
function FancyBorder(props) {
return (
{props.children}
);
}
function Dialog(props) {
return (
{props.title}
{props.message}
{props.children}
);
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}
render() {
return (
);
}
handleChange(e) {
this.setState({login: e.target.value});
}
handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}
如何看待继承?
在 Facebook ,在千万的组件中使用 React,我们还没有发现任何用例,值得我们建议你用继承层次结构来创建组件。
使用 props(属性)
和 组合已经足够灵活来明确、安全的定制一个组件的外观和行为。切记,组件可以接受任意的 props(属性)
,包括原始值、React 元素,或者函数
如果要在组件之间重用非 UI功能,我们建议将其提取到单独的 JavaScript 模块中。组件可以导入它并使用该函数,对象或类,而不扩展它。