本笔记基于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中的不同之处,并只刷新这些地方!
列表渲染报错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官方基础教程的最后一章,有空我再继续翻译吧)
网友评论
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>
)
这里的代码明显有问题吧?完整代码见地址:https://www.zybuluo.com/Arison/note/786567。希望楼主给予回复。
美元:
1
人民币:
6.9339
我们有6.9339美元, 也就是0.14421898210242431元
this.timeId = setInterval((this.tick).bind(this), 1000);
否则react会一直报错:this.setState is not a function, 这是由于this的指向引起的