HTML模版
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
<script src="../build/browse.min.js"></script>
<div id="example"></div>
<script type="text/babel">
//** Our code goes here! **
</script>
之后出现的React代码嵌套入模版中。
1. Hello world
ReactDOM.render{
<h1>Hello world</h1>,
document.getElementById('root')
};
这段代码将一个一级标题插入到指定的DOM节点中。
javaScript笔记
React是一个JS库,学习前请首先确保拥有JS基础。在之后的例子中会使用ES6的语法,但并不多因为相对较新。但鼓励大家对arrow functions,classes,template literals,let,const statements等熟悉起来。
2. JSX介绍
const element = <h1>Hello,world</h1>;
这段看起来既像是字符串,又像是HTML的代码就是JSX。JSX是对JavaScript的语法扩展,它非常强大。
JSX的意义
“表达”逻辑和UI逻辑天生一对儿,React天生就是来撮合它们的,比如产生的“事件“如何触发?页面的状态如何随时间不断改变?数据如何时刻准备着在页面上显示?
与人为的将逻辑处理和标记放置在独立的文件中不同(.js文件和.html文件),React能够将“交合” 的组件😍用不同的规则解析(js规则和html规则)。在后续章节会详细介绍组件,但现在如果你还不适应在JS代码中写 html代码,就快点适应😂。
React开发不一定必须写JSX,但好处多多。
在JSX文件中的嵌入语句
在JSX文件的html标记中,可以直接嵌入JS语句,当然需要用大括号进行转义。语句无论运算表达式、函数表达式、引用等,只要它合法就行。例如:
2+2;
user.firstname;
formatName(user);
const user = {
firstName : 'Harper',
lastName : 'Perez'
};
const element = (
<h1>
hello, {formatName(user)}!
</h1>
);
function formatName(user){
return user.firstName + ' ' + user.lastName;
}
ReactDOM.render(
element,
document.getElementById('root')
);
JSX也是一种表达式
从底层上讲,JSX编译后会变成js函数调用或js对象。因此,允许在if语句、loop语句、赋值语句中使用JSX,甚至可以将它们作为参数、函数返回值使用。例如:
function getGreeting(user){
if(user){
return <h1>Hello,{formatName(user)}!</h1>;
}
return <h1>Hello,Stranger.</h1>;
}
JSX的属性
可以使用指定字符串+引号作为JSX的属性:
const element = <div tabIndex = "0"></div>;
也可以在大括号中嵌入js语句来作为属性:
const element = <img src = {user.avatarUrl}></img>
当嵌入js语句作为参数时,千万不要在大括号外加引号。你要么在字符串外加引号,要么在js语句外加大括号,不要同时用。
警告
*JSX语法和javaScript语法更接近,React DOM使用的是骆驼命名法而不是HTML属性的命名法.
例如:在JSX中类名是className,因此tabindex命名为“tabIndex”.*
JSX的子标签
JSX中,如果一个标签为空,你可以用/>立刻关闭它,就像XML语法一样。而不必须使用<></>。
const element = <img src = {user.avataUrl}/>;
JSX的标签也可以包含子标签:
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
JSX 防止注入攻击
JSX嵌入用户输入是很安全的:
const title = response.potentiallyMaliciousInput;
//This is safe;
const element = <h1>{title}</h1>;
默认情况下, 在渲染之前, React DOM 会格式化(escapes) JSX中的所有值。从而保证用户无法注入任何应用之外的代码。在被渲染之前,所有的数据都被转义成为了字符串处理。 以避免XSS(跨站脚本) 攻击。
JSX表示对象
Babel将JSX编译成React.createElement() 调用。
下面两个例子是完全相同的:
const element = (
<h1 className = "greeting">
Hello,world!
</h1>
);
const element = React.createElement(
'h1',
{className:'greeting'},
'Hello,world!'
);
React.createElement()会执行一些检查来帮助你写无Bug的代码,但基本上它会创建如下所示的对象:
const element = {
type:'h1',
props:{
className:'greeting',
children:'Hello,world'
}
};
这些对象叫做React元素。你可以把它们想象成你想在屏幕上看到的东西的一种描述。React读取这些对象,使用它们构建DOM节点并保持数据的更新。
下一节我们将介绍如何将React元素渲染到DOM节点中。
提示:
我们推荐在你所用的编辑器中加入Babel插件,这样ES6和JSX代码就可以显示高亮。
3. 渲染元素
元素是React应用的最小构建单元
元素描述如下所示:
const element = <h1>Hello, world</h1>;
和浏览器的DOM元素不同,React元素是纯对象,并且创建起来耗费更低。React DOM所关心的是DOM节点与React元素的匹配。
注意
大家可能会讲元素概念与组件概念混淆。我们会在下一章节介绍组件。元素和组件的关系是组成被组成的关系,简而言之,组件是由元素组成的。
在DOM节点中渲染元素
在HTML文件中我们处处能见到<div>:
<div id = "root"></div>
我们称之为根DOM节点,因为其中的一切都是由React DOM管理。
React构建的应用通常只有一个根节点。如果你将React继承入已存在的应用中,如你所愿,你可能有很多个相互独立的根节点。
我们使用ReactDOM.render()来将React元素渲染到根DOM节点中(或称作“插入”),代码如下:
const element = <h1>Hello , world</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
这段代码在页面中显示hello world。
更新被渲染元素
React元素是不可改变的(immutable)。一个元素一旦被创建,你就不能再改变它的子节点和属性。一个元素很像电影中一帧:它表示UI在某一个时刻内的展示。
到目前为止我们所了解的知识,更新UI只有一条路:创建新的元素,然后通过ReactDOM.render()渲染它。
请看下面的时钟例子代码:
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);
tick()函数每一秒被setInterval回调一次,每一次回调都要调用ReactDOM.render()函数。
注意
在实践中,大多数React应用只调用ReactDOM.render()一次。在下一节中我们将学习怎样将这样的代码封装在状态化的组件中。
React仅仅更新那些有必要更新的内容
React DOM会像元素及其所有子元素与前一个(状态/内容)相比较,而后仅仅运用于有必要更新的DOM节点,并将这些DOM节点带入期望的状态。
在上一个例子中,用浏览器工具看到的效果如下:
从上图看到,当我们创建一个元素,即使它每个节拍都在描述整个UI树,那也仅仅是那些内容时刻在变化的文本节点被React DOM更新。
在我们的经验中,思考UI的显示如何和“喂数据”的时候一致,而不是思考怎样才能时刻更新UI,这样才能避免很多Bug。
4. 组件和属性
组件能够让你将UI分割成独立的、可重用的片段,并在隔离中思考每一个片段。
从概念上将,组件更像是js中的函数。它们能够接收任意输入(我们称为属性)并能返回React元素,来描述UI应该展现什么。
函数组件和类组件
最简单的定义一个组件的方式是写一个js函数,代码如下:
function Welcome(props){
return <h1>Hello, {props.name}</h1>;
}
这个函数是一个有效的React组件,因为它接受了单特征对象参数并且返回了一个React元素。我们成这为组件函数化,因为它们和js函数一模一样。
你也可以使用ES6类来定义组件,代码如下:
class Welcome extends React.Component{
render(){
return <h1>hello, {this.props.name}</h1>;
}
}
以上两个组件在React中是等效的。
class有一些额外特征我们会在下一节讨论。
渲染一个组件
在前一章节,我们只见过表示DOM标签的React元素:
const element = <div />;
而元素还能够表示用户自定义的组件:
const element = <Welcome name = "Sara" />;
这段代码中,当React看到一个表示用户自定义组件的元素时,它会把JSX参数作为一个单一对象传递到这个组件。我们把这个对象称为属性。
例如,下面这段代码把“Hello,Sara”显示在屏幕上:
function Welcome(props){
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name = "Sara"/>;
ReactDOM.render(
element,
document.getElementById('root')
);
让我们看看上面一段代码发生了什么:
//1. 我们调用ReactDOM.render()函数,函数带有一个元素<Welcome name="Sara" />
//2. React调用Welcome组件,并将{name:'Sara'}作为属性。
//3. Welcome组件将<h1>Hello,Sara</h1>作为返回值返回。
//4. React DOM将<h1>Hello,Sara</h1>更新到DOM节点中。
附加说明
组件命名的首字母必须大写,否则会报错。
组合组件
组件在它们的输出中能够涉及其他组件。这个特征能够让我们在任意细节层次(LOD)使用相同的组件进行抽象。一个按钮、表单、对话、场景:在React应用中,所有这些都可以作为组件表达。
例如下面的代码,我们创建了一个组件 App,它多次对Welcome组件进行渲染:
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')
};
提炼组件
不要害怕把组件分割成更小的组件。
思考下这个Comment组件,代码如下:
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有三个属性,作者(对象)、文本(字符串)、日期(日期类型),用来在网页中描述评论的社交内容。
这个组件因为嵌套层太多,改变起来非常复杂,并且之后每一部分也很难重用。下面我们就对它进行改写,把它分割成一个个的小组件:
数显,我们来分割Avatar组件,代码如下:
function Avatar(props){
return(
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
然后我们对Comment做了一些小修改,代码如下:
function Comment(props){
return(
<div className = "Comment">
<div className = "UserInfo">
<Avatar user={props.author} />
<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>
);
}
分割组件一开始看起来很像Grunt,但是在更大规模的应用中会得益于拥有大量可重用组件。一个好的经验法则(a rule of thumb)是:如果你的一部分UI会被多次使用(比如Button、Panel、 Avatar)或者它本身很复杂(比如App,FeedStory,Commment),那么这个组件就是很好的做分割的选择。
属性是只读的
无论你将一个组件声明为一个类还是一个函数,它都不能改变它的属性,请看下面这个sum函数:
function sum(a,b){
return a+b;
}
这样的函数我们叫做“纯”函数,因为它们不会改变它们的输入,并且对同样的输入总是做同样的输出。
相反,下面的函数就“不纯了”,因为它改变了自己的输入,代码如下:
function withdraw(account, amount){
//每次调用函数,它的输入都发生变化
account.total -= amount;
}
React虽然很灵活,但是它有一条非常严格的规则:
所有React组件在对待它们的属性时,必须像“纯”函数那样!
当然,应用的UI是动态的、是在时刻变化的。在下一节中,我们将介绍一个新的概念--状态。状态允许React组件在响应用户交互、网络响应等时,能够实时改变它们的输出,但并不违反上述规则。
什么是纯函数
1.给出同样的参数值,该函数总是求出同样的结果。该函数结果值不依赖任何隐藏信息或程序执行处理可能改变的状态或在程序的两个不同的执行,也不能依赖来自I/O装置的任何外部的输入。
2.结果的求值不会促使任何可语义上可观察的副作用或输出,例如易变对象的变化或输出到I/O装置
什么是非纯函数
1.返回当前天星期几的函数是一个非纯函数,因为在不同的时间它将产生不同的结果,它引用了一些全局状态。同样地,任何使用全局状态或静态变量潜在地是非纯函数。
2.random()是非纯函数,因为每次调用潜在地产生不同的值。这是因为伪随机数产生器使用和更新了一个全局的“种子”状态。加入我们修改它去拿种子作为参数,例如random(seed),那么random变为纯函数,因为使用同一种子值的多次调用返回同一随机数。
3.printf() 是非纯函数,因为它促使输出到一个I/O装置,产生了副作用。
6. 状态和生命周期
思考下上一节那个节拍时钟的例子
到目前为止我们只学习了一种更新UI的方法。我们通过调用ReactDOM.render()函数来改变渲染输出,代码如下:
function tick(){
const element = {
<div>
<h1>Hello,world!</div>
<h2>It is {new Date().toLocaleTimeString()}.</h1>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick,1000);
在这一部分中,我们将学习如何真正使Clock组件是可重用的并封装的。Clock组件将能够设置自己的计时器并每秒更新它。
让我们看看clock是如何封装的,代码如下:
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);
但是,上面的代码丢失了一个很关键的要求:Clock组件设置计时器和每秒更新UI都应该在组件内部实现。
理想我们想写成下面这样:
ReactDOM.render(){
<Clock />
document.getElementById('root');
};
为了实现这种,我们需要在Clock组件中增加状态。
状态和属性很相似,但是它是私有的并且完全由组件控制。
我们之前提到过,组件定义为class会有一些额外的特征。本地状态准确来说就是一种仅仅对class有效的特征。
将一个函数转为一个类
要把一个像Clock那样的函数组件转为类组件需要五步:
- 创建一个ES6类,命名相同,继承React.Component类。
- 添加一个单一空方法render()。
- 把函数体放入render()方法中。
- 在render()方法中,用props代替this.props。
- 删掉保留的空函数声明。
示例代码如下:
class Clock extends React.Component{
render(){
return(
<div>
<h1>Hello,world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
正如上面代码,Clock类现在定义成了一个类而不是一个函数。这让我们能够使用一些额外的特征,比如local state、lifecycle hooks。
给类增加一个本地状态
我们将用3步把date从props转为state:
- 在render()方法中,用this.state.date代替this.props.date。
class Clock extends React.Component{
render(){
return(
<div>
<h1>Hello,world!</h1>
//change
<h2>it is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
- 添加一个类构造体,并对this.state进行初始化:
class Clock extends React.Component{
//add
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>
);
}
}
注意我们是怎么把props 传给base constructor的:
constructor(props){
super(props);
this.state = {date:new Date()};
}
类组件总是调用带props的构造体函数。
- 从<Clock />元素中把date属性移除:
ReactDOM.render(
//change
<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')
);
下一步,我们将让Clock组件能够设置自己的计时器和自我更新。
给类添加一个声明周期方法
在带有很多组件的应用中,当组件被销毁时,应用能够释放掉组件占用的资源是很重要的。
当Clock组件首次插入到DOM节点时我们要设置一个计时器,这在React叫做挂载(mounting);当DOM节点把Clock组件移除时,我们要将计时器清除 , 这在React中叫做“卸载”(unmounting)。
当一个组件挂载和卸载时,我们能够在组件类中声明一个特殊的方法来运行一段代码执行它,代码示例如下:
class Clock extends React.Component{
constructor(props){
super(props);
this.state = {date:new Date()};
}
componentDidMount(){
//Mounting code
}
componentWillUnmount(){
//Unmounting code
}
render(){
return (
<div>
<h1>Hello,world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
这个方法我们称作生命周期钩子。
当组件输出被渲染到DOM节点后,componentDidMount()运行。在这个方法里设置计时器非常合适,代码如下:
componentDidMount(){
this.timerID = setInterval(
()=>this.tick(),
1000
);
}
通过componentWillUnmount()卸载计时器,代码如下:
componentWillUnmount(){
clearInterval(this.timerID);
}
代码补全:
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.toLcoaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(){
<Clock />,
document.getElementById('root');
);
代码解析:
//1. <clock />传入ReactDOM.render(),React调用Clock组件的构造体。当Clock需要显示当前时间时,它会对this.state进行初始化,赋值一个当前时间对象.后续将更新整个状态。
//2. React调用Clock组件的render()方法,将Clock的render()输出更新到DOM节点上。
//3. 当Clock的输出插入到DOM节点上时,React调用componentDidMount()方法。在方法中,Clock组件让浏览器设置一个计时器来每秒调用一次 tick()。
//4. 每一秒浏览器调用tick()方法。方法中,Clock组件通过调用setState()打算要更新UI,setState()带有一个当前时间对象的参数。通过调用setState(),React知道状态已经发生变化,并再次调用render()方法来了解这次应该在屏幕中显示什么。这次,在render()中的this.state.date和上一次不同,因此render输出的是一个更新后的时间,React相应的更新DOM。
//5. 如果Clock组件从DOM中移除,React会调用componentWillUnmount(),计时器随即停止。
state和props的介绍与比较
大家看到上面的代码时可能会感到有些懵逼,感觉props和component as function就能解决的问题为什么要用component as class和state呢?下面来介绍下它们的区别和应用场景。
一. state
- state的作用
state是React组件中的一个对象。React把用户界面当做是状态机,想象它有不同的状态然后渲染这些状态,可以轻松让用户界面与数据保持一致。React中,更新组件的状态state,会导致重新渲染UI(不需要操作DOM)。简单来说,就是用户界面会随着state变化而变化。- state的工作原理
常用的通知React数据变化的方法是调用setState(data,callback)。这个方法会合并data到this.state,并重新渲染组件。渲染完成后,调用可选的callback回调。但大部分情况不需要提供callback,因为React会负责把UI更新到最新的状态。- 哪些组件应该有state?
大部分组件的工作应该是从props中取出数据并渲染出来。但是,又是需要对用户输入,服务器请求或者时间变化等作出响应,这是才需要 state。组件应该尽可能的无状态化,这样能隔离state,把它放到最合理的地方(Redux做的就是这个事情?),也能减少冗余并易于解释程序运作的过程。常用的模式就是创建等多个只负责渲染的无状态组(stateless)组件,在它们的上层创建一个有状态的(stateful)组件并把它的状态通过props传递给子级,有状态的组件封装了所有的用户交互逻辑,而这些无状态的组件只负责声明使的渲染数据。- 哪些应该作为state?
state应该包含那些可能被组件的时间处理器改变并触发用户更新的数据。这种数据一般很小且能被JSON序列化。当创建一个状态化的组件时,应该保持数据的精简,然后存入this.state。在render()中再根据state来计算需要的其他数据,因为如果在state里添加冗余数据或计算所得数据,经常需要手动保持同步。- 哪些不应该作为state?
this.state应该仅包含能表示用户界面状态所需的最少数据,因此不应该包括:
- 计算所得数据
- React组件:在render()中使用props和state来创建它。
- 基于props的重复数据:尽量保持用props来作为唯一的数据来源。把props保存到state中的有效的场景是需要知道它以前的值的时候,因为未来props可能会发生变化。
二. props
- props的作用
组件中的props是一种父级向子级传递数据的方式。- 复合组件
代码如下:var Avatar = React.createClass({ render:function(){ return( <div> <ProfilePic username = {this.props.username} /> <ProfilePic username = {this.props.username} /> </div> ); } }); var profilePic = react.createClass({ render:function(){ return( <a href = {'http://www.facebook.com/' + this.props.username}> {this.props.username} </a> ); } }); React.render( <Avatar username = "ryanho" />, document.getElementById("example") );
从属关系:
如果组件Y在render()方法中创建了组件X,那么Y就拥有X。
正确的使用state
关于setState()你需要知道三件事。
(1)不要试图直接修改State
如下代码,这样不能重新渲染组件:
//Wrong
this.state.comment = 'Hello';
用setState()来对状态进行修改:
//Correct
this.setState({Comment:'Hello'});
唯一能部署this.state的地方就是构造题函数。
(2) State的更新可能是异步的
React在一次页面更新中可能要批处理多个setState()。由于this.props和this.state的更新可能是异步的,因此你不应该依赖this.props的值来计算下一次状态。
例如下列代码,这段代码不能够正确更新计数器:
this.setState({
counter:this.state.counter + this.props.increment,
});
为了修复这种可能出现的错误,可以使用setState()的来接受一个函数而不是对象。这个函数将会接收"前状态"作为第一个参数,此时被更新的props作为第二个参数:
//Correct
this.setState((prevState , props) =>({
counter:preState.counter +props.increment
}));
上面的代码我们使用了arrow function,如下代码是常规写法:
//Correct
this.setState(function(prevState, props){
return{
counter : prevState.counter + props.increment
};
});
(3) state的更新是融合的(Merged)
当我们调用setState(),React将我们提供的对象融入到当前状态中。(...一脸懵逼)
例如,你的状态可能包含很多独立的变量:
constructor(props){
super(props);
this.state = {
posts: [],
comment :[]
};
}
而后你要分别调用setState()来更新它们:
componentDidMount(){
fetchPosts().then(response=>{
this.setState({
posts:response.posts
});
});
fetchComments().then(response =>{
this.setState({
comments:response.comments
});
});
}
数据自上而下传递(flow down)
父组件和子组件都不知道某一个组件是有状态的(stateful)还是无状态的(stateless),并且它们也不关心一个组件是被定义成class还是function。这就是为什么状态经常被称为是本地的或者封装的。除非是某个组件的拥有者或者创建者,都不能访问它。
一个组件可以将它的状态作为porps传递给它的子组件,代码如下:
<h2>It is {this.state.date.toLocaleTimeString()}.<h2>
this.state对用户定义的组件也有效,代码如下:
<FormattedDate date={this.state.date} />
FormattedDate组件可以通过props接收到date数据。但是它不知道这个数据是通过Clock组件的状态,还是Clock的props,还是键盘输入。请看如下代码:
function FormattedDate(props){
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
我们将这种传递称为是“自上而下的”或者称为“单向的“。状态是被组件拥有的,因此源于这些状态的数据和UI仅能影响到这些组件的子组件。
如果你把一颗组件树想像为一个 props瀑布,每一个组件的状态就像是一个可以在任意时刻汇入瀑布的支流,当然,支流也是“向下流淌的”。
为了显示所有组件是真正隔绝独立的,在App组件里我们渲染了三个Clock组件,代码如下:
function App(){
return(
<div>
<Clock />
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每个Clock组件都设置了自己的计时器并且独立的更新。
6. 处理事件
带有React元素的处理事件和在DOM节点中的处理事件很相似。但它们有一些句法上的不同:
- React事件使用骆驼命名法(camelCase),而不是小写命名。
- JSX语法中你传递的是一个函数作为event handler,而不是一个字符串。
例如,在HTML中:
<button onClick = "activateLasers()">
Activate Lasers
</button>
在React中有一些轻微的不同,代码如下:
<button onClick = {activateLasers}>
Activate Lasers
</button>
另一个不同是,在React中你不能通过返回false来阻止默认行为。你必须通过调用preventDefault来这样做。例如在纯HTML中,为了阻止打开新页面的默认链接行为,你可以这样写:
<a href="#" onclick="console.log('This link was clicked.');
return false">
Click me
</a>
在React中,要写成下面这样:
function ActionLink(){
function handleClick(e){
e.preventDefault();
console.log('This link was click.');
}
return(
<a href = "#" onClick={handleClick}>
Click me
</a>
);
}
这段代码中,e是一个虚构的事件。React根据W3C spec定义这些虚构事件,因此你不必担心交叉浏览器的互通性。请看SyntheticEvent参考文档来了解更多。
在你使用React时,你一般不必调用addEventListener函数来向DOM元素添加listener。相反,你只需在元素在初始化渲染时提供一个listener。(一脸懵逼)
当你使用ES6来定义组件时,通用的方法是将事件处理定义为类的一个方法。例如,Toggle组件渲染了一个按钮,这个按钮让用户能够在‘NO’‘OFF'间来回切换状态:
class Toggle extend React.Component{
constructor(props){
super(props);
this.state = {isToggleOn:true};
//This binding is necessary to make 'this' work in the callback
this.handleClick = this.handleClick.bing(this);
}
handleClick(){
this.setState(prevState => ({
isToggleon : !prevState.isToggleOn
}));
}
render(){
return(
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'NO' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />
document.getElementById('root')
);
你一定要小心在JSX中'this'的含义。在 js中,类方法默认是不绑定的。如果你忘记绑定this.handlerClick并把它传递到onClick,当函数实际被调用时this会显示未定义。
这并不是React特有的行为,它是js的特性。一般来说,如果你指向的方法后面不带小括号(),像这样
onClick = {this.handleClick}
你应该绑定那个方法。
如果调用bind让你很烦躁,有两种方法可以选。如果你对public class fields语法很熟,可以用class fields来绑定回调(仅限高手使用):
class LoggingButton extends React.Component{
//This syntax ensures 'this' is bound within handleClick.
//Warning:This is *experimental* syntax.
handleClick = () =>{
console.log('this is:', this);
}
render(){
return(
<button coClick = {this.handleClick}>
Click me
</button>
);
}
}
如果你不会class fields的语法,你在回调中可以用剪头函数:
class LoggingButton extends React.Component{
handleClick(){
console.log('this is:',this);
}
render(){
//This syntax ensures 'this' is bound withon handleClick
return(
<button onClick = {(e) =>this.handleClick(e)}>
Click me
</button>
);
}
}
这个语法问题是:每次LoggingButton渲染时都要创建不同的回调。在大多数情况下这没问题,但是,如果回调被作为一个prop传递给下层组件时,那些组件可能会进行额外的重新渲染。我们一般推荐在构造体中渲染或使用class fields语法,来避免这类问题的发生。
向Event Handlers中传递参数
在一个循环中,想要传递一个额外的参数到event handler是很常见的。例如,如果id是一个行ID,下面的两种都有效:
<button onClick={(e) =>this.deleteRow(id,e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this.id)}>Delete Row</button>
上面两种是等效的。使用箭头函数和Function.prototype.bing都可以。
在两个例子中,代表React的e参数将作为ID后的第二个参数被传递。在箭头函数中,我们必须显性的直接传递它。
7. 条件渲染
在React中,你可以创建区域组件,并按照你的需要封装组件行为。然后,你可以根据你的应用的状态,只渲染它们中的一部分。
在React中的条件渲染与js中的条件语句一样。可以是用js的‘if’或其他条件运算符来创建代表当前状态的元素,并且让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(
//Try changing to isLoggedIn = {true};
<Greeting isLoggedIn = {false} />,
document.getElementById('root');
);
这个例子根据不同的isLoggIn属性渲染了不同的greeting组件。
元素变量
你可以使用变量来存储元素。这能帮助你有条件的渲染组件中的一部分而输出的其他部分不会改变。
请看下面两个组件,表示登陆登出按钮:
function LoginButton(props){
return(
<button onClick = {props.onClick}>
Login
</button>
);
}
function LogoutButton(props){
return(
<button onClick = {props.onClick}>
Logout
</button>
);
}
在下面的例子中,我们会创建一个有状态的组件叫做LoginControl。
它根据当前状态,既可以渲染<LoginButton />,也可以渲染<LogoutButton />,代码如下:
class LoginControl extends React.Component{
contructor(props){
super(props);
//necessary binding
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClickbind(this);
}
handleLoginClick(){
this.setState({isLoggedIn:true});
}
handleLogoutClick(){
this.setState({isLoggedIn:true});
}
render(){
const isLoggedIn = this.state.isLoggedIn;
let button = null;
if(isLoggedIn){
button = <LogoutButton onClick={this.handleLogoutClick} />;
}else{
button = <LgoinButton onClick={this.handleLoginClick} />
}
return(
<div>
<Greeting isLoggedIn={isLoggedIn}/>
{button}
</div>
);
}
}
ReactDOM.render(
<LoginControl />,
document.getElementById('root')
);
虽然声明一个变量并用if来条件渲染是很不错的方法,但有时候你可能想使用更段的语法。在JSX中可以用内联条件,下面我们来解释。
带逻辑和操作符的内联if
如果你想在JSX中嵌入任何语句,加个大括号就可以了。这包括js的逻辑与运算符。请看下面代码:
function Mailbox(props){
const unreadMessages = props.unreadMessages;
return(
<div>
<h1>Hello!</h1>
{unreadMessages.lengeh>0 &&
<h2>
You have{unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
const messages = ['React','Re:React','Re:Re:React'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElement('root');
);
这在js中也有效,true&&expression永远是expression,false&& expression永远是false。所以,如果条件是true,&&右边的一大堆就会显示处理啊,如果条件是false,那React会忽略并跳过它(所以就不显示了)。
带条件运算符的内联if-Else
条件渲染内联元素的方法是用js的条件运算符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>
);
}
就像在js中,根据你和你的团队考虑的可读性选择合适的风格。也请记住,当条件变得太复杂了,别忘了分解组件。
阻止组件渲染
在一些很少出现的情景中,你可能想要组件隐藏,即使是它正在被另一个组件渲染。这时我们返回NULL就可以了。
在下面的例子中,<WarningBanner />组件根据props的warn值进行不同的渲染,如果prop的值是false,这个组件就不渲染,代码如下:
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.showWaring} />
<button onClick = {this.handleToggleClick ? 'Hide' : 'Show'}
</button>
</div>
);
}
}
ReactDOM.render(
<Page />
document.getElementById('root');
);
从一个组件render()方法中返回null,不会影响组件生命周期方法的析构。例如,componentWillUpdate和componentDidUpdate仍然会被调用。
8. 列表和关键字
首先我们来回顾下如何在js中转换列表。
在下面代码中,我们使用map()函数获取一个numbers数组,类型是double。我们将map()函数返回的新数组赋给doubled变量并在控制台显示,代码如下:
const numbers = [1,2,3,4,5];
const doubled = numbers.map((number)=>number*2);
console.log(doubled);
这段代码将[2,4,6,8,10]在控制台中显示出来。
在React中,将数组转换成元素列几乎是一样的。
渲染多个组件
允许你创建组件集合并在JSX语法中允许使用{}花括号裹住它们。
在下面代码中,我们使用js的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')
);
基本列表组件
通常你会渲染一个组件列表。
我们能将前一个例子重构成一个组件,这个组件接收一个numbers数组并输出一个无序元素列表,代码如下:
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”是一个特殊的字符串属性,当你创建一个元素列表是应该把它包含在里面。我们在下一节来讨论为什么这个这么重要。
让我们将一个关键字在numbers.map()中赋给我们的列表项,代码如下:
function NumberList(props){
const numbers = props.numbers;
const listItems = number.map((numbers)=>
<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');
);
关键字
关键字帮助React识别哪些项发生了改变、增加了或移除了。关键字应该在数组中就赋给元素项,并且是唯一的、固定不变的,代码如下:
const numbers = [1,2,3,4,5];
const listItems = numbers.map((number)=>
<li key={numbers.toString()}>
{numbers}
</li>
);
选择关键字最好的方法是用字符串,字符串要具有唯一性。大部分使用的关键字就是你数据的ID,代码如下:
const todoItems = todo.map((todo)=>
<li key = {todo.id}>
{todo.text}
</li>
);
但如果你所渲染的列表项没有固定ID的话,你可以使用列表项的索引作为Key,代码如下:
const todoItems = todos.map((todo,index)=>
//Only do this if items have no stables IDs
<li key={index}>
{todo.text}
</li>
);
我们并不推荐使用索引作为关键字,因为列表项的循序可能发生改变。这会产生不好的影响并可能对组件的状态产生问题。但是如果你对列表项不选择一个严格的关键字,React会默认把项索引作为关键字使用。
具体请参考下面的文章:
in-depth explanation about why keys are necessary
按关键字选组件
关键字仅仅对围绕数组的上下文有意义。换句话说,关键字只对一个列表有意义,而对列表中的单一项没有意义。
例如,如果你要选一个ListItem组件,你应该在<ListItem />元素中保留关键字,而不是<li>元素中。代码如下:
错误的代码
function ListItem(props){
const value = props.value;
return(
//Wrong!There is no need to specify the Key here:
<li key = {value.toString()}>
{value}
</li>
);
}
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map((number)=>
//Wrong! The Key should have been specified here:
<ListItem value = {number} />
);
return(
<ul>
{listItems}
</ul>
);
}
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers={numbers} />
document.getElementById('root');
);
正确的代码:
function ListItem(props){
//Correct!There is no need to specify the key here:
return <li>{props.value}</li>
}
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map((number)=>
//Correct! Key should be specified inside the array.
<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')
);
记住简单规则:在map()函数中声明关键字
关键字必须‘局部’唯一
数组中的关键字在遍历该数组时必须显示其唯一性。而不必在全局是唯一的。在不同的数组中可以用相同的关键字,代码如下:
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')
);
关键字是作为线索为React提供服务的,但不能从组件中获取。如果在组件中你需要相同的值,可以通过props传递这个值,但必须命名为不同的名字,代码如下:
const content = posts.map((post)=>
<Post
key = {post.id}
id = {post.id}
title = {post.title} />
);
在上面的代码中,Post组件可以读取props.id,但不能读取props.key。
在JSX中嵌入map()
在上面的例子中,我们声明了一个独立的listItems变量并在JSX中包含了它,代码如下:
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>
);
}
有时候这样写代码会更简洁,但是这种代码风格不要滥用。代码终归是要有可读性的,哪种可读性好用哪种。
9. 表单
在React中,HTML表单的作用和其他DOM不同,表单需要保留一些外部状态。例如下面的代码是表单接收一个单一姓名:
<form>
<label>
Name:
<input type = "text" name = "name" />
</label>
<input type = "submit" value ="Submit" />
</form>
上面代码中,表单的作用就是向服务器提交数据,当用户提交表单时,服务器接收到表单数据,然后响应并产生一个新的页面。如果在React中,这仍然是有效的。在大多数情况下,用js函数来处理表单的提交和访问用户键入表单的数据是很方便的。而达到这种效果的标准方法有一个技术名称,叫“被控组件”。
被控组件
在HTML中,表单元素比如<input>、<textarea>和<select>,都会保持自己的状态并根据用户的输入来更新状态。而在React中,易变的状态是保存在组件的状态属性中,并只能通过setState()来更新。
我们可以通过把React状态作为“唯一信源”来将二者合并。而后渲染表单的React组件当用户数据数据时,也能控制表单发生了什么。我们把这种值被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状态成为“唯一信源”。当每次敲键盘运行handleChange来更新React状态时,被显示的值就更新到用户输入的值。
对于一个被控组件,每次状态变化都和handler函数有关。handler函数能够直接改变用户输入或使用户输入有效。例如,如果我们想要强迫姓名输入写成大写字母,我们可以把 handleChange写成这样:
handleChange(event){
this.setState({value:event.target.value.toUpperCase()});
}
文本域标签
在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 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>
Essay:
<textarea value = {this.state.value} onChange = {this.handleChange} />
</label>
<input type="submit" value = "Submit" />
</form>
);
}
}
注意,this.state.values是在构造体中被初始化的,所以一开始text域中会有显示内容。
<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中,不再使用selected属性,而是在根<select>标签中使用value属性。这在被控组件中更方便,因为你只在一个地方更新就可以了,例如:
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:
//look at this , it is awesome!
<select value = {this.state.value} onChange = {this.handleChange}>
<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属性,用来实现一个被控组件。
注意
你可以传递一个数组到value属性中,允许你在<select>标签中选择多个选项:<select nultiple={true} value={['B','C']}>
文件输入标签
在HTML中,<input type="file">让用户从设备存储中选择一个或更多文件上传到服务器中,或者通过js来进行文件操作。
<input type = "file" />
在React中,<input type="file" />和普通的<input/>很相似,但有一点很重要的不同:它是只读的(你不能以编程的方式设置其值)。而是要用文件相关API来与这些文件交互。
下面这个例子显示了一个ref怎样被用来访问文件的,代码如下:
class FileInput extends React.Component{
constructor(props){
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
);
}
handleSubmit(event){
event.preventDefault();
alert(
'Selected file - ${
this.fileInput.files[0].name
}'
);
}
render(){
return(
<form
onSubmit={this.handleSubmit}>
<label>
Upload file:
<input
type="file"
ref={input => {
this.fileInput = input;
}}
</label>
<br />
<button type = "submit">
Submit
</button>
</form>
);
}
}
ReactDOM.render(
<FileInput />,
document.getElementById('root')
);
多输入处理
当需要处理多个被控 input元素时,可以为每一个元素增加name属性并让handler函数选择基于不同的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>
<br />
<label>
Number of guests:
<input
name = "numberOfGuests"
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()自动将部分状态与当前状态比对,因此我们只需要调用改变的部分。
被控输入为NULL值
为被控组件指定props值可以防止用户改变输入。如果你希望指定一个value但输入仍然是可编辑的,那么你可以将value设置为NULL或未定义。
下面的代码说明了这一点。(输入一开始是锁定的,但短暂延迟后变得可编辑。)
ReactDOM.render(<input value = "hi" />, mountNode);
setTimeOut(function(){
ReactDOM.render(<input value={null} />,mountNode);
},1000);
被控组件的取舍
有时候被控组件是很乏味的,因为你必须为每一种可能导致你数据变化的方法写event handler,并且通过React组件输送所有的输入状态。当你要把一个之前的代码库转为React,或者将React应用集成到一个非React库中时,这就变得非常让人懊恼。在这种情况下,你可以使用非控组件,一种可选的实现输入表单的技术。