React高级特性

689 阅读5分钟

知识点

  • 函数组件
  • 非受控组件
  • Portals
  • context
  • 异步组件
  • shouldComponentUpdate(简称SCU)
  • PureComponent和React.memo
  • 不可变值immutable.js

image.png

函数组件

image.png

  • 纯函数,输入props,输出JSX
  • 没有实例,没有生命周期,没有state
  • 不能扩展其他方法

非受控组件

  • ref
  • defaultValue defaultChecked
  • 手动操作DOM元素 非受控组件使用场景:
  • 必须手动操作DOM元素,setState实现不了
  • 文件上传<input type=file>
  • 某些富文本编辑器,需要传入DOM元素

受控组件vs非受控组件

  • 优先使用受控组件,符合React设计原则(数据驱动视图)
  • 必须操作DOM时,再使用非受控组件

复习受控组件

import React from 'react'

class FormDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            name: 'aa',
            info: '个人信息',
            city: 'beijing',
            flag: true,
            gender: 'male'
        }
    }
    render() {
         // 受控组件
         return <div>
             <p>{this.state.name}</p>
             <label htmlFor="inputName">姓名:</label> {/* 用 htmlFor 代替 for */}
             <input id="inputName" value={this.state.name} onChange={this.onInputChange}/>
         </div>

    }
    onInputChange = (e) => {
        this.setState({
            name: e.target.value
        })
    }
}

export default FormDemo

非受控组件演示

import React from 'react'

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            name: 'aa',
            flag: true,
        }
        this.nameInputRef = React.createRef() // 创建 ref
        this.fileInputRef = React.createRef()
    }
    render() {
         // input defaultValue
         return <div>
             {/* 使用 defaultValue 而不是 value ,使用 ref */}
             <input defaultValue={this.state.name} ref={this.nameInputRef}/>
             {/* state 并不会随着改变 */}
             <span>state.name: {this.state.name}</span>
             <br/>
             <button onClick={this.alertName}>alert name</button>
         </div>

         // checkbox defaultChecked
         return <div>
             <input
                 type="checkbox"
                 defaultChecked={this.state.flag}
             />
         </div>

        // file
        return <div>
            <input type="file" ref={this.fileInputRef}/>
            <button onClick={this.alertFile}>alert file</button>
        </div>

    }
    alertName = () => {
        const elem = this.nameInputRef.current // 通过 ref 获取 DOM 节点
        alert(elem.value) // 不是 this.state.name
    }
    alertFile = () => {
        const elem = this.fileInputRef.current // 通过 ref 获取 DOM 节点
        alert(elem.files[0].name)
    }
}

export default App

Portals(传送门)

  • 组件默认会按照既定层次嵌套渲染
  • 如何让组件渲染到父组件以外? Portals使用场景
  • overflow:hidden
  • 父组件z-index值太小
  • fixed需要放在body第一层级 index.js
import React from 'react'
import PortalsDemo from './PortalsDemo'

class AdvancedUse extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return <div>
            <PortalsDemo>Modal 内容</PortalsDemo>
        </div>
    }
}

export default AdvancedUse

PortalsDemo.jsx

import React from 'react'
import ReactDOM from 'react-dom'
import './style.css'

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
        }
    }
    render() {
        // // 正常渲染
        // return <div className="modal">
        //     {this.props.children} {/* vue slot */}
        // </div>

        // 使用 Portals 渲染到 body 上。
        // fixed 元素要放在 body 上,有更好的浏览器兼容性。
        return ReactDOM.createPortal(
            <div className="modal">{this.props.children}</div>,
            document.body // DOM 节点
        )
    }
}

export default App

style.css

.modal {
    position: fixed;/*重点*/
    width: 300px;
    height: 100px;
    top: 100px;
    left: 50%;
    margin-left: -150px;
    background-color: #000;
    /* opacity: .2; */
    color: #fff;
    text-align: center;
}

context(上下文)

  • 公共信息(语言、主题)如何传递给每个组件?
  • 用props太繁琐
  • 用redux小题大做
import React from 'react'

// 创建 Context 填入默认值(任何一个 js 变量)
const ThemeContext = React.createContext('light')

// 底层组件 - 函数是组件
function ThemeLink (props) {
    // const theme = this.context // 会报错。函数式组件没有实例,即没有 this

    // 函数式组件可以使用 Consumer
    return <ThemeContext.Consumer>
        { value => <p>link's theme is {value}</p> }
    </ThemeContext.Consumer>
}

// 底层组件 - class 组件
class ThemedButton extends React.Component {
    // 指定 contextType 读取当前的 theme context。
    // static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext
    render() {
        const theme = this.context // React 会往上找到最近的 theme Provider,然后使用它的值。
        return <div>
            <p>button's theme is {theme}</p>
        </div>
    }
}
ThemedButton.contextType = ThemeContext // 指定 contextType 读取当前的 theme context。

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
    return (
        <div>
            <ThemedButton />
            <ThemeLink />
        </div>
    )
}

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            theme: 'light'
        }
    }
    render() {
        return <ThemeContext.Provider value={this.state.theme}>
            <Toolbar />
            <hr/>
            <button onClick={this.changeTheme}>change theme</button>
        </ThemeContext.Provider>
    }
    changeTheme = () => {
        this.setState({
            theme: this.state.theme === 'light' ? 'dark' : 'light'
        })
    }
}

export default App

异步加载组件(懒加载)

  • import()
  • React.lazy
  • React.Suspense
import React from 'react'

const ContextDemo = React.lazy(() => import('./ContextDemo'))

class App extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return <div>
            <p>引入一个动态组件</p>
            <hr />
            <React.Suspense fallback={<div>Loading...</div>}>
                <ContextDemo/>
            </React.Suspense>
        </div>

        // 1. 强制刷新,可看到 loading (看不到就限制一下 chrome 网速)
        // 2. 看 network 的 js 加载
    }
}

export default App

shouldComponentUpdate-性能优化

  • shouldComponentUpdate默认返回true,即React默认重新渲染所有子组件
  • 必须配合‘不可变值’一起使用(不直接修改state数据)
  • 可先不用SCU,有性能问题时再考虑使用

shouldComponentUpdate基本用法

shouldComponentUpdate(nextProps,nextState){
    if(nextState.count !== this.state.count){
        return ture //可以渲染
    }
    return false //不重复渲染
}

React 默认:父组件有更新,子组件则无条件也更新


import React from 'react'
import PropTypes from 'prop-types'

class Input extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            title: ''
        }
    }
    render() {
        return <div>
            <input value={this.state.title} onChange={this.onTitleChange}/>
            <button onClick={this.onSubmit}>提交</button>
        </div>
    }
    onTitleChange = (e) => {
        this.setState({
            title: e.target.value
        })
    }
    onSubmit = () => {
        const { submitTitle } = this.props
        submitTitle(this.state.title) // 'abc'

        this.setState({
            title: ''
        })
    }
}
// props 类型检查
Input.propTypes = {
    submitTitle: PropTypes.func.isRequired
}

class List extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        const { list } = this.props

        return <ul>{list.map((item, index) => {
            return <li key={item.id}>
                <span>{item.title}</span>
            </li>
        })}</ul>
    }
}
// props 类型检查
List.propTypes = {
    list: PropTypes.arrayOf(PropTypes.object).isRequired
}

class Footer extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return <p>
            {this.props.text}
            {this.props.length}
        </p>
    }
    componentDidUpdate() {
        console.log('footer did update')
    }
    shouldComponentUpdate(nextProps, nextState) {
        if (nextProps.text !== this.props.text
            || nextProps.length !== this.props.length) {
            return true // 可以渲染
        }
        return false // 不重复渲染
    }

    // React 默认:父组件有更新,子组件则无条件也更新!!!
    // 性能优化对于 React 更加重要!
    // SCU 一定要每次都用吗?—— 需要的时候才优化
}

class TodoListDemo extends React.Component {
    constructor(props) {
        super(props)
        // 状态(数据)提升
        this.state = {
            list: [
                {
                    id: 'id-1',
                    title: '标题1'
                },
                {
                    id: 'id-2',
                    title: '标题2'
                },
                {
                    id: 'id-3',
                    title: '标题3'
                }
            ],
            footerInfo: '底部文字'
        }
    }
    render() {
        return <div>
            <Input submitTitle={this.onSubmitTitle}/>
            <List list={this.state.list}/>
            <Footer text={this.state.footerInfo} length={this.state.list.length}/>
        </div>
    }
    onSubmitTitle = (title) => {
        this.setState({
            list: this.state.list.concat({
                id: `id-${Date.now()}`,
                title
            })
        })
    }
}

export default TodoListDemo

必须配合‘不可变值’一起使用(不直接修改state数据)

import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'

class Input extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            title: ''
        }
    }
    render() {
        return <div>
            <input value={this.state.title} onChange={this.onTitleChange}/>
            <button onClick={this.onSubmit}>提交</button>
        </div>
    }
    onTitleChange = (e) => {
        this.setState({
            title: e.target.value
        })
    }
    onSubmit = () => {
        const { submitTitle } = this.props
        submitTitle(this.state.title)

        this.setState({
            title: ''
        })
    }
}
// props 类型检查
Input.propTypes = {
    submitTitle: PropTypes.func.isRequired
}

class List extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        const { list } = this.props

        return <ul>{list.map((item, index) => {
            return <li key={item.id}>
                <span>{item.title}</span>
            </li>
        })}</ul>
    }

    // 增加 shouldComponentUpdate
    shouldComponentUpdate(nextProps, nextState) {
        // _.isEqual 做对象或者数组的深度比较(一次性递归到底)
        //不推荐深度比较,可以使用浅层比较
        if (_.isEqual(nextProps.list, this.props.list)) {
            // 相等,则不重复渲染
            return false
        }
        return true // 不相等,则渲染
    }
}
// props 类型检查
List.propTypes = {
    list: PropTypes.arrayOf(PropTypes.object).isRequired
}

class TodoListDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            list: [
                {
                    id: 'id-1',
                    title: '标题1'
                },
                {
                    id: 'id-2',
                    title: '标题2'
                },
                {
                    id: 'id-3',
                    title: '标题3'
                }
            ]
        }
    }
    render() {
        return <div>
            <Input submitTitle={this.onSubmitTitle}/>
            <List list={this.state.list}/>
        </div>
    }
    onSubmitTitle = (title) => {
        // 正确的用法
        this.setState({
            list: this.state.list.concat({
                id: `id-${Date.now()}`,
                title
            })
        })

        // // 为了演示 SCU ,故意写的错误用法
        // this.state.list.push({
        //     id: `id-${Date.now()}`,
        //     title
        // })
        // this.setState({
        //     list: this.state.list
        // })
    }
}

export default TodoListDemo

PureComponent(纯组件) 和 memo

  • PureComponent,shouldComponentUpdate中实现了浅比较
  • memo, 函数组件中的PureComponent
  • 浅比较已使用大部分情况(尽量不要做深度比较)

PureComponent使用


class List extends React.PureComponent {
    constructor(props) {
        super(props)
    }
    render() {
        const { list } = this.props

        return <ul>{list.map((item, index) => {
            return <li key={item.id}>
                <span>{item.title}</span>
            </li>
        })}</ul>
    }
    shouldComponentUpdate() {/*浅比较*/}
}
// props 类型检查
List.propTypes = {
    list: PropTypes.arrayOf(PropTypes.object).isRequired
}

React.memo使用

function MyComponent(props){
    /*使用props渲染*/
}
//这是一个类似于shouldComponentUpdate的函数
function areEqual(prevProps,nextProps){
    /*如果把nextProps传入render方法的返回结果与将prevProps
    传入render方法的返回结果一致则返回true,否则返回false*/
}
export default React.memo(MyComponent,areEqual);

immutable.js

  • 彻底拥抱‘不可变值’
  • 基于共享数据(不浊深拷贝),速度好
  • 有一定学习和迁移成本,按需使用
const map1 = Immutable.Map({a:1,b:2,c:3})
const map2 = map1.set('b',30)
map1.get('b') //2
map2.get('b') //30

高阶组件HOC(组件公共逻辑的抽离)

高阶组件基本用法

//高阶组件不是一种功能,而是一种模式
const HOCFactory = (Component) =>{
    class HOC extends React.Component{
        //在此定义多个组件的公共逻辑
        render(){
            return <Component {...this,props}/> //返回拼装的结果
        }
    }
    return HOC
}
const EnhancedComponent1 = HOCFactory(WrappedComponent1)
const EnhancedComponent2 = HOCFactory(WrappedComponent2)

实例Demo

import React from 'react'

// 高阶组件
const withMouse = (Component) => {
    class withMouseComponent extends React.Component {
        constructor(props) {
            super(props)
            this.state = { x: 0, y: 0 }
        }
  
        handleMouseMove = (event) => {
            this.setState({
                x: event.clientX,
                y: event.clientY
            })
        }
  
        render() {
            return (
                <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
                    {/* 1. 透传所有 props 2. 增加 mouse 属性 */}
                    <Component {...this.props} mouse={this.state}/>
                </div>
            )
        }
    }
    return withMouseComponent
}

const App = (props) => {
    const a = props.a
    const { x, y } = props.mouse // 接收 mouse 属性
    return (
        <div style={{ height: '500px' }}>
            <h1>The mouse position is ({x}, {y})</h1>
            <p>{a}</p>
        </div>
    )
}

export default withMouse(App) // 返回高阶函数

redux connect是高阶组件

import {connect} from 'react-redux'
//connect是高阶组件
const VisibleTodoList = connect(
    mapStateToProps,
    mapDispatchToProps
)(TodoList)
export default VisibleTodoList
export const connect = (mapStateToProps,mapDispatchToProps)=>(WrappedComponent)=>{
    class Connect extends Component{
        constructor(){
            super()
            this.state = {
                allProps:{}
            }
        }
        /*中间省略N行代码*/
        render(){
            return <WrappedComponent {...this.state.allProps} />
        }
    }
    return Connect
}

Render Props

//Render Props的核心思想
//通过一个函数将class组件的state作为props传递给纯函数组件
class Factory extends React.Component{
    constructor(){
        this.state = {
            /*state即多个组件的公共逻辑的数据*/
        }
    }
    /*修改state*/
    render(){
        return <div>{this.props.render(this.state)}</div>
    }
}

const App = () => (
    <Factory render = {
        /*render是一个函数组件*/
        (props)=> <p>{props.a} {props.b}...</p>
    } />
)

实例

import React from 'react'
import PropTypes from 'prop-types'

class Mouse extends React.Component {
    constructor(props) {
        super(props)
        this.state = { x: 0, y: 0 }
    }
  
    handleMouseMove = (event) => {
      this.setState({
        x: event.clientX,
        y: event.clientY
      })
    }
  
    render() {
      return (
        <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
            {/* 将当前 state 作为 props ,传递给 render (render 是一个函数组件) */}
            {this.props.render(this.state)}
        </div>
      )
    }
}
Mouse.propTypes = {
    render: PropTypes.func.isRequired // 必须接收一个 render 属性,而且是函数
}

const App = (props) => (
    <div style={{ height: '500px' }}>
        <p>{props.a}</p>
        <Mouse render={
            /* render 是一个函数组件 */
            ({ x, y }) => <h1>The mouse position is ({x}, {y})</h1>
        }/>
        
    </div>
)

/**
 * 即,定义了 Mouse 组件,只有获取 x y 的能力。
 * 至于 Mouse 组件如何渲染,App 说了算,通过 render prop 的方式告诉 Mouse 。
 */

export default App