基本介绍
1. 概念及作用
- Hooks,意为钩子,React Hooks就是一堆钩子函数,react通过这些钩子函数对函数式组件的增强
- 让函数型组件可以存储状态,可以拥有处理副作用的能力,
不同的钩子函数提供了不同的功能,让开发者在不使用类组件的情况,实现相同的功能
2. 副作用
在一个组件中,只要不将数据转换成视图的代码就属于【副作用】,
- 如:获取 DOM 元素并为其添加事件、设置定时器、发送 ajax 请求等。
- 在类组件中通常使用生命周期函数处理副作用,而函数组件使用 Hooks 来处理副作用
3. 类组件的不足(hooks要解决的问题)
- 缺少逻辑复用机制(要说一下HOC和render Props的缺点,从而引出hooks)
- 为了复用逻辑增加无实际渲染效果的组件,增加了组件层级 显示十分臃肿,增加了调试的难度以及运行效率的降低
- 类组件经常会变的很复杂难以维护
- 将一组相干的义务逻辑拆分到了多个声明周期函数中
- 在一个生命周期函数内存在多个不相干的业务逻辑
- 类成员方法不能保证this指向的正确性
React Hooks使用
【钩子函数】React 通过这些钩子函数,对【函数型组件】进行增强,不同的钩子函数提供了不同的功能
2.1 useState()
- 让函数型组件在重新渲染后还能保留 state 的状态
- 用于为函数组件引入状态,一个函数调用完之后,里面的变量就被释放掉了,useState是使用闭包来保存状态的
function App(){const [ count, setCount ] = useState(0)return (<div><span>{count}</span><button onClick={()=>setCount(count+1))}> + 1</button></div>)}
特性:
- 接受唯一的参数(也就是状态初始值),初始值可以是任意数据类型。
- 参数可以是一个函数,函数返回什么,初始状态就是什么,函数只会被调用一次,用在初始值是动态值的情况。
- 返回值为数组,数组中存储状态值和更改状态值的方法。方法名称约定为以set开头,后面加上状态名称
- 方法可以被调用多次,用以保存不同的状态
- 设置状态方法本身是异步的,可以在参数是一个函数中拿到修改后的count
注意1: useState 传入函数设置初始值 ```function App(){const [ person, setPerson ] = useState({name:'Fanny',age:'26'})return (<div><span>{person.name}{person.age}</span><button onClick={()=>setPerson({...person,name:'Vicky'})}> + 1</button>// 如果不对person进行浅拷贝,那么age会为空</div>)}
function App(){ const propsCount = props.count || 0; const [ count, setCount ] = useState(propsCount) // 每次旋绕都会获取,没有意义。只需给useState设置一个初始函数即可。 return (
修改后
function App(){ const [ count, setCount ] = useState(()=>props.count||0) // 只有第一次渲染组件被执行,重新渲染不会执行。 return (
useState(普通值||返回普通值的初始值函数),初始值函数只会执行一次。<br />如果这个初始值是外部接受来的,最好使用初始值函数设置初始值。注意2: setCount是异步函数<br />setCount(这里可以传入函数,直接在这个函数中处理返回值的依赖值)<br />也就是说,如果 `count` 现在是 `0` ,在 `setCount(count+1)` 后紧接着输出 `count` ,它的值仍然为 `0` ,要获取最新的 `count` 值可以在 `setCount` 中对 `count` 进行处理后得到最新的值
setCount(count=>{ const newCount = count + 1; document.title = newCount; return newCount; })
<a name="jCTik"></a>#### 2.2 useReducer()useReducer是另一种让函数组件保存状态的方式<br />它的使用方式与 Redux 中的 reducer 非常相似```javascriptfunction App(){function reducer(state, action) {switch (action.type) {case 'increment':return state + 1;case 'decrement':return state - 1;default"return state;}}const [ count, dispatch ] = useReducer(reducer, 0)return (<div><button onClick={()=>dispatch({type: 'increment'})}><span>{count}</span><button onClick={()=>dispatch({type: 'decrement'})}></div>)}
useReducer VS useState
- useReducer 可以合并多个 setState 方法,也就是可以对变量做出多种改变,
- useState 只能对变量作一种改变,每次改变需要定义相应的方法,但结构相对简单
- useReducer 可以把一堆改变变量的方法(dispatch)直接传递给子组件,子组件瞬间 buff(比如子组件需要修改富组件状态,可以把dispatch传递下去)
2.3 useContext()
在【跨组件层级】获取数据时,简化获取数据的代码
import React ,{createContext} from 'react';const countContext = createContext();function App() {return (<countContext.provider value={100}><Foo /></countContext.provider>)}function Foo() {// 繁琐的写法,需要优化return <countContext.Consumer>{value =>{return <div>{value}</div>}}</countContext.Consumer>}export default App;
import React ,{createContext} from 'react';const countContext = createContext();function App() {return (<countContext.provider value={100}><Foo /></countContext.provider>)}function Foo() {const value = useContext(countContext)return <div>{value}</div>}export default App;
优点
- 跨多个层级的组件也能获取到上层组件传递的值,不用一级一级传递
缺点
-
2.4 useEffect()
class 组件使用【生命周期函数】来处理【副作用】
useEffect 就是用来在函数组件中处理副作用的,根据不同的参数,等价于类组件的componentDidMount, componentDidUpdate,componentWillUnMount 生命周期
useEffect(()=>{}) => componentDidMount, componentDidUpdateuseEffect(()=>{}, []) => componentDidMountuseEffect(()=>()=>{}) => componentWillUnMount
useEffect 的执行时机:
组件挂在完成后
- 组件数据更新后
- 组件被卸载前
2.4.1. 监听所有 observable 变量,一旦发生变化就会执行
// 组件挂载完成之后执行 组件数据更新完成之后执行useEffect(()=>{console.log('123');})
- useEffect可以写多个,可以把不同的用途分开来写
- 将同一组业务逻辑写在一个生命周期函数中,比如挂载和卸载卸载一起
2.4.2.挂载后执行一次,再也不会执行
useEffect(()=>{console.log('挂载后执行一次,再也不会执行');}, [])
2.4.3.组件被卸载之前执行一次
useEffect(()=>{return ()=>{console.log('组件被卸载了')}})
怎样卸载 App 这个组件?
从 ‘react’ 中引入 ReactDOM 调用方法,传入 root 节点进行卸载
2.4.4. 指定数据发生变化时才会触发 effect
useEffect(()=>{document.title = count}, [count])
2.4.5. 在 useEffect 中执行异步函数:IIFE
- useEffect 中第一个参数必须为一个函数,而且不能是异步函数,
- 因为 useEffect 要返回清理资源的函数,异步函数则会默认返回 Promise ```javascript // 不可以,因为useEffect的return是一个卸载的回调函数,而下面这种写法是返回一个promise useEffect(async ()=> { const res = await axios.get() })
// 正确用法 // 在useEffect中写一个自执行函数 useEffect(()=> { (async ()=> { const res = await axios.get() })() })
<a name="B1MIJ"></a>#### useEffect VS 生命周期函数- useEffect 可以被多次调用,根据不用的调用方式产生不同的【生命周期】效果,代码更加简洁、灵活- useEffect 可以将多个【生命周期】逻辑整合,如【初次更新】和【数据更新】后代码逻辑类似,整合之后去除重复代码使代码逻辑更清晰- 类中的【生命周期函数】往往需要将多个业务逻辑写在同一个生命周期函数中,或需要将一个逻辑拆分在不同的生命周期函数中<a name="fsCXJ"></a>#### 2.5 useMemo()- useMemo 的行为类似 Vue 中的【计算属性】,可以监测某个值的变化,根据变化值计算新值- useMemo 会缓存计算结果,如果监测值没有发生变化,即使组件重新渲染也不会重新计算;此行为可以有助于避免在某个渲染上进行昂贵的计算```javascriptconst result = useMemo(()=> {// 如果count变化,重新执行这个函数return result}, [count])
优点
- 缓存昂贵计算值
- 它的返回值可以参与视图渲染
-
2.6 memo()
【性能优化】如果组件中的数据没有发生变化,
memo(组件)可以阻止组件更新;- 类似 class 组件中的
PureComponent和shouldComponentUpdate```javascript const Foo = memo(function Foo() { returnxxx})
`memo(Component)` 的返回值是一个新的组件,这个新的组件会判断传入的组件数据有没有发生变化,例<br /><a name="zmCbK"></a>####<a name="eMvgJ"></a>#### 2.7 useCallback()性能优化,缓存函数,使组件重新渲染使得到相同函数实例<br />函数传递给子组件时,每次重新渲染都得到了一个新的函数实例,这样子组件就又会重新渲染```javascriptfunction App(){const [count, setCount] = useState(0)//function resetCount(){// setCount(0)// }const resetCount = useCallback(()=> setCount(0), [setCount])return (<div><Foo resetCount={resetCount} /></div>)}const Foo = memo(function Foo(props) {return <div><button onClick={props.resetCount}></button></div>})// 这里foo组件重新渲染了,因为count值发生变化,app组件重新渲染,(重新渲染每次都生成了不同的resetCount实例)重新渲染之后resetCount就不是之前的// 函数实例了,值发生了改变,所以导致foo组件也跟着更新
优化后的 resetCount 函数↓,只有当 setCount 发生变化才会重新取得新的 ()=>setCount(0) 函数
2.8 useRef()
2.8.1 用于【获取 DOM 元素对象】
import React, { useRef } from 'react';function App(){const username = useRef();const handler = ()=>console.log(username) // {current:input}return (<input ref={username} onChange={handler}/>)}
在 input 元素上添加 ref 属性后, username 这个对象上的 current 键会指向 input 这个 DOM 元素,达到取值效果
2.8.1 用于【跨组件周期保存数据】
- 即使组件重新渲染,使用 useRef 保存的数据仍然存在
- 保存的数据被更改不会触发组件重新渲染
例:
下面这段代码无法在点击事件之后清除 interval 定时器
import React, { useState,useEffect} from 'react';function App(){const [count, setCount ] = useState(0);let timerId = null; // 重新渲染后timerId为nulluseEffect(()=>{timerId = setInterval(()=>{setCount(count=>count+1)},1000) // 每次count更新都会促发组件的重新渲染},[]);const stopCount = ()=>{clearInterval(timerId)// 这里的timerId 为 null}return <div>{count}<button onClick={stopCount}> stop </button></div>}
解决方法:
import React, { useState,useEffect,useRef } from 'react';function App(){const [count, setCount ] = useState(0);let timerId = useRef(); // {current:null}useEffect(()=>{timerId.current = setInterval(()=>{setCount(count=>count+1)},1000)},[]);const stopCount = ()=>{console.log(timerId) // {current: 定时器ID}clearInterval(timerId.current)}return <div>{count}<button onClick={stopCount}> stop </button></div>}
自定义Hook
目的
在组件之间实现资源共享,共享的通常是一套逻辑,如发送请求获取博客文章,有很多组件需要做这件事,就可以把这段逻辑封装成一个 Hook。自定义hook是标准的封装和共享逻辑的方式。
固定格式
自定义hook是一个函数,其名称以use开头
自定义hook其实就是逻辑和内置hook的组合
它可以返回任何需要的数据和方法
// 例1function useGetPost() {const [post, setPost] = userState({})useEffect(()=> {axios.get(xxx).then(res=> setPost(res.data))}, [])return [post, setPost]}function App() {const [post, setPost] = useGetPost()return (<div>{post.title}</div>)}
// 例2function useUpdateInput (initialValue) {const [value, setValue] = useState(initialValue)return {value,onChange: event => setValue(event.target.value)}}function App() {const username = useUpdateInput('')const password = useUpdateInput('')const submitForm = event => {event.preventDefault();console.log(username.value)console.log(password.value)}return (<form onSubmit={submitForm}><input type='text' {...username} /><input type='password' {...password} /><input type='submit'></form>)}
React路由Hooks
react-router-dom 提供的路由钩子函数
import { useHistory, useLocation, useRouteMatch, useParams } from 'react-router-dom'export default function Home(props) {console.log(props)console.log(useHistory())console.log(useLocation())console.log(useRouteMatch())console.log(useParams())}
- useHistory:获取 history 对象
- useLocation:获取 location 对象
- useRouteMatch:获取 match 对象
useParams:获取 match 对象下的 params 对象(params 对象存储路由参数)
useState 原理
初始值(任意类型)
- 初始值就是只初始化一次—-脱离useState函数存在,判断是否有值,
- 返回值为数组[变量,设置变量的方法]
- useState可以调用多次—-使用数组这个数据结构保存起来,闭包缓存index值
import React from 'react'import ReactDOM from 'react-dom'// 这些必须脱离函数存在,否则组件重新渲染后,函数内的变量会被重新初始化let state = []let setters = []// 声明下标let stateIndex = 0function render() {// 这里需要清零是因为:重新渲染后 stateIndex 不会自动归零,而是继续累加// 如果不清零,在重新渲染后 useState 中的 value 和 setter 便无法取得渲染前对应的 state 数组和 setter 数组中的值stateIndex = 0// 重新渲染视图ReactDOM.render(<App />, document.getElementById('root'))}function createSetter(index) {// 通过闭包【保留索引】不被释放// 下面这个函数中的 index 永远都是上面这个函数被调用时传进去的那个 index 值return function (newState) {// 所以这里修改的也永远是这个 index 对应的 statestate[index] = newState// 重新渲染视图,所以每次调用 setState 都会重新渲染组件render()}}function useState(initialState) {// 1. 能取就取存下来的值,没有就用初始值state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState// 2. 重新渲染后都要【按顺序】重新创建【所有的】 setter,所以 useState 声明的顺序不能随意变动setters.push(createSetter(stateIndex))// 3. 在数组中取值:确保【对应顺序】let value = state[stateIndex]let setter = setters[stateIndex]// 4. 为下一个使用 useState 的情况做准备stateIndex++// 5. 以数组形式返回return [value, setter]}function App() {const [count, setCount] = useState(0)const [name, setName] = useState('Mary')return (<div>{count}<button onClick={() => setCount(count + 1)}>setCount</button>{name}<button onClick={() => setName('James')}>setName</button></div>)}export default App
useEffect 原理
import React from 'react'import ReactDOM from 'react-dom'let state = []let setters = []let stateIndex = 0function render() {stateIndex = 0// 每次渲染都会归零effectIndex = 0ReactDOM.render(<App />, document.getElementById('root'))}function createSetter(index) {return function (newState) {state[index] = newStaterender()}}function useState(initialState) {state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialStatesetters.push(createSetter(stateIndex))let value = state[stateIndex]let setter = setters[stateIndex]stateIndex++return [value, setter]}// 记录上一次的依赖值(useEffect 第二个参数中的变量值)let prevDepsAry = []let effectIndex = 0function useEffect(callback, depsAry) {if (Object.prototype.toString.call(callback) !== '[object Function]') {// 验证 callback 是不是【函数】throw new Error('useEffect函数的第一个参数必须是函数')}// 判断 depsAry 有没有被传递if (typeof depsAry === 'undefined') {// 没有传递callback()} else {if (Object.prototype.toString.call(depsAry) !== '[object Array]') {// 验证 depsAry 是不是【数组】throw new Error('useEffect函数的第二个参数必须是数组')}// 获取上一次的依赖值let prevDeps = prevDepsAry[effectIndex]// 将当前的依赖值和上一次的依赖值做对比 如果有变化(即:depsAry.every === false) 则调用callbacklet hasChanged = prevDeps ? depsAry.every((dep, index) => dep === prevDeps[index]) === false : trueif (hasChanged) {// 依赖值有变化callback()}// 同步依赖值prevDepsAry[effectIndex] = depsAry// 为下一次调用 useEffect 做准备effectIndex++}}function App() {const [count, setCount] = useState(0)const [name, setName] = useState('Mary')useEffect(() => {console.log('count')}, [count])useEffect(() => {console.log('name')}, [name])return (<div>{count}<button onClick={() => setCount(count + 1)}>setCount</button>{name}<button onClick={() => setName('James')}>setName</button></div>)}export default App
useReducer 原理
import React from 'react'import ReactDOM from 'react-dom'let state = []let setters = []let stateIndex = 0function render() {...}function createSetter(index) {...}function useState(initialState) {...}let prevDepsAry = []let effectIndex = 0function useEffect(callback, depsAry) {...}// useState 的增强版本function useReducer(reducer, initialState) {// 初始值其实是为 useState 传入的const [state, setState] = useState(initialState)// 精妙的设计 dispatchfunction dispatch(action) {// 调用传入的 reducer 方法const newState = reducer(state, action)// 使用 reducer 返回的值来为 state 重新赋值setState(newState)}// 以数组形式返回return [state, dispatch]}function App() {// 定义传入 useReducer 的第一个参数,reducer 会被 useReducer 中定义的 dispatch 调用function reducer(state, action) {// 为同一个 state 以不同的 action 方式进行变换switch (action.type) {case 'increment':// 返回什么,新的 state 就是什么return state + 1case 'decrement':// 返回什么,新的 state 就是什么return state - 1default:return state}}const [count, dispatch] = useReducer(reducer, 0)return (<div>{count}<button onClick={() => dispatch({ type: 'increment' })}>+1</button><button onClick={() => dispatch({ type: 'decrement' })}>-1</button></div>)}export default App

