react 函数组件和hook

160 阅读6分钟

class组件的问题

1、大型组件很难拆分和重构,很难测试(即class不易拆分) 2、相同业务逻辑,分散到各个方法中,逻辑混乱 3、复用逻辑复杂,如Mixins、HOC、Render Props

React组件更易用函数表达

1、React提倡函数式编程, view=fn(props) 2、函数更灵活,更易拆分,更易测试 3、但函数组件太简单,需要能力增强--hooks

函数式组件

 import React, { useState } from 'react';
 interface IProps {
 	message: string
 }
 function Example(props:IProps) {
   const [count, setCount] = useState(0);
   return (
     <div>
        <p>You clicked {count} times</p>
        <button onClick={() => setCount(count + 1)}>
        Click me {props.message}
       </button>
     </div>
   );
 }

hook

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

1、只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。

2、只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)

useSate

让函数组件实现state和setState

1、默认函数组件没有state

2、函数组件是一个纯函数,用完即销毁,无法存储state

3、需要State hook,即把state功能“钩”到纯函数中

调用 useState 方法的时候, 它定义一个 “state 变量”。我们的变量叫 count, 但是我们可以叫他任何名字,比如 banana。这是一种在函数调用时保存变量的方式 —— useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。但是函数每次调用这个state又是全新的

function 每次运行称之为每次渲染 每次渲染都有自己的新的props和state 当我们更新状态的时候 react会重新渲染组件 每次渲染都拿到独立的state状态 这个值是函数中的常量 这个常量react记住 重新渲染的时候 react会拿这个常量放到函数中渲染 handeclick 会把点击那一瞬间的数据记住 用点击那一瞬间的值,也就说明每次渲染的状态是独立的

react 更新state 是去替换他而不是合并,所以要把state中所有的变量写全 usestate 纪录组件状态 在组件更新的时候使用最新的状态 可以把所有state写到一起也可以分开 应该在他们中间找到平衡

useEffect

纯函数 输入确定输出确定

effect 副作用

但是在网络的世界 应用不全是确定的页面,内容来源有可能是网络请求 dom操作 订阅数据等 做的和 纯页面渲染(公用的页面逻辑处理) 不相关的事情 导致页面渲染不一样,这些称之为副作用

// // 模拟 class 组件的 DidMount 和 DidUpdate
    // useEffect(() => {
    //     console.log('在此发送一个 ajax 请求')
    // })

    // // 模拟 class 组件的 DidMount
    // useEffect(() => {
    //     console.log('加载完了')
    // }, []) // 第二个参数是 [] (不依赖于任何 state)

    // // 模拟 class 组件的 DidUpdate
    // useEffect(() => {
    //     console.log('更新了')
    // }, [count, name]) // 第二个参数就是依赖的 state

无需清除的副作用

useEffect中写函数,可以根据需要写多个useEffect

先渲染组件在执行effect,告诉react组件渲染后要执行哪些操作,react会记住保存这些函数

默认useEffect 在第一次渲染和 后面的每一次渲染都执行,但我们可以控制它什么时候执行

 useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

需要清除的副作用

在useEffect中return函数退出(函数执行结束时每次都执行)时要执行的函数,函数组件每次执行都会从头执行Effecthook的所有内容 卸载组件 执行effect中的return以后 不再render

useEffect(()=>{
        const updateMouse = (e:MouseEvent)=>{
            setPosition({x: e.clientX, y: e.clientY})
        }
        document.addEventListener('click', updateMouse);
        return ()=>{
            document.removeEventListener('click', updateMouse)
        }
    })
function FriendStatus({ friendId }) {
    const [status] = useState(false)
    console.log('status,,,,',status);
   
    // DidMount 和 DidUpdate
    useEffect(() => {
        console.log(`开始监听 ${friendId} 在线状态`)

        // 【特别注意】
        // 此处并不完全等同于 WillUnMount
        // props 发生变化,即更新,也会执行结束监听
        // 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
        return () => {
            console.log(`结束监听 ${friendId} 在线状态`)
        }
    },[])

    return <div>
      {console.log('render')}
        好友 {friendId} 在线状态:{status.toString()}
    </div>
}

通过跳过 Effect 进行性能优化

正常情况下每次渲染都要执行 相关的effect,这种做法会导致相关的性能问题 如果某些值在两次渲染中间没有变化 可以跳过他们相关的effect的执行,只在组件挂载和卸载的时候调用 effect函数第二参数写 可以控制哪些数据变化的时候执行effect,而不会重复影响性能

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

useRef 用引用

const [like, setLike] = useState(0)// setLike(1) 修改state会出发render
const likeRef = useRef(0) // lileRef.current++ 修改ref不会出发render,那么这个时候就可以使用useRef来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染。
const didMountRef = useRef(false)
const domRef = useRef<HTMLInputElement>(null) // dom操作

useContext

context 用于传递全局共享的数据 如语言 主题,而不用props一层层的传递。 即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

App.tsx 中创建ThemeContext

import React from 'react';
import Hello from './components/Hello';

interface IThemeProps {
  [key:string]:{color: string; background: string} 
}
const themes: IThemeProps = {
  'light': {
    color: 'black',
    background: 'white'
  },
  'dark':{
    color: 'white',
    background: 'black'
  }
}

export const ThemeContext = React.createContext(themes.light)

function App() {
  return (
    <div className="App">
      <ThemeContext.Provider value={themes.light}>
      <Hello></Hello>
      </ThemeContext.Provider>
    </div>
  );
}

export default App;

Hello.tsx中使用 ThemeContext

import React, {useContext} from 'react';
import {ThemeContext} from '../App'
interface IProps {
    message: string
}
const Hello = (props: IProps) => {
    const theme = useContext(ThemeContext);
return (<div style={{color: theme.color, background: theme.background}}>{props.message}</div>)
}
Hello.defaultProps = {
    message: 'hello world'
}
export default Hello;

useReducer

1、useReducer是useState的替代方案,用于state复杂变化 2、useReducer是单个组件状态管理,组件通讯还需要props 3、redux是全局的状态管理,多组件共享数据

import React, { useReducer } from 'react'

const initialState = { count: 0 }

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 }
        case 'decrement':
            return { count: state.count - 1 }
        default:
            return state
    }
}

function App() {
    // 很像 const [count, setCount] = useState(0)
    const [state, dispatch] = useReducer(reducer, initialState)

    return <div>
        count: {state.count}
        <button onClick={() => dispatch({ type: 'increment' })}>increment</button>
        <button onClick={() => dispatch({ type: 'decrement' })}>decrement</button>
    </div>
}

export default App

useMemo

1、react默认会更新所有的子组件 2、class组件使用SCU和pureComponent做优化 3、Hooks使用useMemo,但优化的原理是相同的

import React, { useState, memo, useMemo } from 'react'

// 子组件
// function Child({ userInfo }) {
//     console.log('Child render...', userInfo)

//     return <div>
//         <p>This is Child {userInfo.name} {userInfo.age}</p>
//     </div>
// }
// 类似 class PureComponent ,对 props 进行浅层比较
const Child = memo(({ userInfo }) => {
    console.log('Child render...', userInfo)

    return <div>
        <p>This is Child {userInfo.name} {userInfo.age}</p>
    </div>
})

// 父组件
function App() {
    console.log('Parent render...')

    const [count, setCount] = useState(0)
    const [name, setName] = useState('双越老师')

    // const userInfo = { name, age: 20 }
    // 用 useMemo 缓存数据,有依赖
    const userInfo = useMemo(() => {
        return { name, age: 21 }
    }, [name])

    return <div>
        <p>
            count is {count}
            <button onClick={() => setCount(count + 1)}>click</button>
        </p>
        <Child userInfo={userInfo}></Child>
    </div>
}

export default App

useCallback

缓存函数

自定义hook

必须以use开头 不同的调用hook 之间无关

定义一个hook

import  { useState, useEffect } from 'react'

const useMousePosition = () => {
  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('mousemove', updateMouse)
    return () => {
      console.log('remove effect', positions.x)
      document.removeEventListener('mousemove', updateMouse)
    }
  }, [])
  return positions
}

export default useMousePosition

使用hook

import React from 'react';
import useMousePosition from './hooks/useMousePosition';
function App() {
  const position = useMousePosition()
  return (
    <div className="App">
      <div>x:{position.x},y: {position.y}</div>
    </div>
  );
}

export default App;

常用的请求接口hook

import { useState, useEffect } from 'react'
import axios from 'axios'

const useURLLoader = (url: string, deps: any[] = []) => {
  const [data, setData] = useState<any>(null)
  const [loading, setLoading] = useState(false)
  useEffect(() => {
    setLoading(true)
    axios.get(url).then(result => {
      setData(result.data)
      setLoading(false)
    })
  }, deps)
  return [data, loading]
}

export default useURLLoader

React hooks注意事项

1、useState初始化值,只有第一次有效,props 更改子组件会重新渲染

2、useEffect 参数为[] 时 内部不能修改state

import React, { useState, useRef, useEffect } from 'react'

function UseEffectChangeState() {
    const [count, setCount] = useState(0)

    // 模拟 DidMount
    const countRef = useRef(0)
    useEffect(() => {
        console.log('useEffect...', count)

        // 定时任务
        const timer = setInterval(() => {
            console.log('setInterval...', countRef.current)
            // setCount(count + 1)
            setCount(++countRef.current)
        }, 1000)

        // 清除定时任务
        return () => clearTimeout(timer)
    }, []) // 依赖为 []

    // 依赖为 [] 时: re-render 不会重新执行 effect 函数
    // 没有依赖:re-render 会重新执行 effect 函数

    return <div>count: {count}</div>
}

export default UseEffectChangeState

3、useEffect可能出现死循环