beichensky / Blog

经常写博客的地方,会时常记录一些学习笔记、技术博客或者踩坑历程。
181 stars 12 forks source link

React 超详细入门知识 #1

Open beichensky opened 3 years ago

beichensky commented 3 years ago

前言

本文已收录在 Github: https://github.com/beichensky/Blog 中,欢迎 Star!

Overview

本文共用 19 个例子,详细讲解了 React 基础入门知识,列举了相关 API 的使用方式,并且在每个 API 的说明中给出了详细的使用规则、建议以及注意事项。

对于不熟悉 React 的朋友,可以作为入门文档进行学习

对于已经掌握 React 的朋友,也可以作为 API 参考手册,用来查漏补缺

Index

HTML Template

demo 相关的代码都依赖以下模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React JSX</title>

    <style>
        .blue {
            color: blue;
        }
    </style>
</head>
<body>
    <div id="root"></div>

    <script src="../libs/react.min.js"></script>
    <script src="../libs/react-dom.min.js"></script>
    <script src="../libs/babel.min.js"></script>
    <script type="text/jsx">
        // 真正编写 jsx 代码的地方
    </script>
</body>

</html>

Demo01

demo / source

jsx 语法的基本使用

类似于 xml 的代码格式,但是可以书写 js 逻辑

// 利用 babel 可以直接在 javascript 环境下使用 jsx 语法

// 由于 class 是关键字,所以在 jsx 中给元素设置 class 需要使用 className
const jsx = (
  <div>
    <h1 className="blue">Hello React</h1>
    <p>用于构建用户界面的 JavaScript 库</p>
  </div>
);

ReactDOM.render(jsx, root);

Demo02

demo / source

如何在 jsx 语法中编写 js 代码

下面演示在 jsx 中使用 js 的循环、条件判断语法

const todoList = ['吃饭', '睡觉', '敲代码'];

function handleAlert() {
  alert('Hello React!')
}

const a = 1;
const b = 2;
const showModal = true;
const loadingStatus = 'refreshing';

const jsx = (
  <div>
    {/* style 可以使用对象的形式来写,style 的属性必须使用驼峰法则 */}
    <h1 style={{ fontSize: 24, color: 'blue' }}>Hello React</h1>

    {/* 逻辑运算符 */}
    {a === b && <section>等于</section>}

    {/* 三目运算符 */}
    {showModal ? <section>弹窗组件</section> : null}

    {/* 列表循环生成新数组,数组内元素会被直接渲染到界面,
            每个节点可以给一个 key 值,方便 react 在更新时的 diff 对比 */}
    <ul>
      {todoList.map(todo => <li key={todo}>{todo}</li>)}
    </ul>
    <p>
      {
        {
          'loading': '加载中。。。。',
          'refreshing': '点击刷新重试!',
          'no-more': '没有更多了'
        }[loadingStatus] /** loadingStatus 是 `loading`、`refreshing`、`no-more`  其中一种状态 **/
      }
    </p>

    {/* 添加事件 */}
    <button onClick={handleAlert}>弹出提示</button>
  </div>
);

ReactDOM.render(jsx, root);

Demo03

demo / source

类组件和函数组件的声明和使用

下面的代码演示 React 组件的声明和使用

// 1、类组件,需要继承 React.Component,render 函数的执行结果会被作为界面展示内容
class ClassComponent extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, Class Component</h1>
      </div>
    );
  }
}

// 2、函数组件,本身就是一个函数,函数的执行结果会被作为界面展示内容
const FunctionComponent = () => {
  return (
    <div>
      <h1>Hello, Function Component</h1>
    </div>
  );
}

const App = () => (
  <div>
    <ClassComponent/>
    <FunctionComponent/>
  </div>
)

ReactDOM.render(<App/>, root);

Demo04

demo / source

如何为函数组件和类组件中的元素添加事件绑定?

在类组件中,为元素绑定事件时,事件函数内可能会用到组件的一些属性或者方法,那么此时 this 指向会出现问题。目前可以使用以下三种解决办法:

而函数组件中不存在这个问题

class ClassComponent extends React.Component {

    // 箭头函数
    arrowFunction = () => {
        console.log('使用箭头函数,this 指向:', this);
    }

    // bind 绑定 this
    bindFunction() {
        console.log('使用 bind 改变 this 指向:', this);
    }

    render() {
        return (
            <React.Fragment>
                <h3>类组件</h3>
                <div>
                    <button onClick={ this.arrowFunction }>箭头函数打印 this</button>
                    <br /><br />
                    <button onClick={ this.bindFunction.bind(this) }>bind 函数打印 this</button>
                    <br /><br />
                    <button onClick={() => console.log('匿名函数调用,this 指向:', this)}>匿名函数打印 this</button>
                </div>
            </React.Fragment>
        );
    }
}

/**
  * 在函数组件中,是不存在组件的 this 实例的,因此绑定事件时,不需要有类组件中的顾虑
  */
const FunctionComponent = () => {
    // 箭头函数
    const arrowFunction = () => {
        console.log('使用箭头函数');
    }

    // bind 绑定函数
    const bindFunction = function() {
        console.log('使用 bind 调用函数');
    }

    // 普通函数
    const normalFunction = function() {
        console.log('调用普通函数');
    }
    return (
        <React.Fragment>
            <h3>函数组件</h3>
            <div>
                <button onClick={ arrowFunction }>普通函数</button>
                <br /><br />
                <button onClick={ arrowFunction }>箭头函数</button>
                <br /><br />
                <button onClick={ bindFunction.bind(this) }>bind 函数</button>
                <br /><br />
                <button onClick={() => console.log('匿名函数调用')}>匿名函数</button>
            </div>
        </React.Fragment>
    );
}

const App = () => (
    <div>
        <ClassComponent />
        <FunctionComponent />
    </div>
)

ReactDOM.render(<App />, root);

Demo05

demo / source

React 组件中父子组件传值的方式:props

/**
  * 1、类组件的实例上会挂载 props 属性,包含父组件传递过来的所有参数
  *
  * props 中会包含一个 children 属性,标签内的所有内容都会被存放到 children 中。
  * 可以是标签、组件或者文本
  */
class ClassComponent extends React.Component {
    render() {
        return (
            <div className="box">
                <h1>Class Component</h1>
                <p>Receive Message: { this.props.msg }</p>
                { this.props.children }
            </div>
        );
    }
}

/**
  * 2、函数组件会接受一个 props 参数,包含父组件传递过来的所有参数
  *
  * props 中会包含一个 children 属性,标签内的所有内容都会被存放到 children 中。
  * 可以是标签、组件或者文本
  */
const FunctionComponent = (props) => {
    return (
        <div className="box">
            <h1>Function Component</h1>
            <p>Receive Message: { props.msg }</p>
            { props.children }
        </div>
    );
}

const App = () => (
    <div>
        <h1>App</h1>
        <ClassComponent msg="App 传递过来的 msg 信息">
            App 传递过来的 children 是文本
        </ClassComponent>
        <FunctionComponent  msg="App 传递过来的 msg 信息">
            App 传递过来的 children 是 ClassComponent 组件:
            <ClassComponent msg="Function Component 传递过来的 msg 信息"/>
        </FunctionComponent>
    </div>
);

ReactDOM.render(<App />, root);

Demo06

demo / source

React 类组件如何控制自身状态变化并触发界面更新?

class App extends React.Component {

    state = {
        count: 0
    }

    increment = () => {
        this.setState({
            count: this.state.count + 1
        }, () => {
            console.log(`最新的 state 值:${this.state.count}`)
        });
    }

    decrement = () => {
        this.setState((prevState) => ({
            count: prevState.count - 1
        }));
    }

    render() {
        return (
            <div>
                <p>count: { this.state.count }</p>
                <button onClick={ this.increment }>increment</button>
                <br />
                <br />
                <button onClick={ this.decrement }>decrement</button>
            </div>
        );
    }
}

ReactDOM.render(<App />, root);

Demo07

demo / source

类组件中 setState 用法详解

class App extends React.Component {

    state = {
        count: 0
    }

    handleClick = () => {
        // 1、同一 React 事件内的多次 setState 会被合并,最终的结果 count 只会 + 1

        this.setState({
            count: this.state.count + 2
        });

        this.setState({
            count: this.state.count + 1
        });
    }

    handleCallbackClick = () => {
        /*
          * 2、setState 第二个参数是一个回调函数 callback,当上一次 setState 完成时,会触发这个回调函数
          * 在 callback 内部可以获取到最新的 state 值
          */

        // 3、这种写法 setState 也不会被合并,两次操作都会按顺序执行

        this.setState({
            count: this.state.count + 2
        }, () => {
            this.setState({
                count: this.state.count + 1
            })
        });
    }

    handleSetTimeoutClick = () => {
        // 3、使用 setTimeout 的方式使用 setState 不会被合并,两次操作都会按顺序执行

        setTimeout(() => this.setState({ count: this.state.count + 2 }));
        setTimeout(() => this.setState({ count: this.state.count + 1 }));
    }

    handleOriginClick = () => {
        // 4、在绑定的原生事件中多次调用 setState 不会被合并,两次操作都会执行

        this.setState({
            count: this.state.count + 2
        });

        this.setState({
            count: this.state.count + 1
        });
    }

    componentDidMount() {
        const originBtn = document.querySelector('#originBtn');
        originBtn.addEventListener('click', this.handleOriginClick, false);
    }

    componentWillUnmount() {
        const originBtn = document.querySelector('#originBtn');
        originBtn.removeEventListener('click', this.handleOriginClick, false);
    }

    render() {
        return (
            <div>
                <p>count: { this.state.count }</p>
                <button onClick={ this.handleClick }>increment</button>
                <br />
                <br />
                <button onClick={ this.handleCallbackClick }>callback increment</button>
                <br />
                <br />
                <button onClick={ this.handleSetTimeoutClick }>setTimeout increment</button>
                <br />
                <br />
                <button id="originBtn">origin event increment</button>
            </div>
        );
    }
}

ReactDOM.render(<App />, root);

Demo08

demo / source

在类组件中进行 异步操作 或者 为元素绑定原生事件 的时机:componentDidMount

function fetchData() {
    return new Promise(rseolve => {
        setTimeout(() => {
            const todoList = [
                { id: 1, name: '吃饭'},
                { id: 2, name: '睡觉'},
                { id: 3, name: '敲代码'},
            ];
            rseolve(todoList);
        }, 1000)
    });
}

class App extends React.Component {

    constructor(props) {
        super(props);

        this.state = {
            todos:[],
            toggle: true,
            loading: true,
        }
    }

    handleWindowClick = () => {
        this.setState({ toggle: !this.state.toggle });
    }

    componentDidMount() {

        // 1、网络请求
        fetchData()
            .then(result => {
                this.setState({ todos: result });
            })
            .finally(() => {
                this.setState({ loading: false });
            });

        // 2、添加事件监听
        window.addEventListener('click', this.handleWindowClick, false);
    }

    componentWillUnmount() {
        // 移除事件监听
        window.removeEventListener('click', this.handleWindowClick, false);
    }

    render() {
        const { todos, toggle, loading } = this.state;
        return (
            <React.Fragment>
                <span style={{ color: 'gray', fontSize: 14 }}>随便点点试试</span>
                <h1 className="ani" style={{ height: toggle ? 50 : 200 }}>Hello React</h1>
                {
                    loading ? 
                        <p>Loading ...</p> :
                        <ul>
                            { todos.map(todo => <li key={ todo.id }>{ todo.name }</li>) }
                        </ul>
                }

            </React.Fragment>
        );
    }
}

ReactDOM.render(<App />, root);

Demo09

demo / source

React 类组件各生命周期触发时机

更多内容可参考这里:React 类组件生命周期详解

/**
  * 生命周期执行过程
  *
  * 初始化:constructor -> static getDerivedStateFromProps -> render -> componentDidMount
  * 更新:static getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate
  * 销毁:componentWillUnmount
  */

class LifeCycleComponent extends React.Component {

    /**
      * 组件初次渲染或者更新之前触发
      *
      * 返回值会作为新的 state 值与组件中之前的 state 进行合并
      */
    static getDerivedStateFromProps(nextProps, prevState) {
        console.log('LifeCycleComponent >>>', 'getDerivedStateFromProps ----', 'init or update');
        return null;
    }

    /**
      * 组件创建时调用
      * 可以在这里做一些初始化操作
      */
    constructor(props) {
        super(props);
        console.log('LifeCycleComponent >>>', 'constructor ----', 'init');
        this.state = {
            count: 0
        }
    }

    /**
      * 组件初次挂载完成时触发
      * 可以在这里处理一些异步操作,比如:事件监听,网络请求等
      */
    componentDidMount() {
        console.log('LifeCycleComponent >>>', 'componentDidMount ----', 'mounted');
    }

    /**
      * 组件触发更新时调用,决定组件是否需要更新
      * 返回 true,则组件会被更新,返回 false,则组件停止更新
      */
    shouldComponentUpdate(nextProps, nextState) {
        console.log('LifeCycleComponent >>>', 'shouldComponentUpdate ----', 'need update ? ');
        return true;
    }

    /**
      * 组将 render 之后,提交更新之前触发,返回值会作为 componentDidUpdate 的第三个参数传入
      */
    getSnapshotBeforeUpdate(prevProps, prevState) {
        console.log('LifeCycleComponent >>>', 'getSnapshotBeforeUpdate ----', 'before update');
        return null;
    }

    /**
      * 组件更新结束后触发
      */
    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('LifeCycleComponent >>>', 'componentDidUpdate ----', 'updated');
    }

    /**
      * 组将即将被卸载时触发
      */
    componentWillUnmount() {
        console.log('LifeCycleComponent >>>', 'componentWillUnmount ----', 'will unmount');
    }

    increment = () => {
        const { count } = this.state;
        this.setState({
            count: count + 1
        });
    }

    /**
      * 渲染函数 render
      */
    render() {
        console.log('LifeCycleComponent >>>', 'render');
        const { msg } = this.props;
        const { count } = this.state;
        return (
            <div>
                <h1>LifeCycleComponent</h1>
                <p>Receive Message: { msg }</p>
                <p>count: { count }</p>
                <button onClick={ this.increment }>increment</button>
            </div>
        );
    }
}

class App extends React.Component {

    state = {
        message: 'Hello World',
        show: true
    }

    render() {
        const { message, show } = this.state;
        return (
            <div>
                <button onClick={ () => this.setState({ message: 'Hello React' }) }>修改 message </button> | {' '}
                <button onClick={ () => this.setState({ show: !show }) }>
                    { show ? '销毁 LifeCycleComponent' : '创建 LifeCycleComponent' }
                </button>
                { show && <LifeCycleComponent msg={ message } /> }
            </div>
        );
    }
}

ReactDOM.render(<App />, root);

Demo10

demo / source

React 中受控组件的定义和使用

类似于 vue 中的 v-model

定义:组件的值受 React 中状态的控制,组件的变化会导致 React 中状态的更新

/**
  * 受控组件:
  *      组件的属性值会受到 React state 的控制,
  *      并且在组件的属性值发生变化时,React 的 state 值会做相应的修改。
  */

class App extends React.Component {

    state = {
        username: ''
    }

    handleNameChange = (e) => {
        this.setState({ username: e.target.value });
    }

    handleSubmit = () => {
        const { username } = this.state;
        if (username) {
            alert(`提交成功,username = ${ username }`);
        } else {
            alert('请填写用户名!');
        }
    }

    render() {
        const { username } = this.state
        return (
            <div>
                <p>username: { username }</p>
                <section>
                    <label>用户名:</label>
                    <input
                        placeholader="请输入用户名"
                        value={ username }
                        onChange={ this.handleNameChange }
                    />
                </section>
                <br />
                <button onClick={ this.handleSubmit }>submit</button>
            </div>
        );
    }
}

ReactDOM.render(<App />, root);

Demo11

demo / source

如何在 React 函数组件中控制自身状态变化以及副作用处理?

函数组件中控制自身状态的相关 hooksuseStateuseReducer

函数组件中处理副作用相关的 hooks: useLayoutEffectuseEffect

/**
  * 为了能够在函数组件中也能使用状态、执行副作用等操作,引入了 hooks 的概念
  * 
  * useState: 函数组件也可以拥有自身状态
  *
  * useReducer: useState 的升级版,可以根据不同操作返回不同的状态值
  *
  * useEffect 用法:
  *      1、第一个参数是副作用函数,第二个参数是依赖项集合
  *      2、副作用函数的返回值可以是一个函数,会在当前 useEffect 被销毁时执行,可以在这里做一些状态回收,事件解除等操作
  *      3、依赖项发生变化时,副作用操作会重新执行
  *      4、希望 useEffect 只执行一次,则可以给依赖项一个空数组
  *      5、希望组件的每次更新都执行 useEffect,可以不写依赖项
  */

const { useState, useEffect, useReducer, useLayoutEffect } = React;

const App = () => {
    // 
    const [count, setCount] = useState(0);
    const [num, dispatch] = useReducer((state, action) => {
        switch(action.type) {
            case 'INCREMENT': 
                return state + 1;
            case 'DECREMENT': 
                return state - 1;
            default:
                return state;
        }
    }, 0)

    useEffect(() => {
        console.log('useEffect')
        // 执行异步操作获取数据
        setCount(10);
    }, [])

    useLayoutEffect(() => {
        console.log('useLayoutEffect')
        // 绑定事件
        const handleClick = () => {
            alert(count);
        }
        const box = document.querySelector('#box');
        box.addEventListener('click', handleClick, false);

        return () => {
            box.removeEventListener('click', handleClick, false);
        }
    }, [count])

    return (
        <div>
            <p>count: { count }</p>
            <button onClick={() => setCount(count + 1)}>count increment</button>
            <p>num: { num }</p>
            <button onClick={() => dispatch({ type: 'INCREMENT' })}>num increment</button> | {' '}
            <button onClick={() => dispatch({ type: 'DECREMENT' })}>num decrement</button>
            <br />
            <br />
            <button id="box">alert count</button>
        </div>
    );
}

ReactDOM.render(<App />, root)

Demo12

demo / source

React 中错误边界处理

/**
  * 错误边界处理:组件出现异常,会触发 static getDerivedStateFromError 和 componentDidCatch 生命周期
  * 
  * static getDerivedStateFromError 的返回值会合并到组件的 state 中作为最新的 state 值
  */

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            hasError: false,
            errorMsg: ''
        };
    }

    static getDerivedStateFromError(error) {
        // 更新 state 使下一次渲染能够显示降级后的 UI
        return { hasError: true };
    }

    componentDidCatch(error, errorInfo) {
        this.setState({errorMsg: error.message})
        console.log('异常信息:', error, ' , ', errorInfo )
    }

    render() {
        const { hasError, errorMsg } = this.state;
        if (hasError) {
            // 你可以自定义降级后的 UI 并渲染
            return <h1>Something went wrong, Error Message: {errorMsg}</h1>;
        }

        return this.props.children; 
    }
}

const App = () => {
    const [count, setCount] = React.useState(0);

    if (count > 0) {
        throw TypeError('数据异常');
    }

    return (
        <div>
            <h2>App 组件</h2>
            <p>count: { count }</p>
            <button onClick={() => setCount(count + 1)}>increment</button>
        </div>
    );
}

ReactDOM.render(<ErrorBoundary><App /></ErrorBoundary>, root);

Demo13

demo / source

React 高阶组件(HOC)的两种创建方式

/**
  * 高阶组件的两种创建方式
  *  1、属性代理(Props Proxy):类组件和函数组件都可以使用
  *  2、反向继承(Inheritance Inversion, 缩写II):只用类组件可以使用
  */

  /**
  * 通过属性代理的方式向组件中注入 permission 属性
  */
function ComposeHOC(OriginComponent) {
    const permission = 'edit permission from ComposeHOC'
    return (props) => <OriginComponent {...props} permission={permission} />
}

/**
  * 通过反向继承的方式向组件中注入 DOM 节点
  */
function iiHOC(OriginComponent) {
    return class WrapperComponent extends OriginComponent {

        render() {
            return <div>
                    <h1>Title from iiHOC</h1>
                    { super.render() }
                </div>;
        }
    }
}

const ComponentA = (props) => <h2 className="box">ComponentA props permission: { props.permission }</h2>
class ComponentB extends React.Component {
    render() {
        return <h2 className="box">ComponentB</h2>;
    }
}

// 使用高阶组件包裹 A、B 组件
const WrapperComponentA = ComposeHOC(ComponentA);
const WrapperComponentB = iiHOC(ComponentB);

const App = () => (<div><WrapperComponentA /><WrapperComponentB /></div>);

ReactDOM.render(<App />, root);

Demo14

demo / source

Context 在类组件和函数组件中的使用

更多 Context 用法可参考这里:React 中 Context 用法详解

const { createContext, Component, useContext, useState } = React;

/**
 * createContext:    创建 context 上下文
 * Context.Provider: 需要对子组件进行包裹,在能在子组件中获取到 context 中的 value
 * Context.Consumer: 在类组件中使用 Render Props 的方式 context 上下文
 * useContext:       在函数组件中使用 context 上下文
 */

const UserContext = React.createContext();
const { Provider, Consumer } = UserContext;

class ClassComponent extends React.Component {
    render() {
        return (
            <div className="box">
                <h2>类组件</h2>
                <Consumer>
                    {user => (<div>name: { user.name }</div>)}
                </Consumer>
            </div>
        );
    }
}

const FunctionComponent = () => {
    const user = useContext(UserContext);
    return (
        <div className="box">
            <h2>函数组件</h2>
            <div>name: { user.name }</div>
        </div>
    );
}

const App = () => {
    const [user, setUser] = useState({ name: '孙悟空' });

    return (
        <Provider value={user}>
            <h1>App</h1>
            <label>Change name:</label>
            <input
                placeholder="请输入用户名称"
                value={user.name}
                onChange={(e) => setUser({ name: e.target.value })}
            />
            <ClassComponent />
            <FunctionComponent />
        </Provider>
    );
}

ReactDOM.render(<App />, root);

Demo15

demo / source

类组件和函数组件中 ref 的使用

const { createRef, useRef } = React;

/**
 * createRef:在类组件中为元素设置 ref
 * useRef: 在函数组件中为元素设置 ref
 *
 * 之前使用受控组件的方式进行表单提交。其实也可以使用 ref 的方式操作非受控组件
 */

class ClassComponent extends React.Component{
    inputRef = createRef();

    submit = () => {
        const { value } = this.inputRef.current;
        if (value) {
            alert(`提交成功,用户名为:${ value }`);
        } else {
            alert('请输入用户名!');
        }
    }

    render() {
        return (
            <div className="box">
                <h2>类组件</h2>
                <section>
                    <label>用户名:</label>
                    <input ref={ this.inputRef } placeholder="请输入用户名" />
                </section>
                <br />
                <button onClick={ this.submit }>提交</button>
            </div>
        );
    }
}

const FunctionComponent = () => {
    const inputRef = useRef();

    const submit = () => {
        const { value } = inputRef.current;
        if (value) {
            alert(`提交成功,用户名为:${ value }`);
        } else {
            alert('请输入用户名!');
        }
    }

    return (
        <div className="box">
            <h2>函数组件</h2>
            <section>
                <label>用户名:</label>
                <input ref={ inputRef } placeholder="请输入用户名" />
            </section>
            <br />
            <button onClick={ submit }>提交</button>
        </div>
    );
}

const App = () =>  (
    <div>
        <ClassComponent />
        <FunctionComponent />
    </div>
)

ReactDOM.render(<App />, root);

Demo16

demo / source

在函数组件中,父组件如何调用子组件中的状态或者函数:使用useImperativeHandle

用法:useImperativeHandle(ref, createHandle, [deps])

注意事项

const { useState, useRef, useImperativeHandle, useCallback } = React;

const ChildComponent = ({ actionRef }) => {

  const [value, setValue] = useState('')

  /**
   * 随机修改 value 值的函数
   */
  const randomValue = useCallback(() => {
    setValue(Math.round(Math.random() * 100) + '');
  }, []);

  /**
   * 提交函数
   */
  const submit = useCallback(() => {
    if (value) {
      alert(`提交成功,用户名为:${value}`);
    } else {
      alert('请输入用户名!');
    }
  }, [value]);

  useImperativeHandle(actionRef, () => {
    return {
      randomValue,
      submit,
    }
  }, [randomValue, submit])

  /* !! 返回多个属性要按照上面这种写法,不能像下面这样使用多个 useImperativeHandle
      useImperativeHandle(actionRef, () => {
          return {
              submit,
          }
      }, [submit])

      useImperativeHandle(actionRef, () => {
          return {
              randomValue
          }
      }, [randomValue])
  */

  return (
    <div className="box">
      <h2>函数组件</h2>
      <section>
        <label>用户名:</label>
        <input value={value} placeholder="请输入用户名" onChange={e => setValue(e.target.value)}/>
      </section>
      <br/>
    </div>
  );
}

const App = () => {

  const childRef = useRef();

  return (
    <div>
      <ChildComponent actionRef={childRef}/>
      <button onClick={() => childRef.current.submit()}>调用子组件的提交函数</button>
      <br/>
      <br/>
      <button onClick={() => childRef.current.randomValue()}>随机修改子组件的 input 值</button>
    </div>
  )
}

ReactDOM.render(<App/>, root);

Demo17

demo / source

React 中传送门 Portals 的使用:可以将指定 React 元素挂载到任意的 DOM 节点上去, 虽然在层级关系上,看起来实在父组件下,但在界面上是挂载到了指定的 DOM 节点上

官网解释:Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

用法: ReactDOM.createPortal(child, container)

Portals 的典型用例是当父组件有 overflow: hiddenz-index 样式时, 但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。

注意:尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。 由于 portal 仍存在于 React 树, 且与 DOM 树 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。包含事件冒泡。

/**
 * 通过 createPortal API,将 Modal 组件的真实节点挂载到新建的 div 元素上去
 * 虽然在 React 树中,Modal 组件仍然在 App 组件中,但是在界面上,Modal 节点其实是挂载在了新的 div 节点上
 */

const { useEffect, useState } = React;
const { createPortal } = ReactDOM;

const modalRoot = document.createElement('div');

/**
 * Modal: 弹窗组件
 */
function Modal({ children, onCancel }) {

  useEffect(() => {
    document.body.appendChild(modalRoot);
    return () => {
      document.body.removeChild(modalRoot);
    }
  })

  return createPortal(
    <div className="modal">
      <div className="modal-inner">
        <div className="mask"/>
        <section className="modal-content-wrapper">
          <div className="modal-content">
            <header>
              <h1>提示弹窗</h1>
            </header>
            <hr/>
            <content>{children}</content>
            <footer>
              <button onClick={onCancel}>关闭</button>
            </footer>
          </div>
        </section>
      </div>
    </div>,
    modalRoot
  );
}

const App = () => {
  const [visible, setVisible] = useState(false);
  return (
    <div>
      <h1>App</h1>
      <br/>
      <button onClick={() => setVisible(true)}>展示弹窗</button>
      {visible && <Modal onCancel={() => setVisible(false)}>
        自定义内容
      </Modal>}
    </div>
  );
}

ReactDOM.render(<App/>, root);

Demo18

demo / source

优化 React 组件的几种方式

const { Fragment, PureComponent, memo, useState, Component } = React;

class ClassComponent extends PureComponent {

    render() {
        console.log('PureComponent render');
        return (
            <div className="box">
                <h1>PureComponent 组件</h1>
                <p>count: {this.props.count}</p>
            </div>
        );
    }
}

const FunctionComponent = memo((props) => {
    console.log('memo function render');

    return (
        <div className="box">
            <h1>memo 函数组件</h1>
            <p>num: {props.num}</p>
        </div>
    );
})

const App = () => {
    const [count, setCount] = useState(0);
    const [num, setNum] = useState(0);
    return (
        <Fragment>
            <p>打开控制台查看 render 日志</p>
            <div>
                <button onClick={ () => setCount(count + 1) }>increment count</button> | {' '}
                <button onClick={ () => setNum(num + 1) }>increment num</button>
                <ClassComponent count={ count }/>
                <FunctionComponent num={ num }/>
            </div>
        </Fragment>
    );
}

ReactDOM.render(<App />, root);

Demo19

demo / source

提升函数组件性能常用的两个 hooks: useCallbackuseMemo

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

const { Fragment, useCallback, useMemo, useState } = React;

const App = () => {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  const doubleCount = useMemo(() => {
    return count * 2;
  }, [count])

  const alertNum = useCallback(() => {
    alert(`num 值:${num}`);
  }, [num])

  console.log('render');

  return (
    <Fragment>
      <p>count: {count}</p>
      <p>doubleCount: {doubleCount}</p>
      <p>num: {num}</p>
      <button onClick={() => setCount(count + 1)}>increment count</button>
      | {' '}
      <button onClick={() => setNum(num + 1)}>increment num</button>
      | {' '}
      <button onClick={alertNum}>alert num</button>
    </Fragment>
  );
}

ReactDOM.render(<App/>, root);

写在后面

如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。

如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。

david2tdw commented 3 years ago

写的不错