本笔记基于React官方文档,当前React版本号为15.4.0。
1. 安装
1.1 尝试
开始之前可以先去codePen尝试一下,也可以下载这份HTML文件并编辑它来尝试React。
1.2 Creat React App工具
推荐使用React官方提供的Creat React App工具,来快速新建React单页面应用项目。
npm install -g create-react-app
create-react-app hello-world
cd hello-world
npm start
1.3 推荐工作流
虽然React可以在没有任何构建工具的情况下进行使用,但在生产环境还是应该使用成套的构建工具来将React用于你的项目。一个现代化的(前端)工作流通常由以下三部分组成:
- 包管理器:比如Yarn或Npm,可以让你更方便使用第三方库而不用自己造轮子
- 编译器:比如Babel,能翻译使用了最新语法的代码到浏览器兼容较好的版本
- 打包器 :比如Webpack或Browserify,让你能够编写各种风格的模块化的代码,由它们打包和压缩
基于以上工作流,你可以通过Npm或者Yarn来将React安装到项目,然后使用Babel来编译JSX和ES6语法,最终用于生产环境的代码还需要经过Webpack或Browserify的打包和压缩才能使用。
1.4 CDN服务
<!--开发环境-->
<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
<!--生产环境-->
<script src="https://unpkg.com/react@15/dist/react.min.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.min.js"></script>
2. Hello World
一个最基本的React例子:
ReactDom.render(
<h1>Hello world!</h1>,
document.getElementById('root')
)
你可以在CodePen上尝试修改这段代码看看效果。
React推荐配合ES6语法使用,但仅需要了解() => {}
、const
、let
、`template literals`
和classes
这几个特性即可
3. 初识JSX
const element = <h1>hello world</h1>
上面这段既不是字符串又不是HTML的代码(其实主要指的是<h1>hello world</h1>
)就是JSX了。官方推荐搭配使用JSX,有别于模板语言,JSX是全功能的JavaScript。JSX 用于创建“React元素”。
3.1 JSX是表达式
跟其他JavaScript表达式一样,JSX也是表达式,被React编译后的JSX返回的是普通的JavaScript对象,这意味着你可以类似对待普通JavaScript表达式那样对待一个JSX语句:将它赋值给变量、将他作为函数参数或返回值等等:
function getGreating (user) {
if (user) {
return <h1>hello {formatName(user)}!</h1>
}
return <h1>hello world!</h1>
}
稍微深入一点,Babel会将JSX转换成对react.creatElement()
的调用,所以下面两种写法完全等价:
// JSX
const mine = (
<h1 className="greeting">
这是我的标题
</h1>
)
// javaScript
const yours = react.creatElement(
'h1',
{ className: 'greeting ' },
'这是你的标题'
)
然而react.createElement()
返回的结果是类似下面这样的一个对象:
const element = {
type: 'h1',
props: {
className: 'greeting',
children: '这是谁的标题'
}
// ...
}
这就不难理解JSX的用法了——像一个javaScript表达式那样去使用。
3.2 在JSX中嵌入JavaScript表达式
使用花括号{}
,可以在JSX中嵌入任意JavaScript表达式:
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
为了提升可读性可以对JSX使用缩进和换行,但是为了避免JavaScript自动添加分号的机制给我们带来麻烦,应该在换行的JSX外面添加一对小括号。
在JSX的元素中插入用户输入的内容是安全的,React默认会对元素内的文本进行转义以防止XSS攻击。
3.3 在JSX中声明属性
就像在HTML中声明元素属性,可以在“React元素”上直接声明某个属性。当希望属性值是变量或引用时,则就像在在JSX中嵌入JavaScript表达式,使用花括号{}
来插入“React元素”的值。
// 简单属性值
const element = <div tabIndex="0"></div>;
// 属性值为变量或引用
const element = <img src={user.avatarUrl}></img>;
需要注意的是,JSX中元素的属性名统一使用驼峰写法(camelCase),并且在React的内置元素上,诸如
class
、for
等属性还需要换成className
和htmlFor
来使用(自定义元素可以正常使用)。
3.4 在JSX中声明子元素
如果“React元素”的标签内没有子元素,则可以像在XML中那样使用单标签(包括React内置的HTML元素)。
const element = <img src={user.avatarUrl} />;
如果存在子元素,则就像在HTML中那样直接包裹在父元素中即可(注意换行的JSX要加小括号()
):
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
)
4. 渲染元素
元素是React应用的最小组成部分。元素描绘了界面。不同于浏览器的DOM元素,React元素是简单对象,创建它们比创建真实的DOM元素要节省太多性能,同时React DOM负责将React元素和真实DOM元素对应起来:
const ele = <h1>Hello World!</h1>
不能将React元素和React组件搞混,React元素是React组件的组成部分,一个React组件由一个或多个React元素组成。同时也要注意区别DOM元素和React元素,DOM元素指的是HTML标准中规定的具体的某个元素,而React元素实际上是用于告诉React如何渲染页面、渲染时用到哪些DOM元素的一个配置对象,它与DOM元素不是一个概念。
4.1 将React元素渲染到DOM中
先创建一个React元素,然后用ReactDOM.render()
将其渲染到DOM的某个元素中(就这么简单):
const ele = <h1>Hello World!</h1>
ReactDOM.render(
ele,
document.getElementById('root') // 假设页面上有一个id为root的元素
)
4.2 更新已经渲染的元素
请记住,React元素是不可变的,一旦创建,你就不能再直接改变它的属性或子元素。假如我们要更新上面已经渲染到id
为root
的元素中的React元素,那么在没有其他手段的前提下就只能是像电影胶片一样一帧一帧进行刷新:
function tick() {
const element = (
<div>
<h1>Hello World!</h1>
<p>{new Date().toLocaleTimeString()}</p>
</div>
)
ReactDOM.render(
ele,
document.getElementById('root') // 假设页面上有一个id为root的元素
)
}
setInterval(tick, 1000) // 每秒刷新
当然正常情况下我们不会这么做,但是这里很好的演示了另外一个问题——React在渲染页面时都做了什么?答案是它只渲染了与上次渲染时DOM中不同的部分!React会比较当前渲染与上次渲染时DOM中的不同之处,并只刷新这些地方!
[图片上传失败...(image-89394-1535353259915)]
5. 组件和props
(输入属性)
组件能让你将UI分割成独立的可复用的片段,这些片段都有各自隔离的作用域,不会互相干扰。你可以将组件理解成类似函数的概念,组件从它的props
属性接受参数,然后返回React元素来描述UI。
5.1 用函数和类(class
)定义组件
最简单的定义组件的方式就是写一个构造函数:
function Welcom (props) {
return <h1>hello, {props.name}</h1>
}
上面这个Welcom
构造函数就是一个合法的React组件,因为它接受一个对象作为参数,然后返回React元素。我们称这样的组件为“函数式”的组件因为它就是一个JavaScript构造函数。当然也可以使用ES6的class
特性来定义函数:
class Welcom extends React.Component {
render () {
return <h1>hello, {this.props.name}</h1>
}
}
ES6的class
特性其实是ES5的构造函数和对象继承特性的一个语法糖,上面的写法也完全可以转换为ES5的写法。React推荐这种写法存粹是因为写起来方便,可读性也更强。但这种写法的重点是从React.Component
继承一些核心的属性,后文还会细说。不过目前简单起见,我们暂时还只是用简单函数来创建组件。
5.2 渲染组件
React元素不仅仅可以用于指定需要使用的DOM元素,也可以用于指代自定义的组件:
// 指代需要使用的DOM元素
const ele1 = <div />
// 指代用户自定义的组件
const ele2 = <Welcom name="Sara">
当React遇到像<Welcom name="Sara">
这种自定义组件时,它会将JSX属性(也就是React元素属性)都放在一个对象中(这个对象就是props
)并将其传递给组件的构造函数,构造函数再返回React元素用于渲染。
5.3 组件的组合
既然React元素可用于指代自定义组件,那么组件之间就可以相互嵌套使用:
function Welcom (props) {
return <h1>Hello, {props.name}</h1>
}
function WelcomList () {
return (
<div>
<Welcom name="Sara" />
<Welcom name="Lily" />
<Welcom name="Tom" />
</div>
)
}
function App () {
return <WelcomList />
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
5.4 组件的提取
既然组件可以嵌套组合使用,我们就可以将一个大的组件分割成很多小的组件。React官方鼓励对UI进行切割,分成不同的组件来实现。基本上一组React元素是否要提取成组件,可从以下两点考虑:
- 这组元素在别的地方也要使用
- 这组元素内部的功能相对复杂
这部分其实是组件化的思路,这里不再展开。
5.5 只读的props
类似于“纯函数”的概念(不会改变任何外部的值,包括输入的参数,即与外部完全无耦合),不管是使用构造函数还是类来定义组件,组件都不应该修改它的props
,因为这是输入到组件中的参数。在这一点上,React做了严格限定:
所有的React组件必须像“纯函数”那样永远不修改自己的
props
属性
6. state
(私有状态)和生命周期
我们以上文的时钟的例子来理解组件的私有状态和生命周期。
function tick() {
const element = (
<div>
<h1>Hello World!</h1>
<p>{new Date().toLocaleTimeString()}</p>
</div>
)
ReactDOM.render(
ele,
document.getElementById('root') // 假设页面上有一个id为root的元素
)
}
setInterval(tick, 1000)
首先我们将时钟作为组件提取出来:
// 时钟组件
function Clock(props) {
return (
<div>
<h1>Hello World!</h1>
<p>{props.date.toLocaleTimeString()}</p>
</div>
)
}
// 重新渲染
function tick () {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root') // 假设页面上有一个id为root的元素
)
}
// 每秒刷新
setInterval(tick, 1000)
我们发现对于Clock
组件来说,刷新时间的功能其实完全与外部无关,它不涉及到任何外部的变量,完全可以由Clock
组件自己来实现而不是让外部传递时间给它。此时Clock
组件就需要“私有状态”来实现这个功能了。
6.1 从React.Component
上继承
到目前为止,我们使用简单的构造函数来创建React组件,不管外部输入属性还是私有状态,都需要我们手动创建和管理,诸如修改私有状态后刷新渲染,外部输入属性为只读这类功能,如果我们没有在构造函数中手动实现则不会存在。
这时我们可以从React.Component
这个React内置的构造函数上继承一些有用的方法,这其中就包括对“私有状态”和“生命周期”实现。我们可以使用ES6的class
特性来实现这个继承(当然这不是必须的,完全可以使用ES5的构造函数和原型的写法,但那样会繁琐很多,可读性也大大下降):
class Clock extends React.Component {
render () { // React提供的用于渲染和刷新组件的钩子函数
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
)
}
}
6.2 定义组件私有状态
React.Component
提供了props
和state
来分别访问外部输入属性和内部私有状态。我们可以在时钟组件中通过state
访问私有状态,然后在其构造函数中对该私有状态进行初始化,最后将它渲染到页面上:
class Clock extends React.Component {
constructor (props) {
super(props) // ES6中类的constructor函数可以通过super访问其父类的构造函数
this.state = { date: new Date() }
} // 注意,ES6中类的方法之间不需要任何符号
render () {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
)
}
}
ReactDOM.render(
<Clock />, // 外部不再干涉Clock组件的刷新功能
document.getElementById('root)
)
注意
Clock
类中的constructor
构造函数中,调用了父类的构造函数,这是为了实现完全的继承。使用class
特性创建React组件时应当总是执行这一步。
6.3 添加生命周期函数
从组件被创建到组件被渲染到页面到最终被销毁,React提供了一系列的“生命周期钩子”,用于在组件的不同阶段调用回掉函数。为了让Clock
组件能够自己刷新,我们希望在组件被创建后立即添加一个计时器进行每秒刷新,同时在组件被销毁时一并销毁这个计时器,这样我们就需要用到两个生命周期钩子函数:
-
componentDidMount
:组件被渲染到页面后执行 -
componentWillUnmount
:组件被销毁前执行
class Clock extends React.Component {
constructor (props) {
super(props) // ES6中类的constructor函数可以通过super访问其父类的构造函数
this.state = { date: new Date() }
} // 注意,ES6中类的方法之间不需要任何符号
render () {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
)
}
componentDidMount () {
this.timerID = setInterval(this.tick.bind(this), 1000)
}
componentWillUnmount() {
clearInterval(this.timerID)
}
}
ReactDOM.render(
<Clock />, // 外部不再干涉Clock组件的刷新功能
document.getElementById('root)
)
注意我们将定时器存储在了组件实例上,而不是
state
中,请先记住一个原则:任何没有在组件的render()
函数中使用的变量,都不应该存放在state
中
然后再添加tick
方法。在这个方法中我们需要改变组件state
中的date
的值,这时需要用到方法setState()
,该方法会通知React现在state
已经改变了,而后React会去重新调用组件的Render()
方法刷新DOM。这也是为什么会有**任何没有在组件的render()
函数中使用的变量,都不应该存放在state
中 **一说:
class Clock extends React.Component {
constructor (props) {
super(props) // ES6中类的constructor函数可以通过super访问其父类的构造函数
this.state = { date: new Date() }
} // 注意,ES6中类的方法之间不需要任何符号
render () {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
)
}
componentDidMount () {
this.timerID = setInterval(this.tick, 1000)
}
componentWillUnmount() {
clearInterval(this.timerID)
}
tick () {
this.setState({ date: new Date() }) // 该方法会触发React调用实例的render方法进行重绘
}
}
ReactDOM.render(
<Clock />, // 外部不再干涉Clock组件的刷新功能
document.getElementById('root)
)
6.4 组件生命周期小结
- 当把组件传递给
ReactDOM.render()
函数后,React会调用组件的构造函数constructor
,进行一些初始化
- 当把组件传递给
- 然后React会去调用
Clock
组件的render()
方法将组件渲染出来
- 然后React会去调用
- 当组件渲染完毕后,React会调用
componentDidMount()
生命周期钩子函数
- 当组件渲染完毕后,React会调用
- 当
setState()
函数被调用时,React会重新调用组件的render()
方法进行重绘
- 当
- 当组件被从DOM中移除时,React会调用
componentWillUnmount()
生命周期钩子函数
- 当组件被从DOM中移除时,React会调用
6.5 setState
注意事项
-
不要直接改变
state
直接对组件state
中的属性赋值将不会触发DOM更新,因为React并不知道state
被改变了 -
state
的更新可能是异步的
React会一次处理多个对setState
的调用以提高性能,所以调用setState()
时不应当直接基于另外一些来自state
或props
中的属性进行计算,很有可能当前计算的值并不是最终的值,当用于计算的另一些值再次变化后,React并不会刷新DOM(因为没有再次调用setState()
)。为了修正这点,React提供另一种调用setState()
函数的方式:传入一个函数,而不是对象
// 错误的用法
this.setState({
counter: this.state.counter + this.props.increment
})
// 正确的用法
this.setState((prevState, props) => ({ // 接受一个表示前次state的参数和一个当前props的参数
counter: prevState.counter + props.increment // 这里实际上是返回了一个对象,是ES6箭头函数的简写
}))
-
setState
是对象的合并而不是替换
setState
方法是将传入的参数对象或函数返回的对象与现有的state
对象进行合并,非常类似于使用Object.assign(prevState, newState)
的效果
6.6 单项数据流
在React组件的嵌套中,父组件通过props
向子组件传递数据,不管传递进来的数据是来自于父组件的props
还是state
还是别的地方,子组件不知道也不用关心,因为它不能修改通过props
传递进来的数据而只能读取它。这样,数据就可以从最外层的父组件一路向内传递下去,但反过来却不行。
这就是传说中的“单项数据流”("top-down" or "unidirectional" data flow)了:每个组件只能修改本身和其子组件的数据,而不能修改父组件的数据。这样的好处不言而喻,数据和状态的管理会更加方便,但有时候在应用越来越复杂的时候,可能需要多个组件共享某些数据或状态,因此诞生了很多用于管理数据和状态的库,redux就是其中最有名的一个。
7. 事件
7.1 基本用法
在React中绑定事件跟直接在HTML中绑定事件非常相似,定义一个事件处理函数,并在JSX中绑定它:
function Greeting () {
function sayHi(e) {
e.preventDefault()
console.log('Hi!')
}
return (
<a onClick={sayHi}>Click me to say hi!</a>
)
}
所有事件绑定属性比如onClick
均使用驼峰写法(camelCase),事件绑定属性的值不是字符串而是事件处理函数名称,可以带上()
并传参,无参数时可省略()
;
7.2 使用类定义组件时事件处理函数this
的指向问题
使用ES6的class
特性定义组件时,通常的做法是将事件处理函数当作该类的方法写在类中。但需要注意的是方法的this
指向。
定义在类中的方法的默认的this
指向的是当前的类的实例,但事件处理函数因为是绑定到了具体的元素上,就会丢失定义时this
的指向。如果你的处理函数中使用了this
关键字来指向当前组件实例,那么你需要手动将该方法的this
绑定到当前组件实例,有三种方法可以进行绑定:
1)在类的constructor中调用或在JSX中调用Function.prototype.bind()
手动绑定
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
this.handleClick = this.handleClick.bind(this); // 手动绑定
}
handleClick() {
// console.log(this)
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
// <button onClick={this.handleClick.bind(this)}> // 在这里绑定也可以
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('content')
);
2)在JSX的事件绑定属性中的事件处理函数外层再套一个箭头函数,在其中返回处理函数调用结果
render() {
return (
<button onClick={(e) => this.handleClick(e)}> // 这么绑定也行
Click me
</button>
);
}
3)Babel提供的一个ES8+的实验性质的写法
class LoggingButton extends React.Component {
handleClick = () => { // 纯粹的实验性质的写法,需要babel的支持
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
7.3 事件对象
React的事件对象是一个完全由React给出的事件对象,该对象对各个浏览器做了兼容,同时保留了标准事件对象的接口,详细信息可以查看React官网的参考。使用时需要关心的是如何在事件处理函数中使用事件对象。
在事件绑定的JSX中,处理函数接受一个名为event
的参数来表示事件对象,可以认为event
在事件绑定插值中属于React的保留字,如果需要往事件处理函数中传递更多参数,请使用其他标识符。
另外,7.2小节中不同的事件绑定写法也对事件对象的处置略有不同,主要体现在事件绑定JSX中:
// 无括号
<button onClick={this.handleClick}>
Click me
</button>
// 带括号
<button onClick={this.handleClick(event)}>
Click me
</button>
// 调用了bind()
<button onClick={this.handleClick.bind(this, event)}>
Click me
</button>
- 当事件绑定插值中的处理函数省略了
()
时,处理函数默认接受一个表示事件对象的参数, - 当事件绑定插值中的处理函数未省略
()
时,则需要显示地使用保留字event
来传入事件对象,未传入则为undefined
;注意,不管有没有在constructor
中绑定this
,直接在处理函数名后加()
会导致页面初始化时该函数被立即执行一次,可能会有意想不到的错误,比如不能调用setState()
方法等,所以强烈不建议用这种写法 - 当事件绑定插值中的处理函数调用了
bind()
时,可以显示地使用保留字event
来传入事件对象,否则React会在bind()
函数参数序列的末尾默认增加一个表示事件对象的参数
最后,在React中不能通过return false
来阻止默认事件,而是需要在事件处理函数中显式调用event.preventDefault()
。
8. 条件渲染
所有的JavaScript条件语句都可以用于React条件渲染,因为本质上JSX就是JavaScript的扩展语言。基于此有三种常用的条件渲染:
if...else...
function UserGreeting () {
return <h1>Welcom back!</h1>
}
function GuestGreeting () {
return <h1>Please Sign up.</h1>
}
function App (props) {
if (!props.isLoggedIn) {
return <GuestGreeting />
}
return <UserGreeting />
}
ReactDOM.render(
<App isLoggedIn={false} />,
document.getElementById('root')
)
- 三元运算符
function App (props) {
return props.isLoggedIn ? <UserGreeting /> : <GuestGreeting />
}
- 短路
function App (props) {
return props.isLoggedIn && <UserGreeting /> // props.isLoggedIn为true则显示UserGreeting,否则不显式
}
如果判断逻辑比较复杂,不能用三元或者短路表达式编写,且判断后的结果需要直接用在JSX中(JSX中只能通过{}
插入表达式,而不能使用语句),则可使用if...else...
语句判断并将结果保存到变量,然后再返回变量或通过{}
插值到JSX中:
function UserGreeting () {
return <h1>Welcom back!</h1>
}
function GuestGreeting () {
return <h1>Please Sign up.</h1>
}
function Button (props) {
return <button onClick={ props.handleToggle }>toggle me</button>
}
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
prevState: false
}
}
handleClick () {
this.setState(prevState => ({ isLoggedIn: !prevState.isLoggedIn }))
}
render () {
let greeting = this.state.isLoggedIn ? <UserGreeting /> : <GuestGreeting />
return (
<div>
<div><Button handleToggle={this.handleClick.bind(this)} /></div> // 注意this的重定向
{ greeting }
</div>
)
}
}
ReactDOM.render(
<App isLoggedIn={false} />,
document.getElementById('root')
)
另外,在组件的render
函数中返回假值,会阻止组件渲染,结合条件判断,能够达到隐藏或显示组件的目的。
9. 列表和key
(索引)
9.1 渲染列表
可以像下面这样渲染一个列表:
class List extends React.Component {
constructor (props) {
super(props)
}
render () {
let list = this.props.number.map(number => ( // 拼装li
<li>{number}</li>
))
return (
<ul>{list}</ul>
)
}
}
ReactDOM.render(
<List number={[1, 2, 3, 4, 5]} />,
document.getElementById('root')
)
也可以将map()
调用通过{}
内联到JSX中:
class List extends React.Component {
constructor (props) {
super(props)
}
render () {
return (
<ul>{
this.props.number.map(number => ( // 内联map()方法
<li key={number}>{number}</li>
))
}</ul>
)
}
}
通常会使用数组的map()
方法来从数组拼装列表,这与使用JavaScript拼装HTML类似。但上面的代码运行时会出现警告:
9.2 key
在渲染列表时,React的差异比较算法需要一个在列表范围内的唯一key
来提高性能(通常用于获知哪个列表项改变了)。这个唯一的key
需要我们手动提供。React官方建议使用列表数据中可用于唯一性标识的字段来作为列表项渲染时的key
。如果实在没有,则可使用数组的index
勉为其难,性能上可能会打折扣。
let list = this.props.number.map(number => ( // 拼装li
<li key={number.toString()}>{number}</li>
))
key
的使用需要注意一下几点:
-
只能在数组内指定
key
:准确地说,只能在map()
的回调函数中使用key
-
key
需要在列表范围内保证唯一性:同一个数组中的key
需要保证唯一性,但不同数组中的key
无所谓 -
key
不会作为props
传入组件:可以认为key
是React在JSX中的保留字,你不能用它来向组件传递数据而应该改用其他词
10. 表单
在React中存在一个“受控组件(Controlled Component)”的概念,专门指代被React控制了的表单元素。通过onChange
事件的处理函数将表单元素值的变化映射到组件的state
中,然后再将组件中的这个映射好的值通过{}
在JSX中插值给表单元素的value
,(二者缺一不可)这就是一个被React控制了的组件也即“受控组件”了。
class Form extends React.Component {
constructor (props) {
super(props)
this.state ={
inputTextValue: ''
}
this.handleInputTextChange = this.handleInputTextChange.bind(this)
}
render () {
return (
<form>
<input
value={this.state.inputTextValue} // 从state中将值绑定到表单元素
onChange={this.handleInputTextChange}/>
</form>
)
}
handleInputTextChange (e) {
this.setState({
inputTextValue: e.target.value // 将表单元素的值的变化映射到state中
})
}
}
ReactDOM.render(
<Form />,
document.getElementById('root')
)
基本上所有表单元素的使用都跟上例一样,通过value
来“控制”元素,让state
成为组件唯一的状态保存地。但是有时候在非React项目中使用React或者一些其他原因,我们不希望使用受控组件时,可以选择“非受控组件”技术,这里不再展开。
11. 共享状态提升
考虑下面的需求,页面上有两个输入框,用来输入货币数量,一个输入美元,一个输入人民币,还有一行提示文字例如:“我们有1美元,也就是6.9元”;要求两个输入框随意输入一个,另一个输入框会根据汇率自动显示转换后的货币数量,并且下方提示文字也跟随变化。
通常情况下,我们会编写一个用于输入货币数量的组件,然后在页面上放两个这样的组件:
const exchangeRate = 6.9339
const currency = {
'$': '美元',
'¥': '人民币'
}
class CurrencyInput extends React.Component {
constructor (props) {
super(props)
this.state = {
value: ''
}
this.changeHandler = this.changeHandler.bind(this)
}
render () {
return(
<div>
<label>
{currency[this.props.currency]}:
<input value={this.state.value} onChange={this.changeHandler}/>
</label>
</div>
)
}
changeHandler (e) {
this.setState({
value: e.target.value
})
}
}
class App extends React.Component {
constructor (props) {
super(props)
}
render () {
return(
<div>
<CurrencyInput currency={'$'}/>
<CurrencyInput currency={'¥'} />
<p>我们有{}美元,也就是{}元</p>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
在上面的代码中我们将货币种类通过props
传递给输入框组件,分别显示了美元和人名币的输入框。然后在输入框组件内部,我们使用了上一节的“受控组件”技术,将输入框的值交由组件的state
控制。但并没有完成需求——两个输入框并不同步,同时组件外部也不知道组件中到底填了什么值所以下面的提示语句也没有更新。
很多时候,若干组件需要隐射同一个变化的状态。我们推荐将共享的状态提升至它们最近的共同的祖先上。
就像官方推荐的那样,这时我们就需要用到共享状态提升技术:我们要将两个货币输入框组件共享的“数量”状态,提升到它们最近的祖先组件上,也就是App
组件上。
// ...省略的代码
class CurrencyInput extends React.Component {
constructor (props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
render () {
return(
<div>
<label>
{CURRENCY[this.props.currency]}:
<input value={this.props.value} onChange={this.handleChange}/> // 需要传递额外参数的情况下只能再包一层
</label>
</div>
)
}
handleChange (e) {
this.props.onValueChange(e.target.value, this.props.currency) // 父级传递进来的回调函数
}
}
class App extends React.Component {
constructor (props) {
super(props)
this.state = { // 将共享状态存放在祖先元素上
dollar: '',
yuan: ''
}
this.valueChangeHandler = this.valueChangeHandler.bind(this)
}
render () {
return( // 通过props向下传递共享状态和回调函数,很多情况下子组件共享的状态父级也需要用到
<div>
<CurrencyInput value={this.state.dollar} currency={'$'} onValueChange={this.valueChangeHandler}/>
<CurrencyInput value={this.state.yuan} currency={'¥'} onValueChange={this.valueChangeHandler}/>
<p>我们有{this.state.dollar}美元,也就是{this.state.yuan}元</p>
</div>
)
}
valueChangeHandler (value, type) {
this.setState({
dollar: type === '$' ? value : this.exchange(value, type),
yuan: type === '¥' ? value : this.exchange(value, type)
})
}
exchange (value, type) {
return value * (type === '$' ? EXCHANGERATE : 1 / EXCHANGERATE)
}
}
// ... 省略的代码
其实不管是美元还是人民币,其实背后都只有一个数量,这个数量同时代表了一定数量的美元和一定数量的人民币,所以更好地,我们可以也应该只存放一个状态在父组件上,然后在渲染子组件时计算子组件的状态并传递给他们:
// ... 省略的代码
function exchange (value, type) { // 将转换函数放到全局以便子组件可以访问
return value * (type === '$' ? EXCHANGERATE : 1 / EXCHANGERATE)
}
class CurrencyInput extends React.Component {
// ... 省略的代码
render () {
// 子组件在渲染时自己计算自己的状态
let currentCurrency = this.props.currentCurrency
let currency = this.props.currency
let value = ''
if (currentCurrency.value !== '' && !/^\s+$/g.test(currentCurrency.value)) {
value = currentCurrency.type === currency ?
currentCurrency.value :
exchange(currentCurrency.value, currentCurrency.type)
}
return(
<div>
<label>
{CURRENCY[currency]}:
<input value={value} onChange={this.handleChange}/>
</label>
</div>
)
}
// ... 省略的代码
}
class App extends React.Component {
constructor (props) {
super(props)
this.state = {
currentCurrency: { // 存储一个值,这里具体做法时存储当前改变的值
value: '',
type: ''
}
}
this.valueChangeHandler = this.valueChangeHandler.bind(this)
}
render () {
// 将共享的状态传递给组件,同时父组件需要的状态也自己计算出来
return(
<div>
<CurrencyInput
currentCurrency={this.state.currentCurrency}
currency={'$'}
onValueChange={this.valueChangeHandler}/>
<CurrencyInput
currentCurrency={this.state.currentCurrency}
currency={'¥'}
onValueChange={this.valueChangeHandler}/>
<p>我们有{exchange(this.state.currentCurrency.value, '$')}美元,也就是{exchange(this.state.currentCurrency.value, '¥')}元</p>
</div>
)
}
valueChangeHandler (value, type) { // 这里只需要简单映射关系即可,不再需要计算各个组件的具体状态值
this.setState({
currentCurrency: { value, type }
})
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
上面的例子很好地贯彻了React官方反复强调推荐的“单项数据流”模式。虽然多写了一些代码,但是好处是可以减少因为子组件可以自行修改共享状态而引起的一些bug,毕竟我们将共享状态提升到父级组件上以后,所有对共享状态的修改就都集中在父级组件上了。
另外,再次强调一个原则:任何可以由state
或props
计算出来的状态,都不应该放在state
中。就像上例那样,应该直接在render()
函数中直接计算后使用。
12. 聚合而不是继承
React官方推荐使用聚合而不是继承来在组件之间复用代码。通常有两种服用的情况,一种是组件的部分结构或内容不确定,需要由外部传入,这时组件就相当于一个容器;另一种是从更为抽象的组件创建一个较为具体的组件,比如“弹层”和“登陆弹层”。
12.1 容器
当组件内有部分内容不确定需要外部传入时,可以使用一个特殊的props
属性children
来传入。在组件内部访问props.children
可以获取使用组件时写在组件开始和结束标签内的内容:
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
当组件有多个部分内容不确定都需要外部传入时,单靠props.children
就不能满足需求了。但时不要忘记React组件的props
可以接受任意类型的参数,所以其实组件的内容也完全可以直接使用props
来传递到组件内部:
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() { // JSX中使用{}插入另一个JSX,因为JSX也是表达式
return <SplitPane left={ <Contacts /> } right={ <Chat /> } />
}
12.2 具象化
有时我们希望一个组件是另一个较为抽象的组件的特例(更为具象),官方推荐的做法是将抽象组件包裹在具象组件中,并使用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!" />
);
}
至于继承...忘掉它吧。
13. Think in React (这部分直接翻译的原文)
在React官方看来,React是构建大型、高性能web应用的不二之选。它在Fb和Ins表现得非常好。React最棒呆的地方在于它让你在构建应用时如何看待你的应用。下面给我们通过编写一个搜索列表,来带你体验这个思维过程。
13.1 从效果图开始
假设我们已经有了一个JSON接口,并有了设计师给我们的效果图:
JSON接口返回的数据格式如下:
[
{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"}
]
13.2 第一步:将UI按照组件层级进行分解
要做的第一件事就是在效果图上的组件(和子组件)周围画框框,并命名组件。如果这是你的设计师同事给你的,那么这部分工作他可能已经做完了,去和他唠唠。他的PSD图层名很有可能可以作为你的组件名。
但具体怎么划分组件呢?答案是跟你创建一个函数或对象一样。这其中的一个原则是“单一职责原则”,具体来说就是一个组件应该只做一件事,否则,它应该被拆分成更多的子组件。
如果你经常将JSON数据展现给你的用户,那你应该知道如果你创建了正确的数据模型,你的UI(和组件)将会规划组织的非常好。因为你的UI和数据模型是同一个信息结构,这也意味着划分组件是一件比较繁琐的事情。就将你的组件按照JSON返回的数据结构拆分为就好了。
(未完待续,这是React官方基础教程的最后一章,有空我再继续翻译吧)
(2018年7月27日更新......天道好轮回,苍天饶过谁......最终还是要写react,时隔快2年了,我自己也得回来看文章复习一波...索性把坑填上吧!)
你会看到这个简单的应用中有5个组件。我们将每个组件所代表的数据用斜体表示。
- FilterableProductTable(橙色):示例的全部内容
- SearchBar(蓝色):接受 用户输入
- ProductTable(绿色):基于用户输入显示过滤好的 产品列表
- ProductCategoryRow(青色):为每个 类目 显示一个标题
- ProductRow(红色):为每个 产品 显示一行
ProductTable
的表头(包含Name
和Price
标签)不是一个单独的组件。这是个偏好问题,不管哪种方式都还存在争议。这个例子中,我们将其留在了ProductTable
中,因为ProductTable
的任务是渲染 产品列表 ,而表头也算列表的一部分。然而,当这个表头变得复杂了(比如当我们添加了排序功能),就可以顺理成章地将其抽出来成为ProductTableHeader
组件。
既然现在我们已经将组件都识别出来了,那就把他们结构化一下吧。这很easy。效果图中出现在另一个组件内部的组件,在结构上应该作为前者的子组件:
-
FilterableProductTable
SearchBar
-
ProductTable
ProductCategoryRow
ProductRow
13.3 第二步:用React构建一个静态版本
现在你有了组件结构,是时候开始实现你的应用了。最容易的方式是构建一个带着数据并渲染了UI,但是没有交互的版本。最好是将这两步分开做,因为构建静态版本只需要无脑编写,而添加交互则需要大量思考和少量编写。后面我们会看到这么做的原因。
为了构建你的应用的静态版本,你会想要复用其它组件来构建新的组件,并利用props
来传递数据。props
是从父组件传递数据到子组件的一种方式。如果你熟悉state
,那么构建这个静态版本就完全不要使用state
。state
是专为交互性预留的,数据会随时间改变。目前这还是个静态版本,你还不需要它。
你可以自上而下或者自下而上进行构建。也就是说,你可以从较高层的组件开始构建(比如从FilterableProductTable
开始),也可以从较底层的组件开始(ProductRow
)。在简单的示例中,通常自上而下更容易,而在更大型的项目中,自下而上地构建组件并同时为其编写测试会来的更容易。
在这一步骤的结尾,你讲会有一个可复用的组件库来渲染你的数据。由于目前你的应用还是静态版本,组件们将只有render()
方法。组件结构最顶层的组件(FilterableProductTable
)会将你的数据作为一个"prop"进行接收。如果你修改了基础数据并再次调用ReactDOM.render()
,UI会跟着更新。很容易就能观察到你UI的更新和变化的地方,因为到目前为止还没有什么复杂的事情发生。React 的单向数据流(又被称为单向绑定)使得一切有序而迅捷。
执行这一步时如果需要可以链接到React文档寻找帮助。
小插曲:Props与State
在React中有两种数据模型:props
和state
。理解两种模型的差别非常重要,如果你不了解其中差异,可以跳转到React官方文档进行查看。
13.4 第三步:标识出最小(但完整的)UI状态
为了让你的UI可交互,你需要能够触发变化到你的基础数据上。React用state
将这件事变得很简单。
为了正确构建出你的应用,首先你需要思考你的应用需要的最小可变state
集合。这里的关键是DRY: Don’t Repeat Yourself。搞清楚你的应用需要的最小的state
表示,然后按需要计算出其它的一切。比如,你要构建一个TODO LIST,只需要维护一个TODO项数组即可;不需要单独为数量保存一个state
变量。相反,当你想要渲染TODO的数量时,只需要简单地读取TODO项数组的长度即可。
考虑一下我们示例应用中的所有数据:
- 原始产品列表
- 用户输入的搜索文字
- 复选框的值
- 过滤后的产品列表
我们一个一个过一遍来搞清楚哪个才是state
。只要简单地在每个数据上问三个问题:
- 它是从父组件通过
props
传递进来的吗?如果是,那它不是state
- 它一直不会变化吗?如果是,那它不是
state
- 你能基于组建的其它
state
或props
计算出它来吗?如果是,那它不是state
原始产品列表是从props
传递进来的,所以它不是state
。用户输入的搜索文字和复选框的值好像是state
,因为他们会变化,并且不能基于其它数据计算出来。最后,过滤后的产品列表不是state
,因为他能够基于原始产品列表、用户输入的搜索文字和复选框的值计算出来。
所以最终,我们的state
是:
- 用户输入的搜索文字
- 复选框的值
13.5 第四步:确定你的state
的位置
所以我们现在已经get到了应用需要的最小state
集合,接下来我们需要搞清楚这些状态应该放到哪些组件里。
记住:React的一切都是关于沿着组件层次结构乡下的单项数据流。可能一开始你没法马上搞清楚哪个组件应该拥有哪些状态。这通常对于新手来说是最难的部分了,但你可以按下面的步骤来搞定:
对于你应用里的每一个state
来说:
- 确定基于该
state
渲染的所有组件 - 找到这些组件的一个共同的所有者组件(在层级结构中找到一个包含所有需要该
state
的组件的单一组件) - 要么是共同的所有者组件,要么是更高层的祖先组件应该拥有该
state
- 如果你没办法找到一个合适的组件来控制这个
state
,那就在共同的所有者组件之上再为其创建一个父组件来控制这个state
让我们按照这个策略来看一下我们的应用:
-
ProductTable
需要基于state
来过滤产品列表,SearchBar
需要展示搜索文字和复选框状态 - 他们共同的所有者组件是
FilterableProductTable
- 那么理论上把过滤文字和复选框的值放到
FilterableProductTable
上是比较合适的
好,我们已经决定将我们的state
放在FilterableProductTable
上了。首先,在FilterableProductTable
的constructor
中添加一个实例属性this.state = { filterText: '', inStockOnly: false }
来初始化你的应用的state
。然后将filterText
和inStockOnly
作为props
传递给ProductTable
和SearchBar
。最后,用这俩props
来过滤ProductTable
的行,以及设置SearchBar
的值。
你已经可以看到你的应用的一些表现了:将filterText
设置为ball
并刷新你的应用,你将看到数据表格正确地更新了。
13.6 第五步:添加反向数据流
目前我们已经构建了一个应用,props
和state
沿着层级结构自上而下流转并正确渲染出来。现在是时候支持另一种方式的数据流了:层级结构深处的的表单组件需要更新FilterableProductTable
上的state
。
React将这种数据流做的非常直白,让你能更好理解地你的程序是如何运转的。但相较于传统的双向数据绑定,它的确会需要多写一点代码。
如果你尝试在当前这个版本的示例中输入或点击复选框,React将会忽略你的输入。这是故意的,因为我们已经将input
的value
设置为总是等于从FilterableProductTable
传递进来的state
。
让我们思考一下我们想做什么。我们希望无论何时用户修改了表单,都要更新state
来反映用户输入。因为组件应该只更新它们自己的state
,FilterableProductTable
会给SearchBar
传递回调函数,当state
需要更新时执行该回调函数即可。我们可以使用input
的onChange
事件来通知其更新。FilterableProductTable
传递的回调函数中会调用setState()
,应用就会更新了。
虽然这听起来很复杂,但实际上却只有几行代码而已。并且你的数据如何在应用中流转已经非常明确了。
13.7 终于搞定了
希望这篇文章能给你一些如何用 React思考构建组件和应用的启发。虽然这可能比你原来的代码多一些,但请记住,读代码的次数要远远多于写代码的次数,而且读这种模块化的、直白的代码非常容易。当你开始构建大型组件库时,你会感激这种明晰性和模块化,并且随着代码的重用,你的代码会越写越少。:)