一、什么是hook?react为什么要用到hook?
react中hook一般指的是:
一系列以
“use”作为开头的方法,它们提供了让你可以完全避开class式写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。
react组件一般分成class组件和函数式组件两种。
class组件对比函数式组件有以下的优势:
- class组件可以定义自己的state,用来保存组件自己内部的状态。函数式则不行因为每次调用都会重新产生新的临时变量。
- class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑。
比如class组件可以在
componentDidMount发送网络请求,并且只在组件挂载后执行一次。 但是如果在函数式组件发送网络请求,那么在函数式组件每次重新渲染都会重新请求一次网络请求。 - class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等。 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次。
但是class组件相比函数式组件又有一定缺陷:
- 复杂class组件变得难以理解。随着业务的增多,我们的class组件会变得越来越复杂。 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在 componentWillUnmount中移除),而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
- 难以理解的class: 比如在class中,我们必须搞清楚this的指向到底是谁,如果用错了会造成不必要的bug
- 组件复用状态很难: 像redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用。比如react-router要用到navigate时就得使用HOC(高阶组件),将原本的class组件转换成函数式组件,以达到使用对应hook的目的。
所以hook的出现就是解决这些痛点的。让我们在不编写class的情况下(编写函数式组件情况下)使用state以及其他的React特性。
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
二、必用的hook
1、useState
const [message,setMessage]=useState('message初始值')
useState只有唯一入参:state的初始值。返回值则是一个数组,我们可以通过数组的解构,arr[0]是当前状态的值,arr[1]则是设置这个值的函数。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
2、useEffect
useEffect第一个参数是个回调函数,第二参数是个数组填写对应依赖变量。根据第二个参数分成以下几种情况:
1.不传入第二个参数
useEffect(() => {
//当该组件DOM被渲染后就会执行该回调函数
//代码一
return () => {
//代码二
}
})
第一次渲染或者之后更新每次渲染(组件挂载或者更新)都会执行代码一的操作。返回值的代码二操作则是在组件更新或卸载时才会执行。
2.传入第二个参数
useEffect(() => {
//当该组件DOM被渲染后就会执行该回调函数
//代码一
console.log("发送网络请求, 从服务器获取数据")
return () => {
//代码二
console.log("会在组件被卸载时, 才会执行一次")
}
},[])
第二个参数决定了是否在组件更新时执行回调函数。
如果是空数组的话就意味着没有依赖任何变量,即代码一执行时机:第一次渲染DOM。代码二执行时机:组件被卸载。这个时候就可以在代码一里实现网络请求,添加订阅操作。代码二添加移除订阅等对应操作。
如果将[counter,message]作为第二个参数,就意味着当组件因为counter,message两变量而进行更新渲染时,代码一二都将被执行。
当组件内容变复杂时useEffect函数则十分的复杂,这个时候可以根据不同功能将useEffect给分解成多个useEffect。
三、常用的hook
useRef
用法一:获取DOM元素。
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
用法二:保存一个数据,这个对象在整个生命周期中可以保存不变。
useRef 会在每次渲染时返回同一个 ref 对象。所以组件每次渲染时,通过useRef赋值的对象是地址指向不变的。可以用该方法解决useCallback的闭包陷阱的问题。
useCallback(性能优化)
useCallback实际的目的是为了进行性能的优化。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback会返回一个函数的 memoized(记忆)的方法, 在依赖不变的情况下,多次定义的时候,返回的方法是相同的。
为什么能性能优化: 如果A组件通过Props传递fun1给子组件B,A组件更新渲染,对应的函数fun1也进行重新声明,这个时候B组件应为B组件Props发生改变而要重新渲染。这个样子就十分浪费资源。
优化方法: 将fun1函数使用useCallback返回有"记忆"的fun2,将该函数传给子组件。当A组件更新渲染时,只要useCallback的依赖没有发生改变,那么返回的fun2永远是同一个函数,这样B组件的Props没有发生改变,因此B组件以及B组件的子组件不会进行多余的更新。以达到性能优化的目的。
但是这样如果fun1函数依赖了count变量,就得在useCallback第二个参数数组中加入count。如果不加入则会掉入“陷阱”里。
闭包陷阱
const [count, setCount] = useState(0)
const increment = useCallback(function foo() {
setCount(count+1)
}, [])
<button onClick={increment}>+1</button>
如上代码,当点击button后执行increment,useCallback返回的一直是记忆的foo函数,也就是这个函数foo只进行一次声明,在声明的时候捕获的count值一直是0所以setCount改变count后一直是1。无法实现想要的功能。当依赖值为count时,count发生改变则会返回第二个foo函数这个时候捕获的count为1。
进一步性能优化:
上面的闭包陷阱只需要在useCallback添加对应的依赖就可以解决。但是添加了count的依赖后,如果将A组件的increment传递给子组件B,当A组件的count发生改变时,increment也相应的改变了,B组件的Props也发生改变了,B以及他的子组件都将更新。这个时候可以用到useRef来进一步性能优化。
const [count, setCount] = useState(0)
const countRef = useRef()
countRef.current = count
const increment = useCallback(function foo() {
console.log("increment")
setCount(countRef.current + 1)
}, [])
这样就实现了increment一直都是同一个函数,B组件也不会因为increment改变而进一步进行更新渲染,也解决了闭包陷阱的问题。
useMemo(性能优化)
useMemo实际的目的也是为了进行性能的优化。useMemo返回的是一个 memoized(记忆的)值。(useCallback返回的是一个函数) 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
作用一: 计算后返回值存储在一个变量里,如果依赖的变量没有发生改变,那么存储计算值变量就一直不变。(应该和vue计算属性这块功能差不多。)
作用二性能优化: 原理和useCallback差不多,只是useMemo处理完后是一个值,如果讲这个值传给子组件,子组件也可以避免没必要的渲染更新。
useContext
组件中使用共享的Context有两种方式:
-
类组件可以通过 类名.contextType = MyContext方式,在类中获取context;
-
多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;
但是多个context共享时会存在大量的嵌套。就不方便取context里的值。这个时候可以用useContext来解决该痛点。
import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from "./context"
const App = memo(() => {
// 使用Context
const user = useContext(UserContext)
const theme = useContext(ThemeContext)
return (
<div>
<h2>User: {user.name}-{user.level}</h2>
<h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2>
</div>
)
})
export default App
useReducer(用的少)
很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是。
-
useReducer仅仅是useState的一种替代方案。
-
在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分。
-
或者这次修改的state需要依赖之前的state时,也可以使用。
import React, { memo, useReducer } from 'react'
// import { useState } from 'react'
function reducer(state, action) {
switch(action.type) {
case "increment":
return { ...state, counter: state.counter + 1 }
case "decrement":
return { ...state, counter: state.counter - 1 }
case "add_number":
return { ...state, counter: state.counter + action.num }
case "sub_number":
return { ...state, counter: state.counter - action.num }
default:
return state
}
}
const App = memo(() => {
const [state, dispatch] = useReducer(reducer, { counter: 0, })
return (
<div>
<h2>当前计数: {state.counter}</h2>
<button onClick={e => dispatch({type: "increment"})}>+1</button>
<button onClick={e => dispatch({type: "decrement"})}>-1</button>
</div>
)
})
就是返回一个state和dispatch方法。把useState的setValue方法改成dispatch形式来改变state。该方法很少用。
useImperativeHandle
通过ref和forwardRef获取子组件的DOM:
-
通过forwardRef可以将ref转发到子组件;
-
子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;
forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:
-
直接暴露给父组件带来的问题是某些情况的不可控;
-
父组件可以拿到DOM后进行任意的操作;
下面的案例中,需求父组件可以操作focus,以及提供一个setValue函数用来设置input值,其他并不希望它随意操作;
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 inputRef = useRef()
function handleDOM() {
inputRef.current.focus()
// inputRef.current.value = ""不能使用
inputRef.current.setValue("哈哈哈")
}
return (
<div>
<HelloWorld ref={inputRef}/>
<button onClick={handleDOM}>DOM操作</button>
</div>
)
})
这个例子通过ref拿到了子组件input的DOM,然后再通过useImperativeHandle这个hook在子组件规定可以使用这个DOM的什么方法。起到了规定权限的功能。
useLayoutEffect
useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
- useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
- useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。
自定义hook
自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。
例子:
import React, { memo } from 'react'
import { useUserToken } from "./hooks"
// User/Token
const Home = memo(() => {
//const user = useContext(UserContext) 重复代码抽取
//const token = useContext(TokenContext)
const [user, token] = useUserToken()
return <h1>Home Page: {user.name}-{token}</h1>
})
const About = memo(() => {
//const user = useContext(UserContext) 重复代码抽取
//const token = useContext(TokenContext)
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
//useUserToken.js
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
四、redux的hook:useSelector(性能优化)、useDispatch
使用react-redux里的Provider和connect可以达到使用redux的目的,但是这样使用redux则需要在connect里传入fuc1,fun2两个回调函数。每当组件需要用到redux时都得这样编写有点繁琐。所以redux提供了两个hook。
import React, { memo } from 'react'
import { useSelector, useDispatch, shallowEqual } from "react-redux"
import { addNumberAction, changeMessageAction, subNumberAction } from './store/modules/counter'
// memo高阶组件包裹起来的组件有对应的特点: 只有props发生改变时, 才会重新渲染
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>
)
})
上述例子如果useSelector没有传入第二个参数,那么父组件的count进行了修改,不但父组件进行了渲染更新子组件也进行了渲染更新。因为不传入第二个参数那么useSelector就会比较state是否发生改变,如果发生改变组件也发生改变.上面例子,父子组件都用到了store的counter模块的数据,因此只要其中一个改变那么state就发生了变化。
解决方法: useSelector第二个参数如果传入shalloEqual函数就会进行浅比较。就不会进行多余的更新。
五、react18新增的hook
useId
useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。
什么是hydration
在进行 SSR 时,我们的页面会呈现为HTML,但仅 HTML 不足以使页面具有交互性。例如,浏览器端 JavaScript 为零的页面不能是交互式的(没有 JavaScript 事件处理程序来响应用 户操作,例如单击按钮)。
为了使我们的页面具有交互性,除了在 Node.js 中将页面呈现为 HTML 之外,我们的 UI 框架(Vue/React/...)还在浏览器中加载和呈现 页面。(它创建页面的内部表示,然后将内部表示映射到我们在 Node.js 中呈现的 HTML 的 DOM 元素。)这个过程就叫做hydration
什么是SSR
- SSR(Server Side Rendering,服务端渲染)指的是页面在服务器端已经生成了完成的HTML页面结构,不需要浏览器解析。
- 对应的是CSR(Client Side Rendering,客户端渲染),我们开发的SPA页面通常依赖的就是客户端渲染。
SPA(单页面应用)向服务器获取index.html静态资源(往往只有app/root根组件)所以不利于SEO,接下来向服务器获取js文件,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,打包文件十分的大,所以往往浏览器请求下来然后在浏览器渲染时间长,会出现首屏加载白屏的问题。
为了解决SPA缺点问题我们可以借助node来帮助我们执行javascript代码,提前完成页面的渲染。 这样子我们又牵扯出了同构应用的概念
同构应用
一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用。
同构是一种SSR的形态,是现代SSR的一种表现形式。
- 当用户发出请求时,先在服务器通过SSR渲染出首页的内容。
- 但是对应的代码同样可以在客户端被执行。
- 执行的目的包括事件绑定等以及其他页面切换时也可以在客户端被渲染;
所以useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。 避免客户端渲染后因为客户端渲染的页面ID和服务端通过SSR渲染出的页面ID不同而导致hydration不匹配。
useTransition
返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。
const [ pending, startTransition ] = useTransition()
useTransition在告诉react对于某部分任务的更新优先级较低,可以稍后进行更新。
import React, { memo, useState, useTransition } from 'react'
import namesArray from './namesArray'
const App = memo(() => {
const [showNames, setShowNames] = useState(namesArray)
const [ pending, startTransition ] = useTransition()
function valueChangeHandle(event) {
startTransition(() => {
const keyword = event.target.value
const filterShowNames = namesArray.filter(item => item.includes(keyword))
setShowNames(filterShowNames)
})
}
return (
<div>
<input type="text" onInput={valueChangeHandle}/>
<h2>用户名列表: {pending && <span>data loading</span>} </h2>
<ul>
{
showNames.map((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
</div>
)
})
export default App
比如模糊查询根据input的value实时展示对应的数据,如果input的onchange方法和展示数据方法一起启动,如果展示筛查数据过多,可能会有些卡顿,但是如果用户输入时input卡顿会导致体验感很差,所以这个时候需要使用useTransition返回的startTransition方法把对应的筛选数据逻辑给包裹起来,这样就会优先执行input操作,然后在筛选。在筛选的时间里pending为true,如果startTransition完成后pending改变成false,所以可以根据pending确定数据是否加载完成,加载中的话加个Loading啥的。
useDeferredValue
useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。 在明白了useTransition之后,我们就会发现useDeferredValue的作用是一样的效果,可以让我们的更新延迟。
import React, { memo, useState, useDeferredValue } from 'react'
import namesArray from './namesArray'
const App = memo(() => {
const [showNames, setShowNames] = useState(namesArray)
const deferedShowNames = useDeferredValue(showNames)
function valueChangeHandle(event) {
const keyword = event.target.value
const filterShowNames = namesArray.filter(item => item.includes(keyword))
setShowNames(filterShowNames)
}
return (
<div>
<input type="text" onInput={valueChangeHandle}/>
<h2>用户名列表: </h2>
<ul>
{
deferedShowNames.map((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
</div>
)
})
export default App