不可变数据
为什么setState中的数据需要是不可变的
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
不使用不可变数据
import { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
users: [
{ name: 'Klaus' },
{ name: 'Steven' },
{ name: 'Alex' }
]
}
}
render() {
return (
<>
<ul>
{
this.state.users.map(user => <li key={user.name}>{ user.name }</li>)
}
</ul>
<button onClick={ () => this.append() }>append user</button>
</>
)
}
append() {
const userInfo = { name: 'Jhon' }
this.state.users.push(userInfo)
this.setState({
users: this.state.users
})
}
}
此时功能看上去,没有什么大的问题,但是在实际开发中,我们一般会将组件继承自pureComponent或者自主去实现SCU
而puerComponent默认实现的SCU方法本质上是 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
而使用push方法更新数组,实际上是向原数组中插入数据,也就是oldState.users === newState.users
此时SCU方法就会返回false,并不会触发对应的render方法
因此,在React中使用setState进行数据更新的时候,一定要保证不去主动修改state中的数据,也就是保证state中的数据不可变性
import { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
users: [
{ name: 'Klaus' },
{ name: 'Steven' },
{ name: 'Alex' }
]
}
}
render() {
return (
<>
<ul>
{
this.state.users.map(user => <li key={user.name}>{ user.name }</li>)
}
</ul>
<button onClick={ () => this.append() }>append user</button>
</>
)
}
append() {
const userInfo = { name: 'Jhon' }
const users = [...this.state.users, userInfo]
this.setState({
users
})
}
}
事件总线
Context主要实现的是数据的共享, 而且主要是爷孙级组件进行数据的共享
在开发中如果有跨组件之间的事件传递,可以使用events或mitt这类第三方库来帮助我们进行实现
# 安装 -- 这里以events为例
$ yarn add events
events常用的API:
-
创建EventEmitter对象: eventBus对象
-
发出事件: eventBus.emit("事件名称", 参数列表)
-
监听事件: eventBus.addListener("事件名称", 监听函数)
-
移除事件: eventBus.removeListener("事件名称", 监听函数)
-
如果removeListener值只传递了第一个参数,那么表示移除所有事件名为参数1的事件
-
如果需要移除某一个事件所对应的具体事件处理函数,
addListener中的函数和removeListener中的函数必须是同一个事件处理函数,也就是他们的引用地址必须是一致的
-
示例
import { PureComponent } from 'react'
import { EventEmitter } from 'events'
// 实际开发中,这个对象一般是一个全局对象,会被放置到工具js中在被导出
// 这里只是个小案例,所以直接在这里生成EventEmitter的实例对象
const events = new EventEmitter()
class Cpn extends PureComponent {
constructor(props) {
super(props)
this.state = {
msg: 'default value'
}
}
render() {
return <h2>{ this.state.msg }</h2>
}
// 在componentDidMount进行事件订阅(监听)
componentDidMount() {
events.addListener('sendMsg', this.hanldSendMsg)
}
// 在componentWillUnmount取消事件订阅(监听)
componentWillUnmount() {
events.rawListeners('sendMsg', this.hanldSendMsg)
}
hanldSendMsg = (msg1, msg2) => {
// 注意这里需要使用箭头函数
// 因为事件被抽取后,传入EventEmitter对象进行使用的时候
// 内部的this会被修改为EventEmitter对象,而非当前组件实例对象
this.setState({
msg: `${msg1} ${msg2}`
})
}
}
class Foo extends PureComponent {
render() {
return <button onClick={ () => this.emitEvent() }>click me</button>
}
emitEvent() {
events.emit('sendMsg', 'Hello', 'World')
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<Cpn />
<Foo />
</div>
)
}
}
ref
在React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素
但是某些特殊的情况,确实需要获取到DOM进行某些操作
获取DOM元素的方式有2种
- 在
componentDidMount事件中去使用document.getElementById之类的原生DOM方法去获取和操作DOM元素 - 使用
ref来获取和操作DOM元素
ref基本使用
字符串- 通过 this.refs.传入的字符串格式获取对应的元素
- 早期react获取DOM元素的方式,现在已经不推荐使用
import { PureComponent } from 'react'
export default class App extends PureComponent {
render() {
return (<h2 ref="titleRef">Hello World</h2>)
}
componentDidMount() {
console.log(this.refs.titleRef)
}
}
对象
- 对象是通过 React.createRef() 方式创建出来的
- 使用时获取到创建的对象其中有一个current属性就是对应的元素
- 目前react推荐的获取DOM的方式
import { PureComponent, createRef } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
// 创建ref对象
this.titleRef = createRef()
}
render() {
return (<h2 ref={this.titleRef}>Hello World</h2>)
}
componentDidMount() {
// 获取到的实际上一个对象 =>{ current: dom元素 }
console.log(this.titleRef.current)
}
}
函数
- 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象
- 使用时,直接拿到之前保存的元素对象即可
import { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.titleRef = null
}
render() {
return (<h2 ref={ el => this.titleRef = el }>Hello World</h2>)
}
componentDidMount() {
console.log(this.titleRef)
}
}
ref类型
- 如果是普通DOM元素,那么使用ref获取到的就是普通的DOM元素
- 如果是组件
- 如果是类组件,那么获取到的就是类组件的实例对象
- 注意是实例对象,不是DOM元素 [ 这里没有类似于vue中的el属性 ]
- 如果是函数组件,那么无法使用ref获取到DOM元素对象,因为函数组件没有实例对象
- 如果是类组件,那么获取到的就是类组件的实例对象
受控组件
受控组件存在于表单中(其实在vue中对于这种书写方式提供了语法糖写法,就是v-model)
如果一个表单元素满足以下两个条件,就可以认为这个表单元素是一个受控组件
- 可变状态(mutable state)通常保存在组件的 state 属性中
- 只能通过使用 setState()来更新
// 单值表单组件
import { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
username: ''
}
}
render() {
return (
<form onSubmit={e => this.handleSubmit(e)}>
{/* 此时这个input就是受控组件 */}
用户名: <input
type="text"
onChange={e => this.handleUsernameChange(e)}
value={ this.state.username }
/>
<button>sumbit</button>
</form>
)
}
handleSubmit(e) {
// 阻止表单的默认提交行为
e.preventDefault()
console.log(this.state.username)
}
handleUsernameChange(e) {
this.setState({
username: e.target.value
})
}
}
// 多值表单组件
import { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
users: [
{ name: 'Alex', checked: false },
{ name: 'Steven', checked: false },
{ name: 'Klaus', checked: false }
]
}
}
render() {
return (
<form onSubmit={ e => this.handleSubmit(e) }>
<ul>
{
this.state.users.map((user, index) => (
<li key={user.name}>
<input
type="checkbox"
name="users"
checked={user.checked}
onChange={ e => this.change(e, index) }
/>
{ user.name }
</li>
))
}
</ul>
<button>submit</button>
</form>
)
}
handleSubmit(e) {
e.preventDefault()
console.log(this.state.users)
}
change(e, i) {
const users = [...this.state.users]
users[i].checked = e.target.checked
this.setState({
users
})
}
}
| Element | Value Property | Change Callback | New Value in the callback |
|---|---|---|---|
<input type="text" /> | value="string" | onChange | event.target.value |
<input type="checkbox" /> | checked = {boolean} | onChange | event.target.checked |
<input type="radio" /> | checked = {boolean} | onChange | event.target.checked |
<textarea /> | value = "string" | onChange | event.target.value |
<select /> | value = "option value" | onChange | event.target.value |
import { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
username: '',
password: ''
}
}
render() {
return (
<form onSubmit={e => this.handleSubmit(e)}>
用户名: <input
type="text"
name="username"
onChange={e => this.handleChange(e)}
value={ this.state.username }
/>
密码: <input
type="text"
name="password"
onChange={e => this.handleChange(e)}
value={ this.state.password }
/>
<button>sumbit</button>
</form>
)
}
handleSubmit(e) {
// 阻止表单的默认提交行为
e.preventDefault()
console.log(this.state.username, this.state.password)
}
handleChange(e) {
// 当在受控组件上设置name属性的时候
// 传入的合成事件对象上就存在一个name属性,其值就是我们在受控组件上设置的name属性
this.setState({
[e.target.name]: e.target.value
})
}
}
非受控组件
React推荐大多数情况下使用 受控组件 来处理表单数据,因为在一个受控组件中,表单数据是由 React 组件来管理的
但是如果某些情况下,我们就需要使用DOM操作来进行表单元素值的获取,而这类组件就被称之为非受控组件
原则上是不推荐我们在React中自己来操作DOM元素的,所以原则上也不推荐使用非受控组件
import { PureComponent, createRef } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.usernameRef = createRef()
}
render() {
return (
<form onSubmit={e => this.handleSubmit(e)}>
用户名: <input
type="text"
ref={this.usernameRef}
/>
<button>sumbit</button>
</form>
)
}
handleSubmit(e) {
e.preventDefault()
// 在原生表单元素上有一个value属性
// 可以帮助我们获取到表单元素的值
console.log(this.usernameRef.current.value)
}
}
非受控组件中通常使用defaultValue来设置默认值
同样,<input type="checkbox"> 和 <input type="radio"> 支持 defaultChecked, <select> 和 <textarea> 支 持 defaultValue
render() {
return (
<form onSubmit={e => this.handleSubmit(e)}>
{/*
使用defaultValue设置元素的默认值
而不是去使用原本的value属性
*/}
用户名: <input
type="text"
defaultValue="Klaus"
ref={this.usernameRef}
/>
<button>sumbit</button>
</form>
)
}
Protals
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元 素上的)
比如界面中的Model弹框
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案
- 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment
- 第二个参数(container)是一个 DOM 元素
Cpn
import ReactDOM from 'react-dom'
export default function Cpn(props) {
// 手动定义挂载点
const div = document.createElement('div')
div.setAttribute('id', 'model')
document.body.appendChild(div)
return ReactDOM.createPortal(props.children, document.getElementById('model'))
}
App
import { PureComponent } from 'react'
import Cpn from './Cpn'
export default class App extends PureComponent {
render() {
return <Cpn>
<h2>Cpn1</h2>
<h2>Cpn2</h2>
<h2>Cpn3</h2>
</Cpn>
}
}
fragments
在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素
但很多情况下,内容外层的那个div其实是多余的,没有必要的
此时我们可以使用React内置的Fragment组件
React在编译的时候,会自动识别Fragment组件,其在使用上和div是一致的,只不过Fragment组件最终不会进行任何的渲染
import { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
name: 'Klaus',
age: 23
}
}
render() {
return (
<div>
{/*
不使用fragment --- 有且必须要有一个根元素
*/}
<span>name: { this.state.name }</span>
<span>age: { this.state.age }</span>
</div>
)
}
}

import { PureComponent, Fragment } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
name: 'Klaus',
age: 23
}
}
render() {
return (
<Fragment key="foo">
{/*
1. 在实际渲染的是Fragment并不会被渲染,其在功能上和div其实是一致的
2. 使用Fragment标签的时候,需要手动引入Fragment标签
3. 可以在Fragment标签上添加对应的key
*/}
<span>name: { this.state.name }</span>
<span>age: { this.state.age }</span>
</Fragment>
)
}
}

import { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
name: 'Klaus',
age: 23
}
}
render() {
return (
<>
{/*
1. <></> ---- <Fragment></Fragment>的语法糖表示 --- 短语法表示
2. 使用<></>的时候,不需要手动引入Fragment
3. 不可以在<></>上添加任何的属性
*/}
<span>name: { this.state.name }</span>
<span>age: { this.state.age }</span>
</>
)
}
}
StrictMode
StrictMode 是一个用来突出显示应用程序中潜在问题的工具。
- 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI
- 它为其后代元素触发额外的检查和警告
- 严格模式检查仅在开发模式下运行;它们不会影响生产构建
严格模式的检查
- 检测过时的API
- 识别不安全的生命周期
- 使用过时的ref API
- 检查意外的副作用
- 这个组件的constructor会被调用两次
- 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作
- 在生产环境中,是不会被调用两次的