React示例探索

150 阅读10分钟

1、首先来认识什么是JSX

1.1 JSX简介

  1. 如下这个有趣的标签既不是字符串(没有引号)也不是HTML,它被称为 JSX,是一个 JavaScript 的语法扩展,全称为 JavaScript XML;
  2. 在 JSX 语法中,你可以在大括号内放置任何有效的 JavaScript 表达式。例如,2 + 3,user.firstName 或 formatName(user) 都是有效的 JavaScript 表达式;
  3. JSX 里标签的 class 变成了 className;
  4. 假如一个标签里面没有内容,你可以使用 /> 来闭合标签,就像 XML 语法一样。
// 1.单纯的JSX语法
const element = <h1>Hello, world!</h1>;

 // 2.处理JS逻辑的JSX
const element = <h1>Hello, {name}, {2 + 3}</h1>;
 
 // 3.JSX调用函数的返回结果进行渲染
function formatName(name) {
    return name;
}
const element = <h1>Hello, {formatName('xiaoming')}</h1>;

// 4.自闭和
const element = <img src={user.avatarUrl} />;

1.2 JSX的内部机制

Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。

const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

// React.createElement() 会预先执行一些检查,以帮助你编写无错代码,但实际上createElement创建了一个这样的对象。注意:这是简化过的结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};

属性展开:如果你已经有了一个 props 对象,你可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象。以下两个组件是等价的:

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

2、熟悉JSX中的React语法

2.1 遍历 map

<ul>
    {
        this.state.listArr.map((item, index) => {       // 遍历
            return <li key={index}>{item}</li>;
        })
    }
</ul>

2.2 点击事件this指向

只在class类组件里面才有this的使用,函数式组件里面是没有this的

点击事件的this的使用详见下面的“3.1.1、应用”

2.3 state的操作

class类编程的时候,不能使用hook,可以使用this。

constructor() {
    super(props);
    // 在构建函数里面初始化state
    this.state = {
        language: 'zh'
    };
}

// 更新数据必需使用setState
this.setState({
    language: 'en'
});

// 注意:State 的更新可能是异步的。出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。
// 因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。
// 错误
this.setState({
    counter: this.state.counter + this.props.increment,
});
// 正确
this.setState((state, props) => ({
    counter: state.counter + props.increment
}));

不要直接操作state里面的数据 拷贝一个副本

deleteItem(index){
    const list = [...this.state.list]  // 不要直接操作state里面的数据 拷贝一个副本
    list.splice(index, 1)
    this.setState({
        list                 
    })
}
<todoItem key={index} index={index} delete={this.deleteItem.bind(this)} content={item}></todoItem>  // 父组件

函数式编程的时候,可以使用hook,不能使用this(因为函数组件本质是一个js函数,函数是没有this的)。

// 初始化定义username
const [username, setUsername] = useState('');  // 数组解构
// 更新username使用 setUsername
setUsername('xiaoming');

useState是内置的hook,如下只是解读一下实现原理

function useState(initialState) {
  let state = initialState; // 初始化状态值
  const setState = (newState) => {
    state = newState; // 更新状态值
    // 触发组件重新渲染
    // ...
  };
  return [state, setState]; // 返回状态值和更新状态的函数
}

2.4 不使用redux下的父子组件传值(建议使用Redux)

父传子

  • 父组件通过属性的形式向子组件传递参数
  • 子组件通过props接受父组件传递过来的数据
  • 单项数据流 子组件不可以直接改变父组件的数据
<todoItem content={item}></todoItem>        // 父组件
<li>{this.props.content}</li>              // 子组件

子传父 子组件

handleDeleteClick(){
    const {delete, index} = this.props      // es6解构赋值
    this.props.delete(index)
}
<li onClick={this.handleDeleteClick.bind(this)}>{this.props.content}</li>              // 子组件

2.5 组件里面编写css样式

// 第一个{}指的是JSX语法的表达式  第二个{}表示一个对象
<button style={ {background: 'red'; color: 'white'} }>add</button>

2.6 运算符

if (isLoggedIn) {
    button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
    button = <LoginButton onClick={this.handleLoginClick} />;
}
{unreadMessages.length > 0 && (
    <h2>
      You have {unreadMessages.length} unread messages.
    </h2>)
  }
{isLoggedIn ? <LogoutButton onClick={this.handleLogoutClick} />
            : <LoginButton onClick={this.handleLoginClick} />
}

3、上手组件应用

3.1、class类组件

3.1.1、应用
  1. 组件一创建自动加载constructor构造函数,初始化this.state
  2. 执行render函数,输出一个DOM结构
  3. 在执行componentWillUnmount生命周期函数
  4. this.setState({})来更新数据
class TodoList extends Component {
    constructor(props) {
        super(props);
        this.state = {
            list: [],
            inputVal: '',
        };
        this.inputChange = this.inputChange.bind(this); // 点击事件 确定this的指向
        this.handleClick = this.handleClick.bind(this); // 点击事件 确定this的指向
    }
    
    componentWillUnmount() { }
    // 按钮点击事件
    handleClick() {
        this.setState({
            list: [...this.state.list, this.state.inputVal],
            inputVal: '',
        });
    }
    // input改变事件
    inputChange(e) {
        this.setState({
            inputVal: e.target.value,
        });
    }
    // 删除
    deleteData(index) {
        // 操作state里面的数据的时候 建议拷贝一个副本 不要直接操作state数据
        const list = [...this.state.list];
        list.splice(index, 1);
        this.setState({
            list,
        });
    }
    // 渲染的数据在抽出一个小的组件
    getItems() {
        return (
            // 遍历
            this.state.list.map((item, index) => {
                return (
                    <li key={index} onClick={this.deleteData.bind(this, index)}>
                        {item}
                    </li>
                );
            })
        );
    }
    render() {
        return (
            // 使用 Fragment 标签可以避免创建额外的 DOM 元素,使代码更加简洁
            <Fragment>
                <div>
                    <input value={this.state.inputVal} onChange={this.inputChange} />
                    <button className="btn-color" onClick={this.handleClick}>
                        add
                    </button>
                </div>
                <ul>{this.getItems()}</ul>
            </Fragment>
        );
    }
}
3.1.2、生命周期
  1. 创建时:创建时调用constructor的构造函数,更新render函数,更新DOM和refs,当这个component渲染到DOM节点之后,会调用componentDidMount方法
  2. 更新时:新的props传入和调用setState,重新调用render函数,更新DOM和refs,当这个component渲染到DOM节点之后,会调用componentDidUpdate方法
  3. 卸载时:调用componentWillUnmount

image.png

更加详细的生命周期解释,但是大部分钩子函数都是不常用的,记住上面三个钩子函数就可以

  • 组件的挂载流程:constructor ==> getDerivedStateFromProps ==> render ==> componentDidMount
  • setState更新流程:getDerivedStateFromProps ==> shouldComponentUpdate ==> render ==> getSnapshotBeforeUpdate ==> componentDidUpdate
  • 组件的卸载流程:componentWillUnmount

getDerivedStateFromProps

该方法必须是静态方法,不允许访问组件的实例(this),它接收两个参数:propsstate。因此,它应该是一个纯粹的函数,只根据传入的参数来计算和返回新的状态,不应该有任何副作用操作。如果需要进行副作用操作或异步操作,应该使用 componentDidUpdate 或其他适当的生命周期方法。

static getDerivedStateFromProps(nextProps, prevState) { 
    // 根据新的 props 和当前的 state 来计算新的 state 
    if (nextProps.value !== prevState.value) { 
        return { value: nextProps.value }; 
    } 
    return null; // 返回 null 表示不需要更新状态 
}

shouldComponentUpdate

shouldComponentUpdate 用于决定组件是否需要进行更新渲染。它在组件更新之前被调用,并且接收两个参数:nextPropsnextState,分别表示组件即将接收的新属性和新状态。 默认情况下,shouldComponentUpdate 返回 true,即每次组件接收新的属性或状态都会触发更新。但是,在某些情况下,我们可以通过自定义实现 shouldComponentUpdate 来优化组件的性能,避免不必要的更新操作。

shouldComponentUpdate(nextProps, nextState) { 
    // 根据需要的条件判断是否需要更新 
    if (this.props.someProp !== nextProps.someProp) { 
        return true; // 需要更新 
    } if (this.state.someState !== nextState.someState) { 
        return true; // 需要更新 
    } 
    return false; // 不需要更新 
}

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate方法可以用于获取组件更新前的 DOM 快照或其他有用的信息。它的返回值将作为参数传递给 componentDidUpdate 方法。通常情况下,它用于处理组件更新前后需要进行比较或操作的场景,例如在组件更新前保存滚动位置等。

getSnapshotBeforeUpdate(prevProps, prevState) { 
    // 获取组件更新前的快照 
    if (prevProps.someProp !== this.props.someProp) { 
        // 根据需要进行比较或操作 
        const snapshot = this.myRef.current.scrollTop; 
        return snapshot; 
    } 
    return null; 
}

3.2、函数式组件

3.2.1、应用
  1. useState初始化username
  2. useEffect钩子函数,当jwt更新时会执行useEffect
  3. 点击事件不需要bind(this)
import React, { useEffect, useState } from 'react';
import { Dropdown, Space, Button, Menu } from 'antd';
import { useSelector } from '../../redux/hooks'
import { useNavigate } from 'react-router-dom'
export const Header: React.FC = () => {
    const [username, setUsername] = useState('');
    const navigate = useNavigate();  // 路由
    const jwt = useSelector(s => s.user.token); // RTK方式 jwt一种认证机制
    useEffect(() => {
        if (jwt) {
            setUsername('xiaoming');
        }
    }, [jwt])
    const menuClickHnadler = (e: any) => { };
    const onLogout = () => { navigate("/"); }
    return (
        <>
            <Dropdown.Button
                overlay={
                    <Menu
                        onClick={menuClickHnadler}
                        items={[
                            ...languageList.map(item => {
                                return { key: item.code, label: item.name };
                            }),
                            { key: 'new', label: '添加新语言' },
                        ]}
                    ></Menu>
                }
            >
                {language === 'zh' ? '中文' : 'English'}
            </Dropdown.Button>
            {jwt ? (
                <Button.Group className="button-group">
                    <Button onClick={onLogout}>注销</Button>
                </Button.Group>
                ) : (
                    <Button.Group className="button-group">
                        <Button onClick={() => navigate("/register")}>注册</Button>
                    </Button.Group>
                )
            }
        </>
    );
};

对比 image.png

3.2.2、useEffect

函数式组件没有生命周期,因为生命周期函数是 React.Component 类的方法实现的,函数式组件没有继承 React.Component,所以也就没有生命周期。但是useEffect有生命周期函数同样的能力。

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

  1. useEffect第二个参数,如果是[], 表示componentDidMount, dom初始化完成调用;
  2. useEffect第二个参数,如果是[count], 可以理解state里面的count发生变化才会调用
  3. useEffect第二个参数,如果不加,则可以理解为react的所有的生命周期,很容易陷入死循环,所以要避免第二个参数为空的情况
  4. 一个函数式组件里面可以多次使用useEffect
useEffect(()=>{
    console.log("DidMount")
},[])

useEffect(()=>{
    console.log("DidUpdate")
},[count])

4、更进一步:路由系统

  1. react-router 整个路由系统的核心,他提供了最基本的路由功能
  2. react-router-dom 用于浏览器,专门处理Web App的路由
  3. react-router-native 用于React Native,专门用来处理手机app的路由
  4. react-router-redux 提供了一整套中间件, 用于处理redux的集成
  5. react-router-config 用来静态配置路由

路由跳转

// JSX跳转
<Link to={`/detail/${id}`}>  

// use hook跳转
const navigate = useNavigate();  
onClick={() => navigate('/register')}  

路由配置

import React, { useEffect } from 'react';
import styles from './App.module.css';
// HashRouter (BrowserRouter / HashRouter as Router)
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'; 
import { HomePage, SignInPage, RegisterPage, DetailPage, SearchPage, ShoppingCartPage, PlaceOrderPage } from './pages';
// 私有路由
const PrivateRoute = ({ component, isAuthenticated, ...rest }) => {
    // `rest`是一个包含剩余未解构的属性的对象
    const routeComponent = props => {
        return isAuthenticated ? React.createElement(component, props) : <Redirect to={{ pathname: '/signIn' }} />;
    };
    return <Route render={routeComponent} {...rest} />;
};
function App() {
    return (
        <div className={styles.App}>
            <BrowserRouter>
                <Switch>
                    // 跟路由
                    <Route exact path="/" component={HomePage} />
                    // 普通路由
                    <Route path="/signIn" component={SignInPage} />
                    <Route path="/register" component={RegisterPage} />
                    // 路由携带参数
                    <Route path="/detail/:touristRouteId" component={DetailPage} />
                    <Route path="/search/:keywords?" component={SearchPage} />
                    // 私有路由
                    <PrivateRoute isAuthenticated={jwt !== null} path="/shoppingCart" component={ShoppingCartPage} />
                    <PrivateRoute isAuthenticated={jwt !== null} path="/placeOrder" component={PlaceOrderPage} />
                    // 404路由
                    <Route render={() => <h1>404 not found 页面去火星了 !</h1>} />
                </Switch>
            </BrowserRouter>
        </div>
    );
}
export default App;

常用的 hook useHistory:用于访问浏览器的历史记录对象,可以用来进行页面跳转和导航

import { useHistory } from 'react-router-dom';

function MyComponent() {
  const history = useHistory();

  // 例子:点击按钮后跳转到指定页面
  const handleClick = () => {
    history.push('/my-page');
  };

  return (
    <button onClick={handleClick}>跳转</button>
  );
}

useLocation:用于获取当前页面的位置信息,包括路径名、搜索参数等。

import { useLocation } from 'react-router-dom';

function MyComponent() {
  const location = useLocation();

  // 例子:根据当前路径名显示不同内容
  let content;
  if (location.pathname === '/home') {
    content = <h1>首页</h1>;
  } else if (location.pathname === '/about') {
    content = <h1>关于我们</h1>;
  } else {
    content = <h1>未知页面</h1>;
  }

  return (
    <div>{content}</div>
  );
}

useParams:用于获取路由参数。

import { useParams } from 'react-router-dom';

function MyComponent() {
  const { id } = useParams();

  // 例子:根据路由参数显示不同内容
  return (
    <div>当前用户ID:{id}</div>
  );
}

5、进阶:hook

5.1、什么是React Hook

  1. React Hook是React 16.8带来的全新特性,即将替换class的写法,使用函数式组件代替class组件的一种写法
  2. 没有破坏性改动、完全可选、百分百向后兼容、没有计划从React移除class
  3. React组件一直是函数,使用Hook完全拥抱函数
  4. 使用Hook规则: 只在最顶层使用Hook只在React函数中调用Hook

5.2 常见的hook

  1. useState(第一个hook)
  2. useEffect(第二个hook)
  3. 自定义hook(第三个hook):将组件逻辑提取到可重用的函数中,必须用use开头
import React, { useState, useEffect } from 'react';
// 必须用use开头
const useMouseMove = () => {
    const [positions, setPositions] = useState({ x: 0, y: 0 });
    useEffect(() => {
        console.log('add effect', positions.x);
        const updateMouse = (e: MouseEvent) => {
            setPositions({ x: e.clientX, y: e.clientY });
        };
        document.addEventListener('click', updateMouse);
        return () => {
            console.log('remove effect', positions.x);
            document.removeEventListener('click', updateMouse);
        };
    }, []);
    return positions;
};
export default useMouseMove;

6、升华:Redux

6.1、Redux是什么

Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理。

Redux使用不依赖任何框架,可以在React里面使用,也可以在Vue或这Angular里面使用

那么我们什么时候需要使用Redux呢?

  • 第一,组件需要共享数据(或者叫做状态state)的时候;
  • 第二,某个状态需要在任何地方都可以被随时访问的时候;
  • 第三,某个组件需要改变另一个组件的状态的时候。

如下图简单概括一下:统一保存数据store,在隔离了数据与UI的同时,负责处理数据的绑定。

image.png

6.2、实战应用

npm install redux

// 创建数据存储store
import {createStore} from 'redux';
const defaultLanguage: IDefaultLanguage = {
    value: 'zh',
    languageList: [
        {name: '中文', code: 'zh'},
        {name: 'English', code: 'en'}
    ]
}
// reducer函数是一个纯函数,return直接返回结果,不经过其它的业务逻辑处理
const languageReducer = (state = defaultLanguage, action: IAction): IDefaultLanguage => {
    switch (action.type) {
        case 'change_language':
            return {...state, value: action.payload}
        case 'add_language':
            return {...state, languageList: [...state.languageList, action.payload]}
        default:
            return state
    }
}
const store = createStore(languageReducer);

// 引入自己创建的store仓库
import store from '../../redux/store';
// 获取store仓库里面的数据;
const [language, setLanguage] = useState(store.getState());
// store.dispatch派发数据
store.dispatch(addLanguageCreator(action));
// 改变语言的action
export const addLanguageCreator = (state: string): IAction => {
    return {
        type: CHANGE_LANGUAGE,
        payload: state
    }
}

// store.subscribe订阅数据
store.subscribe(() => {
    setLanguage(store.getState());
});