一、 不可变数据的力量
示例代码
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props)
this.state = {
friends: [
{ name: 'Klaus' },
{ name: 'Steven' },
{ name: 'Ted' },
]
}
}
render() {
return (
<div>
<h2>好友列表</h2>
<ul>
{
this.state.friends.map(item => <li key={ item.name }>{ item.name }</li>)
}
</ul>
<button onClick={ () => this.insertItem() }>insert</button>
</div>
)
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.friends !== this.state.friends
}
insertItem() {
this.state.friends.push({
name: 'Kobe'
})
this.setState({ friends: this.state.friends })
}
}
export default App;
效果
发现 虽然修改了
state中的状态,但是界面中的数据并没有发生任何的修改这是因为
shouldComponentUpdate函数返回的是false,
this.state.friends.push( ... )方法,会修改原始数组,并且返回新数组的长度那么此时
this.state === newState,所以此时界面是不会执行render函数的
所以,我们在开发中不应该修改state中的数据后再执行setState方法,这样界面是不会执行render函数的
参考 React文档-性能优化
所以上述的应进行如下的修改:
insertItem() {
this.setState({
friends: [ ...this.state.friends, {
name: 'Kobe'
} ]
})
}
根据如上的示意图可以数组的解构只是一层浅拷贝,其根据数组中每一项的引用生成了一个新的数组
而之后的数据插入是在新数组中进行操作的,所以在
setState中的state和newState是值不同的2个对象,所以会调用
render方法,重新渲染界面
示例
import React, { PureComponent } from 'react';
class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
friends: [
{ name: 'Klaus', age: 18 },
{ name: 'Steven', age: 23 },
{ name: 'Ted', age: 35 },
]
}
}
render() {
return (
<div>
<h2>好友列表</h2>
<ul>
{
this.state.friends.map((item, index) => (
<li key={ item.name }>
{ item.name } { item.age }
<button onClick={ () => this.increment(index) }>increment</button>
</li>
))
}
</ul>
</div>
)
}
increment(index) {
this.state.friends[index].age += 1
this.setState({
friends: this.state.friends
})
}
}
export default App;
可以看到的是,
this.state.friends[index].age += 1修改的依旧是state数据本身所以没有调用
render函数,界面依旧是没有发生任何的变化
因此进行如下修改:
increment(index) {
const newFriends = [ ...this.state.friends ]
newFriends[index].age += 1
this.setState({
friends: newFriends
})
}
}
newFriends只是friends的浅拷贝,所以本质上修改newFriends中的age,就是修改friends中的age但是因为
newFriends和friends的地址值是不用的,所以shallowEqual方法的返回值为false,所以依旧会调用render函数
二、 事件总线
前面通过Context主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?
- 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus),来完成操作;
- 在React中,我们可以依赖一个使用较多的库 events 来完成对应的操作;我们可以通过npm或者yarn来安装events
import React, { PureComponent } from 'react'
// 引入事件发射器(在写项目的时候,可以把全局的变量引入到一个单独的文件中)
import { EventEmitter } from 'events'
// 定义事件总线
// EventBus 本质上就是一个全局对象,
// 主要用于多个组件之间进行跨组件事件传递
const EventBus = new EventEmitter()
class Header extends PureComponent {
constructor(props) {
super(props)
this.state = {
msg: '这是测试数据'
}
}
// 在这里监听事件
componentDidMount() {
// 这里的事件是交给Event去回调的,不是我们自己去回调的,
// 所以这里依旧需要包裹上箭头函数,否则内部this的指向会是emitEvent
EventBus.addListener('handleEventEmit', (msg, num) => this.handleEventEmit(msg, num))
}
// 在这里移除事件, 需要和componentDidMount中的绑定的事件保持一一对应
componentWillUnmount() {
// 这样移除的是EventBus中所有的handleEventEmit
// EventBus.removeListener('handleEventEmit')
// 如果只希望移除当前组件中的handleEventEmit事件
EventBus.removeListener('handleEventEmit', this.handleEventEmit)
}
// 这个事件必须定义在外面
// 因为addListener中的事件对象,必须和 removeListener中的事件对象保持一致才可以被移除
handleEventEmit(msg, num) {
// console.log(msg, num) // Hello World 23
this.setState({
msg
})
}
render() {
return (
<div>
Header
<span>
{ this.state.msg }
</span>
</div>
)
}
}
class Footer extends PureComponent {
render() {
return (
<div>
Footer
<button onClick={ () => this.handleEmit() }>点我发射事件</button>
</div>
)
}
handleEmit() {
// 发射事件
EventBus.emit('handleEventEmit', 'Hello World', 23)
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<Header />
<Footer />
</div>
)
}
}
三、ref的使用
在React的开发模式中,通常情况下不需要、也不建议直接操作DOM原生,但是某些特殊的情况,确实需要获取到DOM进行某些操作,此时就可以使用ref(reference)
在react中获取原生DOM的方式:
- 在
ComponentDidMount方法中使用原生的方式(document.getElementById)来获取DOM元素 - 使用
ref属性
3.1 如何创建refs来获取对应的DOM
- 方式一:传入字符串
- 使用时通过 this.refs.传入的字符串格式获取对应的元素;
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
render() {
return (
<div>
<h2 ref="titleRef">Hello World</h2>
<button onClick={ () => this.changeText() }>changeText</button>
</div>
)
}
changeText() {
// this.refs.changeText --- 就是原生的DOM对象
this.refs.titleRef.innerHTML = 'Hello React'
}
}
- 方式二:传入一个对象
官方推荐- 对象是通过
React.createRef()方式创建出来的; - 使用时获取到创建的对象其中有一个
current属性就是对应的元素;
- 对象是通过
// 这里需要多引入一个函数 createRef
import React, { PureComponent, createRef } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
// 注意这个是单独写的,不是在state中
this.titleRef = createRef()
}
render() {
return (
<div>
{/* 这里传递一个特别的对象 */}
<h2 ref={ this.titleRef }>Hello World</h2>
<button onClick={ () => this.changeText() }>changeText</button>
</div>
)
}
changeText() {
// 对象里面有一个current属性,存储的就是对应的原生DOM元素
this.titleRef.current.innerHTML = 'Hello React'
}
}
- 方式三:传入一个函数
- 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;
- 使用时,直接拿到之前保存的元素对象即可;
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
// 这里定义需要使用的ref变量
this.titleRef = null
}
render() {
return (
<div>
{/* 在回调函数中的形参就是原生dom对象 */}
<h2 ref={ args => this.titleRef = args }>Hello World</h2>
<button onClick={ () => this.changeText() }>changeText</button>
</div>
)
}
changeText() {
this.titleRef.innerHTML = 'Hello React'
}
}
3.2 ref的类型
ref 的值根据节点的类型而有所不同:
- 当 ref 属性用于 HTML 元素时,获取的就是原生DOM对象;
- 当 ref 属性用于自定义 class 组件时,获取的就是自定义的class组件;
- 你不能在函数组件上使用 ref 属性,因为他们没有实例,所以也就无法使用ref;
示例 --- 在父组件中调用子组件的事件
import React, { PureComponent, createRef } from 'react'
class Cpn extends PureComponent {
render() {
return (
<h2 onClick={ () => this.handleClick() }>Hello React</h2>
)
}
handleClick() {
console.log('子组件的函数被调用了')
}
}
export default class App extends PureComponent {
constructor(props) {
super(props)
this.cpnRef = createRef()
}
render() {
return (
<div>
<Cpn ref={ this.cpnRef } />
<button onClick={ () => this.clickHandler() }>点我</button>
</div>
)
}
clickHandler() {
// this.cpnRef ---- 对应的Cpn子组件
// 在父组件调用子组件中的方法
this.cpnRef.current.handleClick()
// result: 子组件的函数被调用了
}
}
函数式组件是没有实例的,所以无法通过ref获取他们的实例:
- 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
- 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref
四、 受控组件
无论是受控组件还是非受控组件都是指的是表单元素
在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state
比如下面的HTML表单元素:
- 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
- 在React中,并没有禁止这个行为,它依然是有效的;
- 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
- 实现这种效果的标准方式是使用“受控组件”
示例
react中的数据流是
单向数据流输入 --- 修改 state ,state中发生改变 --- 修改界面中的展示
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
username: ''
}
}
render() {
return (
{/*
使用react合成的表单提交事件
来阻止默认的表单提交行为
并监听对应的提交事件
*/}
<form onSubmit={ e => { this.handleSubmit(e) } }>
<label htmlFor="username">
userName:
{/*
使用onChange事件来修改state中的对应状态
使用value值来使用控件的值来源于state中的对应状态
*/}
<input id="username" type="text" onChange={ e => this.handleChange(e) } value={ this.state.username } />
<input type="submit" value="submit" />
</label>
</form>
)
}
handleChange(e) {
// e.target.value
// input输入框输入的值
// 因为react是合成事件对象,所以原生事件所有的属性 在合成事件对象的身上其依然存在且可以正常使用
this.setState({
username: e.target.value
})
}
handleSubmit(e) {
// 阻止默认事件
e.preventDefault()
// 输出username
console.log(this.state.username)
}
}
在 HTML 中,表单元素之类的表单元素通常自己维护 state,并根据用户输入进行 更新。
而在 React 中
可变状态(mutable state)通常保存在组件的state 属性中,并且只能通过使用setState()来更新。
我们将两者结合起来,使React的state成为“唯一数据源”;
- 由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源
渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;
被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;
Element Value Property Change Callback New Value in the callback <input type="text" />value="string"onChangeevent.target.value<input type="checkbox" /checked={boolean}onChangeevent.target.checked<input type="radio" />checked={boolean}onChangeevent.target.checked<textarea />value="string"onChangeevent.target.value<select />value=“option value”onChangeevent.target.value
示例
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
fruits: [
'apple',
'banana',
'orange'
],
fruit: 'orange'
}
}
render() {
return (
<form onSubmit={e => this.handleSubmit(e)}>
<select name="fruit"
onChange= {e => this.handleChange(e)}
value={this.state.fruit}
>
{
this.state.fruits.map(item => (
<option key={item}>
{ item }
</option>
))
}
</select>
<button>submit</button>
</form>
)
}
handleChange(e) {
this.setState({
fruit: e.target.value
})
}
handleSubmit(e) {
e.preventDefault();
console.log(this.state.fruit)
}
}
处理多输入情况
import React, { PureComponent } from 'react';
class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
username: '',
password: ''
}
}
render() {
return (
<form onSubmit={ e => this.handleSubmit(e) }>
<label htmlFor="username">
<span>userName:</span>
<input
type="text"
name="username"
value={ this.state.username }
onChange={e => this.handleChange(e)}
/>
</label>
{/*
label是行内块元素,所以所有的label都会在一行,
如果需要换行可以使用br标签,或者在label外包裹一层div标签
*/}
<label htmlFor="password">
<span>password:</span>
<input
type="password"
name="password"
value={ this.state.password }
onChange={e => this.handleChange(e)}
/>
</label>
<button>submit</button>
</form>
)
}
handleChange(e) {
// 使用ES6中的计算属性名 (Computed property names)
this.setState({
[e.target.name]: e.target.value
})
}
handleSubmit(e) {
e.preventDefault()
const {username, password} = this.state
console.log(username, password)
}
}
export default App;
五、 非受控组件
React推荐大多数情况下使用 受控组件 来处理表单数据:
一个受控组件中,表单数据是由 React 组件来管理的;
另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据。
- 使用ref来获取input元素;
- 在非受控组件中通常使用defaultValue来设置默认值;
- 同样,
<input type="checkbox">和<input type="radio">支持 defaultChecked,<select>和<textarea>支 持 defaultValue。
import React, { PureComponent,createRef } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.usernameRef = createRef()
this.passwordRef = createRef()
}
render() {
return (
<form onSubmit={ e => this.handleSubmit(e) }>
username: <input type="text" ref={ this.usernameRef } defaultValue='klaus' />
passeord: <input type="password" ref={ this.passwordRef } />
<button>submit</button>
</form>
)
}
handleSubmit(e) {
e.preventDefault()
console.log(this.usernameRef.current.value, this.passwordRef.current.value)
}
}
因为非受控组件是自己去操作原生DOM,来获取对应的元素值,所以这种方式在react中是不被推荐的