react注意点
event 对象
和普通浏览器一样,事件监听函数会被自动传入一个 event
对象,这个对象和普通的浏览器 event
对象所包含的方法和属性都基本一致。不同的是 React.js 中的 event
对象并不是浏览器提供的,而是它自己内部所构建的。React.js 将浏览器原生的 event
对象封装了一下,对外提供统一的 API 和属性,这样你就不用考虑不同浏览器的兼容性问题。这个 event
对象是符合 W3C 标准( W3C UI Events )的,它具有类似于event.stopPropagation
、event.preventDefault
这种常用的方法。
我们来尝试一下,这次尝试当用户点击 h1
的时候,把 h1
的 innerHTML
打印出来:
class Title extends Component { handleClickOnTitle (e) { console.log(e.target.innerHTML) } render () { return ( <h1 onClick={this.handleClickOnTitle}>React 小书</h1> ) } }
再看看控制台,每次点击的时候就会打印”React 小书“。
关于事件中的 this
一般在某个类的实例方法里面的 this
指的是这个实例本身。但是你在上面的 handleClickOnTitle
中把 this
打印出来,你会看到 this
是 null
或者 undefined
。
... handleClickOnTitle (e) { console.log(this) // => null or undefined } ...
这是因为 React.js 调用你所传给它的方法的时候,并不是通过对象方法的方式调用(this.handleClickOnTitle
),而是直接通过函数调用 (handleClickOnTitle
),所以事件监听函数内并不能通过 this
获取到实例。
如果你想在事件函数当中使用当前的实例,你需要手动地将实例方法 bind
到当前实例上再传入给 React.js。
class Title extends Component { handleClickOnTitle (e) { console.log(this) } render () { return ( <h1 onClick={this.handleClickOnTitle.bind(this)}>React 小书</h1> ) } }
bind
会把实例方法绑定到当前实例上,然后我们再把绑定后的函数传给 React.js 的 onClick
事件监听。这时候你再看看,点击 h1
的时候,就会把当前的实例打印出来:
你也可以在 bind
的时候给事件监听函数传入一些参数:
class Title extends Component { handleClickOnTitle (word, e) { console.log(this, word) } render () { return ( <h1 onClick={this.handleClickOnTitle.bind(this, 'Hello')}>React 小书</h1> ) } }
这种 bind
模式在 React.js 的事件监听当中非常常见,bind
不仅可以帮我们把事件监听方法中的 this
绑定到当前组件实例上;还可以帮助我们在在渲染列表元素的时候,把列表元素传入事件监听函数当中——这个将在以后的章节提及。
如果有些同学对 JavaScript 的 this
模式或者 bind
函数的使用方式不是特别了解到话,可能会对这部分内容会有些迷惑,参考博客里面的this的详解。
setState 接受函数参数
这里还有要注意的是,当你调用 setState
的时候,React.js 并不会马上修改 state。而是把这个对象放到一个更新队列里面,稍后才会从队列当中把新的状态提取出来合并到 state
当中,然后再触发组件更新。这一点要好好注意。可以体会一下下面的代码:
... handleClickOnLikeButton () { console.log(this.state.isLiked) this.setState({ isLiked: !this.state.isLiked }) console.log(this.state.isLiked) } ...
你会发现两次打印的都是 false
,即使我们中间已经 setState
过一次了。这并不是什么 bug,只是 React.js 的 setState
把你的传进来的状态缓存起来,稍后才会帮你更新到 state
上,所以你获取到的还是原来的 isLiked
。
所以如果你想在 setState
之后使用新的 state
来做后续运算就做不到了,例如:
... handleClickOnLikeButton () { this.setState({ count: 0 }) // => this.state.count 还是 undefined this.setState({ count: this.state.count + 1}) // => undefined + 1 = NaN this.setState({ count: this.state.count + 2}) // => NaN + 2 = NaN } ...
上面的代码的运行结果并不能达到我们的预期,我们希望 count
运行结果是 3
,可是最后得到的是 NaN
。但是这种后续操作依赖前一个 setState
的结果的情况并不罕见。
这里就自然地引出了 setState
的第二种使用方式,可以接受一个函数作为参数。React.js 会把上一个 setState
的结果传入这个函数,你就可以使用该结果进行运算、操作,然后返回一个对象作为更新 state
的对象:
... handleClickOnLikeButton () { this.setState((prevState) => { return { count: 0 } }) this.setState((prevState) => { return { count: prevState.count + 1 } // 上一个 setState 的返回是 count 为 0,当前返回 1 }) this.setState((prevState) => { return { count: prevState.count + 2 } // 上一个 setState 的返回是 count 为 1,当前返回 3 }) // 最后的结果是 this.state.count 为 3 } ...
这样就可以达到上述的利用上一次 setState
结果进行运算的效果。
默认配置 defaultProps
上面的组件默认配置我们是通过 ||
操作符来实现。这种需要默认配置的情况在 React.js 中非常常见,所以 React.js 也提供了一种方式 defaultProps
,可以方便的做到默认配置。
class LikeButton extends Component { static defaultProps = { likedText: '取消', unlikedText: '点赞' } constructor () { super() this.state = { isLiked: false } } handleClickOnLikeButton () { this.setState({ isLiked: !this.state.isLiked }) } render () { return ( <button onClick={this.handleClickOnLikeButton.bind(this)}> {this.state.isLiked ? this.props.likedText : this.props.unlikedText} 👍 </button> ) } }
注意,我们给点赞组件加上了以下的代码:
static defaultProps = { likedText: '取消', unlikedText: '点赞' }
defaultProps
作为点赞按钮组件的类属性,里面是对 props
中各个属性的默认配置。这样我们就不需要判断配置属性是否传进来了:如果没有传进来,会直接使用 defaultProps
中的默认属性。 所以可以看到,在 render
函数中,我们会直接使用 this.props
而不需要再做判断
props
参数
props参数一旦传入,你就不可以在组件内部对它进行修改。但是你可以通过父组件主动重新渲染的方式来传入新的 props
,从而达到更新的效果。
渲染存放 JSX 元素的数组
之前说过 JSX 的表达式插入 {}
里面可以放任何数据,如果我们往 {}
里面放一个存放 JSX 元素的数组会怎么样?
... class Index extends Component { render () { return ( <div> {[ <span>React.js </span>, <span>is </span>, <span>good</span> ]} </div> ) } } ReactDOM.render( <Index />, document.getElementById('root') )
我们往 JSX 里面塞了一个数组,这个数组里面放了一些 JSX 元素(其实就是 JavaScript 对象)。到浏览器中,你在页面上会看到:
审查一下元素,看看会发现什么:
React.js 把插入表达式数组里面的每一个 JSX 元素一个个罗列下来,渲染到页面上。所以这里有个关键点:如果你往 {}
放一个数组,React.js 会帮你把数组里面一个个元素罗列并且渲染出来。
key! key! key!
React.js 的是非常高效的,它高效依赖于所谓的 Virtual-DOM 策略。简单来说,能复用的话 React.js 就会尽量复用,没有必要的话绝对不碰 DOM。对于列表元素来说也是这样,但是处理列表元素的复用性会有一个问题:元素可能会在一个列表中改变位置。例如:
<div>a</div> <div>b</div> <div>c</div>
假设页面上有这么3个列表元素,现在改变一下位置:
<div>a</div> <div>c</div> <div>b</div>
c
和 b
的位置互换了。但其实 React.js 只需要交换一下 DOM 位置就行了,但是它并不知道其实我们只是改变了元素的位置,所以它会重新渲染后面两个元素(再执行 Virtual-DOM 策略),这样会大大增加 DOM 操作。但如果给每个元素加上唯一的标识,React.js 就可以知道这两个元素只是交换了位置:
<div key='a'>a</div>
<div key='b'>b</div>
<div key='c'>c</div>
这样 React.js 就简单的通过 key
来判断出来,这两个列表元素只是交换了位置,可以尽量复用元素内部的结构。
这里没听懂没有关系,后面有机会会继续讲解这部分内容。现在只需要记住一个简单的规则:对于用表达式套数组罗列到页面上的元素,都要为每个元素加上 key
属性,这个 key
必须是每个元素唯一的标识。一般来说,key
的值可以直接后台数据返回的 id
,因为后台的 id
都是唯一的。
受控组件
用户可输入内容一个是用户名(username),一个是评论内容(content),我们在组件的构造函数中初始化一个 state
来保存这两个状态:
...
class CommentInput extends Component {
constructor () {
super()
this.state = {
username: '',
content: ''
}
}
...
}
...
然后给输入框设置 value
属性,让它们的 value
值等于 this.state
里面相应的值:
...
<div className='comment-field'>
<span className='comment-field-name'>用户名:</span>
<div className='comment-field-input'>
<input value={this.state.username} />
</div>
</div>
<div className='comment-field'>
<span className='comment-field-name'>评论内容:</span>
<div className='comment-field-input'>
<textarea value={this.state.content} />
</div>
</div>
...
可以看到接受用户名输入的 <input />
和接受用户评论内容的 <textarea />
的 value
值分别由 state.username
和 state.content
控制。这时候你到浏览器里面去输入内容看看,你会发现你什么都输入不了。
这是为什么呢?React.js 认为所有的状态都应该由 React.js 的 state 控制,只要类似于 <input />
、<textarea />
、<select />
这样的输入控件被设置了 value
值,那么它们的值永远以被设置的值为准。值不变,value
就不会变化。
例如,上面设置了 <input />
的 value
为 this.state.username
,username
在 constructor
中被初始化为空字符串。即使用户在输入框里面尝试输入内容了,还是没有改变 this.state.username
是空字符串的事实。
所以应该怎么做才能把用户内容输入更新到输入框当中呢?在 React.js 当中必须要用 setState
才能更新组件的内容,所以我们需要做的就是:监听输入框的 onChange
事件,然后获取到用户输入的内容,再通过 setState
的方式更新 state
中的 username
,这样 input
的内容才会更新。
...
<div className='comment-field-input'>
<input
value={this.state.username}
onChange={this.handleUsernameChange.bind(this)} />
</div>
...
上面的代码给 input
加上了 onChange
事件监听,绑定到 this.handleUsernameChange
方法中,该方法实现如下:
...
handleUsernameChange (event) {
this.setState({
username: event.target.value
})
}
...
在这个方法中,我们通过 event.target.value
获取 <input />
中用户输入的内容,然后通过 setState
把它设置到 state.username
当中,这时候组件的内容就会更新,input
的 value
值就会得到更新并显示到输入框内。这时候输入已经没有问题了:
类似于 <input />
、<select />
、<textarea>
这些元素的 value
值被 React.js 所控制、渲染的组件,在 React.js 当中被称为受控组件(Controlled Component)。对于用户可输入的控件,一般都可以让它们成为受控组件,这是 React.js 所推崇的做法
input输入框的enter回车事件
inputlist.js import React, { Component } from 'react' import './InputList.css' class InputList extends Component{ constructor(){ super() this.state={ content:'' } } handleContentChange(event){ this.setState({ content:event.target.value }) } handleSubmit(event){ if(event.nativeEvent.keyCode === 13){ if(this.props.onSubmit){ this.props.onSubmit(this.state.content) } } } render(){ return ( <div className='input-header'> <div className='container'> <label htmlFor="" className='input-title'>ToDoList</label> <input type="text" className='input-content' placeholder='添加ToDo' required='required' autoComplete='off' onChange={this.handleContentChange.bind(this)} onKeyUp={this.handleSubmit.bind(this)}/> </div> </div> ) } } export default InputList
组件的生命周期
一个组件可以插入页面,当然也可以从页面中删除。
-> constructor()
-> componentWillMount()
-> render()
// 然后构造 DOM 元素插入页面
-> componentDidMount()
// ...
// 从页面中删除
React.js 也控制了这个组件的删除过程。在组件删除之前 React.js 会调用组件定义的 componentWillUnmount
:-> constructor()
-> componentWillMount()
-> render()
// 然后构造 DOM 元素插入页面
-> componentDidMount()
// ...
// 即将从页面中删除
-> componentWillUnmount()
// 从页面中删除
ref和react中的DOM操作
在 React.js 当中你基本不需要和 DOM 直接打交道。React.js 提供了一系列的 on*
方法帮助我们进行事件监听,所以 React.js 当中不需要直接调用 addEventListener
的 DOM API;以前我们通过手动 DOM 操作进行页面更新(例如借助 jQuery),而在 React.js 当中可以直接通过 setState
的方式重新渲染组件,渲染的时候可以把新的 props
传递给子组件,从而达到页面更新的效果。
React.js 这种重新渲染的机制帮助我们免除了绝大部分的 DOM 更新操作,也让类似于 jQuery 这种以封装 DOM 操作为主的第三方的库从我们的开发工具链中删除。
但是 React.js 并不能完全满足所有 DOM 操作需求,有些时候我们还是需要和 DOM 打交道。比如说你想进入页面以后自动 focus 到某个输入框,你需要调用 input.focus()
的 DOM API,比如说你想动态获取某个 DOM 元素的尺寸来做后续的动画,等等。
React.js 当中提供了 ref
属性来帮助我们获取已经挂载的元素的 DOM 节点,你可以给某个 JSX 元素加上 ref
属性:
class AutoFocusInput extends Component {
componentDidMount () {
this.input.focus()
}
render () {
return (
<input ref={(input) => this.input = input} />
)
}
}
ReactDOM.render(
<AutoFocusInput />,
document.getElementById('root')
)
可以看到我们给 input
元素加了一个 ref
属性,这个属性值是一个函数。当 input
元素在页面上挂载完成以后,React.js 就会调用这个函数,并且把这个挂载以后的 DOM 节点传给这个函数。在函数中我们把这个 DOM 元素设置为组件实例的一个属性,这样以后我们就可以通过 this.input
获取到这个 DOM 元素。
然后我们就可以在 componentDidMount
中使用这个 DOM 元素,并且调用 this.input.focus()
的 DOM API。整体就达到了页面加载完成就自动 focus 到输入框的功能(大家可以注意到我们用上了 componentDidMount
这个组件生命周期)。
我们可以给任意代表 HTML 元素标签加上 ref
从而获取到它 DOM 元素然后调用 DOM API。但是记住一个原则:能不用 ref
就不用。特别是要避免用 ref
来做 React.js 本来就可以帮助你做到的页面自动更新的操作和事件监听。多余的 DOM 操作其实是代码里面的“噪音”,不利于我们理解和维护。
顺带一提的是,其实可以给组件标签也加上 ref
,例如:
<Clock ref={(clock) => this.clock = clock} />
这样你获取到的是这个 Clock
组件在 React.js 内部初始化的实例。但这并不是什么常用的做法,而且也并不建议这么做,所以这里就简单提及,有兴趣的朋友可以自己学习探索。
props.children和容器类组件
有一类组件,充当了容器的作用,它定义了一种外层结构形式,然后你可以往里面塞任意的内容。这种结构在实际当中非常常见,例如这种带卡片组件:
组件本身是一个不带任何内容的方形的容器,我可以在用这个组件的时候给它传入任意内容:
基于我们目前的知识储备,可以迅速写出这样的代码:
class Card extends Component {
render () {
return (
<div className='card'>
<div className='card-content'>
{this.props.content}
</div>
</div>
)
}
}
ReactDOM.render(
<Card content={
<div>
<h2>React.js 小书</h2>
<div>开源、免费、专业、简单</div>
订阅:<input />
</div>
} />,
document.getElementById('root')
)
我们通过给 Card
组件传入一个 content
属性,这个属性可以传入任意的 JSX 结构。然后在 Card
内部会通过 {this.props.content}
把内容渲染到页面上。
这样明显太丑了,如果 Card
除了 content
以外还能传入其他属性的话,那么这些 JSX 和其他属性就会混在一起。很不好维护,如果能像下面的代码那样使用 Card
那想必也是极好的:
ReactDOM.render(
<Card>
<h2>React.js 小书</h2>
<div>开源、免费、专业、简单</div>
订阅:<input />
</Card>,
document.getElementById('root')
)
如果组件标签也能像普通的 HTML 标签那样编写内嵌的结构,那么就方便很多了。实际上,React.js 默认就支持这种写法,所有嵌套在组件中的 JSX 结构都可以在组件内部通过 props.children
获取到:
class Card extends Component {
render () {
return (
<div className='card'>
<div className='card-content'>
{this.props.children}
</div>
</div>
)
}
}
把 props.children
打印出来,你可以看到它其实是个数组:
React.js 就是把我们嵌套的 JSX 元素一个个都放到数组当中,然后通过 props.children
传给了 Card
。
由于 JSX 会把插入表达式里面数组中的 JSX 一个个罗列下来显示。所以其实就相当于在 Card
中嵌套了什么 JSX 结构,都会显示在 Card
的类名为 card-content
的 div
元素当中。
这种嵌套的内容成为了 props.children
数组的机制使得我们编写组件变得非常的灵活,我们甚至可以在组件内部把数组中的 JSX 元素安置在不同的地方:
class Layout extends Component {
render () {
return (
<div className='two-cols-layout'>
<div className='sidebar'>
{this.props.children[0]}
</div>
<div className='main'>
{this.props.children[1]}
</div>
</div>
)
}
}
这是一个两列布局组件,嵌套的 JSX 的第一个结构会成为侧边栏,第二个结构会成为内容栏,其余的结构都会被忽略。这样通过这个布局组件,就可以在各个地方高度复用我们的布局。
style
React.js 中的元素的 style
属性的用法和 DOM 里面的 style
不大一样,普通的 HTML 中的:
<h1 style='font-size: 12px; color: red;'>React.js 小书</h1>
在 React.js 中你需要把 CSS 属性变成一个对象再传给元素:
<h1 style={{fontSize: '12px', color: 'red'}}>React.js 小书</h1>
style
接受一个对象,这个对象里面是这个元素的 CSS 属性键值对,原来 CSS 属性中带 -
的元素都必须要去掉 -
换成驼峰命名,如 font-size
换成 fontSize
,text-align
换成 textAlign
。
用对象作为 style
方便我们动态设置元素的样式。我们可以用 props
或者 state
中的数据生成样式对象再传给元素,然后用 setState
就可以修改样式,非常灵活:
<h1 style={{fontSize: '12px', color: this.state.color}}>React.js 小书</h1>
只要简单地 setState({color: 'blue'})
就可以修改元素的颜色成蓝色。
规范
大家可以注意到我们组件的命名和方法的摆放顺序其实有一定的讲究,这里可以简单分享一下个人的习惯,仅供参考。
组件的私有方法都用 _
开头,所有事件监听的方法都用 handle
开头。把事件监听方法传给组件的时候,属性名用 on
开头。例如:
<CommentInput
onSubmit={this.handleSubmitComment.bind(this)} />
这样统一规范处理事件命名会给我们带来语义化组件的好处,监听(on
)CommentInput
的 Submit
事件,并且交给 this
去处理(handle
)。这种规范在多人协作的时候也会非常方便。
另外,组件的内容编写顺序如下:
- static 开头的类属性,如
defaultProps
、propTypes
。 - 构造函数,
constructor
。 - getter/setter(还不了解的同学可以暂时忽略)。
- 组件生命周期。
_
开头的私有方法。- 事件监听方法,
handle*
。 render*
开头的方法,有时候render()
方法里面的内容会分开到不同函数里面进行,这些函数都以render*
开头。render()
方法。