Hooks
直译是 “钩子”,它并不仅是 react
,甚至不仅是前端界的专用术语,而是整个行业所熟知的用语。 一般指:系统运行到某一时期时,会调用被注册到该时机的回调函数。比如: windows
系统的钩子能监听到系统的各种事件,浏览器提供的 onload
或 addEventListener
能注册在浏览器各种时机被调用的方法。
在React中,Hooks
是 React 16.8 新增的特性,是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的特殊JS函数
,它可以让你在不编写 class 的情况
下使用 state 以及其他的 React 特性。 简言之:Hooks是一系列方法,提供了在函数式组件
中完成开发工作的能力。
之前使用 state 或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有 , Hooks 的出现可以让你在函数组件直接使用state等功能
React Hooks官方文档
假设任何以 use
开头并紧跟着一个大写字母的函数就是一个 Hook
只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用
只能在 React 的函数组件中调用 Hook,不要在普通JS函数中调用
useState(数据驱动更新)
:给函数组件添加state状态,并进行状态的读写操作 ,函数组件通过 useState 可以让组件重新渲染,更新视图。
useState语法:
import React,{ useState } from 'react'
const [xxx, setXxx] = useState(initState)
参数initState
: 第一种情况是非函数
,将作为 xxx 初始化的值。 第二种情况是函数
,函数的返回值作为 useState 初始化的值。
返回值[xxx, setXxx]
: 包含2个元素的数组 ,xxx
是当前状态值,setXxx
是修改状态值的函数
xxx
: 提供给 UI ,作为渲染视图的数据源setXxx
: 改变 xxx
的异步函数 ,推动函数组件渲染的渲染函数, 要在下次重新渲染才能获取新值 ,下面是setXxx的两种写法:
useState基础用法:
import React, { useState } from 'react'
const Demo = () => {
let [number, setNumber] = useState(0)
console.log(number, 'number')/* 这里的number能够即时更新的,这里是在重新渲染后调用的 */
return (
<div>
<span>{number}</span>
<button
type="primary"
onClick={() => {
setNumber(number + 1)
console.log(number,'number-=') /* 这里number不能够即时更新的,setNumber是异步更新 */
}}
>
+1按钮
</button>
</div>
)
}
注:
补充:方括号的作用
[ ]语法叫数组解构
,它意味着我们同时创建了 fruit 和 setFruit 两个变量,fruit 的值为 useState 返回的第一个值,setFruit是返回的第二个值
const [fruit, setFruit] = useState('banana');
//等价于
var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组
var fruit = fruitStateVariable[0]; // 数组里的第一个值
var setFruit = fruitStateVariable[1]; // 数组里的第二个值
useReducer(订阅状态,更新视图)
是useState
的替代方案,对于复杂的state操作逻辑,嵌套的state的对象, 或者下一个 state 依赖于之前的 state 等 ,推荐使用useReducer。
useReducer语法:
const [xxx, setXxx] = useReducer(reducer, initState);
参数reducer
:类似于 redux 中的 reducer 函数,(state, action) => newState
,接收当前应用的state和触发的动作action,计算并返回最新的state ,如果返回的 state 和之前的 state ,内存指向相同,那么组件将不会更新。
参数initState
: 第一种情况是非函数
,将作为state初始化的值。 第二种情况是函数
,函数的返回值作为 useReducer 初始化的值。
返回值[xxx, setXxx]
: 包含2个元素的数组 ,xxx
是当前状态值,setXxx
是修改状态值的函数
xxx
: 提供给 UI ,作为渲染视图的数据源setXxx
: 改变 xxx
的异步函数 ,用来触发reducer函数,计算对应的state,其余和useState
类似useReducer基础用法:
//demo.js
import React, { useReducer } from 'react'
import MyChildren from './children'
const Demo = () => {
/* number为更新后的state值, dispatchNumbner 为当前的派发函数 */
const [number, dispatchNumber] = useReducer((state, action) => {
const { payload, name } = action
/* return的值为新的state */
switch (name) {
case 'add':
return state + 1
case 'sub':
return state - 1
case 'reset':
return payload
}
return state
}, 0)
return (
<div>
当前值:{number}
{/* 派发更新 */}
<Button onClick={() => dispatchNumber({ name: 'add' })}>增加</Button>
<Button onClick={() => dispatchNumber({ name: 'sub' })}>减少</Button>
<Button onClick={() => dispatchNumber({ name: 'reset', payload: 666 })}>
赋值
</Button>
{/* 把dispatch 和 state 传递给子组件 */}
<MyChildren dispatch={dispatchNumber} state={number} />
</div>
)
}
//children.js
const MyChildren = props => {
const { dispatch, state } = props
return <div>MyChildren中值:{state}</div>
}
注:
任何刚开始写 react 组件的工程师应该都能发现,组件会一直重新渲染
。比如我们在使用React开发过程中经常会遇到父组件引入子组件的情况,往往会造成子组件不必要的重复渲染,造成主线程阻塞,页面渲染卡顿,带来性能问题。 缓存组件
可以优化性能 ,能够很好解决这一问题。
什么是 memoization?
Hooks逻辑
React中的浅比较:
Object.is
函数,只比较对象第一层的属性和值,不是使用严格相等 === 运算符;key
和数组值为value
的对象是等价的,比如:{ 0: 2, 1: 3 }
等价于 [2, 3]
;Object.is
比较的+0
和 -0
、Number.NaN
和 NaN
是不相等的,所以在复杂结构中比较时,这也是适用的;{ someKey: {} }
和 { someKey: [] }
是不相等的。shallowCompare({}, {}) // => true
shallowCompare({ a: 1, b: 2 }, { a: 1, b: 2 }) // => true
shallowCompare({ a: 1, b: 2 }, { a: 1, c: 2 }) // => false
shallowCompare({ a: 1, b: { 2 }}, { a: 1, b: { 2 }}) // => false
React.memo
:将组件的渲染结果缓存,并有效复用,减少主线程的阻塞。
React.memo语法:
const MemoComponent = React.memo(component, Func)
参数component
:自定义组件
参数Func
:一个函数,用来判断组件需不需要重新渲染。如果省略第二个参数,默认会对该组件的props进行浅比较,当 props 没有改变时,组件就不需要重新渲染
如果 props 中存在回调函数或者多层嵌套的复杂对象,或每次父组件更新时子组件的props都会生成新的内存地址,这样浅比较无效,仅使用react.memo是无法达到目的的,需要自己写比较函数,或者搭配 useCallback 使用。
何时使用 React.memo
useMemo(派生新状态)
: 可以在函数组件 render 上下文中同步执行一个函数逻辑,这个函数的返回值可以作为一个新的状态缓存起来, 同时保证了返回值的引用地址不变 。 把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算缓存值 ,让组件中的函数跟随状态更新,用于进行高开销、复杂场景的计算 。
useMemo语法:
const memoizedValue = useMemo(create,deps)
//useMemo(() => computeExpensiveValue(a, b), [a, b]);
第一个参数 create
:创建函数,函数的返回值作为缓存值
第二个参数deps
:依赖项数组,为当前 useMemo 的依赖项,在函数组件下一次执行的时候, 通过浅比较对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。 没有提供依赖项数组,useMemo在每次渲染时都会计算新的值 。 一般来说,所有create函数中引用的值都应该出现在依赖项数组deps中
返回值memoizedValue
:执行 create 的缓存值
。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。
useMemo 何时使用:
create
导致了页面的卡顿create
需要跟随组件渲染执行。create
本身有优化空间useMemo基础用法:
function Scope() {
const keeper = useKeep()
const { cacheDispatch, cacheList, hasAliveStatus } = keeper
/* 通过 useMemo 得到派生出来的新状态 contextValue */
const contextValue = useMemo(() => {
return {
cacheDispatch: cacheDispatch.bind(keeper),
hasAliveStatus: hasAliveStatus.bind(keeper),
cacheDestory: (payload) => cacheDispatch.call(keeper, { type: ACTION_DESTORY, payload })
}
}, [keeper])
return <KeepaliveContext.Provider value={contextValue}>
</KeepaliveContext.Provider>
如上通过 useMemo 得到派生出来的新状态 contextValue ,只有 keeper 变化的时候,才改变 Provider 的 value 。
function Scope(){
const style = useMemo(()=>{
let computedStyle = {}
// 经过大量的计算
return computedStyle
},[])
return <div style={style} ></div>
}
function Scope ({ children }){
const renderChild = useMemo(()=>{ children() },[ children ])
return <div>{ renderChild } </div>
}
useCallback(保存状态)
: useCallback 与useMemo类似,它们接收的参数一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于useCallback 缓存的是函数本身以及它的引用地址,而不是返回值
useCallback语法:
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
// 不使用 useCallback
const handleLog = (message) => console.log(message)
// 使用 useCallback
const handleLogMessage = useCallback(handleLog, [message])
把回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的缓存版本,该缓存版本的回调函数仅在某个依赖项改变时才会更新 ,跟随状态更新执行。
注意:
返回的是一个函数,不再是值
useMemo
第一次渲染时执行,缓存变量,之后只有在依赖项改变时才会更新缓存,如果依赖不更新,返回的永远是缓存的那个变量useCallback
第一次渲染时执行,缓存函数,之后只有在依赖项改变时才会更新缓存 ,如果依赖不更新,返回的永远是缓存的那个函数props
的时候,如果当前组件不更新,不会触发子组件的重新渲染(最重要的用途)使用 useCallback 要对依赖数组做浅比较,对性能带来的负面影响,同时又提升了代码的复杂度。如果使用不当,很可能得不偿失。
useEffect(异步执行副作)
: 在函数组件中执行副作用操作 (用于模拟类组件中的生命周期钩子) , 在执行 DOM 更新之后调用 。
可以把 useEffect Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
副作用
:指那些没有发生在数据向视图转换过程中的逻辑,如 发送ajax请求获取数据、 设置订阅 / 启动定时器及手动修改DOM 。
在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的
不需要清除的effect
: 只想在 React 更新 DOM 之后运行一些额外的代码,如发送网络请求,手动变更 DOM,记录日志
需要清除的 effec
: 如订阅外部数据源 ,清除工作可以防止引起内存泄露
默认情况下,React 会在每次渲染后都会执行useEffect
,通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。 将 useEffect放在组件内部让我们可以在 effect 中直接访问 state 变量(或其他 props),我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中
useEffect(() => {
// 执行副作用操作【挂载+更新】
return () => { //返回清除副作用的函数(可选),如订阅,定时器,在组件卸载的时候执行清除操作
}
}, [stateValue]) // 仅在stateValue更改时执行更新
//如果某些特定值在两次重渲染之间没有发生变化,你可以通知React跳过对useEffect的调用,只要传递数组作为 useEffect 的第二个可选参数即可
//如果想执行只运行一次的useEffect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数,这样effect内部的props和state就会一直拥有其初始值
useRef
可以用来获取元素,缓存状态,保存标签对象,接受一个状态 initValue 作为初始值,返回一个 ref 对象 ,ref上有一个 current 属性
就是 ref 对象需要获取的内容。
const refContainer = useRef(initValue);
console.log(refContainer.current)
useRef
返回一个可变的 ref 对象,其 current
属性被初始化为传入的参数(initValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
useRef
可以在函数组件中存储/查找组件内的标签或任意其它数据,相当于创建一个额外的容器来存储数据,我们可以在外部拿到这个值
重新赋值 ref.current
不会触发重新渲染
案例
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useContext
用来获取父级组件传递过来的 context 值,这个当前值就是最近的父级组件 Provider 设置的 value 值,useContext 参数一般是由 createContext 方式创建的,也可以父级上下文 context 传递的 ( 参数为 context )。useContext 可以代替 context.Consumer 来获取 Provider 中保存的 value 值。
const contextValue = useContext(context)
useContext 接受一个参数,一般都是 context 对象,返回值为 context 对象内部保存的 value 值。
import React, { useContext, createContext } from 'react'
//创建context对象
const MyContext = React.createContext()
export default function Hook() {
const [num, setNum] = React.useState(1)
return (
<h1>
//Provider确定数据共享范围,value分发数据
<Context.Provider value={num}>
<Item num={num} />
</Context.Provider>
</h1>
)
}
function Item() {
//接收contex对象,并返回该context的值
//该值由上层组件中距离当前组件最近的的value prop决定
const num = useContext(MyContext)//useContext的参数必须是context对象本身:
//调用了useContext的组件总会在context值变化时重新渲染,上层数据发生改变,肯定会触发重新渲染
return <div>子组件 {num}</div>
}
自定义 Hook
是一个函数,其名称以 use
开头,是 React Hooks 聚合产物,其内部有一个或者多个 React Hooks 组成,用于解决一些复杂逻辑。 通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
自定义 hooks的步骤:
例如:模拟数据请求的 Hooks
import React, { useState, useEffect } from "react";
function useLoadData() {
const [num, setNum] = useState(1);
useEffect(() => {
setTimeout(() => {
setNum(2);
}, 1000);
}, []);
return [num, setNum];
}
export default useLoadData;
我们希望 reducer 能让每个组件来使用,我们自己写一个 hooks,自定义一个自己的 LocalReducer
import React, { useReducer } from "react";
const store = { num: 1210 };
const reducer = (state, action) => {
switch (action.type) {
case "num":
return { ...state, num: action.num };
default:
return { ...state };
}
};
function useLocalReducer() {
const [state, dispatch] = useReducer(reducer, store);
return [state, dispatch];
}
export default useLocalReducer;