react

225 阅读8分钟

react

用于构建用户界面的js库。

印记中文

中文文档,比如:react、react-router、redux、react-redux等

特点

组件化、声明式、使用虚拟DOM+diff算法减少与真实DOM的交互。

babel

es6转es5、jsx转js、默认开启了严格模式(use strict)

虚拟DOM

本质是object类型的对象、虚拟DOM比较轻(属性比较少),最终会被转为真实dom

渲染页面

ReactDOM.render(<Component />, document.getElementById('root'))

react解析组件标签,找到对应的组件,调用该组件,将返回的虚拟dom转为真实dom,呈现页面

jsx

全称JavaScript xml,本质是React.createElement(component, props, children)方法的语法糖。

使用规则:

  • 不要写引号
  • 在标签中混入js表达式要用{}
  • 类名写className(因为es6中类定义的关键字是class,为了避免所以使用className)
  • 内联样式style={{key: value}}
  • 只能有一个根标签
  • 标签必须闭合
  • 标签名小写,则转html里的同名标签,没有则报错
  • 标签名大写,react渲染对象的组件,没有则报错
  • xml早期用于存储和传输数据
  • 注释 {/* content */}

jsx是 React.createElement() 的语法糖

{{}}

外层括号表示要写js表达式,内层括号表示要写对象

组件

  • 类式组件(适用于复杂组件(有State))
  • 函数式组件(适用于简单组件(无state))当然现在可以使用hook函数了useState()

state

state是一个对象

类式组件:修改state,使用setState,修改后的state会和原state合并
        state = {};
        对象式:this.setState({}, [callback])
        函数式:
            // 函数式会接收到两个参数,state、props
            this.setState((state, props) => {}, [callback])
函数式组件:使用useState
    let [lxh, setLxh] = useState({name: "lxh", age: 24});
    修改state:修改后的state会覆盖原state
        第一种写法:setLxh({...lxh, age: 25});
        第二种写法:setLxh((value) => newValue);
        
    参数:第一次初始化的值在内部做缓存

props

props是一个只读对象

类式组件:vscode快捷键(rcep)
    第一种:(常用)
        // 限制类型
        static propTypes = {
                   name: PropTypes.string.isRequired,
                   fun: PropTypes.func,
                   boo: PropTypes.bool,
        }
       // 设置默认值
       static defaultProps = {
            name: "lxh",
       }
       
   第二种:
       组件名.propTypes = {}
       组件名.defaultProps = {}
函数式组件:vscode快捷键(rafcp)
    组件名.propTypes = {}
    组件名.defaultProps = {}

constructor构造器

可写可不写

写了

  • 接收props,并且传给super(props),这样在构造器里就能拿到props
  • 不接收props,在构造器里props就是undefined

ref

请勿过度使用ref

类式组件

第一种:字符串(不推荐,过时,效率不高),标签<input ref='xxx' />,通过this.refs.xxx获取

第二种:回调函数形式
    如果是内联函数,在更新过程中会执行两次:
        第一次传的值是null
        第二次才传的DOM节点
    使用标签<input ref={c => this.名称 = c} />
    获取this.名称
    这种情况无关要紧,可以写内联函数
    如果需要避免执行两次:
        不写内联函数,写成class绑定函数的形式
        <input ref={this.xxx} />
        xxx = (e) => {
            this.yyy = e
            console.log(e)
        }
        
第三种:React.createRef,专人专用,如果使用多次,需要定义多次
<input ref={this.xxx} />
this.xxx = React.createRef()

函数式组件

使用useRef()
const lxh = React.useRef()
lxh.current.value

context

常用于祖组件和后代组合间通信
使用:
    // 创建context容器对象
    const lxhContext = React.createContext()
    
    // 渲染子组件
    <lxhContext.Provider value={数据}>
        子组件
    </lxhContext.Provider>
    
    // 后代组件读取数据
    // 第一种方式:只适用于类式组件
    static contextType = lxhContext // 声明接收context
    this.context // 读取context中的value数据
    
    // 第二种方式:函数组件与类式组件都可以
    <lxhContext.Consumer>
        {
            value => {
                // value就是context中的value数据
                return `${value}`
            }
        }
    </lxhContext.Consumer>
    // 可以使用useContext获取数据
    const ctx = useContext(lxhContext)
    
    // 开发中一般不用context,一般用它来封装组件

受控组件、非受控组件

表单中收集数据
受控组件:通过input提供的onChange()事件
<input onChange={this.xxx('yyy')} />
xxx = (name) => {
    return (e) => {
        this.setState({[name]: e.target.value})    
    }
}

<input onChange={(e) => this.xxx('yyy', e)} />
xxx = (name, e) => {
    this.setState({[name]: e.target.value})
}

非受控组件:现取现用,操作DOM(ref),获取数据

推荐使用受控组件

高阶函数、函数的柯里化、纯函数


高阶函数:函数的参数是函数,函数的返回值是函数。符合其一就是高阶函数。
例如:Promise()、setTimeout()、map()等

函数的柯里化:
    this.lxh(1)(2)
    lxh = (a) => {
        return (b) => {
            // a = 1、b = 2
        }
    }
    
纯函数:只要是同样的输入(实参),必定得到同样的输出(返回值)

生命周期

类式组件

used.png

render() {
    // 必须使用
}

componentDidMount() {
    // 常用,一般用于开启定时器、发送网络请求、订阅消息
}

componentWillUnmount() {
    // 常用:一般开闭定时器、取消订阅消息
}

componentWillReceiveProps(props) {
    /**
     * 第一次接收props时,不执行,props改变时,执行,即将移除此生命周期
     * 使用:UNSAFE_componentWillReceiveProps() {}
     **/
}

componentWillMount() {
    // 即将移除此生命周期,使用:UNSAFE_componentWillMount() {}
}

componentWillUpdate() {
    // 即将移除此生命周期,使用:UNSAFE_componentWillUpdate
}

shouldComponentUpdate() {
    // 必须写返回值,不然报错
    return true // 不写这个生命周期,默认返回true
}

 this.forceUpdate() // 强制更新
 卸载组件:
   ReactDOM.unmountComponentAtNode(document.getElementById('xxx'))
   触发componentWillUnmount(),但不能卸载createRoot()创建的组件
   
   createRoot()创建的组件卸载方式:
     const ROOT = createRoot(document.getElementById('xxx'))
     (注意:后面加render函数这样写:ROOT.render())
     export default ROOT
     import root from '../index'
     root.unmount()

new.png

    废弃了3个,新增了2个
    过期的生命周期:
        UNSAFE_componentWillMount()
        UNSAFE_componentWillReceiveProps()
        UNSAFE_componentWillUpdate()

    getDerivedStateFromProps(props, state) {
        /**
         * 不常用
         * 若state的值在任何时候都取决于props,就可以使用
         * 缺点:派生状态会导致代码冗余,并使组件难以维护
         * 使用时,前面加static
         **/
         return props // 必须返回一个state对象 或者 null,是对象与state合并
    }
    
    getSnapshotBeforeUpdate() {
        /**
         * 不常用
         * 组件能在发生更改之前从DOM中捕获一些信息(如滚动位置),它的返回值传递给componentDidUpdate()
         * 必须和componentDidUpdate一起使用,不然报错
         * 必须返回一个值或者null,不能返回undefined
         **/
         return "snapshotValue" // 传给componentDidUpdate的第三个参数
    }
    
    componentDidUpdate(preProps, preState, snapshotValue) {
        // 更新完成
    }
    
    例子:
    import React, { Component } from "react";

    class Home extends Component {
      state = { arr: [] };
      componentDidMount() {
        let timer = setInterval(() => {
          this.setState(
            (state) => ({
              arr: ["新闻" + (state.arr.length + 1), ...state.arr],
            }),
            () => {
              if (this.state.arr.length >= 20) {
                clearInterval(timer);
                timer = null;
              }
            }
          );
        }, 1000);
      }
      
      componentWillUnmount() {}
      
      getSnapshotBeforeUpdate() {
        return this.refs.refStr.scrollHeight;
      }
      
      componentDidUpdate(preProps, preState, snapshot) {
        this.refs.refStr.scrollTop += this.refs.refStr.scrollHeight - snapshot;
      }
      
      render() {
        return (
          <div
            style={{
              height: 100,
              width: 200,
              backgroundColor: "pink",
              overflow: "auto",
            }}
            ref="refStr"
          >
            {this.state.arr.map((item, index) => {
              return (
                <div style={{ height: 20 }} key={index}>
                  {item}
                </div>
              );
            })}
          </div>
        );
      }
    }

    export default Home;

函数式组件

使用useEffect,可以在函数式组件中执行副作用操作(用于模拟类式组件里的生命周期钩子)
主要用于:
    发送ajax请求
    设置订阅、启动定时器
    手动更改真实DOM
用法:
    useEffect(() => {
        // 在此可是执行任意带副作用操作
        return () => {
            // 在组件卸载前执行,比如清除定时器、取消订阅
        }
    }, []) // 如果指定的是[],回调函数只会在render()后执行
    // 没有写[],就是监测所有state状态
    /**
     * 可以把useEffect看成是三个生命周期的组合
     * componentDidMount
     * componentDidUpdate
     * componentWillUnmount
     **/

路由

SPA:单页面应用,整个应用只有一个完整的页面,点击页面的链接,不会刷新页面,只会做页面的局部更新。

路由:一个路由就是一个映射关系(key:value)
     key:路径、value:component或function
         
前端路由:浏览器端路由,value是component,用于展示页面内容
         注册路由:<Route to='' component='' /> v5的方式
                  <Route to='' element='' /> v6的方式
         工作过程:当浏览器的path等于对应的路由时,当前的路由就会变成对应的组件。

后端路由:value是function,用于处理客户端提交的请求
         注册路由:router.get(path, (req, res) => {})
         工作过程:当node接收到一个请求时,根据请求的路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据。
    

react-router-dom

安装:npm i -S react-router-dom@5 // v5的版本,现在默认安装的是v6
     npm i -S react-router-dom // v6版本
     
浏览器:强制刷新,按住shift,点刷新

v5

// NavLink用于高亮,它的属性activeClassName用于选中的样式
<NavLink activeClassName='' className='' to='' replace></NavLink>
// Link没有activeClassName属性,默认是push模式
<Link className='' to='' replacee></Link>

// children,通过props来获取,下面两个相同
<NavLink>content</NavLink>
<NavLink children='content'></NavLink>

// Switch用于匹配到适合的第一个path时,就不往下匹配了
<Switch></Switch>

// 解决样式丢失问题
<link href='./css/bootstrap.css' />
第一种解决:<link href='/css/bootstrap.css' /> // 不用用./css...
第二种解决:<link href='%PUBLIC_URL%/css/bootstrap.css'>
第三种解决:用hash模式

// 严格匹配exact,不要随便开启,有时候开启不能匹配到二级路由
<Route path='' component='' exact></Route>

// Redirect重定向,一般写在路由的最下方
<Redirect to=''></Redirect>

路由传参:
// params方式:刷新不会丢失
<Link to={`/home/${id}`}></Link>
<Route path='/home/:id' /> // 需要占位
// 获取参数通过props.match.params
// 编程式
this.props.history.push(`/home/${id}`)
this.props.history.replace(`/home/${id}`)

// search方式(相当于ajax的query参数)刷新不会丢失
<Link to={`/home/?id=${id}&title=${title}`}></Link>
// 编程式
this.props.history.push(`/home?id=${id}`)
this.props.history.replace(`/home?id=${id}`)
/**
 * 不需要声明接收
 * 获取参数通过props.location.search,比如拿到'?id=xxx&title=lxh'
 * react默认安装了querystring
 */
import qs from 'querystring';
const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1)) // 需要去掉前面的问号

// state方式:history模式刷新不会丢失,hash模式刷新会丢失
<Link to={{pathname: '/home', state: {id: 'xxx'}}}></Link>
// 获取参数通过props.location.state
// 编程式
this.props.history.push({path: '/home', state: {id: ''}})
this.props.history.replace({path: '/home', state: {id: ''}})


history里的其他方法
this.props.history.go(-1)
this.props.history.goBack() // 后退
this.props.history.goForward() // 前进

// 一般组件使用路由的跳转,使用withRouter()
src/router/index.js
import React, { lazy } from "react";

const Home = lazy(() => import("../pages/home"));
const Login = lazy(() => import("../pages/login"));
const Error = lazy(() => import("../pages/error"));
// 无权限
export const commonRouter = [
    {
      path: "/login",
      component: Login,
      exact: true,
      meta: {
          title: "登录",
      }
    },
    {
      path: "/404",
      component: Error,
      exact: true,
      meta: {
          title: "404",
      }
    },
];
// 有权限
export const authRouter = [
    {
      path: "/",
      component: Home,
      exact: true,
      meta: {
          title: "首页",
      }
    },
];

src/app.jsx
import React, { Suspense } from "react";
import { BrowserRouter, Switch, Route, Redirect } from "react-router-dom";
import { commonRouter, authRouter} from "./router";
import Layout from "./layout";

const App = () => {
  return (
    <BrowserRouter>
        <Switch>
            <Suspense fallback={<>loading...</>}>
                // 无权限
                {
                   commonRouter.map(item => {
                       return (<Route
                                   key={item.path}
                                   exact={item.exact}
                                   render={(props) => {
                                       document.title = item.meta.title;
                                       return (<item.component {...props} />)
                                   }}
                               ></Route>)
                   }) 
                }
                // 有权限
                {
                   authRouter.map(item => {
                       return (<Route
                                   key={item.path}
                                   exact={item.exact}
                                   render={(props) => {
                                       document.title = item.meta.title;
                                       sessionStorage.getItem("token")
                                       ? <Redirect to="/login" />
                                       : <Layout><item.component {...props} /><Layout>
                                   }}
                               ></Route>)
                   }) 
                }
                <Redirect to="/404" />
            </Suspense>
        </Switch>
    </BrowserRouter>
  );
};

v6

Switch移除了,Routes代替
Route必须被Routes包裹,不然报错

component被element代替,caseSensitive区分path的大小写
<Route path='' element='' caseSensitive>

Navigate重定向,可以写replace属性,Redirect移除了
<Navigate to='' replace></Navigate>
<Route path='' element={<Navigate to='' />} />

NavLink的高亮样式,yyy选中的样式,写法不一样了,子组件高亮,不希望父组件高亮,父组件加end属性
<NavLink className={({isActive}) => isActive ? 'xxx yyy' : 'xxx'}></NavLink>
const xxxClassName = ({isActive}) => isActive ? 'xxx yyy' : 'xxx';
<NavLink className={xxxClassName} end></NavLink>


useRoutes路由表
useRoutes([
    {
        path: '/home/:id',
        element: <Home />,
    },
    {
        path: '/about',
        element: <About />,
        children: [
            {
                path: 'message', // 不写斜杠/
                element: < />,
            },
        ]
    },
    {
        path: '*',
        element: <Navigate to='/home' />,
    },
])
/**
 * <NavLink to='message'></NavLink> // 跳转子路由的写法
 * <NavLink to='./message'></NavLink> // 跳转子路由的写法
 * <Route index element={<Components />} /> 嵌套路由index是默认路由
 * Outlet指定路由组件呈现的位置
 */

路由传参的方式:3种
params方式
<Link to={`/home/${id}`}></Link>
// 需要声明Route或useRoutes里:id
// 获取参数使用hooks函数,useParams
import { useParams } from 'react-router-dom';
const { id } = useParams()

search方式:相当于(query)
<Link to={`/home?id={id}&title=${title}`}></Link>
// 不需要声明
// 获取参数useSearchParams
import { useSearchParams } from 'react-router-dom';
const [search, setSearch] = useSearchParams() // 返回一个数组
const id = search.get('id') // 通过get获取对应的参数值
setSearch('id=xxx&title=yyy') // 更新search的参数

state方式:
<Link to=`/home` state={{id: ''}}></Link>
// 不需要声明
// 获取参数useLocation
import { useLocation } from 'react-router-dom';
const { state: { id } } = useLocation(); // 解构赋值直接使用id

编程式路由导航
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
navigate('/home', {
    replace: true,
    state: {
        id: 'xxx',
    }
});
navigate(1) // 前进
navigate(-1) // 后退

用的不多的hooks函数
import { useMatch,
         useInRouterContext,
         useNavigationType,
         useOutlet,
         useResolvedPath,
         } from 'react-router-dom';
useMatch('/home/:id') // 必须传入路径
useInRouterContext() // 返回Boolean值,查看是否是Route包裹的组件
useNavigationType() // 返回PUSH、REPLACE、POP,查看当前的导航类型,刷新后就变成的POP
useOutlet() // 查看当前组件的嵌套路由,返回null说明还没有挂载
useResolvedPath('/user/#/asd?id=xxx&title=yyy') // 传入一个url值,返回path、hash、search值
src/router/index.js
import React, { lazy } from "react";
import { useRoutes, Navigate, Outlet } from "react-router-dom";

const Home = lazy(() => import("../pages/home"));
const Login = lazy(() => import("../pages/login"));
const Error = lazy(() => import("../pages/error"));

export default () => {
  return useRoutes([
    {
      path: "/login",
      element: <Login />,
    },
    {
      path: "/",
      element: <Home><Outlet /></Home>, // Outlet可写组件里
      children: [
        {
          path: "table",
          element: <Table />,
        },
      ],
    },
    {
      path: "/404",
      element: <Error />,
    },
    {
      path: "*",
      element: <Navigate to='/404' />,
    },
  ]);
};

src/app.jsx
import React, { Suspense } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import RouterAll from "./router";

const App = () => {
  return (
    <Router>
      <Suspense fallback={<>loading...</>}>
        <RouterAll></RouterAll>
      </Suspense>
    </Router>
  );
};

export default App;

类式组件跳转

// tsx版
import React, { ComponentClass } from "react";
import {
  NavigateFunction,
  useLocation,
  useNavigate,
  useParams,
} from "react-router";

export interface IRouteProps<Params = any, State = any> {
  location: State;
  navigate: NavigateFunction;
  params: Params;
}

export function withRouter<P extends IRouteProps>(Child: ComponentClass<P>) {
  return (props: Omit<P, keyof IRouteProps>) => {
    const location = useLocation();
    const navigate = useNavigate();
    const params = useParams();
    return (
      <Child
        {...(props as P)}
        navigate={navigate}
        location={location}
        params={params}
      />
    );
  };
}

// 在组件里使用
import { withRouter, IRouteProps } from "../classRouter.tsx";

class Tags extends Component<IRouteProps> {
    render() {
        return <>
            <button onClick={() => {
                this.props.navigate('/home')
            }}>跳转</button>
        </>
    }
}

export default withRouter(Tags)

Fragment

import React, { Fragment } from "react";
// 循环遍历使用Fragment,它只有一个属性key
<Fragment key={1}></Fragment>
<></> // 不能接收任何属性,一般用在根标签

PureComponent、memo

PureComponent

 重写了:
        shouldComponentUpdate(nextProps, nextState) {
        return 布尔值
    }
    /**
     * 只有props和state数据改变了,才会返回true
     * 注意:只是进行了props和state数据的浅比较,如果只是数据对象内部数据改变了,返回false
     * 不要直接修改state数据,而是要产生新数据
     * 比如:state = {name: "lxh", arr: [1,2,3]}
     *      修改1:let obj = this.state
     *            obj.name = "LXH"
     *            this.setState(obj) // 这样会返回false
     *      修改2:let {arr} = this.state
     *            arr.push(4)
     *            this.setState({arr}) // 返回false
     *            this.setState({arr: [...arr, 4]}) // 返回ture
     *            // 不直接修改,产生新数据
     **/ 

memo

主要用于函数组件
import React, { memo } from "react";
const Child = () ={
    return (<div>child</div>)
}

export default memo(Child)

useCallbackusememo

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

useRef

Hook API 索引

children props、render props

children props

<A>
    <B></B>
</A>
{this.props.children}

<div children='xxx'></div>
<div>xxx</div>
问题:如果B组件需要A组件的数据,做不到

render props

三个组件:
parent组件:<A render={(data) => <B data={data}></B>} />
A组件:{this.props.render(内部state数据data)}
B组件:读取A组件传过来的数据{this.props.data}

错误边境

错误边境:只捕获后代组件生命周期产生的错误。
当子组件出现错误的时候,触发getDerivedStateFromError调用,并携带错误信息

state = {hasError: ""}
static getDerivedStateFromError(error) {
    // error是错误信息
    return {hasError: error}
}
render() {
    return (
        <>
            {this.hasError ? <>当前网络不稳定,请稍后再试</> : <Child />}
        </>
    )
}

注意:错误边境只适用于生产环境

componentDidCatch() {
    // 渲染组件时出错,执行
    // 主要用于统计页面的错误,发送请求给后台,反馈给服务器,通知编码人员进行bug的解决
}
使用方式:getDerivedStateFromError 配合 componentDidCatch

react+typescript写法

安装@types/下,如:npm i @types/react-router-dom

import React, { Component, createRef } from "react";

interface propsType {
  id?: string;
  [propsName: string]: any;
}
interface stateType {
  name: string;
}

// 第一个props属性约定类型,第二个state状态约定类型,不写占位可以使用any<any,any>
class Tags extends Component<propsType, stateType> {
  state = {
    name: "liaoxianhui",
  };
  refName = createRef<HTMLInputElement>();
  render() {
    return (
      <div>
        <input type="text" ref={this.refName} />
        <button
          onClick={() => {
            // as 断言 , ? 或者 ! 都可以三种方式
            console.log((this.refName.current as HTMLInputElement).value);
          }}
        >
          获取输入框的值
        </button>
      </div>
    );
  }
}

export default Tags;
import React, { FC, useRef, useState } from "react";

// 方法1:约定props里的属性类型
interface propsTypeOne {
  id?: string;
}

// 方法2
interface propsTypeTwo {
  id?: string;
  name?: () => void;
  [propName: string]: any;
}

// const Form = (props: propsTypeOne) => { // 方法1
const Form: FC<propsTypeTwo> = (props) => {
  // 方法2
  const [name, setName] = useState<string>("lxh");
  const refName = useRef<HTMLInputElement>(null);
  return (
    <div>
      <input type="text" ref={refName} />
      <button
        onClick={() => {
          // as 断言 , ? 或者 ! 都可以三种方式
          console.log(refName.current?.value);
        }}
      >
        获取输入框的值
      </button>
    </div>
  );
};

export default Form;

消息订阅与发布机制

插件PubSubJs

安装:npm install pubsub-js
     yarn add pubsub-js
     
引入:import PubSub from 'pubsub-js';

发布:PubSub.publish("xxx","data")

订阅:PubSub.subscribe("xxx",(msg, data)=>{ // msg消息名、date数据
        console.log(date)
     })

跨域

// http-proxy-middleware react下载好了
const { createProxyMiddleware } = require('http-proxy-middleware') // react18中使用
// const createProxyMiddleware = require('http-proxy-middleware') // react17中使用

module.exports = function (app) {
    app.use(
        createProxyMiddleware('/api1', { // 遇到/api1的请求,触发该代理
            target: 'http://loaclhost:5000', // 后端请求的基础路径
            changeOrigin: true, // 控制服务器收到的请求头中host字段的值
            pathRewrite: { // 去除请求前缀
                '^/api1': ''
            }
        })
    )

    app.use(
        createProxyMiddleware('/api2', {
            target: 'http://localhost:6000',
            changeOrigin: true,
            pathRewrite: {
                '/api2': ''
            }
        })
    )
}

serve

测试打包好的项目在静态服务器,打包好后会提示serve -s build

  • 安装:npm i -g serve
  • 使用:serve -s build
  • 使用指定端口号:serve -s build -l 4000

vue2

vue3

fetch

不定期更新,目前结束,谢谢