react学习

144 阅读12分钟

类组件

类组件传参类型与默认值的设置

import { Component } from 'react'
import navBarPropType from 'prop-types'

export default class App extends Component {
    state = {}
	// 设置参数的类型
    static propTypes = {
        title: navBarPropType.string,
        leftShow: navBarPropType.bool
    }
	// 设置参数的默认值
    static defaultProps = {
        leftShow: true
    }
    render () {
        return <div>
            { this.props.leftShow && <button>返回</button> }
            navbar-{ this.props.title }
        </div>
    }
}

表单的受控与非受控

  1. 非受控组件

    import React, { Component } from 'react'
    
    export default class App extends Component {
        myusername = React.createRef()
        render () {
            return (
                <div>
                    {/* <input value="xiaoming"></input> */}
                    <input defaultValue="xiaoming"></input>
                    <button onClick={()=>{console.log(this.myusername.current.value)}}>提交</button>
                    <button onClick={()=>{this.myusername.current.value=''}}>重置</button>
                </div>
            )
        }
    }
    
    

    在react渲染生命周期时,表单元素上的value将会覆盖DOM节点中的值,在非受控组件中,你经常希望react赋予组件一个初始值,但是不去控制组件后续的更新。在这种情况下,你可以制定一个defaultValue属性

  2. 受控组件

    在 HTML 中,表单元素(如<input><textarea><select>)通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用setState()来更新。

    我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作,被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

    例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:

    class NameForm extends React.Component {
      constructor(props) {
        super(props);
        this.state = {value: ''};
    
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleChange(event) {
        this.setState({value: event.target.value});
      }
    
      handleSubmit(event) {
        alert('提交的名字: ' + this.state.value);
        event.preventDefault();
      }
    
      render() {
        return (
          <form onSubmit={this.handleSubmit}>
            <label>
              名字:
              <input type="text" value={this.state.value} onChange={this.handleChange} />
            </label>
            <input type="submit" value="提交" />
          </form>
        );
      }
    }
    

为什么要有受控组件?

引入受控组件不是说它有什么好处,而是因为 React 的 UI 渲染机制,对于表单元素不得不引入这一特殊的处理方式。

在浏览器 DOM 里面是有区分 attributeproperty 的。attribute 是在 HTML 里指定的属性,而每个 HTML 元素在 JS 对应是一个 DOM 节点对象,这个对象拥有的属性就是 property(可以在 console 里展开一个 DOM 节点对象看一下,HTML attributes 只是对应其中的一部分属性),attribute 对应的 property 会从 attribute 拿到初始值,有些会有相同的名称,但是有些名称会不一样,比如 attribute class 对应的 property 就是 className

回到 React 里的 <input> 输入框,当用户输入内容的时候,输入框的 value property 会改变,但是 value attribute 依然会是 HTML 上指定的值(attribute 要用 setAttribute 去更改)。受控组件既可以解决这种问题

父子组件通信

1、使用props

class NavBar extends Component {
    render() {
        return (
            <div>
                <span>NavBar</span>
                <button onClick={() => this.props.handelEvent()}>click</button>
            </div>
        )
    }
}

export default class App extends Component {
    state = {
        isShow: true,
    }
    render() {
        return (
            <div>
                <NavBar
                    handelEvent={() => {
                        this.setState({
                            isShow: !this.state.isShow,
                        })
                    }}
                ></NavBar>
                <div>
                    {this.state.isShow && (
                        <ul>
                            <li>aa</li>
                            <li>bb</li>
                            <li>nn</li>
                            <li>ff</li>
                        </ul>
                    )}
                </div>
            </div>
        )
    }
}

父传子:父组件通过属性传值,子组件通过 this.props.属性值

子传父:父组件通过回调函数,子组件通过 this.props.回调函数()

2、发布订阅模式

import React, { Component } from 'react'
var Bus = {
    list: {},
    subscribe (key, callBack) {
        this.list[ key ] = callBack
    },
    publish (key, value) {
        this.list[ key ](value)
    }
}
class FilmItm extends Component {
    render () {
        const { poster, name, grade, actors, synopsis, filmId } = this.props.info
        return <div className='filmItem' onClick={ () => {
            Bus.publish('info', synopsis)
            Bus.publish('filmId', filmId)
        } }>
            <img src={ poster } alt={ name } className='img'></img>
            <div className='item-info'>
                <p className='name'>{ name }</p>
                <p className='grade'>观众评分:{ grade }</p>
                <p className='actors'>主演:{ actors && actors.map(x => x.name).join(' ') }</p>
            </div>
        </div>
    }
}

class FilmDetail extends Component {
    constructor () {
        super()
        this.state = {
            info: '',
            filmId: ''
        }
        Bus.subscribe('info', (synopsis) => { this.setState({ info: synopsis }) })
        Bus.subscribe('filmId', (filmId) => { this.setState({ filmId: filmId }) })
    }
    render () {
        return <div className='filmDetail'>
            <div>{ this.state.info }</div>
            <div>{ this.state.filmId }</div>
        </div>
    }
}
export default class App extends Component {
    constructor () {
        super()
        this.state = {
            filmList: [],
        }
        //通过axios接口获取filmList数据
    }
    render () {
        return (
            <div>
                <div>
                    { this.state.filmList.map(x => <FilmItm key={ x.filmId } info={ x }></FilmItm>) }
                </div>
                <FilmDetail></FilmDetail>
            </div>
        )
    }
}


3、通过context 实现通信

组件的传值方式,在新版本中应该使用useContext()获取,老版本可以通过GoldContext.Consumer获取,GoldContext.Consumer包裹的是回调函数,参数为父组件传过来的值

import React, { Component, createContext, useContext } from "react"
const GoldContext = createContext()
const FilmItm = (props) => {
    const { poster, name, grade, actors } = props.info
    const detail = useContext(GoldContext)
    return (
        <div
            className="filmItem"
            onClick={() => {
                detail.changeDetail(props.info)
            }}
        >
            <img src={poster} alt={name} className="img"></img>
            <div className="item-info">
                <p className="name">{name}</p>
                <p className="grade">观众评分:{grade}</p>
                <p className="actors">
                    主演:
                    {actors && actors.map((x) => x.name).join(" ")}
                </p>
            </div>
        </div>
    )
}

class FilmDetail extends Component {
    constructor() {
        super()
        this.state = {
            info: "",
            filmId: "",
        }
    }
    render() {
        return (
          /* GoldContext.Consumer在老版本使用,新版本建议使用 useContext() */
            <GoldContext.Consumer>
                {(value) => (
                    <div className="filmDetail">
                        <div>{value.detail.synopsis}</div>
                        <div>{value.detail.filmId}</div>
                    </div>
                )}
            </GoldContext.Consumer>
        )
    }
}

export default class App extends Component {
    constructor() {
        super()
        this.state = {
            filmList: [],
            detail: {},
        }
      //axios调接口获取filmList数据
    }

    render() {
        return (
            <GoldContext.Provider
                value={{
                    detail: this.state.detail,
                    changeDetail: (detail) => {
                        this.setState({
                            detail: detail,
                        })
                    },
                }}
            >
                <div>
                    <div>
                        {this.state.filmList.map((x) => (
                            <FilmItm key={x.filmId} info={x}></FilmItm>
                        ))}
                    </div>
                    <FilmDetail></FilmDetail>
                </div>
            </GoldContext.Provider>
        )
    }
}

插槽

父组件可以通过props.children获取传过来的jsx,实现插槽。

props.children是一个数组,可以通过下标值来规定插槽的显示位置 。

import React, { Component } from "react"
class Child extends Component {
    render() {
        return (
            <div>
                Child
                {this.props.children[1]}
                {this.props.children[2]}
                {this.props.children[0]}
            </div>
        )
    }
}

export default class App extends Component {
    render() {
        return (
            <div>
                <Child>
                    <div>0000</div>
                    <div>1111</div>
                    <div>2222</div>
                </Child>
            </div>
        )
    }
}
/* 页面显示
Child
1111
2222
0000 */


生命周期

import React, { Component } from "react"

export default class App extends Component {
    UNSAFE_componentWillMount() {
        // 不安全,原因:新版本更新算法后,componentWillMount的优先级比较低,可能会被打断,打断后,等队列释放,会重启进程,无法保证唯一性,不建议使用
    }
    render() {
        return <div id="myname"></div>
    }
    componentDidMount() {
        // 挂载完成,可以获取到真实DOM节点
        // 数据获取、订阅监听事件或操作 DOM 节点
    }
    shouldComponentUpdate(nextProps, nextState) {
        // 判断是否调用render更新
        // nextProps:组件即将用来渲染的下一个 props
        // nextState:组件即将渲染的下一个 state
        // 可以通过this.props获取当前组件渲染的props,也可以通过this.state获取当前组件的状态值
        // 如果你希望组件重新渲染的话就返回 true。这是也是默认执行的操作。返回 false 来告诉 React 可以跳过重新渲染。
    }
    getSnapshotBeforeUpdate() {
        // React 会在 React 更新 DOM 之前时直接调用它。
        // 此生命周期方法返回的任何值都将作为参数传递给 componentDidUpdate。必须配合componentDidUpdate()一起使用
    }

    componentDidUpdate(prevProps, prevState, snap) {
        // 更新完成,可以获取到更新状态之后的DOM
        // prevProps:更新之前的 props。
        // prevState:更新之前的 state。
        // snap: getSnapshotBeforeUpdate生命周期传过来的参数
    }

    static getDerivedStateFromProps(props, state) {
        // React 会在初始挂载和后续更新时调用 render 之前调用它
        // 返回一个对象来更新 state,或返回 null 不更新任何内容。
        // props:父组件传过来的props, state: 组件的当前状态
        // 静态方法无法获取到this
        // 通常配合componentDidUpdate()一起使用
    }
    componentWillUnmount() {
        // 组件卸载之前被调用
        // 解绑监听事件、关闭定时器
    }
}

性能优化

  1. shouldComponentUpdate

    控制组件自身或者子组件是否需要更新,尤其是在子组件非常多的情况下,需要进行优化。

  2. PureComponent

    PureComponen但是当 props 和 state 与之前保持一致时会跳过重新渲染。

    注意:如果你的state或props[永远都会变](比如倒计时),那么PureComponent并不会比加快,因为频繁对比会消耗性能。

函数式 组件HOOKS

useState

const [state, setState] = useState(initialState);

useEffect

useEffect(setup, dependencies?)

  • setup:处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。当组件被添加到 DOM 的时候,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数
  • 可选 dependenciessetup 代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。依赖项列表的元素数量必须是固定的,并且必须像 [dep1, dep2, dep3] 这样内联编写。React 将使用 Object.is 来比较每个依赖项和它先前的值,如果发生改变将运行setup函数。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。
useEffect(setup, dependencies?)
useEffect(() => {
        if (props.type === 1) {
            // 业务逻辑
        } else {
            // 业务逻辑
        }
        window.onresize = () => {
            console.log("调整视图")
        }
        return () => {
            //组件卸载时调用
            window.onresize = null
        }
    }, [props.type])

useEffect与useLayoutEffect的区别

useLayoutEffect与useEffect用法相同 useLayoutEffect(setup, dependencies?)

但是它们的运行时机不同,useEffect是在页面渲染完成后运行。而useLayoutEffect是在完成DOM更新之后马上同步执行,会阻塞页面渲染

如果想要操作DOM,建议在useLayoutEffect中执行,会一起渲染,只有一次回流、重绘。如果在useEffect中操作DOM则会进行两次渲染,可能会操成页面抖动

useCallback

useCallback 只应作用于性能优化

const handleSubmit = useCallback(fn, dependencies);
/*
fn:想要缓存的函数。此函数可以接受任何参数并且返回任何值。React 将会在初次渲染而非调用时返回该函数。当进行下一次渲染时,如果 dependencies 相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。

dependencies:有关是否更新 fn 的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。依赖列表必须具有确切数量的项,并且必须像 [dep1, dep2, dep3] 这样编写。React 使用 Object.is 比较每一个依赖和它的之前的值。
*/

useMemo

useMemo(calculateValue, dependencies)

  • calculateValue:要缓存计算值的函数。
  • dependencies:所有在 calculateValue 函数中使用的响应式变量组成的数组。

相当于VUE 的计算属性

useRef

  1. 通过useRef操作DOM,当 React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为你的 ref 对象的 current 属性

    function Input() {
        const myRef = useRef()
        return <input ref={myRef}></input>
    }
    // 可以通过myRef.current获取input的属性
    

  2. 通过useRef保存值

    const intervalRef = useRef(0);
    

    useRef 返回一个具有单个 current 属性 的 ref 对象,并初始化为你提供的 initial value

    在后续的渲染中,useRef 将返回相同的对象。你可以改变它的 current 属性来存储信息,并在之后读取它。这会让你联想起 state,但是有一个重要的区别。

    改变 ref 不会触发重新渲染。 如果要更新 ref 里面的值,你需要手动改变它的 current 属性

useReducer

const [state, dispatch] = useReducer(reducer, initialArg)

reducer:用于更新 state 的纯函数。参数为 state 和 action,返回值是更新后的 state。
initialArg:用于初始化 state 的任意值。
useReducer 返回一个由两个值组成的数组:
当前的 state。初次渲染时,它是 init(initialArg) 或 initialArg (如果没有 init 函数)。
dispatch 函数。用于更新 state 并触发组件的重新渲染。

const initialArg = {
    count: 0,
}
const reducer = (state, action) => {
    let newSate = { ...state }
    switch (action.type) {
        case "minu":
            newSate.count--
            return newSate
        case "add":
            newSate.count++
            return newSate
        default:
            return newSate
    }
}
const [state, dispatch] = useReducer(reducer, initialArg)
dispatch({ type: "minu",})
dispatch({ type: "add",})

路由

1.路由的使用

npm i react-router-dom@5

路由的使用

import React from 'react'
import { HashRouter, Route, Redirect, Switch } from 'react-router-dom'
import Films from '../views/films.jsx'
import Cinemas from '../views/cinemas.jsx'
import Center from '../views/center.jsx'
import NotFound from '../views/notFound.jsx'

export default function indexRouter () {
    return (
        <div>
            <HashRouter>
                {/* Switch:的作用是匹配第一个条件就跳出循环,类似于switch循环 */ }
                <Switch>
                    <Route path="/films" component={ Films }></Route>
                    <Route path="/cinemas" component={ Cinemas }></Route>
                    <Route path="/center" component={ Center }></Route>
                    {/* Redirect: 重定向路由 */ }
                    {/* <Redirect from='/' to='/films'></Redirect>  
                    from是模糊匹配,只需要路由含有‘/’,就跳转‘/films’;
                    加上属性 'exact' 则为精准匹配,只有路由为 '/' ,才跳转 '/films' */ }
                    <Redirect from='/' to='/films' exact></Redirect>
                    {/* 没有匹配上,就跳转空页面 */ }
                    <Route component={ NotFound }></Route>
                </Switch>
            </HashRouter>
        </div>
    )
}

2.嵌套路由

嵌套路由(二级路由)是在父组件中添加路由结构,路由的添加规则与一级路由完全相似。类似于子组件的用法

import React from "react"
import { Redirect, Route, Switch } from "react-router-dom"
import ComingSoon from "./films/comingSoon.jsx"
import NowPlaying from "./films/nowPlaying.jsx"
import NotFound from "./notFound.jsx"

export default function Films() {
    return (
        <div>
            <div>头部轮播</div>
            <div>导航栏</div>
            <Switch>
                <Route path="/films/nowPlaying" component={NowPlaying}></Route>
                <Route path="/films/comingSoon" component={ComingSoon}></Route>
                <Redirect from="/films" to="/films/nowPlaying" exact></Redirect>
                <Route component={NotFound}></Route>
            </Switch>
        </div>
    )
}

3.路由跳转

声明式导航和编程式导航

必须包裹在 里面,会根据路由自动设置激活节点,激活节点会添加类名‘ active' 。也可以通过属性activeClassName重新设置激活的类名。

import { NavLink } from "react-router-dom"
<ul>
    <li>
        <NavLink to="/films" activeClassName="activeClassName"> films </NavLink>
    </li>
    <li>
        <NavLink to="/cinemas" activeClassName="activeClassName"> cinemas </NavLink>
    </li>
    <li>
        <NavLink to="/center" activeClassName="activeClassName"> center </NavLink>
    </li>
</ul>

编程式导航

1.通过 "react-router-dom" 的自定义hooks ’useHistory ‘ 做路由跳转

import { useHistory } from "react-router-dom"
const history = useHistory()

history.goBack()
history.push('/detail')

2.通过props参数进行路由跳转,组件必须包裹在 里面

export default function Cinemas(props) {
    props.history.push('/detail')
}

4.路由传参

1.通过动态路由设置传参

在 的path属性要设置 ‘/:参数名’,好处是:刷新页面参数不会丢失

<Route path="/detail/:filmId" component={ Detail }></Route>

history.push(`/detail/${x.filmId}`)

props.match.params.filmId //获取动态路由的参数

2.使用params 传参

缺点:刷新页面,参数会丢失

history.push({
	pathname: "/detail",
 	params: { filmId: x.filmId },
})

props.location.params  //获取params 参数

3.使用state传参

缺点:刷新页面,参数会丢失

history.push({
	pathname: "/detail",
 	state: { filmId: x.filmId },
})

props.location.state  //获取params 参数

5.路由拦截

使用render渲染,render接受一个回调函数,通过回调函数判断是返回当前组件还是重定向组件,从而达到路由拦截的目的

import React from 'react'
import { HashRouter, Route, Redirect, Switch } from 'react-router-dom'
import Films from '../views/films.jsx'
import Center from '../views/center.jsx'
import NotFound from '../views/notFound.jsx'
const isAuth = () => {
    return localStorage.getItem('TOKEN')
}

export default function indexRouter () {
    return (
        <div>
            <HashRouter>
                <Switch>
                    <Route path="/films" component={ Films }></Route>
                    {/* 路由拦截 */ }
                    <Route path="/center" render={ (props) => {
                        {/* props可以将history对象传递给子组件 */ }
                        return isAuth() ? <Center {...props}></Center> : <Redirect to='/Login'></Redirect>
                    } }></Route>
                    <Redirect from='/' to='/films' exact></Redirect>
                    <Route component={ NotFound }></Route>
                </Switch>
            </HashRouter>
        </div>
    )
}

6.withRouter

一般组件无法调用history对象。

我们可以利用 react-router-dom 对象下的 withRouter 函数来对我们导出的 一般组件进行包装,这样我们就能获得一个拥有 history 对象的一般组件

import { withRouter } from 'react-router-dom'

function Film (props) {
  return <div onClick={()=>{
      props.history.goBack()
    }}>后退</div>
}
// 在最后导出对象时,用 `withRouter` 函数对 Film 进行包装
export default withRouter(Film);

反向代理

新建文件目录 src/setupProxy

const { creatProxyMiddleware } = require('http-proxy-middleware')

module.exports = function (app) {
    app.use(
        '/api', //接口前缀
        creatProxyMiddleware({
            target: 'https://i.maoyan.com', //代理目标域名
            changeOrigin: true
        })
    )
}

css module

注意:在react中引入css文件会注入成全局样式,统一注入到 <style></style> 标签中。

样式模块化,将 film.css 文件名改为 film.module.css

import style from './film.module.css'
// 文件名加module,react会对类名进行处理,加模块化的唯一标识
// 通过style的属性选择react处理好的类名
function Film (props) {
  return <div className={style.active}></div>
}

注意: 在模块化css文件中尽量使用类名选择器或者ID选择器,单独的元素选择会被当成全局样式注入到<style></style> 标签中。如果非要使用元素选择器,请使用与类名组合使用,或者使用类名包裹。

在css模块化文件中要写成全局样式,可以使用 :global() ,如下:

:global(.active) {
  color: rede
}