react 高级特性整理

这一篇会整理一些react常见的高级特性以及它们的应用场景:

  • 函数组件
  • 非受控组件
  • protals
  • context
  • 异步加载组件
  • shouldComponentUpdate 优化
  • pureComponent 和 memo优化
  • 了解Immutable概念

还有一部分关于组件公共逻辑抽离的高级特性,由于篇幅太长,我会另写一篇来介绍:

  • HOC 高阶组件
  • Render Props

函数组件

  • 纯函数,输入props,输出JSX
  • 没有实例,没有生命周期,不包含state状态
  • 不能扩展其它方法
类组件和函数组件对比

两者的选择:

  • 如果仅是视图展示,没有状态的话,逻辑简单,建议使用函数组件;
  • 如果要定义内部状态,逻辑比较复杂,可能用到生命周期的话,建议使用类组件

非受控组件

非受控组件,就是不受组件内部state控制的组件,这时表单数据将交由 DOM 节点来处理:

  • 初始值由defaultValuedefaultChecked来使用state赋值
  • 但表单内容修改后,对应的state值不会修改,因为没有通过onChange等事件回传
  • 要拿到表单内容修改的值,会使用refref通过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
image.png

使用场景:

  • 必须手动操作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
portal

使用场景:

  • 父级元素 overflow:hidden,遮挡子元素的展示
  • 父组件的z-index值太小,导致子组件内容被遮挡
  • fixed布局的组件需要放在body第一层级的,比如上面例子的弹窗

context

上下文context用于向每个组件传递一些公共信息,当组件嵌套层级过深时,使用props传递太过麻烦,使用redux又太过度设计,这时就会使用context来传递,它的作用类似于vue中的provide inject的作用
它的使用方法如下:

  1. 创建一个自定义的context上下文对象,比如ThemeContext
    这个对象是祖先子孙组件中的枢纽,所有组件要通过它来进行通信
// contextType.js
import React from 'react'
// 1. 通过createContext创建一个`context`对象
// theme: 是自定义的上下文名称
// ThemeContext: 是自定义的上下文对象,是后续祖先孙子组件的中间承接者,所以要导出方便子孙组件使用
export const ThemeContext = React.createContext('theme')
  1. 在父组件中引入刚定义的上下文对象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'
    })
  }
}
  1. 在子组件中使用时,类组件函数组件使用context对象的方式是不一样的,下面我使用两个组件例子来说明,子组件使用类组件,孙组件中使用函数组件

  2. 子组件(类组件)使用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
  1. 孙组件(函数组件)中使用
    • 导入上下文对象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>
}
context使用

context使用

异步组件加载

  • 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 优化

shouldComponentUpdatereact的一个生命周期,顾名思义,就是用于设置是否进行组件更新,常用的场景是用来优化子组件的渲染
SCU默认返回true,即react默认重新渲染所有子组件,当父组件内容更新时,所有子组件都要更新,无论这子组件内容是否有更新;
我们可以在子组件的shouldComponentUpdate生命周期中设置,只有当子组件某些状态(注意这里最好是用不可变状态来判断,否则性能优化代价太大)发生更新时,我们才返回true让其重新渲染,从而提升渲染性能;否则返回false,不渲染

shouldComponentUpdate(nextProps, nextState) {
     // 只有父组件xxx状态改变时,当前子组件才重新渲染
    if(nextProps.xxx !== this.props.xxx) {
      return true;
    }
    return false;
  }

因为这个讲起来篇幅太长,这里不再扩展,想具体了解的,可以参考 shouldComponentUpdate

PureComponent 和 memo

PureComponentmeno其实就是react内部提供的具有SCU浅比较优化的Component组件,PureComponent(纯组件)针对的是类组件的使用方式,而meno针对的是函数组件的使用方式,当props或者state改变时,PureComponent将对propsstate进行浅比较,如果有发生改变的话,则重新渲染,否则不渲染。

注意,使用PureComponentmeno的前提是,使用不可变值的状态,否则这个浅比较是起不到优化作用的

对大部分需求来说,PureComponentmeno已经能满足性能优化的需求了,但这要求我们设计的数据层级不要太深,且要使用不可变量

PureComponent的使用非常简单,就是把React.Component换成React.PureCompoennt就可以了,它会隐式在SCU中对propsstate进行浅比较。

// 改为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>
}

简单总结一下:

  • 是不可变值的
  • 基于共享数据(但不是深拷贝),速度好
  • 有一定的学习和迁移成本,按需使用
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容