React框架基础 - 5、React Hook钩子、路由

1,076 阅读22分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

React Hooks、路由

路由基本使用

需要使用react-router-dom包来实现

课程采用hash方式,以下三个方法都是作为标签实现

  • 使用react-router-dom的HashRouter方法包裹App根组件开启路由功能
  • 使用react-router-dom的Link方法创建路由链接,会被渲染成a标签用于切换
  • 使用react-router-dom的Route方法包裹对应路由组件用于展示指定路由试图
// src/index.js  开启路由功能

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
// 引入 HashRouter 开启路由功能
import { HashRouter } from 'react-router-dom'

ReactDOM.render(
  <React.StrictMode>
    {/* HashRouter 包裹 App 开启路由 */}
    <HashRouter><App /></HashRouter>
  </React.StrictMode>,
  document.getElementById('root')
)
// src/App.js  使用路由

import { Component } from 'react'
// 引入 Link, Route
import { Link, Route } from 'react-router-dom'
// 引入需要用到的组件
import Home from './components/home'
import About from './components/about'

class App extends Component {
  render() {
    return (
      <>
        <div>
          {/* 路由链接 */}
          <Link to='./home'>首页</Link>
          <Link to='./about'>关于</Link>
        </div>
        <div>
          {/* 路由组件,注意这里path没有. */}
          <Route path='/home' component={Home} />
          <Route path='/about' component={About} />
        </div>
      </>
    )
  }
}

export default App

默认展示和exact精准匹配

如果想要在/匹配首页(./home)组件,可以设置

设置默认展示页

给项目的/进行规则匹配为香烟展示的组件<Route *path*='/' *component*={Home} />

设置精准匹配规则

默认匹配规则为模糊匹配

当设置默认展示组件后,我们发现点击其他路由这个默认展示的组件总是会展示出来这是我们可以使用exact进行精准匹配设置

import { Component } from 'react'
// 引入 Link, Route
import { Link, Route } from 'react-router-dom'
// 引入需要用到的组件
import Home from './components/home'
import About from './components/about'

class App extends Component {
  render() {
    return (
      <>
        <div>
          {/* 路由链接 */}
          <Link to='./home'>首页</Link>
          <Link to='./about'>关于</Link>
        </div>
        <div>
          {/* 路由组件 */}
          {/* 设置默认匹配规则并且开精准匹配,避免重复匹配 */}
          <Route path='/' component={Home} exact />
          <Route path='/home' component={Home} />
          <Route path='/about' component={About} />
        </div>
      </>
    )
  }
}

export default App

路由匹配规则

  • 从上向下匹配,匹配到的规则都会被展示,如果存在两个相同的规则那么这两个规则对应的组件都会被展示
    • 可以使用Switch方法进行强行打断,后续出现相同的匹配规则就不会执行
  • 如果想让匹配不到的页面展示统一的内容(例如404页面),那么书写一个不需要pathRouter就可以了
import { Component } from 'react'
// 引入 Link, Route
import { Link, Route, Switch } from 'react-router-dom'
// 引入需要用到的组件
import Home from './components/home'
import About from './components/about'
import NotFinde from './components/404'

class App extends Component {
  render() {
    return (
      <>
        <div>
          <Link to='./home'>首页</Link>
          <Link to='./about'>关于</Link>
        </div>
        <div>
          {/* 使用Switch对相同路由匹配规则进行强行打断 */}
          <Switch>
            <Route path='/' component={Home} exact />
            <Route path='/home' component={Home} />
            <Route path='/about' component={About} exact/>
            {/* 两个完全相同的规则可以使用switch进行强行打断匹配 */}
            {/* <Route path='/about' component={About} /> */}
            {/* 不给路由规则添加path,那么如果匹配不到规则机会默认展示这个meiyoupath的组件 */}
            <Route component={NotFinde} />
          </Switch>
        </div>
      </>
    )
  }
}

export default App

动态路由传参

场景:点击列表展示相应内容,页面相同但是展示内容根据参数的不同而变化

  1. 在定义匹配规则的时候设置路由参数占位符:使用占位

    // src/App.js
    
    import { Component } from 'react'
    // 引入 Link, Route
    import { Link, Route, Switch } from 'react-router-dom'
    // 引入需要用到的组件
    import Home from './components/home'
    import About from './components/about'
    import Item from './components/item'
    import NotFinde from './components/404'
    
    class App extends Component {
      render() {
        return (
          <>
            <div>
              <Link to='./home'>首页</Link>
              <Link to='./about'>关于</Link>
            </div>
            <div>
              {/* 使用Switch对相同路由匹配规则进行强行打断 */}
              <Switch>
                <Route path='/' component={Home} exact />
                <Route path='/home' component={Home} />
                <Route path='/about' component={About}/>
                {/* 书写一个动态匹配规则,:id可以是任意值 */}
                <Route path='/item/:id' component={Item} />
                <Route component={NotFinde} />
              </Switch>
            </div>
          </>
        )
      }
    }
    
    export default App
    
    
  2. 触发操作的时候传递具体的参数

    // src/components/about.js
    
    import { Component } from 'react'
    import { Link } from 'react-router-dom'
    
    class About extends Component {
      render() {
        return (
          <>
            {/* 书写多个跳转链接,跳转到同一页面但是携带不同参数 */}
            <p><Link to='./item/1'>item1</Link></p>
            <p><Link to='./item/2'>item2</Link></p>
            <p><Link to='./item/3'>item3</Link></p>
          </>
        )
      }
    }
    
    export default About
    
  3. 具体的组件中使用传递过来的参数

    // src/components/item.js
    
    import { Component } from 'react'
    
    class Item extends Component {
      render(props) {
        return (
          // 携带过来的参数会存在this.props.match.params中
          <div>{this.props.match.params.id}</div>
        )
      }
    }
    
    export default Item
    

查询参数传参

当跳转的时候才参数跳转的话可以在目标页面的this.props中获取传递的参数,因为传递过来的参数直接使用不便于阅读,可以使用第三方包 - qs进行转译处理

 //  跳转并且设置参数
<p><Link to='./home?title="我是参数,来自list"'>带参数回到home</Link></p>
import { Component } from 'react'// 引入第三方包qsimport qs from 'qs'class Home extends Component {  render() {    // 传递过来的参数会存储在this.props.location.search中    console.log(this.props.location.search)    return (      // 因为传递过来的参数直接使用不便于阅读,可以使用qs进行转译      // ignoreQueryPrefix设置是否去除前面的?,默认为false      <div>{qs.parse(this.props.location.search, { ignoreQueryPrefix: true }).title}</div>    )  }}export default Home

路由嵌套

  • 如果一个路由内部还需要添加二级路由灯,直接在组件内部书写Link和Route即可
  • 注意如果有嵌套路由,那么你不要给当前路由写精确匹配
  • 为了让二级路由不受一级路由改变的影响,可以把二级路由前面以及路由部分写为动态的值,路径和地址对应着this.props.match中的url(Link)和path(Route)
import { Component } from 'react'import { Link, Route } from 'react-router-dom'// 两个二级路由import Inner from './inner'import Outer from './outer'class About extends Component {  render() {    return (      <>        <div>          <div>            {/* 常规相对路径写法 */}            {/* <Link to='/about/inner'>国内新闻</Link>            <Link to='/about/outer'>国际新闻</Link> */}                        {/* 使用动态路由路径,这样当上层路由地址改变后就二级路由的路径也会随之改变,url对应to*/}            <Link to={`${this.props.match.url}/inner`}>国内新闻</Link>            <Link to={`${this.props.match.url}/outer`}>国际新闻</Link>          </div>          <div>            {/* 常规相对路径写法 */}            {/* <Route path='/about/inner' component={Inner} />            <Route path='/about/outer' component={Outer} /> */}                        {/* 这里同样可以使用动态路径,path对应path */}            <Route path={`${this.props.match.path}/inner`} component={Inner} />            <Route path={`${this.props.match.path}/outer`} component={Outer} />          </div>        </div>      </>    )  }}export default About

路由跳转和重定向

  • 需要利用一个内置工具包(不需要安装)history中的createHashHistory方法进行跳转
  • 重定向利用react-router-dom中的Rediret设置to属性进行操作,一旦执行重定向访问某个页面时会自动跳转到指令的另一个页面
// src/components/item.jsimport { Component } from 'react'// 引入路由跳转方法import { createHashHistory } from 'history'class Item extends Component {  render() {    return (      <>        {/* 添加点击事件,进行路由跳转 */}        <button onClick={() => createHashHistory().push('/home')}>回home</button>      </>    )  }}export default Item
// src/components/home.jsimport { Component } from 'react'// 引入重定向方法import { Redirect} from 'react-router-dom'class Home extends Component {  render() {    // 信号值    const login = false    // 判断是否符合条件    if (!login) {      // 符合条件进行路由重定向      return (<Redirect to='/about' />)    }    return (      <div>home内容</div>    )  }}export default Home

路由守卫

在指定路由跳转之前进行校验,只有通过校验才能允许跳转

// src/auth/auth.js  创造一个守卫// 虚拟一个守卫class Auth {  constructor() {    // 设置初始值(未登录)    this.singin = false  }  // 登录方法  login() {    // 修改为登录    this.singin = true    console.log('登录')  }  // 退出方法  logout() {    // 修改为未登录    this.singin = false    console.log('退出登录')  }  // 导出是否登陆  LoginOrNot() {    return this.singin  }}// 导出守卫实例对象export default new Auth()
// src/App.js  使用路由守卫import { Component } from 'react'import { Link, Route, Switch, Redirect } from 'react-router-dom'// 引入需要用到的组件import Home from './components/home'import About from './components/about'import Item from './components/item'import NotFinde from './components/404'// 导入路由守卫实例对象import Auth from './auth/auth'class App extends Component {  // 点击登录按钮方法  login() {    Auth.login()  }  // 点击退出按钮方法  logout() {    Auth.logout()  }  render() {    return (      <>        <div>          <Link to='./home'>首页</Link>          <Link to='./about'>关于</Link>        </div>        <div>          <Switch>            <Route path='/' component={Home} exact />            <Route path='/home' component={Home} />            {/* 使用路由守卫,这里使用render属性替代component属性,其中参数为路由跳转信息,因为render无法自动携带信息需要手动处理 */}            <Route path='/about' render={props => {              // 判断是否登陆              if (Auth.LoginOrNot()) {                // 登录显示About组件                return <About {...props} />              } else {                // 未登录重定向到home                return <Redirect to='/home' />              }            }} />            {/* 书写一个动态匹配规则,:id可以是任意值 */}            <Route path='/item/:id' component={Item} />            <Route component={NotFinde} />          </Switch>          <button onClick={() => this.login()}>登录</button>          <button onClick={() => this.logout()}>退出</button>        </div>      </>    )  }}export default App

路由守卫组件封装

当多个路由匹配规则都需要使用路由守卫功能的时候,我们可以将守卫功能单独封装为一个单独组件

单独封装的守卫组件替换掉原来的Route组件,传参和之前使用Route相同

// src/auth/auth.js// 虚拟一个守卫class Auth {  constructor() {    // 设置初始值(未登录)    this.singin = false  }  // 登录方法  login() {    // 修改为登录    this.singin = true    console.log('登录')  }  // 退出方法  logout() {    // 修改为未登录    this.singin = false    console.log('退出登录')  }  // 导出是否登陆  LoginOrNot() {    return this.singin  }}// 导出守卫实例对象export default new Auth()
// src/App.js  使用封装好的路由守卫组件,可以简化书写import { Component } from 'react'import { Link, Route, Switch } from 'react-router-dom'// 引入需要用到的组件import Home from './components/home'import About from './components/about'import Item from './components/item'import NotFinde from './components/404'// 导入路由守卫实例对象import Auth from './auth/auth'// 导入路由守卫组件import Guard from './components/guard'// 导入路由跳转方法import {createHashHistory} from 'history'class App extends Component {  // 点击登录按钮方法  login() {    Auth.login()  }  // 点击退出按钮方法  logout() {    Auth.logout()    // 跳转回首页    createHashHistory().push('/home')  }  render() {    return (      <>        <div>          <Link to='./home'>首页</Link>          <Link to='./about'>关于</Link>        </div>        <div>          <Switch>            <Route path='/' component={Home} exact />            <Route path='/home' component={Home} />            {/* 直接使用封装的路由守卫组件,写法和 Route 相同 */}            <Guard path='/about' component={About} />            <Route path='/item/:id' component={Item} />            <Route component={NotFinde} />          </Switch>          <button onClick={() => this.login()}>登录</button>          <button onClick={() => this.logout()}>退出</button>        </div>      </>    )  }}export default App
// src/components/guard.js  单独封装路由守卫组件import { Component } from 'react'// 引入需要使用的方法import { Route, Redirect } from 'react-router-dom'// 引入守卫实例对象import auth from '../auth/auth'// 路由守卫组件class Guard extends Component {  render() {    // 将 app 组件传递过来的参数接收并重新解构赋值    // Component为要守卫的组件,剩下的作为参数存起来    const {component: Component, ...rest} = this.props    return (      <>        {/* 和没封装之前写法差不多,吧其他参数传递下去,组件使用 render 属性设置 */}        <Route {...rest} render={ (props) => {          // 判断是否登陆          if (auth.LoginOrNot()) {            // 登录展示组件,这里的组件是之前传递过来的,所以算是动态组件,不会写死            return <Component {...props} />          } else {            // 未登录重定向到首页            return <Redirect to='/home' />          }        }} />      </>    )  }}// 导出路由守卫组件export default Guard

路由模块懒加载

所有组件不需要立马都导入进来,只需要导入首屏内容即可,消耗性能且拖慢首次加载速度

利用第三方包 - @loadable/Component下面的loadComponent方法,修改一下组件引入方式即可,其他不需要更改

import { Component } from 'react'import { Link, Route, Switch } from 'react-router-dom'// 导入路由守卫实例对象import Auth from './auth/auth'// 导入路由守卫组件import Guard from './components/guard'// 导入路由跳转方法import {createHashHistory} from 'history'// 引入组件懒加载方法import load from '@loadable/component'// 设置组件懒加载const Home = load(() => import('./components/home'))const About = load(() => import('./components/about'))const Item = load(() => import('./components/item'))const NotFinde = load(() => import('./components/404'))class App extends Component {  // 点击登录按钮方法  login() {    Auth.login()  }  // 点击退出按钮方法  logout() {    Auth.logout()    // 跳转回首页    createHashHistory().push('/home')  }  render() {    return (      <>        <div>          <Link to='./home'>首页</Link>          <Link to='./about'>关于</Link>        </div>        <div>          <Switch>            <Route path='/' component={Home} exact />            <Route path='/home' component={Home} />            {/* 直接使用封装的路由守卫组件,写法和 Route 相同 */}            <Guard path='/about' component={About} />            <Route path='/item/:id' component={Item} />            <Route component={NotFinde} />          </Switch>          <button onClick={() => this.login()}>登录</button>          <button onClick={() => this.logout()}>退出</button>        </div>      </>    )  }}export default App

非路由组件传递路由信息

路由组件下面的子组件需要使用路由信息,默认情况下非路由的子组件是无法获取路由信息的

可以使用react-router-dom包的withRouter方法解决

  1. 引入路由信息获取方法
  2. 使用 withRouter 方法包裹组件导出
import { Component } from 'react'// 引入路由信息获取方法import { withRouter } from 'react-router-dom'class Item_Son extends Component {  render() {    // 打印路由信息    console.log(this.props)    return (      <p>Item_Son内容</p>    )  }}// 使用 withRouter 方法包裹组件导出即可获得父组件的路由信息export default withRouter(Item_Son)

React Hook基本使用

Hook就是一个特殊的函数,让函数式组件拥有类组件的特性,函数组件比类组件书写简单,只能在函数组件中使用

为什么使用Hook

  • 学习成本相对于类组件较低
  • 数据共享:类组件需要使用store实现
  • 多个业务逻辑代码可能存在于同一个生命周期中

Hook是React16.8之后推出的自带功能,不需要额外安装

useState函数

可以实现在函数组件中保存状态的Hook函数

  • 参数:保存状态的初始值
  • 返回值:【当前保存的状态,修改状态的方法】
// 引入 Hook 函数import { useState } from 'react'// 函数组件function App() {  // 利用 Hook 的 useState 函数初始化状态,并把返回值解构赋值  let [state, setState] = useState(0)  return (    <div>      {/* 添加点击事件,调用修改状态函数,函数内部直接书写结构 */}      <button onClick={ () => { setState( state + 1 ) } }> +1 </button>      {/* 直接使用状态 */}      { state }      <button onClick={ () => { setState( state - 1 ) } }> +1 </button>    </div>  )}export default App

注意点

  • 最好不要使用原本React内置的函数名,建议自定义函数名,这样更有意义
  • 函数式组件中可以多次使用同一个Hook函数
  • 函数允许使用复杂数据类型参数,例如数组、对象等
  • 当同时书写了多个修改操作,只有最后一次可以执行,如果想让多次操作都执行,那么修改方法内部使用回调函数操作即可,回调函数参数就是上次操作的结果
  • 默认情况下,不调用修改函数直接修改状态是不允许的,所以只能使用修改函数来修改数据,建议使用用新的覆盖掉旧的(展开旧的,再将指定数据修改成新数据)
// 引入 Hook 函数import { useState } from 'react'// 函数组件function App() {  // Hook 函数可以多次调用  let [countState, setcountState] = useState(0)  let [presonState, setpresonState] = useState([    {name: 'zs', age: 18},    {name: 'ls', age: 18},    {name: 'ww', age: 17},  ])  let [titleState, settitleState] = useState({    title: '拉勾',    describe: '是一家招聘网站旗下的教育机构'  })    // 数值+1  function addCount () {    // 不允许直接修改状态    // countState = countState + 1    // 执行多次状态修改只有最后一次能执行    // setcountState(countState + 1)    // setcountState(countState + 1)    // setcountState(countState + 1)    // 想要吃多次执行生效必须使用回调函数    setcountState( (old) => old + 1)    setcountState( (old) => old + 1)    setcountState( (old) => old + 1)  }  // 修改title  function changeTitle () {    // 修改采用覆盖的方式,展开后修改指定状态    settitleState({...titleState, title: '拉勾教育'})  }  return (    <div>      {/* 点击事件使用自定义函数 */}      <button onClick={ () => { addCount()} }> +1 </button>      { countState }      <button onClick={ () => { setcountState( countState - 1 ) } }> -1 </button>      <hr />      {/* 遍历状态 */}      { presonState.map(preson => (        <p>{preson.name} ----- {preson.age}</p>      ))}      <hr />            {/* 点击修改title */}      <button onClick={() => changeTitle() }>更改title为拉勾教育</button>      <p>{titleState.title}:{titleState.describe}</p>    </div>  )}export default App

注意:useState返回的修改状态函数调用后返回的是一个完成的状态,不是直接修改状态,新状态覆盖旧状态

useEffect函数

相当于类组件的三个生命周期函数的集合:组件挂载、更新、卸载

  • 参数1:一个回调函数,在挂载和更新的时候触发
    • 返回值:也是一个回调函数,在卸载的时候触发
  • 参数2:数组,内部书写依赖的状态,依赖的状态更新的时候才会触发
// 引入 Hook 函数import { useState, useEffect } from 'react'// 子组件function Son() {  // 自组件设置状态  const [name, nameState] = useState('拉勾教育')  const [money, moneyState] = useState(1000)  // 使用 useEffect 设置生命周期函数  // 参数1:函数 - 组件加载、更新执行  //      返回值:函数 - 组件卸载执行  // 参数2:依赖状态名  useEffect(() => {    console.log('组件挂载 / 更新了')    return () => {      console.log('组件卸载了');    }  }, [money]) // 设置只依赖money状态  return (    <>      {/* 展示并添加按钮修改数据 */}      <p>{name}<button onClick={() => nameState('大前端')}>修改数据</button></p>      <p>{money}<button onClick={() => moneyState(10000)}>修改数据</button></p>    </>  )}// 父组件function App() {  // 设置父组件状态  const [show, showState] = useState(true)  return (    <div>      {/* 点击按钮实现切换状态 */}      <button onClick={() => showState(!show)}>显示切换</button>      <hr />      {/* 根据状态决定是否渲染子组件 */}      {show && <Son />}    </div>  )}export default App

函数优点

因为Hook函数可以重复书写,所以可以把多个动作单独写在一个useEffect中(例如添加时间监听、数据请求等),这样都可以执行,避免在一个生命周期书写多个逻辑

useContext函数

常用于处理函数组件的父向子传递数据问题

函数组件父向子传递数据(常规)

// 引入 Hook 函数 - createContext用于生产和消费数据import { createContext } from 'react'// 创建一个实例const My = createContext({})// 获取里面的两个方法const {Provider, Consumer} = My// 子组件function Son() {  return (    // 消费父组件传递过来的数据    <Consumer>      {        // value就是传递过来的数据        value => {          return (            <>              {/* 使用数据 */}              <p>{value.name}</p>              <p>{value.age}</p>            </>          )        }      }    </Consumer>  )}// 父组件function App() {  return (    <div>      {/* 生产数据,并且直接传递下去 */}      <Provider value={{name: 'zs', age: 18}}>        <Son />      </Provider>    </div>  )}export default App

使用useContext优化多数据传递

// 引入 Hook 函数 - createContext用于生产和消费数据// useContext迎来直接获取传递过来的数据,优化原本繁琐的写法import { createContext, useContext } from 'react'// 创建两个实例const My = createContext({})const Attr= createContext({})// 子组件function Son() {  // useContext处理传递过来的数据并赋值  const value1 = useContext(My)  const value2 = useContext(Attr)  return (    // 原本的处理方法(繁琐)    // // 消费第一个实例    // <My.Consumer>    //   {  // 第一个实例传递过来的数据    //     value1 => {    //       return (    //         // 消费第二个实例    //         <Attr.Consumer>    //           {  // 第二个实例传递过来的数据    //             value2 => {    //               return (    //                 <>    //                 {/* 两个实例数据都可以使用了 */}    //                   <p>{value1.name}</p>    //                   <p>{value1.age}</p>    //                   <p>{value2.width}</p>    //                   <p>{value2.height}</p>    //                 </>    //               )    //             }    //           }    //         </Attr.Consumer>    //       )    //     }    //   }    // </My.Consumer>    <>      {/* 直接消费即可 */}      <p>{value1.name}</p>      <p>{value1.age}</p>      <p>{value2.width}</p>      <p>{value2.height}</p>    </>  )}// 父组件function App() {  return (    <div>      {/* 两个实例依次生产数据 */}      <My.Provider value={{name: 'zs', age: 18}}>        <Attr.Provider value={{width: 300, height: 300}}>          <Son />        </Attr.Provider>      </My.Provider>    </div>  )}export default App

useReducer函数

用来替代useState使用,优化useState代码,如果多个组件中都有相同的业务流程,如果使用useState的话需要重复书写多次,这时就可以使用useReducer来进行指令化操作

  • 参数1:指令及其逻辑代码
  • 参数2:数据状态

常规setState方法设置状态,如果多个组件使用相同的逻辑就只能重复书写

// 引入方法import { useState } from 'react'// 子组件function Son1() {  // 设置初始状态  const [num, numSet] = useState(0)  return (    <div>      {/* 点击修改状态 */}      <button onClick={() => numSet(num - 1)}>-1</button>      {num}      <button onClick={() => numSet(num + 1)}>+1</button>    </div>  )}// 同上function Son2() {  const [num, numSet] = useState(10)  return (    <div>      <button onClick={() => numSet(num - 1)}>-1</button>      {num}      <button onClick={() => numSet(num + 1)}>+1</button>    </div>  )}// 父组件function App() {  return (    <div>      <Son1 />      <Son2 />    </div>  )}export default App

使用useReducer改造,虽然代码多了几行但是实际上如果项目庞大会很方便

// 引入方法 - useReducerimport { useReducer } from 'react'// 书写useReducer要使用的指令及其逻辑const reducer = (state, action) => {  // 条件分支语句,根据不同指令名输出不同结果  switch (action.type) {    case 'sub':      return {...state, num: state.num - 1}    case 'add':      return {...state, num: state.num + 1}    default:      return {...state}  }}// 子组件function Son1() {  // 直接使用useReducer设置初始值和需要使用的指令集  const [state, dispatch] = useReducer(reducer, {num: 0})  return (    <div>      {/* 点击修改状态,直接传递指令即可 */}      <button onClick={() => dispatch({type: 'sub'})}>-1</button>      {state.num}      <button onClick={() => dispatch({type: 'sub'})}>+1</button>    </div>  )}// 同上function Son2() {  const [state, dispatch] = useReducer(reducer, {num: 10})  return (    <div>      <button onClick={() => dispatch({type: 'sub'})}>-1</button>      {state.num}      <button onClick={() => dispatch({type: 'sub'})}>+1</button>    </div>  )}// 父组件function App() {  return (    <div>      <Son1 />      <Son2 />    </div>  )}export default App

useCallback优化代码

  • 场景1:a和b都是app的子组件,但是当app组件的内容发生变化时,会强迫带着a和b组件一起重新挂载,就会导致性能浪费
    • 解决方案:使用react的memo方法包装一下a和b,这样app在更新就不会呆着a和b一起更新了
// 引入memoimport {memo} from 'react'// 使用memo包装const MenoA = memo(a)const MenoB = memo(b)// 后面时候包装后的MenoA和MenoB就可以了
  • 场景2:当app把修改自己状态的方法交给子组件后,子组件触发后这个方法又会导致app及其下面的子组件又重新挂载(即使设置了memo),因为app重新渲染,导致方法重新传递给子组件,导致重新渲染
    • 解决方案:使用useCallback处理一下传递的函数,只要函数依赖的值没有变化,就不会导致重新渲染
    • 参数1:函数要执行的逻辑
    • 参数2:依赖的状态,只要状态不变就认为这个函数在挂载前后没有变化,就不会引起子组件重新加载
// 引入方法 - useState保存状态,memo - 包装组件,useCallback - 处理函数import { useState, memo, useCallback } from 'react'// 子组件function Son1(props) {  // 组件渲染后打印  console.log('son1渲染了')  return (    <>      {/* 点击调用app传递来的方法修改数据 */}      <button onClick={() => props.add()}>num+1</button>    </>  )}// 同上function Son2(props) {  console.log('son2渲染了')  return (    <>      <button onClick={() => props.add()}>age+1</button>    </>  )}// 包装两个子组件,避免因为app内部状态变化导致重新加载const MemoSon1 = memo(Son1)const MemoSon2 = memo(Son2)function App() {  console.log('app渲染了')  // 创建状态  const [num, numSet] = useState(0)  const [age, ageSet] = useState(10)  // 常规写法,避免不了子组件变化  // function addnum() {  //   numSet(num + 5)  // }  // 使用useCallback处理函数,可以自行判断加载前后函数是否发生变化,如果无变化不会重新渲染对应组件  const addnum = useCallback(() => {    numSet(num + 5)  }, [num]) // num是useCallback依赖状态,只要监测到num无变化就不会重玄渲染组件  // 同上  // function addage() {  //   ageSet(age + 5)  // }  const addage = useCallback(() => {    ageSet(age + 5)  }, [age])  return (    <div>      <p>num:{num}</p>      <p>age:{age}</p>      {/* 把两个函数传递给两个组件 */}      <hr />      <MemoSon1 add={addnum} />      <hr />      <MemoSon2 add={addage} />    </div>  )}export default App

useMemo函数

useMemo和useCallback作用相似,都是为了优化代码(避免不必要渲染)而存在,但是useMemo可以支持类型更多,useCallback只能返回函数

  • 参数1:函数,函数返回需要的结构
  • 参数2:依赖,和useCallback类似
// 引入方法 - useState保存状态,memo - 包装组件,useCallback、useMemo - 处理函数import { useState, memo, useCallback, useMemo } from 'react'function Son1(props) {  console.log('son1渲染了')  return (    <>      <button onClick={() => props.add()}>num+1</button>    </>  )}// 同上function Son2(props) {  console.log('son2渲染了')  return (    <>      <button>age+1</button>    </>  )}const MemoSon1 = memo(Son1)const MemoSon2 = memo(Son2)function App() {  console.log('app渲染了')  const [num, numSet] = useState(0)  const [age, ageSet] = useState(10)  // 创建my  let my = {name: 'zs', age: 18}  const addnum = useCallback(() => {    numSet(num + 5)  }, [num])  // 使用useMemo包装my,避免发生变化,否则当我更改app的其他状态的时候就会触发MemoSon2的重新渲染  my = useMemo(() => {    return {name: 'zs', age: 18}  }, [])  const addage = useCallback(() => {    ageSet(age + 5)  }, [age])  return (    <div>      <p>num:{num}</p>      <p>age:{age}</p>      <hr />      <MemoSon1 add={addnum} />      <hr />      {/* 把my数据传递给组件 */}      <MemoSon2 my={my}/>    </div>  )}export default App// 正常情况下,不使用useMemo包装,app重新渲染会导致my重新传递给MemoSon2,这样就会导致MemoSon2重新渲染,包装后会避免这种情况

useMemo实用场景

问题:当组件内部有一个非常耗时的计算操作或者接口请求,正常情况下,如果修改了组件内其他数据会导致整个组件重新渲染,进而导致这个耗时的操作也会被重新操作,会导致性能浪费

诉求:在组件更新的时候,除非耗时操作的结果发生变化,只要结果不变,那就不重启这个耗时操作

// 引入方法 - useState保存状态,memo - 包装组件,useCallback、useMemo - 处理函数import { useState, useMemo } from 'react'// 模拟一个很消耗性能的操作 - 累加器function fn() {  console.log('fn调用了')  let x = 0  for (let i = 0; i < 10000; i++) {    x += i  }  return x}function App() {  console.log('app渲染了')  // 组件可变状态  const [num, numSet] = useState(0)  // 原始写法。这样写无法阻止因为 num 的变化导致的 my 重新计算  // const my = fn()    // 使用 useMemo 包装后,只要 fn() 的结果不变,就不会因为 num 的变化导致的 my 重新计算  const my = useMemo(() => {    return fn()  }, [])  return (    <div>      <button onClick={() => numSet(num + 1)}>+1</button>      <p>num:{num}</p>      <p>{my}</p>    </div>  )}export default App

useRef函数

useRef可用与获取元素、可以保存数据、保存的数据除非手动修改否则是不改变的

  • createRef和useRef获取元素
// 引入方法 - import { createRef, useRef, Component } from 'react'// 函数式组件不能直接支持useRef和createRef,需要使用ForwardRef包装一下// function Son() {//   return(//     <div>Son内容</div>//   )// }class Son extends Component {  render() {    return (      <div>Son内容</div>    )  }}function App() {  // createRef 和 useRef 功能类似,都可以获取元素  const oA = createRef()  const oS = useRef()  // 打印两个获取到的元素  function go() {    console.log(oA.current, oS.current)  }  return (    <div>      {/* 绑定ref */}      <p ref={oA}>App内容</p>      <Son ref={oS} />      <button onClick={() => go()}>打印</button>    </div>  )}export default App
  • useRef和useState保存状态
// 引入方法 - import { useRef, useState } from 'react'function App() {  // 创建状态  const [num, numSet] = useState(10)  // 创建 useRef ,并且把把 num 设置为 Ref 内容保存起来  const obj = useRef(num)  return (    <div>      {/* 打印两个值 */}      <p>obj的值:{obj.current}</p>      <p>num的值:{num}</p>      {/* 点击让num+ */}      <button onClick={() => numSet(num + 1)}>操作</button>    </div>  )}export default App// 可以发现点击后num可以正常 +1 ,但是 obj.current 始终不变

useRef中保存的状态除非手动更改否则不会改变,内部可以保存多种类型,甚至是一个元素,这就可以保证在整个组件生命周期中使用这个状态都不会有变化

uselmperativeHandle函数

对某一个函数式组件内部操作进行拦截,自定义暴漏出来什么

  • 参数1:要拦截的Ref(指令)
  • 参数2:函数,返回要给父元素返回的东西
// 引入方法import { useRef, forwardRef, useImperativeHandle } from 'react'// 子组件,父元素传递过来的ref单独作为一个参数function Son(props, oSon) {  // 获取元素  const oInput = useRef()  // 拦截获取元素,进行处理,拦截 oSon ,  useImperativeHandle(oSon, () => {    // 只给父元素返回一些方法,而不直接把input整个元素返回    return {      setValue: () => {        oInput.current.value = '拉勾教育'      }    }   })  return (    <div>      <p>Son</p>      {/* 使用自己组件的 ref  */}      <input ref={oInput} />      {/* 原始获取数据的方法 */}      {/* <input ref={oSon} /> */}    </div>      )}// 包装一下函数式组件const ForwardRefSon = forwardRef(Son)function App() {  // 创建Ref  const oSon = useRef()  // 事件函数  function go() {    console.log(oSon)    // 直接调用函数    oSon.current.setValue()  }  return (    <div>      {/* 把 Ref 设置给子组件 */}      <ForwardRefSon ref={oSon} />      <button onClick={() => go()}>操作</button>    </div>  )}export default App// useImperativeHandle拦截住oSon的元素获取,并输入想让父元素得到的东西// 原本父元素可以直接获取input,并可以直接操作元素// 拦截后useImperativeHandle只会给父元素他想给的功能,父元素无法直接获取元素本身

useLayoutEffect函数

useLayoutEffect和useEffect作用几乎相同,区别点在于

  • useLayoutEffect和useEffect同时出现,useLayoutEffect会优先执行
  • useLayoutEff会在DOM元素插进视图之前执行,useEffect则会在挂载之后执行,所以useLayoutEff更适合作为调整DOM元素样式的作用
// 引入 Hook 函数
import { useEffect, useLayoutEffect, useRef } from 'react'
// 引入样式
import './app.css'

// 子组件
function Son() {
  // 创建Ref
  const op = useRef()

  // 普通 useEffect
  // useEffect(() => {
  //   // 可以隐约看到p元素在0px位置闪了一下,机器性能好的话可能看不见,说明这里是组件挂载之后才执行的
  //   op.current.style.left = '0px'
  //   op.current.style.left = '300px'
  //   console.log('useEffect执行')
  //   console.log('挂载|更新')
  //   return () => {
  //     console.log('卸载')
  //   }
  // })

  // useLayoutEffect处理
  // 虽然 useLayoutEffect 在后面 但是却比 useEffect 先执行
  useLayoutEffect(() => {
    // 这里看不到上面的闪烁,说明在挂载之前就已经完成了样式调整
    op.current.style.left = '0px'
    op.current.style.left = '300px'
    console.log('useLayoutEffect执行')
    console.log('挂载|更新')
    return () => {
      console.log('卸载')
    }
  })
  return (
    <p ref={op}>123</p>
  )
}

// 父组件
function App() {
  return (
    <div>
      <Son />
    </div>
  )
}

export default App

如果需要在渲染之前调整Dom元素(样式等),可以使用useLayoutEffect,其他情况下两者哪个都可以

自定义Hook(钩子)函数

  • 不同组件中出现相同逻辑的时候,可以利用自定义钩子函数实现
  • 本质上也是一个函数,可以传递参数
// 引入钩子
import { useState, useEffect } from 'react'

// 自定义钩子函数,前面只需要加上 use即可,默认情况下 Hook函数是不可以写在组件外边的,加上 use 就可以了
function useGo(name) {
  useEffect(() => {
    console.log(`${name}挂载|更新|添加事件监听等`)
    return () => {
      console.log(`${name}卸载|删除事件监听等`)
    }
  })
}

function Son1() {
  // useEffect(() => {
  //   console.log(`Son1挂载|更新|添加事件监听等`)
  //   return () => {
  //     console.log(`Son1卸载|删除事件监听等`)
  //   }
  // })
  // 直接调用自定义钩子
  useGo('Son1')
  return (
    <p>Son1内容</p>
  )
}
function Son2() {
  // useEffect(() => {
  //   console.log(`Son2挂载|更新|添加事件监听等`)
  //   return () => {
  //     console.log(`Son2卸载|删除事件监听等`)
  //   }
  // })
  // 直接调用自定义钩子
  useGo('Son2')
  return (
    <p>Son2内容</p>
  )
}

// 父组件
function App() {
  const [show, showSet] = useState(true)
  return (
    <div>
      <button onClick={() => showSet(!show)}>切换</button>
      {show && <Son1 />}
      {show && <Son2 />}
    </div>
  )
}

export default App
// 把重复使用的逻辑单独书写