这一篇会整理一些react
常见的高级特性以及它们的应用场景:
- 函数组件
- 非受控组件
- protals
- context
- 异步加载组件
- shouldComponentUpdate 优化
- pureComponent 和 memo优化
- 了解
Immutable
概念
还有一部分关于组件公共逻辑抽离的高级特性,由于篇幅太长,我会另写一篇来介绍:
- HOC 高阶组件
- Render Props
函数组件
- 纯函数,输入
props
,输出JSX
- 没有实例,没有生命周期,不包含
state
状态 - 不能扩展其它方法
两者的选择:
- 如果仅是视图展示,没有状态的话,逻辑简单,建议使用函数组件;
- 如果要定义内部状态,逻辑比较复杂,可能用到生命周期的话,建议使用类组件
非受控组件
非受控组件,就是不受组件内部state
控制的组件,这时表单数据将交由 DOM 节点来处理:
- 初始值由
defaultValue
或defaultChecked
来使用state
赋值 - 但表单内容修改后,对应的
state
值不会修改,因为没有通过onChange
等事件回传 - 要拿到表单内容修改的值,会使用
ref
(ref
通过createRef
来创建)来获取对应dom
,然后获取对应的值
import React, {Component} from 'react'
// // class 类组件
class NonFormInput extends Component {
constructor(props) {
super(props)
this.state = {
name: '小花',
flag: true
}
// 创建ref,react要通过createRef来创建,不能像vue一样直接使用字符串
this.nameInputRef = React.createRef()
this.fileInputRef = React.createRef()
}
render() {
const {name, flag} = this.state
return <div>
{/*
使用defaultValue赋初始值
ref的作用就是用来标识dom的,如vue中的ref="xxx"
*/}
<input defaultValue={name} ref={this.nameInputRef}/>
{/* this.state.name不会随着表单内容改变 */}
<span>state.name:{name}</span>
<br/>
<button onClick={this.alertName}>alert name</button>
<hr/>
<input type="file" ref={this.fileInputRef}/>
<button onClick={this.alertFile}>alert file</button>
</div>
}
alertName = () => {
// ref指代的dom元素,<input value="小花">
console.log(this.nameInputRef.current)
// value值
alert(this.nameInputRef.current.value)
}
alertFile = () => {
const ele = this.fileInputRef.current
console.log(ele.files[0].name)
}
}
export default NonFormInput
使用场景:
- 必须手动操作Dom元素,setState实现不了的
- 常见的有
文件上传<input type=file>
,因为它的值只能由用户设置,不能通过代码控制 - 某些富文本编辑器,需要传入
dom
元素
受控和非受控选择:
- 优先使用受控组件,符合
react
设计原则 - 必须操作
dom
时,再使用非受控组件
Portals
Portals
是将组件渲染到指定到dom
元素上,可以是脱离父组件甚至是root
根元素,放到其以外的元素上,类似vue3 teleport
的作用
先看下未使用Portals
样子:
// ProtalsDemo.js
import React, {Component} from 'react'
class ProtalsDemo extends Component {
constructor(props) {
super(props)
}
render() {
return <div className="model">
{/* this.props.children等于vue中的slot插槽 */}
{this.props.children}
</div>
}
}
export default ProtalsDemo
// 在index.js引入组件
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import reportWebVitals from './reportWebVitals';
import ProtalsDemo from './advance/ProtalsDemo'
ReactDOM.render(
<React.StrictMode>
<ProtalsDemo>
model内容
</ProtalsDemo>
</React.StrictMode>,
document.getElementById('root')
);
reportWebVitals();
使用Portals
后:
需要使用ReactDOM.createPortal
创建protal
对象,它有两个参数:1. 要改变位置的组件, 2. 要改变的目标dom位置
// ProtalsDemo.js
import React, {Component} from 'react'
// 需要先引入 ReactDOM
import ReactDOM from 'react-dom'
class ProtalsDemo extends Component {
constructor(props) {
super(props)
}
render() {
// 使用 Portal 将组件放在指定的dom元素上
// 两个参数:第一个为要显示组件
// 第二个为要放至的dom元素位置
return ReactDOM.createPortal(
<div className="model">
{this.props.children}
</div>,
document.body
)
}
}
export default ProtalsDemo
使用场景:
- 父级元素
overflow:hidden
,遮挡子元素的展示 - 父组件的
z-index
值太小,导致子组件内容被遮挡 -
fixed
布局的组件需要放在body
第一层级的,比如上面例子的弹窗
context
上下文context
用于向每个组件传递一些公共信息,当组件嵌套层级过深时,使用props
传递太过麻烦,使用redux
又太过度设计,这时就会使用context
来传递,它的作用类似于vue
中的provide inject
的作用
它的使用方法如下:
- 创建一个自定义的
context
上下文对象,比如ThemeContext
这个对象是祖先子孙组件中的枢纽,所有组件要通过它来进行通信
// contextType.js
import React from 'react'
// 1. 通过createContext创建一个`context`对象
// theme: 是自定义的上下文名称
// ThemeContext: 是自定义的上下文对象,是后续祖先孙子组件的中间承接者,所以要导出方便子孙组件使用
export const ThemeContext = React.createContext('theme')
- 在父组件中引入刚定义的上下文对象
ThemeContext
,并使用ThemeContext.Provide
组件包裹所有子孙组件,并在其value
属性上设置要共享的状态
// Fahter.js
import React from 'react'
// 导入context上下文对象
import { ThemeContext } from './contextType'
import Son from './Son'
export default class Father extends React.Component {
constructor(props) {
super(props)
// 2. 最外层组件定义要共享的变量,比如这里共享主题颜色 themeColor
this.state = {
themeColor: 'light'
}
}
render() {
let { themeColor } = this.state
// 3. 由最外层组件使用上下文变量ThemeContext,通过Provide提供要共享的数据value
return <ThemeContext.Provider value={themeColor}>
<div>
这是父组件的内容内容内容
<Son />
<button onClick={this.changeTheme}>改变主题</button>
</div>
</ThemeContext.Provider>
}
changeTheme = () => {
this.setState({
themeColor: 'dark'
})
}
}
在子组件中使用时,类组件和函数组件使用
context
对象的方式是不一样的,下面我使用两个组件例子来说明,子组件使用类组件,孙组件中使用函数组件-
子组件(类组件)使用
context
- 导入上下文对象
ThemeContext
- 给类组件设置当前组件的
contextType
,指明这个组件要共享的上下文对象:Son.contextType = ThemeContext
- 通过
this.context
获取父组件传的共享状态并使用
- 导入上下文对象
import React from 'react'
// 4. 子组件中导入上下文对象
import { ThemeContext } from './contextType'
// 导入孙子组件
import Grandson from './Grandson'
class Son extends React.Component {
render() {
// 6. 通过this.context获取共享数据
const theme = this.context
// 7. 在子组件中正常使用即可
return <div>
这是子组件的内容,从父组件中获取的共享数据为: {theme}
<Grandson />
</div>
}
}
// 5. 类组件设置当前组件的contextType,指明这个组件要共享的上下文对象
Son.contextType = ThemeContext
export default Son
- 孙组件(函数组件)中使用
- 导入上下文对象
ThemeContext
- 使用上下文对象的
Consumer
组件,通过回调函数方式来获取对应的共享状态
- 导入上下文对象
// 8. 孙组件中导入上下文对象
import { ThemeContext } from './contextType'
export default function Grandson(props) {
// 9. 函数组件没办法从this中获取context,所以要借助上下文对象ThemeContext的Consumer来获取
return <ThemeContext.Consumer>
{ value => <p>这是孙子函数组件,从Father组件中获取到共享数据: {value}</p> }
</ThemeContext.Consumer>
}
异步组件加载
- React.lazy
React.lazy
通常会和Suspense
结合,来达成异步加载的效果,它类似vue3 defineAsyncComponent
的作用;
import React,{ Component, Suspense } from 'react';
// 异步导入组件
const AsyncComp = React.lazy(() => import('./FormInput'))
class SuspenseDemo extends Component {
render() {
// fallback代表异步操作之前的展示效果
return <Suspense fallback={<div>Loading...</div>}>
{/* 这里是异步引入的组件 */}
<AsyncComp/>
</Suspense>
}
}
export default SuspenseDemo;
shouldComponentUpdate 优化
shouldComponentUpdate
是react
的一个生命周期,顾名思义,就是用于设置是否进行组件更新,常用的场景是用来优化子组件的渲染
SCU
默认返回true
,即react
默认重新渲染所有子组件,当父组件内容更新时,所有子组件都要更新,无论这子组件内容是否有更新;
我们可以在子组件的shouldComponentUpdate
生命周期中设置,只有当子组件某些状态(注意这里最好是用不可变状态来判断,否则性能优化代价太大)发生更新时,我们才返回true
让其重新渲染,从而提升渲染性能;否则返回false
,不渲染
shouldComponentUpdate(nextProps, nextState) {
// 只有父组件xxx状态改变时,当前子组件才重新渲染
if(nextProps.xxx !== this.props.xxx) {
return true;
}
return false;
}
因为这个讲起来篇幅太长,这里不再扩展,想具体了解的,可以参考 shouldComponentUpdate
PureComponent 和 memo
PureComponent
和 meno
其实就是react
内部提供的具有SCU浅比较
优化的Component
组件,PureComponent
(纯组件)针对的是类组件的使用方式,而meno
针对的是函数组件的使用方式,当props
或者state
改变时,PureComponent
将对props
和state
进行浅比较,如果有发生改变的话,则重新渲染,否则不渲染。
注意,使用PureComponent
和 meno
的前提是,使用不可变值的状态,否则这个浅比较是起不到优化作用的
对大部分需求来说,PureComponent
和 meno
已经能满足性能优化的需求了,但这要求我们设计的数据层级不要太深,且要使用不可变量
PureComponent
的使用非常简单,就是把React.Component
换成React.PureCompoennt
就可以了,它会隐式在SCU
中对props
和state
进行浅比较。
// 改为PureComponent
import React, { PureComponent } from 'react';
export default class PureCompDemo extends PureComponent {
// ...
}
memo
的用法,稍微麻烦一些,需要自己手写一个类似的scu
浅拷贝的方法,然后通过React.memo
将这个方法应用到函数组件返回:
import React from 'react'
// 要使用的函数组件
function Mycomponent(props) {
console.log('render')
return <p>{props.name}</p>
}
// 需要自己手写一个类似scu的方法
function areEqual(preProps, nextProps) {
// console.log(preProps.name, nextProps.name)
if(preProps.name !== nextProps.name) {
return true;
}
return false;
}
// 通过memo将手写的SCU使用到函数组件中
export default React.memo(Mycomponent, areEqual)
了解 Immutable
前面我们多次提到Immutable
不可变值的理念,但是是怎么使用的呢?
Immutable
顾名思义,就是不可改变的值,它是一种持久化数据。一旦被创建就不会被修改。修改Immutable
对象的时候返回新的Immutable
。但是原数据不会改变。使用旧数据创建新数据的时候,会保证旧数据同时可用且不变,同时为了避免深度复制复制所有节点的带来的性能损耗,Immutable
使用了结构共享,即如果对象树种的一个节点发生变化,只修改这个节点和受他影响的父节点,其他节点则共享。
Immutable
其实不单react
中可以使用,在其它地方也可以使用,只不过它和react
的理念十分紧密,所以通常会结合起来一起使用和说明。
先看下它的基本使用:
npm i immutable
import immutable from "immutable";
export default function ImmutableDemo() {
let map = immutable.Map({
name: '小花',
age: 3
})
console.log(map) // Map {size: 2, ...}
// map原对象永远不会改变,只有创建新对象
let map1 = map.update('name', (val) => '小小')
return <div>
<p>{map.get('name')}</p>
<p>{map.get('age')}</p>
<p>{map1.get('name')}</p>
</div>
}
简单总结一下:
- 是不可变值的
- 基于共享数据(但不是深拷贝),速度好
- 有一定的学习和迁移成本,按需使用