初识Hook
我们到底为什么需要hook那?
函数组件类组件存在问题
函数组件存在的问题:
javascript
import React, { PureComponent } from 'react'
function HelloWorld2(props) {
let message="Hello world"
// 函数式组件存在的缺陷:
// 1.修改message之后,组件不会重新渲染
// 2.就算页面重新渲染:函数会被重新执行,第二次执行时,又会重新给message赋值未Hello world
// 3.不能编写类似生命周期的回调:网络请求...
return (
<div>
<h1 onClick={e =>message="你好啊"}>内容2:{message}</h1>
<button>修改文本</button>
</div>
)
}
export class App extends PureComponent {
render() {
return (
<div>
<h1>App</h1>
<hr />
<HelloWorld2 />
</div>
)
}
}
export default App
class组件存在的问题:
1.复杂组件变得难以理解
- 随着业务增加,class组件会变得越来越复杂
- 在挂载阶段,可能包含大量的逻辑代码:网络请求、事件监听等;还得在卸载阶段移除掉
- 逻辑往往混在一起,class难以拆分
2.更加复杂:需要搞清this的指向到底是谁,熟练ES6class
3.组件复用难
- 状态复用需要通过高阶组件
- 或者类似Provider、Consumer来共享一些状态,但多次使用Consumer,会导致我们代码存在很多嵌套,难以维护
Hook的出现
hooks的出现就是为了解决上述问题:
- 它可以在不编写class的情况下使用state以及其它React特性
- 基本可替代之前所有使用class组件的地方
- 并不需要直接将所有的代码重构为hooks,因为它向下兼容,可渐进式使用它
- 只能在函数组件中使用,不能在类组件中使用
Hook实现计数器

javascript
import React, { memo,useState } from 'react'
// useState()返回的是一个数组
// 元素一:当前状态的值(第一次为初始值),如果不设置则为undefined
// 元素二:更新当前值的函数
const CounterFunction = memo(() => {
const [counter,setCounter] = useState(0)
return (
<div>
<h2>CounterFunction:{counter}</h2>
<button onClick={e => setCounter(counter+1)}>+1</button>
<button onClick={e => setCounter(counter-1)}>-1</button>
</div>
)
})
export default CounterFunction
State/Effect
useState
1.useState来自react,需要从react中导入,它是一个hook;
- 参数:初始化值,如果不设置为undefined;
- 返回值:数组,包含两个元素;
- 元素一:当前状态的值(第一调用为初始化值);
- 元素二:设置状态值的函数;
2.点击button按钮后,会完成两件事情:
- 调用setCount,设置一个新的值;
- 组件重新渲染,并且根据新的值返回DOM结构;
3.Hook 就是 JavaScript 函数,这个函数可以帮助你 钩入(hook into) React State以及生命周期等特性;
4.但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用
如果逻辑太复杂,也可以单独封装一个函数修改数据
javascript
import React, { memo,useState } from 'react'
const CounterFunction = memo(() => {
const [message,setMessage] = useState("HelloWorld")
function changeMessage(){
setMessage("你好呀")
}
return (
<div>
<h2>CounterFunction:{message}</h2>
<button onClick={changeMessage}>修改文本</button>
</div>
)
})
export default CounterFunction
注意:为什么是useState而不是createState?
解答:因为state只在组件首次渲染时被创建,在下次渲染时,useState返回给我们当前的state。如果每次都创建新的变量,那就不该是状态了。
useEffect
它可以帮在函数组件中完成类似于class生命周期的功能。如网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects)。因此对于完成这些功能的Hook就被称为Effect Hook
1.基本使用
我们现在来完成一个需求:使页面的title总是显示count的数字:

代码实现:

useEffect传入一个回调函数, 这个回调函数在每次页面渲染完成后自动执行。也就是说,每次在函数式组件执行的顺序是:
执行函数组件 => 定义初始状态 => 渲染DOM => 执行useEffect中的回调=> 修改数据 => 重新执行函数组件 => 更新状态 => 渲染最新DOM => 执行useEffect中的回调
不难看出,其实useEffect中的回调相当于完成了componentDidMount和componentDidUpdate做的事情。
2.清除Effect
在类组件编写过程中,某些副作用我们需要在componentWillUnmount中进行清除,如事件总线、Redux中手动调用的subscribe等
useEffect传入的回调函数A本身有一个返回值,返回值里面是回调函数B,可在函数B中进行清除
javascript
// 复杂告知react,在执行完当前渲染之后要执行的副作用代码
useEffect(() => {
console.log("监听redux中数据变化");
// 传入的回调函数,又会返回一个回调函数=> 组件被重新渲染或卸载时执行
return () => {
console.log("取消监听redux中的数据变化");
}
})
3.使用多个Effect
将这些处理逻辑都放在同一个函数里面难维护,要清除也比较混乱,在使用中我们可将逻辑分离到不同的useEffect中:React 将按照 effect 声明的顺序依次调用组件中的每一个 effect;
如下图:

但是有一个问题,难道每渲染一次页面,我们的所有effect就要重新调用一次吗?
某些逻辑只需要执行一次即可,怎样才能解决这个问题嘞?
实际上,Effect有两个参数:
- 参数一:执行的回调函数;
- 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
javascript
import React, { memo, useEffect, useState } from 'react'
const App = memo(() => {
const [count,setCount] = useState(0)
const [message,setMessage] = useState("HelloWorld")
useEffect(() => {
console.log("修改title");
},[count])
useEffect(() => {
console.log("监听redux中的数据");
return () => {
}
},[])
useEffect(() => {
console.log("监听eventBus的why事件");
return () => {
}
},[])
// 我希望它第一次渲染时执行一次即可
useEffect(() => {
console.log("发生网络请求");
return () => {
console.log("会在我们组件被卸载时才会执行");
}
},[])
return (
<div>
<h2>Count-{count} </h2>
<button onClick={e => setCount(count+1)}>+1</button>
<button onClick={e => setMessage("你好啊")}>message({message})</button>
</div>
)
})
export default App
打印结果如下图:

Context/Reducer
useContext
我们之前在组件中使用类名.contextType = MyContext或MyContext.Consumer来共享数据
但是当多个Context共享时就会存在大量的嵌套:
javascript
import React, { Component } from 'react'
import { UserContext,ThemeContext } from './context'
export default class App extends Component {
// 使用Conetxt
render() {
return (
<div>
<UserContext.Consumer>
{
value => {
return (
<h2><ThemeContext.Consumer>
{value => <span>{value.color}</span>}
</ThemeContext.Consumer> </h2>
)
}
}
</UserContext.Consumer>
</div>
)
}
}
Context Hook允许我们通过Hook来直接获取某个Context的值
javascript
import React, { memo ,useContext} from 'react'
import { UserContext,ThemeContext } from './context'
const App = memo(() => {
// 使用Conetxt
const user = useContext(UserContext)
const theme = useContext(ThemeContext)
return (
<div>
<div>
<span>用户信息-{user.name}-{user.age} </span>
<span>主题色-{theme.color} </span>
</div>
</div>
)
})
export default App
注意:当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的context value 值。因此,该数据的更新也是响应式的。
Callback/Memo(性能优化)
useCallback
通常使用useCallback是不希望子组件进行多次渲染,并不是为了进行函数缓存
1.存在问题

javascript
import React, { memo,useCallback,useState } from 'react'
const HYHome = memo(function(props) {
const { increment } = props
console.log("HYHome被渲染")
return (
<div>
<button onClick={increment}>increment+8</button>
{/* 100个子组件 */}
</div>
)
})
const App = memo(() => {
const [count, setCount] = useState(0)
const increment = function() {
console.log("increment");
setCount(count + 8)
}
return (
<div>
<h2>App-{count}</h2>
<button onClick={increment} >+8</button>
<HYHome increment={increment}/>
</div>
)
})
export default App
我希望当子组件没有变化时,不需要重新渲染:
useCallback()返回一个函数的记忆值,在依赖不变的情况下,多次定义返回值是相同的
javascript
import React, { memo, useState, useCallback, useRef } from 'react'
// useCallback性能优化的点:
// 1.当需要将一个函数传递给子组件时, 最好使用useCallback进行优化, 将优化之后的函数, 传递给子组件------这样的话在某些情况下,子组件就不会重新渲染
// props中的属性发生改变时, 组件本身就会被重新渲染
const HYHome = memo(function(props) {
const { increment } = props
console.log("HYHome被渲染")
return (
<div>
<button onClick={increment}>increment+1</button>
{/* 100个子组件 */}
</div>
)
})
const App = memo(function() {
const [count, setCount] = useState(0)
const [message, setMessage] = useState("hello")
// 闭包陷阱: useCallback
// const increment = useCallback(function foo() {
// console.log("increment")
// setCount(count+1)
// }, [count])
// 进一步的优化: 当count发生改变时, 也使用同一个函数(了解)
// 做法一: 将count依赖移除掉, 缺点: 闭包陷阱
// 做法二: useRef, 在组件多次渲染时, 返回的是同一个值
const countRef = useRef()
countRef.current = count
const increment = useCallback(function foo() {
console.log("increment")
setCount(countRef.current + 1)
}, [])
// 普通的函数
// const increment = () => {
// setCount(count+1)
// }
return (
<div>
<h2>计数: {count}</h2>
<button onClick={increment}>+1</button>
<HYHome increment={increment}/>
<h2>message:{message}</h2>
<button onClick={e => setMessage(Math.random())}>修改message</button>
</div>
)
})
export default App
useMemo
1.使用场景:
- 有大量逻辑计算时,若逻辑输入值无变化,则无需重新计算
- 对子组件传递相同内容对象时,若无改变则不重新执行
2.函数传入两个参数:
参数一:回调函数
参数二:[逻辑执行依赖的变量]
3.useMemo和useCallback的对比
useMemo拿到的是传入回调函数的返回值,useCallback是回调函数本身
javascript
import React, { memo, useCallback } from 'react'
import { useMemo, useState } from 'react'
const HelloWorld = memo(function(props) {
console.log("HelloWorld被渲染~")
return <h2>Hello World</h2>
})
// 计算数字
function calcNumTotal(num) {
// console.log("calcNumTotal的计算过程被调用~")
let total = 0
for (let i = 1; i <= num; i++) {
total += i
}
return total
}
const App = memo(() => {
const [count, setCount] = useState(0)
// const result = calcNumTotal(50)
// 1.不依赖任何的值, 进行计算
const result = useMemo(() => {
return calcNumTotal(50)
}, [])
// 2.依赖count
// const result = useMemo(() => {
// return calcNumTotal(count*2)
// }, [count])
// 3.useMemo和useCallback的对比
function fn() {}
// const increment = useCallback(fn, [])
// const increment2 = useMemo(() => fn, [])
// 4.使用useMemo对子组件渲染进行优化
// 重新渲染时会定义一个新对象,传入值是没有什么区别的
// const info = { name: "why", age: 18 }
const info = useMemo(() => ({name: "why", age: 18}), [])
return (
<div>
<h2>计算结果: {result}</h2>
<h2>计数器: {count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<HelloWorld result={result} info={info} />
</div>
)
})
export default App
Ref/LayoutEffect
useRef
1.获取DOM
javascript
import React, { memo, useRef } from 'react'
const App = memo(() => {
const titleRef = useRef()
const inputRef = useRef()
function showTitleDom() {
console.log(titleRef.current)
inputRef.current.focus()
}
return (
<div>
<h2 ref={titleRef}>Hello World</h2>
<input type="text" ref={inputRef} />
<button onClick={showTitleDom}>查看title的dom</button>
</div>
)
})
export default App
2.useRef解决闭包陷阱问题
useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变
javascript
import React, { memo, useRef } from 'react'
import { useCallback } from 'react'
import { useState } from 'react'
let obj = null
const App = memo(() => {
const [count, setCount] = useState(0)
// useRef返回的是一个不变的对象(地址不变)
const nameRef = useRef()
console.log(obj === nameRef)
obj = nameRef
// 通过useRef解决闭包陷阱
const countRef = useRef()
countRef.current = count
const increment = useCallback(() => {
setCount(countRef.current + 1)
}, [])
return (
<div>
<h2>Hello World: {count}</h2>
<button onClick={e => setCount(count+1)}>+1</button>
<button onClick={increment}>+1</button>
</div>
)
})
export default App
useImperativeHandle/useLayoutEffect(了解)
useImperativeHandle
通过forwardRef可以将子组件的DOM直接暴露给父组件,但是这样也产生了一些问题:
父组件可以拿到子组件的DOM进行任意操作,这样就有点危险了,我们有什么办法解决吗?
- 可以通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
- 在父组件中,使用 inputRef.current时,实际上使用的是返回的对象
代码示例:
javascript
import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react'
const HelloWorld = memo(forwardRef((props, ref) => {
const inputRef = useRef()
// 如果直接绑定它,权限有点多,对于子组件来说太危险了------我希望只给它保留特定的权限
// 子组件对父组件传入的ref进行处理
useImperativeHandle(ref, () => {
return {
focus() {
console.log("focus")
inputRef.current.focus()
},
setValue(value) {
inputRef.current.value = value
}
}
})
return <input type="text" ref={inputRef}/>
}))
const App = memo(() => {
const titleRef = useRef()
const inputRef = useRef()
function handleDOM() {
// console.log(inputRef.current)
inputRef.current.focus()
// inputRef.current.value = ""
inputRef.current.setValue("哈哈哈")
}
return (
<div>
<h2 ref={titleRef}>哈哈哈</h2>
<HelloWorld ref={inputRef}/>
<button onClick={handleDOM}>DOM操作</button>
</div>
)
})
export default App
useLayoutEffect
useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
- useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
- useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。
javascript
import React, { memo, useEffect, useLayoutEffect, useState } from 'react'
const App = memo(() => {
const [count, setCount] = useState(100)
// 点击按钮后,会出现闪烁现象-先变成0,再变成99.xxxx,
useEffect(() => {
console.log("useEffect")
if (count === 0) {
setCount(Math.random() + 99)
}
})
// 不会出现闪烁现象,直接变成99.xxxx
useLayoutEffect(() => {
console.log("useLayoutEffect")
if (count === 0) {
setCount(Math.random() + 99)
}
})
console.log("App render")
return (
<div>
<h2>count: {count}</h2>
<button onClick={e => setCount(0)}>设置为0</button>
</div>
)
})
export default App
自定义Hook
本质:对函数代码的抽取,实现复用,减少代码逻辑
需求一:所有的组件在创建和销毁时都进行打印
javascript
import React, { memo, useEffect, useState } from 'react'
// 自定义Hook
function useLogLife(cName) {
useEffect(() => {
console.log(cName + "组件被创建")
return () => {
console.log(cName + "组件被销毁")
}
}, [cName])
}
const Home = memo(() => {
useLogLife("home")
return <h1>Home Page</h1>
})
const About = memo(() => {
useLogLife("about")
return <h1>About Page</h1>
})
const App = memo(() => {
const [isShow, setIsShow] = useState(true)
useLogLife("app")
return (
<div>
<h1>App Root Component</h1>
<button onClick={e => setIsShow(!isShow)}>切换</button>
{ isShow && <Home/> }
{ isShow && <About/> }
</div>
)
})
export default App
需求二:Context的共享
1.有两个自定义的context
javascript
import { createContext } from "react";
const UserContext = createContext()
const TokenContext = createContext()
export {
UserContext,
TokenContext
}
2.通过.Provider去包裹根组件
javascript
root.render(
<userContext.Provider value={{name:'dimple', age: 22}}>
<tokenContext.Provider value={'dimple555'}>
<App />
</tokenContext.Provider>
</userContext.Provider>
);
3.封装一个hook方法,只要一调用就能拿到共享的数据
javascript
import { useContext } from "react"
import { UserContext, TokenContext } from "../context"
function useUserToken() {
const user = useContext(UserContext)
const token = useContext(TokenContext)
return [user, token]
}
export default useUserToken
4.在子组件中使用
javascript
import React, { memo } from 'react'
import { useUserToken } from "./hooks"
// User/Token
const Home = memo(() => {
const [user, token] = useUserToken()
return <h1>Home Page: {user.name}-{token}</h1>
})
const About = memo(() => {
const [user, token] = useUserToken()
return <h1>About Page: {user.name}-{token}</h1>
})
const App = memo(() => {
return (
<div>
<h1>App Root Component</h1>
<Home/>
<About/>
</div>
)
})
export default App
需求三:获取鼠标滚动位置
1.定义一个hook
javascript
import { useState, useEffect } from "react"
function useScrollPosition() {
const [ scrollX, setScrollX ] = useState(0)
const [ scrollY, setScrollY ] = useState(0)
useEffect(() => {
function handleScroll() {
// console.log(window.scrollX, window.scrollY)
setScrollX(window.scrollX)
setScrollY(window.scrollY)
}
window.addEventListener("scroll", handleScroll)
//使用完销毁
return () => {
window.removeEventListener("scroll", handleScroll)
}
}, [])
return [scrollX, scrollY]
}
export default useScrollPosition
2.在各个组件中使用
javascript
import React, { memo } from 'react'
import useScrollPosition from './hooks/useScrollPosition'
import "./style.css"
const Home = memo(() => {
const [scrollX, scrollY] = useScrollPosition()
return <h1>Home Page: {scrollX}-{scrollY}</h1>
})
const About = memo(() => {
const [scrollX, scrollY] = useScrollPosition()
return <h1>About Page: {scrollX}-{scrollY}</h1>
})
const App = memo(() => {
return (
<div className='app'>
<h1>App Root Component</h1>
<Home/>
<About/>
</div>
)
})
export default App
redux hooks
在redux开发中,我们为了让组件和redux结合起来,我们使用了react-redux的connect,这种方式必须使用高阶函数结合返回的高阶组件,且必须编写mapStateToProps和mapDispatchToProps映射的函数,这样其实挺麻烦的
Redux7.1开始,可获取仓库的数据和方法
useSelector:将store映射到组件中
参数一:传入一个回调函数,参数为state,返回需要的数据对象
参数二:通过比较决定组件是否重新渲染
javascript
import React, { memo } from 'react'
import { useSelector, useDispatch, shallowEqual } from "react-redux"
import { addNumberAction, changeMessageAction, subNumberAction } from './store/modules/counter'
// memo高阶组件包裹起来的组件有对应的特点: 只有props发生改变时, 才会重新渲染
//它默认会比较比较返回的两个对象是否相等,也就是必须返回两个完全相等的对象才可以不引起重新渲染
//那比如message属性值没发生改变就不该渲染该组件的,所以加了shallowEqual
const Home = memo((props) => {
const { message } = useSelector((state) => ({
message: state.counter.message
}), shallowEqual)
const dispatch = useDispatch()
function changeMessageHandle() {
dispatch(changeMessageAction("你好啊, 师姐!"))
}
console.log("Home render")
return (
<div>
<h2>Home: {message}</h2>
<button onClick={e => changeMessageHandle()}>修改message</button>
</div>
)
})
const App = memo((props) => {
// 1.使用useSelector将redux中store的数据映射到组件内
const { count } = useSelector((state) => ({
count: state.counter.count
}), shallowEqual)
// 2.使用dispatch直接派发action
const dispatch = useDispatch()
function addNumberHandle(num, isAdd = true) {
if (isAdd) {
dispatch(addNumberAction(num))
} else {
dispatch(subNumberAction(num))
}
}
console.log("App render")
return (
<div>
<h2>当前计数: {count}</h2>
<button onClick={e => addNumberHandle(1)}>+1</button>
<button onClick={e => addNumberHandle(6)}>+6</button>
<button onClick={e => addNumberHandle(6, false)}>-6</button>
<Home/>
</div>
)
})
export default App