1、初识react
安装命令行工具
npm install -g create-react-app
create-react-app
是FB出品的零配置命令行工具。我们使用这个命令行工具的创建的项目是基于webpack+ES6的。
create-react-app my-app
创建完成后进入项目目录
cd my-app
生成的项目目录
运行后可以在浏览器输入http://localhost:3000/
进行查看,此时是出于开发模式的,热替换也开启,我们的改变可以实时的反映到浏览器,产生的错误也会在命令行或者刘看起实时显示
运行
npm start
运行后的终端和浏览器
如果需要发布的话,使用
编译
npm run build
使用npm run build
编译得到生产环境,此时代码会被编译到build目录下,此时会自动将整个应用打包发布,它会自动使用Webpack控件进行优化与压缩。
到这里,就完成了一个最简单的react app的制作。
2、JSX简介
先看一个例子
const element = <h1>Hello, world!</h1>;
这种语法看起来很怪异,既不是字符串也不是Html。这种怪异的语法被称作JSX。它是javascript语言的语法的一种语法扩展。react使用这种语法来描述UI。JSX看起来像是一个模板型语言,但是它具有javascript的全部功能。
2.1、JSX中嵌入表达式
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);//多行书写可读性更强,这里推荐使用括号将他们包裹起来,以避免自动添加分号。
ReactDOM.render(
element,
document.getElementById('root')
);
经过编译之后,JSX表达式会变成常规的Javascript对象。这意味着,可以使用JSX的if语句和for循环,用JSX给变量赋值,将JSX作为函数的参数,还可以在函数中将它作为返回值返回。
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
2.2、JSX 指定属性
可以使用分号加字符串的方式给属性赋值
const element = <div tabIndex="0"></div>;
也可以使用花括号{}
嵌入javascript表达式的方式给属性赋值
const element = <img src={user.avatarUrl}></img>;
不要在花括号两边使用引号,否则JSX是将它作为字符串而不是表达式。
2.3、JSX 指定子元素
如果是一个空标签(如img,hr,input),在结束的时候应该立即使用/>
将它关闭
const element = <img src={user.avatarUrl} />;
如果包含多个字标签
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
注:
由于JSX更接近于JavaScript而不是HTML,React DOM 使用驼峰属性的方式命名用来代替原来的HTML属性名
例如,class
变成了className
,tabindex
变成了tabIndex
2.4、JSX 防注入攻击
安全的书写方式
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
默认的,React DOM 会对所有嵌入JSX的值进行转义在渲染他们之前。这确保了就算在没有使用书面的稳妥安全的写法的情况下也无法注入任何东西。所有的东西在被渲染之前都会转换为字符串。这种方式帮助了React阻止XSS攻击
2.5、JSX 代表对象
Babel 编译 JSX 直到React.createEkement()
被调用
下面的两个例子是等价的
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React.createElement()
会执行一些检查帮助你写出没有错误的代码,但这本质上是创建了一个对象
// Note: this structure is simplified
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
};
这样的对象被称为React元素
。他们是呈现在屏幕的东西的描述。React读取这些对象,然后使用他们构建DOM。
3、渲染元素
元素是构建React apps
的最小的单位。
元素描述的是你在屏幕上看到的东西。
const element = <h1>Hello, world</h1>;
和浏览器DOM元素不同的是,React元素是普通的对象,它们很轻易地被创建。React DOM 会关注并更新DOM和它所匹配的React元素
注:
一个很容易混淆的概念是元素和组件。组件是由元素构造出来的。
3.1、渲染一个元素进DOM
这是一个HTML文件中的<div>
:
<div id="root"></div>
我们称之为“root”DOM节点,因为所有的在它内部的东西都将被React DOM管理。只有React的应用程序通常只包含一个根DOM节点。如果是将React整合到已有的app中,可以有多个根DOM节点。
渲染一个元素进根DOM节点,需要使用ReactDOM.render()
同时去元素和根DOM节点
const element = <h1>Hello,world</h1>
ReactDOM.render(
element,
document.getElementById('root')
);
3.2、更新已经渲染的元素
React元素是不可改变的。已经创建的元素的子元素和属性都不能被改变。一个元素像是电影里的一个帧,这个帧在时间轴上只是一个点。
更新UI的唯一方式是创建一个新的元素,然后渲染它。
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
这是一个时钟程序,每隔获取本地时间并创建一个新元素,然后渲染它。
但在实际的开发中,许多React apps只调用ReactDOM.render()
一次。
3.3、局部渲染
React DOM 会对比当前元素和以前的元素,但是仅仅会应用必要的DOM更新以达到目标状态。也就是说,React只会渲染发生改变的一部分,而其他的则保持不变,不会刷新整个页面。
造这个例子中,尽管我们每一秒都创建了一个新的元素并渲染了它,但在DOM中只有时钟的文本节点发生了改变。这是React DOM的更新机制,保证了程序的高效。
4、Components和Props
使用组件可以将UI分割成独立的,可复用的模块,模块之前是完全隔离开的。
从概念上讲,组件像是JavaScript函数,它们接受任意输入(也成为参数)然后返回React元素。
4.1、Functiona and CLass Components
定义组件的最简的方式就是创建一个JavaScript函数
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
这个函数是一个有效的React组件,因为它接收了一个“props”对象作为参数,然后返回了一个React元素。我们称这类组件为“Functional”,个人译为函数性组件,因为它从字面上看上去很像是javascript函数。
也可以使用ES6的class
定义一个组件
class Welcom extend React.Component {
render() {
return <h1>Hello,{this.props.name}</h1>
}
}
上面的两种观点是等价的。
类拥有一些额外的特征。
4.2、渲染组件
以前我们只遇到过代表DOM标签的React 元素
const element = <div />;
然而,元素也可以代表自定义的组件
const element = <Welcome name="Sara" />;
当React发现一个元素代表的是一个自定义组件的时候,它会传递JSX的属性给这个组件,作为一个单独的对象。我们称这个对象为“props”(属性)。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;//React会将组件里的attribute打包为一个props对象。在组件内部就可以使用对象属性的方式访问。
ReactDOM.render(
element,
document.getElementById('root')
);
首先调用
ReactDOM.render()
渲染<Welcome name="Sara" />
React 将
{name: 'Sara'}
作为属性调用Welcome
组件Welcome
组件返回一个React元素<h1>Hello, Sara</h1>
React DOM 高效地更新DOM
注:组件的首字母大写
4.3、组件的组成
组件可以在它的输出里面引用其他组件。我们可以使用这个组件的概念抽象出任何等级的东西。一个按钮,一个对话框,一个屏幕:在React apps中,所有的东西通常都可以表达为一个组件。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
典型的例子就是,一个新的React apps,在它的最顶层有一个独立的App
组件。无论怎样,如果你想把React整合到其他的app终,你应该从最底层最小的组件开始逐步构建。
注:组件必须返回一个单一的根元素
4.4、提取组件
不要害怕将组件拆分成更小的组件
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
这个Comment
组件接收auhor
对象,text
字符串,和date
时间作为props,描述了一个社交网站的一条评论。
这个组件可以灵活地变化,因为它所有的都是嵌套的,同时它在个别的不见上也很难被复用。
继续分隔成更细小的组件
unction Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
这个Avatar
组件并不需要知道它是否在Comment被提出。这就是我们为什么可以给他的props使用更通用的名称。因为这个组件在其他地方可以再被复用。
推荐命名props的时候从它自身出发,还不是它被使用的上下文环境。
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />//使用Avatar组件代替之前的
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
然后,我们还可以提取出UserInfo
组件
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
这时的Comment
:
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
4.5、Props是只读的
不论声明的组件是一个函数还是一个类,它必须绝对不修改它自己的props
function sum(a, b) {
return a + b;
}
这样的函数被称为“纯函数”,因为他们,因为他们从不去尝试改变他们的输入,而且对于相同的输入总返回相同的值
相反的,下面的这个函数是不存的,因为它改变了自己的输入
function withdraw(account, amount) {
account.total -= amount;
}
React 是优雅的灵活的但是它有一个必须要遵守的规则:
所有的React 组件的行为都必须像一个纯函数,相对于他们的props
当然,应用程序的UI是动态的,会随着时间改变的。
5、状态和生命周期
到目前为止我们值学习了一种更新UI的方法。
在前面的时钟例子中,我们直接调用ReactDOM.render()
来改变已经渲染输出的。
这里我们来学习如何创建一个真实可复用以及密封的时钟组件,它可以建立自己的时间然后每秒更新自己。
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);
可是,它缺少了决定性的必要条件:事实上,这个时钟建立了一个计时器,然后每秒更新更新UI,应该是一个时钟的实现细节。
理想情况下,我们想要只写一次,然后时钟更新自己
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
要实现这个目标,我们需要添加状态在这个时钟组件上。
状态与props相似,但是它是组件私有的,并且被组件完全控制的。
我们之前在学习使用ES6的类创建组件的时候提到过,类方式创建的组件有用一些额外的特性。
局部状态恰好就是这样:一个只对class有用的特性。
5.1、将函数转为类
我们可以转换一个函数组件为一个类组件,要实现这个目的,有五步:
使用相同的名字创建一个ES6的类,它继承自
React.Component
class Clock extends React.Component { }
新建一个单独的空方法到它调用
render()
class Clock extends React.Component{ render(){} }
将函数中主体移入
render()
方法class Clock extends React.Component { render(){ return ( <div> <h1>Hello, world!</h1> <h2>It is {props.date.toLocaleTimeString()}.</h2> </div> ); } }
在render()函数中使用
this.props
替代props
class Clock extends React.Component { render(){ return ( <div> <h1>Hello, world!</h1> <h2>It is {this.props.date.toLocaleTimeString()}.</h2> </div> ); } }
- 删除剩余的空函数声明
删除掉原来函数
5.2、为类添加局部状态
我们将迁移date
从props到状态,这个过程需要三步:
在
render()
方法中使用this.state.date
替换this.props.date
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }
为类添加构造函数用来初始化
this.state
class Clock extends React.Component { constructor(props) { super(props); //通过这句话,将props传递到基础构造函数里 this.state = {date: new Date()}; } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }
类组件应该总是基础的构造函数中调用props
将
date
prop从<Clock />
中移除ReactDOM.render( <Clock />, document.getElementById('root') );
添加状态后的时钟组件
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
5.3、为类添加生命周期方法
在拥有许多组件的应用程序中,当组件被销毁的时候,释放被组件使用的资源也是非常重要的。
只要这个时钟组件第一次被渲染到DOM时,我们想创建一个计时器。这个在React中被称为mounting
。
我们也想在Clock被移出DOM时清除这个定时器。这个在React中被称为unmounting
我们能在声明特别的方法在类组件中,当一个组件装配和卸载的时候执行这些函数里的代码:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() { //Mount时执行
}
componentWillUnmount() { //unMount时执行
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
这些方法被称为生命周期钩子
componentDidMount()
钩子在组件输出被渲染到DOM之后执行。这是一个设置定时器的很好的时机:
componentDidMount() {
//在这里设置定时器的ID
this.timerID = setInterval(
() => this.tick(),
1000
);
}
当this.props
被React创建,this.state
也拥有了特殊的含义时,如果你需要存储一些不用与可视化输出的东西,你就有空闲给类手动添加额外的字段。
如果你不使用一些在render()
函数中,那它不应该出现在state中
我们将拆卸掉这个计时器在componentWillUnmount()
钩子中:
componentWillUnmount() {
clearInterval(this.timerID);
}
最后,我们每秒将执行tick()
方法
React将使用this.setState()
将更新加入到组件的局部状态中
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({ //设置状态
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
这个Clock
的执行顺序是怎样的呢?
当
<Clock />
传递到ReactDOM.render()
,React 调用Clock
组件的构造函数。由于Clock
需要显示当前的时间,它初始化this.state
为一个当前时间的对象。我们将稍后更新这个状态React然后会调用
Clock
组件的render()
方法。这样React就知道什么应该显示在屏幕上。然后React更新DOM来匹配Clock的渲染输出。当
Clcok
已经插入DOM之后,React 调用componentDidMount()
钩子。在这个钩子函数内部,Clock
组件要求浏览器设置一个定时器来每秒调用tick()
一次。每一秒浏览器都会调用
tick()
方法,在tick()
内部,Clock
组件安排一个使用包含当前时间的对象的setState()
调用来进行UI更新。得益于setState()
的调用,React知道了状态已经改变,接着就会调用render()
方法来得知在屏幕上应该是什么。这一次,this.state.date
在render()
方法中被改变,所以这个渲染输出会包含已经被改变的时间。React会相应地更新DOM如果
Clock
组件被从DOM终永久地移除了,React会调用componentWillUnmount()
钩子,所以这个计时器就停止了
5.4、恰当地使用状态
5.4.1、不要直接修改状态
this.state.commet = 'Hello"; //这样是无效的
应该使用setState()
方法
this.setState({comment: 'Hello'});
5.4.2、状态更新应该是异步的
为了提高性能,在一个单一的更新终,React应该分批处理多个setState()
。
由于this.props
和this.state
可能被异步地更新,你不应该依赖他们的值计算下一个状态。
this.setState({counter: this.state.counter + this.props.increment});//这个代码有可能无法更新这个counter
要修复这个问题,使用第二种方式的setState()
,它接受一个函数作为参数,而不是一个对象。这个函数接收以前的状态作为第一个参数,在应用这些更新的时候将props作为第二个参数。
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
上面我们使用箭头函数,但它在工作的时候也是规则的函数
// Correct
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});
5.4.3、状态更新是合并式的
当调用setState()
的时候,React 合并你提供的对象到当前的状态中去。
当你的状态包含多个独立的变量的时候:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
然后你可以单独地更新他们的某一些:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
这个合并是浅合并,所以this.setState({comments})
离开this.state.posts
原封不动的,但是完整的替换了this.state.comments
6、数据流
父组件和字组件都不能知道是否某一个组件是有状态的或者无状态的,以及他们不应该关心是否被定义为函数或者是类。
这就是为什么状态经常被称作局部的或者密封的。除了自己之外的所有的组件都是不可访问的。
一个组件应该选择将它的状态作为属性向下传递给他的子组件
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
同样也适用于自定义组件
<FormattedDate date={this.state.date} />
这个FormattedDate
组件将会接收date
在它的属性中,将不会知道是否它来自于Clock
的状态,还是来自于Clock
的属性,或者是手工输入:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
这种通常被称作“自上而下”或者“单向”的数据流。所有状态总是被一些特殊的组件所拥有,一些数据或者UI派生于那些仅仅影响树状结构中处于下面的组件。
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
上面的例子创建了一个App组件,它包含三个Clock子组件,每一个Clock 创建它们自己的计时器然后独立地更新。
在React apps中,一个组件是否有状态是它的一个实现细节,这个组件可能会随着时间而改变。你可以在有状态的组件内使用无状态的组件,反之亦然。
6、事件处理
React元素的时间处理跟DOM元素上的事件处理是非常类似的。
语法不同点:
- React事件命名是以驼峰方式命名的。而DOM是小写。
- 在JSX中传递函数作为事件的处理程序,而不是一个字符串。
比如在HTML中的事件
<button onclick="activateLasers()">
Activate Lasers
</button>
而React
<button onClick={activateLasers}>
Activate Lasers
</button>
另一个不同之处是,在React中,不能返回false来阻止默认行为。你必须明确地调用preventDefault
。例如,在纯HTML中,要阻止链接的默认打开新页面的行为可以这样写:
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>
在React中,应该这样写:
function ActionLink() {
function handleClick(e) {
e.preventDefault(); //阻止默认行为
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
在上面的例子中,e
是一个合成事件。React 定义了这些跟W3C标准对应的合成事件,所以你不需要担心跨浏览器的兼容性问题。更多事件请参考
当使用React时,你应该通常不会需要调用addEventListener
来给一个创建好的DOM元素添加监听器。只需要在这个元素最初被渲染的时候提供一个监听器。
当你使用ES6语法的Class
定义了一个组件,在class中定义一个事件处理方法,这种模式很常用。比如,下面的这个toggle
组件渲染了一个按钮用来切换“开”和“关”的状态
Class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
this.handleClick = this.handleClick.bind(this);//将函数的作用域绑定到this上,这一步非常重要。
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
<button onClick = {this.handleCLick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
注:你不得不注意这个this
在JSX中的含义。在JavaScript中,class方法默认是未被绑定的。如果你忘记了绑定this.handleClick
和传递它到onClick
,当函数被调用时,这个this
将是undefined
。
这不是React的特性,这是函数如何在javascript工作的一部分。一般来讲,如果你没有在方法后使用()
,比如onClick={this.handleCLick}
,你应该绑定这个方法。
如果调用bind
使你很困扰,你有两种方式去避免。如果你想使用实验性的属性初始化语法,你可以使用属性初始器准确的绑定回掉函数
class LoggingButton extends React.Component {
// This syntax ensures `this` is bound within handleClick. 这个语法确保'this'绑定在handleClick上
// Warning: this is *experimental* syntax. 警告:这个一个实验性质的语法
handleClick = () => {
console.log('this is:', this); //这个语法的关键在于,箭头函数的this指向谁?指向它的词法作用域
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
在Create React App中,这个语法默认被开启
如果你不想使用这个属性初始化语法,你还可以使用箭头函数:
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// This syntax ensures `this` is bound within handleClick
return (
<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
);
}
}
这种语法的问题是,每次LoggingButton
被渲染的时候都会创建一个新的回调。大多数情况下,这都是没问题的。然而,如果这个回调作为prop传递到下面的组件,那些组件可能会多些额外的重新渲染的工作。我们通常,建议在构造函数或者是使用属性初始化语法绑定,来避免这种方式带来的性能问题。
7、条件渲染
在React中,你可以创建封装你需要的行为的独特的组件。然后,你可以根据你的应用程序的状态来渲染他们其中的一部分。也就是有条件的渲染。
条件渲染在React中的工作方式和在JavaScript的条件语句的工作方式相同。使用JavaScript运算符例如if
或者是条件运算符condition ? expr1 : expr2
来创建元素以表示当前的状态,然后让React更新UI。
下面是两个组件:
function UserGreeting(props) {
return <h1>Welcome back!</h1>;
}
function GuestGreeting(props) {
return <h1>Please sign up.</h1>;
}
我们将创建一个Greeting
组件来显示上面的两个组件其中之一,显示哪一个取决于用户输入
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
ReactDOM.render(
<Greeting isLoggedIn={false} />,
document.getElementById('root')
);
7.1、元素变量
你可以使用变量来存储元素。当输出没有发生改变的时候,这可以帮助你有条件地渲染一部分组件。
下面是两个组件,一个登录按钮,一个登出按钮:
function LoginButton(props) {
return (
<button onClick={props.onClick}>
Login
</button>
);
}
function LogoutButton(props) {
return (
<button onClick={props.onClick}>
Logout
</button>
);
}
在这个示例的下面,我们将创建一个有状态的组件LoginControl
。它将渲染<LoginButton />
或者是<LogoutButton />
,这取决于它的当前状态。它也会渲染之前的例子中提到的<Greeting />
。
class LoginControl extends React.Component {
constructor(props) {
super(props); //导入props对象
this.handleLoginClick = this.handleLoginClick.bind(this); //绑定登录按钮点击处理函数到this
this.handleLogoutClick = this.handleLogoutClick.bind(this); 绑定登出按钮点击处理函数到this
this.state = {isLoggedIn: false}; //初始化状态
}
/**登录点击处理函数*/
handleLoginClick() {
this.setState({isLoggedIn: true});
}
/**登出点击处理函数*/
handleLogoutClick() {
this.setState({isLoggedIn: false});
}
render() {
const isLoggedIn = this.state.isLoggedIn;
let button = null;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}
ReactDOM.render(
<LoginControl />,
document.getElementById('root')
);
虽然声明一个变量并使用if
逻辑判断来有条件的渲染组件是一个很好的方式,有时候你会想要使用一个更短一些的语法。下面就是JSX的一些内联方法
7.1.1、内联if和逻辑运算符
在JSX中你可以在花括号{}
内嵌入任意字符。它包含了JavaScript的逻辑和运算符。它可以方便的有条件的包含一个元素
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElementById('root')
);
它工作的原理是,因为在JavaScript中,true
和expression
始终被求值为表达式,false
和expression
时钟被求值为false
因此,如果条件为真,这个元素右侧&&
之后会出现在输出中。如果是false
,React会忽视并跳过它。
7.1.2、内联if-Else条件运算符
另一个条件渲染元素的方法是使用JavaScript的条件运算符condition ? true : false.
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
</div>
);
}
我们还可以使用更长的的表达式:
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn ? (
<LogoutButton onClick={this.handleLogoutClick} />
) : (
<LoginButton onClick={this.handleLoginClick} />
)}
</div>
);
}
正如在JavaScript中,到底使用哪一种方式完全取决于你和你的团队认为哪一种更易于阅读。另外请记住,不论条件变得多么复杂,它都可以在恰当的时间提取出一个组件。
7.2、阻止组件呈现
在少数情况下,你可能想要一个组件隐藏它自己,即使是它在其他组件终被渲染了的时候。要达到这个目的,只需要在组件的render()
中返回null
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {showWarning: true}
this.handleToggleClick = this.handleToggleClick.bind(this);
}
handleToggleClick() {
this.setState(prevState => ({
showWarning: !prevState.showWarning
}));
}
render() {
return (
<div>
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick}>
{this.state.showWarning ? 'Hide' : 'Show'}
</button>
</div>
);
}
}
ReactDOM.render(
<Page />,
document.getElementById('root')
);
从一个组件的render()
方法返回null
并不影响组件的声明追妻函数被触发。比如,componentWillUpdate
和componentDidUpdate
始终都会被调用。
8、Lists and Keys
首先,让我们来复习一下在JavaScript中如何转变列表
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);
在React中,转换数组为元素列表也是同样的。
8.1、渲染多个组件
你可以创建元素集合,然后使用{}
将它包含进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')
);
8.2、基础列表组件
我们经常要渲染列表到一个组件。
我们能重构之前的示例:
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
当你运行这段代码,你会得到一个警告:列表项需要提供一个key。“key”是在创建列表的元素时需要为每一个元素包含的一个特别的字符串属性。它是非常重要的。
我们为前面的示例添加key:
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
8.3、Keys
Keys帮助React鉴别列表中哪一个发生了改变,添加了新的项目,或者被移除了。Keys应该被数组中的元素中,给这些元素一个牢固的身份。
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
挑选key最好的方式是使用一个能够在其兄弟列表元素唯一标识一个列表项目。大多数时候你可能从你的数据中使用ID作为keys
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
当你没有稳定的ID来渲染列表的时候,你可以使用列表的索引作为key,这种应该是最次的选择。
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
如果列表项会重新整理,我们不建议使用索引作为key,因为这样会重新渲染,会很慢。
了解更多Key的必要性
8.3.1、提取带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>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
正确的示例:
unction ListItem(props) {
<!--正确-->
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<!--正确-->
<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')
);
8.3.2、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')
);
8.3.3、在JSX中嵌入map()
在上面的示例中,我们在JSX中声明了一个独立的listItems
变量:
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
JSX允许在{}
嵌入任何表达式,所以我们能使用内联函数map()
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
有时这会使代码更清晰,但是这种方式也会可能会被滥用。就像在JavaScript一样,为了可读性是否提取出一个变量,这完全取决于你。记住这种map()
的嵌套方式,在适当的时机可能会被用来提取代码块。
9、Forms表单
在React中,HTML表单元素的工作原理跟其他DOM元素的有一点差别,因为表单元素天然地保持着一些内置的状态。比如,在纯HTML中,表单接受一个单一的name
<form>
<label>
Name:
<input type="text" name="name" />
</label>
<input type="submit" value="Submit" />
</form>
这个表单幽默的HTML表单的行为:当用户提交这个表单的时候,会访问一个新页面。在React中,如果你想要这个行为,它就会照常工作。但是大多数情况下,需要有一个JavaScript函数用来处理表单提交、存取用户输入表单中的数据。实现这一标准的方法是被称作控制组件
(controlled component)的技术。
9.1、控制组件
在HTML中,表单元素诸如<input>
、<textarea>
、<select>
通常保持自己的状态并根据用户输入更新它。在React中,可变的状态通常保存在组件的state
属性里,仅由setState
进行更新。
我们可以通过使React state 成为“single source of truth”来结合他们两个。然后,React组件渲染一个表单,并控制发生在表单上的用户输入。一个input表单元素的值是由React中的“控制组件”控制的。
例如,当一个表单被提交的时候,如果我们想要创建一个之前的示例记录这个名字,,我们可以写一个表单作为控制组件
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''}; //初始化状态
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
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}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} /> //输入框
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
由于这个value
属性设置在表单元素上,这个显示出来的value一直都是this.state.value
,这使得React state 是真正的来源。由于handleChange
在每一次键盘输入都会更新React state,因此显示的值会更新为用户想要的值。
因为有控制组件,每一次状态改变都会触发关联的处理函数。这使得React能够直接了当的修改或者确认用户输入。比如,如果我们想要强制使name被写为大写字母,我们可以这样书写handleChange
handleChange(event) {
this.setState({value: event.target.value.toUpperCase()});
}
9.2、textarea标签
在HTML中,一个<textarea>
元素靠它的子元素定义它的文本。
<textarea>
Hello there, this is some text in a text area
</textarea>
在React中,一个<textarea>
使用了一个value
属性代替。这种方式中,一个多行文本<textarea>
元素能够很简单的书写,就如同但行文本<input>
一样。
class EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 'Please write an essay about your favorite DOM element.'
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('An essay was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<textarea value={this.state.value} onChange={this.handleChange} /> //写法跟React中的input无异
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
注:this.state.value
是在构造函数中初始化的,所以这个文本域开始就会有一些文字在里面。
9.3、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>
在上面的例子中,Coconut选项是默认被选中的,因为它有一个selected
属性。在React中,在根select
标签上使用value
属性来代替HTML中的selected
属性。在控制组件中这样是很方便的,因为你只需要在一个地方更新它。例如:
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('Your favorite flavor is: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Pick your favorite La Croix flavor:
<select value={this.state.value} onChange={this.handleChange}> //在select标签中添加value属性,值为下拉列表的默认值。
<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
属性,你可以使用它来实现一个控制器组件。
9.4、处理多个input
当你需要处理多个受控制的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; //获取事件元素的name属性值
this.setState({
[name]: value //将name和value组成名值对存储在state中
});
}
render() {
return (
<form>
<label>
Is going:
<input
name="isGoing" //name
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Number of guests:
<input
name="numberOfGuests" //name
type="number"
value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}
我们使用ES6的计算属性名语法更新跟名字对应的状态
this.setState({
[name]: value //对象的计算属性名用[]括起来
});
等价的ES5写法:
var partialState = {};
partialState[name] = value;
this.setState(partialState);
另外,由于setState()
自动地合并部分状态到当前状态,我们只需要调用它被改变的部分
9.4、替代控制组件
有时候使用控制组件显得有些冗长,因为你需要写一个事件处理器,用来处理那些可以通过一个React组件来改变或者输送所有的输入状态。当你转变一个过去的代码库到React,或者是集成一个非React库到React程序的时候,这会特别地令人心烦。在这些情况中,你可能想要看看不受控制的组件,另一种实现表单输入的技术。
10、状态提升
更多的时候,几个组件需要对相同的数据变化做出反应。我们推荐提升共享的状态到他们最近的共同祖先。让我看看它是怎么工作的。
在这个小结中,我们将创建一个温度计来计算这水是否在一个给定的温度沸腾。
我们将创建一个组件叫做BoilingVerict
.它接收一个celsius
温度作为一个prop,然后打印水是否沸腾
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
接着,我们将创建一个叫Calulator
的组件。它渲染一个<input>
用来接受用户输入温度,然后将它的值存放在this.state.temperature
此外,它呈现一个BoilingVerdict
适合于当前输入的值。
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
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} />
<BoilingVerdict
celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
10.1、添加第二个输入
我们的新需求是这样的,添加一个摄氏温度的输入框,我们提供华氏温度,它们俩保持同步。
我们先从Calculator
提取出TemeratureInput
组件。然后添加一个新的scale
prop给它
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
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>
);
}
}
我们现在能够改变Calculator
来呈现两个单独的温度输入框
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
现在我们有两个输入框,但是当你输入一个温度到他们中的任意一个的时候,另一个不会更新。这跟我们想要的不一样。
我们也不能显示BoilingVerdict
。这个Calculator
不知道当前的温度因为它被隐藏在TemperatureInput
中
10.2、写一个转换函数
首先,我们写两个函数来转换摄氏温度和华氏温度
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
这两个函数转换数字。我们将写另一个函数用来获取temperature
字符串,将一个转换函数作为参数,然后返回一个字符串。我们将使用它来基于一个输入框的值计算另一个输入框的值。
它返回一个空的字符串在一个无效的temperature
,然后存储时保留3个小数位
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();
}
比如,tryConvert('abc', toCelsius)
returns an empty string, and tryConvert('10.22', toFahrenheit) returns '50.396'.
10.3、状态提升
一般地,两个TemperatureInput
组件都独立地在局部状态里存储他们的值:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
然而,我们想要这两个输入框的状态是同步的。当我们更新摄氏温度输入框,华氏温度输入框也会跟着一起变化。
在React中,共享状态是一种技巧,通过移动它到需要共享状态的组件的最近的共同的祖先上。这被称作“状态提升”。我们将移除TemperatureInput
组件上的局部状态,将它移动到Calculator
里。
如果Calculator
控制这个共享状态,它将变成当前两个温度的“真正的源”。他能通知她们两个同时都拥有值,并且与另一个保持一致。由于两个TemperatureInput
上的props都来自于同一个父组件Calculator
组件,所以它们俩的输入时钟都是同步的
那需要怎样的步骤呢?
首先,我们将使用this.props.temperature
替换TemperatureInput
组件中的this.state.temperature
。到目前为止,让我们假装this.state.temperature
已经存在,尽管我们需要在将来从Calculator
传递它。
render() {
// 之前: const temperature = this.state.temperature;
const temperature = this.props.temperature;
我们知道“props”是只读的。当temperature
在局部状态中的时候,TemperatureInput
只能调用this.setState()
来改变。然而,现在temperature
来自于付组件的prop,TemperatureInput
无法控制它。
现在,当TemperatureInput
想要更新它的温度的时候,它会调用this.props.onTemperatureChange
:
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
注:上面的例子中,在自定义的组件里,temperature
或者是onTemperatureChange
都没有什么特别的含义。我们可以叫他们任何名字。
onTemperatureChange
属性和temperature
属性将被一起提供给Calculator
组件。它将同归修改它自己的局部属性来处理改变,因此,两个输入鲁昂都会被使用新的值进行重新渲染。
在Calculator
计算变化之前,让我来看看这些改变是怎样被应用于TemperatureInput
组件的。我们之前已经移动了局部状态,然后替换了this.state.temperature,
我们现在读取this.props.temperature
。当我们想要做出一些改变的时候,不再使用this.setState()
而是调用Calculator
中的this.props.onTemperatureChange()
.
lass TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
让我们回头看看Calculator
组件
我们将存储当前的输入的temperature
和scale
在Calculator
组件的局部状态中。这个状态是从输入框提取出来的,它将担当起输入框的“真正的源”的任务。我们需要知道以便渲染这两个输入框,这是表现所有数据的最简约的方法。
例如,如果我能输入37在摄氏温度的输入框中,Calculator
组件的状态将变为:
{
temperature: '37',
scale: 'c'
}
如果我们稍后编辑华氏温度为212,那这个状态将变为:
{
temperature: '212',
scale: 'f'
}
我们能够存储两个输入框的值,但是结果是必须的。它足以存储最近改了的值,和这个scale。然后我们就可以基于这两个值推算出其他输入框的值了。
这两个输入框就会保持同步了,因为她们的值都是从同一个状态计算来的:
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(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={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
现在,不论你编辑哪一个输入框,Calculator
里的this.state.temperature
和this.state.scale
都能获得更新。输入框中的任何一个获得了值,所以用户输入是被保护的,其他另外的输入框的值都能基于它被重新计算。
让我们来看看当编辑输入框的时候到底发生了什么:
React调用DOM
<input>
上指定的onChange
处理函数。在我们的例子中,它是TemperatureInput
组件中的handleChange
方法。在这个
TemperatureInput
组件中的handleChange
方法中会调用this.props.onTemperatureChange()
。它的props包含了由父组件Calculator
提供的onTemperatureChange
。当它预先被渲染的时候,
Calculator
已经指定了摄氏温度的TemperatureInput
的onTemperatureChange
为Calculator
的handleCelsiusChange
方法,华氏温度的TemperatureInput
的onTemperatureChange
为Calculator
的handleFahrenheitChange
方法。所以不管哪一个输入框被编辑都会调用到Calculator
上对应的方法。在这些方法中,
Calculator
组件会要求React在编辑的时候使用新输入的值和当前的scale调用this.setState()
来重新渲染。React调用
Calculator
组件的render
方法来获知应该呈现的UI.两个输入框的值都基于当前的温度和活跃的scale来计算。温度值在这里发生转换。React使用
Calculator
指定的新的props作为参数调用TemperatureInput
组件独立的render
方法。React DOM相应地更新DOM。我们刚刚编辑过的输入框接受它的当前值,另一个输入框更新转化之后的温度.
10.4、经验总结
在React程序中,应该有一个所有数据的单一的“真正来源”。通常,state第一次被添加到组件上时需要渲染它。然后,如果其他组件也需要他,你可以将它提升到公共的祖先上。取代尝试在不同组件之间同步状态,你应该依靠[单向数据流]。
(https://facebook.github.io/react/docs/state-and-lifecycle.html#the-data-flows-down)
提升状态就涉及到要写更多的”样板”代码,比起两种捆绑途径来说。但是也有好处,它花费更少的工作去发现和隔离错误。由于所有的状态“存活”于一些组件中,那些组件可以独立地改变它,这就使得留给错误的空间大大地减少了。此外,你可以实现任何自定义逻辑来拒绝和转换用户输入。
如果有些东西是从props和state衍生出来的,它大概不会出现在state中。举例来说,用来代替存储摄氏温度和华氏温度,我们存储最后修改的temperature
和scale
就可以了。另一个输入框的值可以在render()
方法中计算出来了。这让我们清除或者舍入到其他领域中而不会损失精度。
当你看到UI中有一些错误的时候,你可以使用React开发者工具来检查props和向上查找树直到你找到与更新的状态相关的组件。这让你跟踪bug到他们的源头。
11、Composition vs Inheritance 组合 vs 继承
React 有一个非常强大的composition模块,我们推荐使用composition代替inheritance在组件之前重复使用一些代码。
11.1、Containment
一些组合并不能提前知道他们的children。特别常见的组件像是侧边栏或者是对话框,它都代表通用的“boxes”
我们推荐这样的组件使用特殊的children
prop直接地来传递子元素到输出:
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
这让其他组件通过嵌套的JSX传递任意的children给他们
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
任何在JSX的<FancyBorder>
标签之内的任何东西都被作为children
prop传递进FancyBorder
组件。由于FancyBorder
渲染{props.children}
在一个<div>
里面,被传递的元素出现在最后的输出中。
虽然这不常见,有时你可能需要多个“holes”在一个组件中。在下面的示例中,你可以使用你自己的惯例来代替使用children
:
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
React元素如同<Contacts>
和<Chat>
只是对象,所以你可以传递他们作为props对象就像任何其他数据。
11.2、Specialization
有时候我们考虑组件作为“特别的案例”区别于其他案例。例如,我们可以说WelcomeDialog
是一个Dialog
的特殊情况
在React中,这也是由composition实现的,一个更特殊的组件渲染一个更通用的,使用props进行配置。
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
</FancyBorder>
);
}
function WelcomeDialog() {//更特殊的组件
return (
<Dialog //更通用的组件
title="Welcome"
message="Thank you for visiting our spacecraft!" />
);
}
Composition对组件定位为类同样有效
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children} //props.children代表的Dialog组件的子元素
</FancyBorder>
);
}
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 (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}
handleChange(e) {
this.setState({login: e.target.value});
}
handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}
11.3、关于Inhreitance
在FB,我们在上千个组件里使用React,我们没有发现在我们推荐创建组件的案例中有继承层次结构。说白了,就是没有使用继承
Props和composition 给你所有灵活的你需要自定义一个组件的样子和行为的一个明确和安全的方式。记住组件可以接收任意的props,包括简单的值,Reacr元素,亦或是函数。
如果你想要在组件之间复用非UI功能的函数,我们建议提取它到一个单独的JavaScript模块。这些组件可以导入它并使用这个函数,对象,或者是class,而不需要扩展它。
12、Thinking in React 编程 思想
React是,在我们的观点中,它是使用JavaScript构建大型的,快速的web apps的第一方式。它已经为我们被缩放得非常好了在 Facebook 和 Instagram上。
在React许多强大的部分中的一个是,它使你思考怎样构建你的应用程序。在这个文档中,我们将带你使用React,通过思考过程,构建一个能够搜索产品数据的表格
12.1、从Mock数据开始
想象我们已经有一个JSON API 并获得了一个模拟数据:
我们的JSON API返回了一些数据:
[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
12.2 编写程序
Step1: 把UI分解为层级结构的组件
第一件你想要做的事是在每个组件周围画一个盒子,在模拟数据中给所有的盒子命名。如果你和设计师一起工作,他们可能已经做完这部分工作,所以跟他们交流交流。他们的PS图层命名可能最终反应的是React组件的名字。
但是你怎么知道什么应该成为一个组件呢?只要使用相同的手法来决定你应该创建一个函数还是对象。这个手法就是“单一责任原则”,也就是说,一个组件理想化地应该只做一件事。如果它结束成长,它应该被分解为更小的字组件。
由于你经常要呈现JSON数据模块给用户,你会发现如果你的模块是被正确构建的,你的UI将会精美的呈现。那是因为UI和数据模块都倾向于遵守相同的信息架构,这意味着分离你的UI组件的工作通常是微不足道的。只需要分割成组件,然后正确的表现数据模型中的一小块。
你可以看到这里我们有五个组件。我们用斜体字表示每个组件表现的数据:
FilterableProductTable(orange)
:包含全部东西SearchBar(Blue)
:接受用户输入ProductTable(green)
:显示基于用户输入过滤的信息ProductCategoryRow(turquoise)
:显示每个分类的标题ProductRow(red)
:显示一条商品
如果你观察ProdyctTable
,你会看到表头不是自己的组件。这个个人喜好的问题,这是一个有争论的问题。比如,我们离开ProdyctTable
的一部分,因为ProductTable
的职责是呈现数据集的一部分。然而,如果这个头部变得复杂,构造它自己的ProductTableHeader
组件就有意义了。
现在,我们已经确定了组件,让我们来整下一下他们的层级。这是很简单的,出现在其他组件中的组件在层级结构中应该作为字组件。
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
Step2: 在React中构建静态的版本
现在你有了你的组件层级,是时候实现你的app了。创建一个版本的最简单的方式就是使用你的数据模块并渲染这个UI,但它不具有交互性。最好解耦这些过程,因为创建一个静态版本需要很多输入、不用思考,增加交互性需要很多思考不需要输入。
创建一个你的app的静态版本呈现你的数据模型。你想要创建的组件是可以复用其他组件,还可以通过props传递数据,不要全部都使用state来创建静态的版本.State 是仅被用作交互性,那就是说,数据能够随时间而变化。由于这是一个静态的app,因此你不需要使用它。
在这一步的最后,你将有一个可复用的组件库,用来渲染你的数据模型。由于这是一个静态的app,因此这些组件只有render()
方法。最顶层的组件FilterableProductTable
将把你的数据模型作为props.如果你在你的下面的数据模型进行了改变并且再次调用了ReactDOM.render()
,这个UI会被更新。很容易看到你的UI是怎样更新的和那里发生了改变,因为发生的这些这都不复杂。React的单项数据流保证了每样东西都是模块化和快速的。
执行这一步的时候如果需要帮助,请简单查阅React 文档
插曲:Props vs State
他们是React中模型数据的两个类型:props和state.理解他们的区别是非常重要的,如果你不确定他们的区别在哪里,请略读
Step3: 确定UI状态的最小表现
构造交互式的UI,你需要能够触发底层数据模型的改变。React使用state让这一切都变得简单。
正确创建你的app,你首先需要考虑你的app需要的,可变状态的最小集合。这里的关键是:“不要重复自己”。求出你的程序需要的绝对最小的state表示,计算出任何你需要随需应变。比如,你构建一个待办事项列表,只需要保持待办事项的一个数组,不要保持一个单独的state计数变量。反而,当你想如安然一个待办事件的总数,最简单的方法是取得待办事件数组的长度。
思考我们的示例程序的数据的所有部分。
- 原始商品列表
- 用户输入框
- 复选框
- 过滤后的商品列表
让我们检查每一个,并计算出它们的状态。简单询问他们关于每一块数据:
- 需要通过父元素的props传递吗?如果需要,它可能不是state
- 它是否需要保持不变?如果需要,它可能不是state
- 你能否基于组件中的其他state或者props计算出它?如果可以,它不是state
原始的商品列表是通过props传递的,所以它不是state。搜索关键字和复选框看起来像是state,因为它们可能在某时刻改变,而且不能通过计算得出。最后,过滤后的商品列表不是状态,因为它能通过结合原始上皮列表和搜索关键字以及复选框状态被计算出。
所以最终,我们的state为:
- 用户输入的收索关键字
- 复选框的值
Step4: 确定你的状态应该出现在什么地方
我们已经确定了app中最小的state。接着,我们需要确定哪一个组件改变或者是控制这个状态。
记住,React是一种单项数据流构成的层次结构。它可能不清楚哪一个组件应该拥有什么状态。这对新人来说,这一部分最难理解,所以跟着这些步奏来搞定它:
在应用程序中的每一个状态:
- 确定每一个组件都基于那个state呈现
- 寻找一个公共的所有者组件(在层次结构中,所有需要状态的组件之上的一个独立的组件)。
- 公共的所有者或者更高层次的拥有这些状态的组件
- 在拥有这个状态是有意义的地方,如果你不能找到一个组件,简单地创建一个支持这个状态的新组件并添加它公共拥有者组件之上的某个地方。
让我们执行这个策略:
ProductTable
需要基于state和搜索条显示的关键字和复选框状态过滤商品- 这个公共的拥有者组件是
FilterableProductTable
- 它在概念上使关键字和复选框的值在
FilterableProductTable
有意义
我们已经明确了state存在于FilterableProductTable
。首先,添加一个实例属性this.state = {filterText: '', inStockOnly: false}
到FilterableProductTable
的constructor
来显示初始的状态。然后,将filterText
和inStockOnly
作为prop传递到ProductTable
和SearchBar
。最后,使用这些props来筛选ProductTable
中的每一条数据并在SearchBar
中设置表单字段的值
你可以看到你的程序将会如何运转:设置fillterText
到ball
并且刷新你的程序,你将看到数据表格正确的更新了。
Step5: 添加逆向数据流
到目前为止,我们已经构建了一个app,它能够作为正确显示,作为一个props方法和层次结构的状态流。现在,是时候方向相反的数据流了:在层次结构的表单组件深处需要在FilterableProductTable
更新state
React使这个数据流很清楚,使其容易理解程序是如何工作的,但是它需要比传统的双向数据绑定稍微多一些的分类。
如果你尝试对当前版本的盒子进行分类和检查,你将会看到React忽略了你的输入。这是策略,我们已经设置了input
输入框的value
prop始终和FilterableProductTable
表单传递的state进行比对。
让我们思考一下,我们想要怎么做。我们想要确认,不管什么时候用户改变了表单,我们就更新状态来显示用户输入。由于组件应该只更新它们自己的状态,FilterableProductTable
将传递回调函数到SearchBar
,不管什么时候状态更新时,回调回调函数都会被触发。我们可以在输入框使用onChange
事件来监听它。这个回调函数被FilterableProductTable
传递,会调用setState()
,然后这个app将会被更新。
这虽然听起来很复杂,它事实上只是几行代码。你的数据流在app中如何流动是非常明确的。
**
我希望在你想使用React构建组件和应用程序的时候,这能带给你一些启发。当比起你过去惯用的,它需要编写更多代码的时候,记住代码被阅读的频率远远高于写,并且,模块化的,清晰的代码是极易被阅读的。当你开始构建大型的组件库,你将会领会到清晰性和模块化,以及可复用的代码,你的代码行数将开始萎缩减少。
二级标题
三级标题
四级标题
注: