目录
一、?列表 和 键
列表(List), 键(Key)
多组件渲染
基本列表组件
键(Keys)
使用 Keys 提取组件
keys 在同辈元素中必须唯一
直接在JSX中使用map()
二、 表单
受控组件(Controlled Components)
textarea标签
select 标签
多选select
利用e.target合并多个输入元素的处理事件
受控 Input 组件的 null 值
三、 状态提升 (Lifting State Up)
温度计算器
状态提升
^状态提升经验总结
一、?列表 和 键
列表(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 数组循环处理。对于每一项,我们返回一个 <li> 元素。最终,我们将结果元素数组分配给 listItems :
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
)
? 再把整个 listItems 数组包含到一个 <ul> 元素,并渲染到DOM
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
)
基本列表组件
? 通常情况下,我们会在一个组件中渲染列表而不是直接放到root 上。重构一下上例
function NumberList(props) {
const numbers = props.number;
const listItems = numbers.map((number) =>
<li>{number}</li>
)
return (
<ul>{listItems}</ul>
)
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={number} />,
document.getElementById('root')
)
? 当运行上述代码的时候,将会受到一个警告:a key should be provided for list items ,要求应该为元素提供一个键(注:min版本react无提示)。要去掉这个警告也简单,只需要在listItem 的每个li 中增加key 属性即可,增加后的每个<li> 如下
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
? 当创建元素列表时,“key” 是一个你需要包含的特殊字符串属性,那为什么要包含呢?
键(Keys)
? 键Keys 帮助React标识那个项被修改、添加或者移除了。数组中的每一个元素都应该有一个唯一不变的键来标识。
?挑选key 最好的办法是使用一个在它的同辈元素中不重复的表示字符串。多数情况下可以使用数据中的IDs 来作为Keys 。但是还是会遇到没有id 字段的数据,这种情况你可以使用数据项的索引值
cosnt todoItems = todos.map((todo, index) =>
// 数据项没有IDs时使用该办法
<li key={index}>
{todo.text}
</li>
)
? 如果列表项可能被重新排序,这种用法存在一定的性能问题,React会产生时间复杂度为O(n^3)的算法执行。因此优先使用数据项本身的字段内容来设置键
使用 Keys 提取组件
? Keys 只有在数组的上下文中存在意义。例如,如果你提取了一个ListItem 组件,应该把key 放置在数组处理的<ListItem /> 元素中,而不能放在ListItem 组件自身的<li> 根元素上。
?以下的用法就是错误的
function ListItem(props) {
const value = props.value;
return (
// 错误!不需要再这里指定 key
<li key={value.toString()}> {value}</li>
)
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 错误!key 应该在这里指定:
<ListItem value={number} />
);
return (
<ul> {listItems} </ul>
);
}
? 应该写成如下
function ListItem(props) {
// 正确!这里不需要指定 key :
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 正确!key 应该在这里被指定
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
keys 在同辈元素中必须唯一
? 在数组中使用的 keys 必须在它们的同辈之间唯一。然而它们并不需要全局唯一。我们可以在操作两个不同数组的时候使用相同的 keys :
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
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(
<Blog posts={posts} />,
document.getElementById('root')
);
? 【注意】键是一个内部映射,他不会作为props 传递给组件内部,如果你需要在组件中使用到这个值,可以自定义一个属性名将该值传入到props 中,如下例中我们定义了一个id 属性传入给props .
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
? 在这个例子中,我们能读取props.id ,但是读取不了props.key
直接在JSX中使用map()
? 在上例中我们先声明了一个listItem 然后在jsx中引用,然而我们也能在JSX 中直接引用,称之为 内联map()
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{ numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
? 至于选用哪种风格编写,只要遵循代码清晰易读原则即可 ?
二、 表单
?HTML 表单元素与 React 中的其他 DOM 元素有所不同,因为表单元素自然地保留了一些内部状态。例如,这个纯 HTML 表单接受一个单独的 name:
<form>
<label>
Name:
<input type="text" name="name" />
</label>
<input type="submit" value="Submit" />
</form>
? 该表单和 HTML 表单的默认行为一致,当用户提交此表单时浏览器会打开一个新页面。如果你希望 React 中保持这个行为,也可以工作。但是多数情况下,用一个处理表单提交并访问用户输入到表单中的数据的 JavaScript 函数也很方便。实现这一点的标准方法是使用一种称为“受控组件(controlled components )”的技术。 ?
受控组件(Controlled Components)
? 在 HTML 中,表单元素如 <input> ,<textarea> 和 <select> 表单元素通常保持自己的状态,并根据用户输入进行更新。而在 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 (
<form onSubmit={this.handleSubmit.bind(this)}>
<label>
Name:
<input type="text" value={this.state.value}
onChange={this.handleChange.bind(this} />
</label>
<input type="submit" value="Submit" />
</form>
)
}
}
? 设置表单元素的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 中,<textarea> 元素通过它的子节点定义了它的文本值:
<textarea>
Hello there, this is some text in a text area
</textarea>
? 在 React 中,<textarea> 的赋值使用 value 属性替代。这样一来,表单中 <textarea> 的书写方式接近于单行文本输入框 :
class EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 'Please write an esay about your favorite DOM element.'
}
}
// ...
render() {
return (
<form onSubmit={this.handle}>
<label>
Name:
<textarea value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
)
}
}
? 注意,this.state.value 在构造函数中初始化,所以这些文本一开始就出现在文本域中。
select 标签
? 在 HTML 中,<select> 创建了一个下拉列表用法如下
<select>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option selected value="coconut">Coconut</option>
<option value="mango">Mango</option>
<select>
? html利用selected 默认选中,但在React中,不使用selected ,而是给<select> 标签中增加一个value 属性,这使得受控组件使用更加方便,因为你只需要更新一处变量即可。
class FlavorForm extends React.Component {
// ...
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<label>
Pick your favorite La Croix flavor:
<select value={this.state.value} onChange={this.handleChange.bind(this)}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
? 总的来说,这使 <input type="text"> , <textarea> 和 <select> 都以类似的方式工作 —— 它们都接受一个 value 属性可以用来实现一个受控组件。
多选select
? 使用多选select 时,需要给select 标签增加value 属性,同时给value 属性赋值一个数组
<select multiple={true} value={['B', 'C']}>
?
利用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 (
<form>
<label>
Is going:
<input name="isGoing" type="checkbox" checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<label>
Number of guests:
<input name="numberOfGuests" type="number" value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}
ReactDOM.render(
<Reservation />,
document.getElementById('root')
);
? 注意这里使用ES6计算的属性名称语法来更新与给定输入名称相对应的 state (状态) 键的办法
this.setState({ [name]: value })
?
var partialState = {};
partialState[name] = value;
this.setState(partialState);
? 由于 setState() 自动将部分状态合并到当前状态,所以我们只需要调用更改的部分即可。
?在 受控组件上指定值 prop 可防止用户更改输入,除非您希望如此。 如果你已经指定了一个 value ,但是输入仍然是可编辑的,你可能会意外地把 value 设置为undefined 或 null 。
? 以下代码演示了这一点。 (输入首先被锁定,但在短暂的延迟后可以编辑。)
ReactDOM.render(<input value="hi" />, mountNode);
setTimeout(function() {
ReactDOM.render(<input value={null} />, mountNode);
}, 1000);
?
三、 状态提升 (Lifting State Up)
? 通常情况下,同一个数据的变化需要几个不同的组件来反映。我们建议提升共享的状态到它们最近的祖先组件中。为了更好的理解,从一个案例来分析
温度计算器
? 在本案例中,我们采用自下而上的方式来创建一个温度计算器,用来计算在一个给定温度下水是否会沸腾(水温是否高于100C)
(1)创建一个 BoilingVerdict 组件,用来判水是否会沸腾并打印
function Bioling Verdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>
}
return <p>The water would not boil.</p>
}
(2)有了判断温度的组件之后,我们需要一个Calculator 组件,他需要包含一个<input> 提供我们输入文图,并在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 (
<fieldset>
<legend>Enter temperature in Celsius;</legend>
<input
value={temperature}
onChange={this.handleChange.bind(this)} />
<BoilingVerdict celsius={parseFloat(templature)} />
</fieldset>
)
}
}
(3)现在我们实现了基础的父子组件通信功能,假设我们有这样的需求:除了一个设施文图的输入之外,还需要有一个华氏温度输入,并且要求两者保持自动同步
? 我们从Calculator 中提取出TemperatureInput ,然后增加新的scale 属性,值可能是c 或f
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
}
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.state = { temperature: e.target.value }
}
handleChange(e) {
this.setState({ temperature: e.target.value });
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}</legend>
<input
value={temperature]
onChange={this.handleChange} />
</fieldset>
)
}
}
? 抽出TemperatureInput 之后,Calculator 组件如下
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
? 现在有了两个输入框,但这两个组件是独立存在的,不会互相影响,也就是说,输入其中一个温度另一个并不会改变,与需求不符
? 我们不能再Calculator 中显示BoilingVerdict , Calcultor 不知道当前的温度,因为它是在TemperatureInput 中隐藏的, 因此我们需要编写转换函数
(4)编写转换函数 ? 我们先来实现两个函数在摄氏度和华氏度之间转换
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
functin toFahrenheit(celsius0 {
return (celsius * 9 / 5) + 32;
}
?接下来,编写函数用来接收一个字符串temperature 和一个转化器函数作为参数,并返回一个字符串,这个函数在两个输入之间进行相互转换。为了健壮性,对于无效的temperature 值,返回一个空字符串,输出保留三位小数
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
cosnt 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<input> 一样接收一个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 来处理数据变更,从而通过新值重新渲染两个输入。现在我们的代码如下
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale< this.prps.scale;
return (
<fieldset>
<legend>Enter temperature in {scalenames[scale]}</legend>
<input value={tempearature} onChange={this.handleChange.bind(this)} />
</fieldset>
)
}
}
? 我们将当前输入的 temperature 和scale 存储在本地的state 中,这是我们冲输入“提升”的state(状态) ,他将作为连个输入的“单一数据源”。为了渲染这两个输入,我们需要知道的所有数据的最小表示,如摄氏度输入37,这时Calculator 组件状态将是:
{
temperature: '37',
scale: 'c'
}
? 我们确实可以存储两个输入框(摄氏度和华氏度)的值,但事实证明是不必要的。我们只要存储最近更改的输入框的值,以及他们所表示的度量衡(scale )就足够了。然后推断出另一个值。这也是我们实现两个输入框保持同步的途径
class Calculator extends React.Component {
constructor(props) {
super(props);
this.state = { temperature: '', scale: 'c' }
}
handleCelsiusChange(temperature) {
this.setState({ scale: 'c', temperature });
}
handleRahrenheitChange(temperature) {
this.setState({scale: 'f', temperature })
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsuis}
onTemperatureChange={this.handleCelsiusChange.bind(this)} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange.bind(this)} />
<BiolingVerdict celsius={parseFloat(celsius)} />
</div>
)
}
}
? 现在,无论你编辑哪个输入框,Calculator 中的 this.state.temperature 和 this.state.scale 都会更新。其中一个输入框获取值,所以任何用户输入都被保留,并且另一个输入总是基于它重新计算值。
? 让我们回顾一下编辑输入时会发生什么:
- React 调用在 DOM
<input> 上的 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
|