這一篇會整理一些react
常見的高級特性以及它們的應用場景:
- 函數(shù)組件
- 非受控組件
- protals
- context
- 異步加載組件
- shouldComponentUpdate 優(yōu)化
- pureComponent 和 memo優(yōu)化
- 了解
Immutable
概念
還有一部分關于組件公共邏輯抽離的高級特性酷含,由于篇幅太長,我會另寫一篇來介紹:
- HOC 高階組件
- Render Props
函數(shù)組件
- 純函數(shù),輸入
props
跃闹,輸出JSX
- 沒有實例惩阶,沒有生命周期蜈亩,不包含
state
狀態(tài) - 不能擴展其它方法
兩者的選擇:
- 如果僅是視圖展示,沒有狀態(tài)的話短纵,邏輯簡單远舅,建議使用函數(shù)組件闰蛔;
- 如果要定義內部狀態(tài)竞思,邏輯比較復雜,可能用到生命周期的話钞护,建議使用類組件
非受控組件
非受控組件盖喷,就是不受組件內部state
控制的組件,這時表單數(shù)據(jù)將交由 DOM 節(jié)點來處理:
- 初始值由
defaultValue
或defaultChecked
來使用state
賦值 - 但表單內容修改后难咕,對應的
state
值不會修改课梳,因為沒有通過onChange
等事件回傳 - 要拿到表單內容修改的值,會使用
ref
(ref
通過createRef
來創(chuàng)建)來獲取對應dom
余佃,然后獲取對應的值
import React, {Component} from 'react'
// // class 類組件
class NonFormInput extends Component {
constructor(props) {
super(props)
this.state = {
name: '小花',
flag: true
}
// 創(chuàng)建ref暮刃,react要通過createRef來創(chuàng)建,不能像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實現(xiàn)不了的
- 常見的有
文件上傳<input type=file>
步势,因為它的值只能由用戶設置氧猬,不能通過代碼控制 - 某些富文本編輯器,需要傳入
dom
元素
受控和非受控選擇:
- 優(yōu)先使用受控組件坏瘩,符合
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
創(chuàng)建protal
對象,它有兩個參數(shù):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元素上
// 兩個參數(shù):第一個為要顯示組件
// 第二個為要放至的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
的作用
它的使用方法如下:
- 創(chuàng)建一個自定義的
context
上下文對象撑柔,比如ThemeContext
這個對象是祖先子孫組件中的樞紐,所有組件要通過它來進行通信
// contextType.js
import React from 'react'
// 1. 通過createContext創(chuàng)建一個`context`對象
// theme: 是自定義的上下文名稱
// ThemeContext: 是自定義的上下文對象您访,是后續(xù)祖先孫子組件的中間承接者铅忿,所以要導出方便子孫組件使用
export const ThemeContext = React.createContext('theme')
- 在父組件中引入剛定義的上下文對象
ThemeContext
,并使用ThemeContext.Provide
組件包裹所有子孫組件灵汪,并在其value
屬性上設置要共享的狀態(tài)
// 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提供要共享的數(shù)據(jù)value
return <ThemeContext.Provider value={themeColor}>
<div>
這是父組件的內容內容內容
<Son />
<button onClick={this.changeTheme}>改變主題</button>
</div>
</ThemeContext.Provider>
}
changeTheme = () => {
this.setState({
themeColor: 'dark'
})
}
}
在子組件中使用時,類組件和函數(shù)組件使用
context
對象的方式是不一樣的峻凫,下面我使用兩個組件例子來說明渗鬼,子組件使用類組件,孫組件中使用函數(shù)組件-
子組件(類組件)使用
context
- 導入上下文對象
ThemeContext
- 給類組件設置當前組件的
contextType
荧琼,指明這個組件要共享的上下文對象:Son.contextType = ThemeContext
- 通過
this.context
獲取父組件傳的共享狀態(tài)并使用
- 導入上下文對象
import React from 'react'
// 4. 子組件中導入上下文對象
import { ThemeContext } from './contextType'
// 導入孫子組件
import Grandson from './Grandson'
class Son extends React.Component {
render() {
// 6. 通過this.context獲取共享數(shù)據(jù)
const theme = this.context
// 7. 在子組件中正常使用即可
return <div>
這是子組件的內容譬胎,從父組件中獲取的共享數(shù)據(jù)為: {theme}
<Grandson />
</div>
}
}
// 5. 類組件設置當前組件的contextType,指明這個組件要共享的上下文對象
Son.contextType = ThemeContext
export default Son
- 孫組件(函數(shù)組件)中使用
- 導入上下文對象
ThemeContext
- 使用上下文對象的
Consumer
組件命锄,通過回調函數(shù)方式來獲取對應的共享狀態(tài)
- 導入上下文對象
// 8. 孫組件中導入上下文對象
import { ThemeContext } from './contextType'
export default function Grandson(props) {
// 9. 函數(shù)組件沒辦法從this中獲取context堰乔,所以要借助上下文對象ThemeContext的Consumer來獲取
return <ThemeContext.Consumer>
{ value => <p>這是孫子函數(shù)組件,從Father組件中獲取到共享數(shù)據(jù): {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 優(yōu)化
shouldComponentUpdate
是react
的一個生命周期驶冒,顧名思義苟翻,就是用于設置是否進行組件更新,常用的場景是用來優(yōu)化子組件的渲染
SCU
默認返回true
骗污,即react
默認重新渲染所有子組件袜瞬,當父組件內容更新時,所有子組件都要更新身堡,無論這子組件內容是否有更新邓尤;
我們可以在子組件的shouldComponentUpdate
生命周期中設置,只有當子組件某些狀態(tài)(注意這里最好是用不可變狀態(tài)來判斷贴谎,否則性能優(yōu)化代價太大)發(fā)生更新時汞扎,我們才返回true
讓其重新渲染,從而提升渲染性能擅这;否則返回false
澈魄,不渲染
shouldComponentUpdate(nextProps, nextState) {
// 只有父組件xxx狀態(tài)改變時,當前子組件才重新渲染
if(nextProps.xxx !== this.props.xxx) {
return true;
}
return false;
}
因為這個講起來篇幅太長仲翎,這里不再擴展痹扇,想具體了解的,可以參考 shouldComponentUpdate
PureComponent 和 memo
PureComponent
和 meno
其實就是react
內部提供的具有SCU淺比較
優(yōu)化的Component
組件溯香,PureComponent
(純組件)針對的是類組件的使用方式鲫构,而meno
針對的是函數(shù)組件的使用方式,當props
或者state
改變時玫坛,PureComponent
將對props
和state
進行淺比較结笨,如果有發(fā)生改變的話,則重新渲染,否則不渲染炕吸。
注意伐憾,使用PureComponent
和 meno
的前提是,使用不可變值的狀態(tài)赫模,否則這個淺比較是起不到優(yōu)化作用的
對大部分需求來說树肃,PureComponent
和 meno
已經(jīng)能滿足性能優(yōu)化的需求了,但這要求我們設計的數(shù)據(jù)層級不要太深瀑罗,且要使用不可變量
PureComponent
的使用非常簡單扫外,就是把React.Component
換成React.PureCompoennt
就可以了,它會隱式在SCU
中對props
和state
進行淺比較廓脆。
// 改為PureComponent
import React, { PureComponent } from 'react';
export default class PureCompDemo extends PureComponent {
// ...
}
memo
的用法筛谚,稍微麻煩一些,需要自己手寫一個類似的scu
淺拷貝的方法停忿,然后通過React.memo
將這個方法應用到函數(shù)組件返回:
import React from 'react'
// 要使用的函數(shù)組件
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使用到函數(shù)組件中
export default React.memo(Mycomponent, areEqual)
了解 Immutable
前面我們多次提到Immutable
不可變值的理念驾讲,但是是怎么使用的呢?
Immutable
顧名思義席赂,就是不可改變的值吮铭,它是一種持久化數(shù)據(jù)。一旦被創(chuàng)建就不會被修改颅停。修改Immutable
對象的時候返回新的Immutable
谓晌。但是原數(shù)據(jù)不會改變。使用舊數(shù)據(jù)創(chuàng)建新數(shù)據(jù)的時候癞揉,會保證舊數(shù)據(jù)同時可用且不變纸肉,同時為了避免深度復制復制所有節(jié)點的帶來的性能損耗,Immutable
使用了結構共享喊熟,即如果對象樹種的一個節(jié)點發(fā)生變化柏肪,只修改這個節(jié)點和受他影響的父節(jié)點,其他節(jié)點則共享芥牌。
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原對象永遠不會改變,只有創(chuàng)建新對象
let map1 = map.update('name', (val) => '小小')
return <div>
<p>{map.get('name')}</p>
<p>{map.get('age')}</p>
<p>{map1.get('name')}</p>
</div>
}
簡單總結一下:
- 是不可變值的
- 基于共享數(shù)據(jù)(但不是深拷貝)弃理,速度好
- 有一定的學習和遷移成本溃论,按需使用