Hooks 各个击破
大纲链接 §
[toc]
React
内置的Hooks
主要有以下几个:
useState
组件状态useEffect
副作用useLayoutEffect
useContext
上下文useMemo
缓存useCallback
回调
useReducer
统一状态管理(替代Redux
)useRef
引用useImperativeHandle
useDebugValue
自定义
1. useState
⇧
使用组件状态
const [n, setN] = React.useState(0)
原始类型const [user, setUser] = React.useState({name: 'F'})
复杂类型
注意事项1:不可局部更新属性,不自动合并 ⇧
- 如果
state
是一个复杂类型(对象等),禁止部分setState
,示例代码- 因为
setState
不会自动合并属性,缺失没有写入的属性 useReducer
也不会自动合并
- 因为
- 解决方法:使用
...
,在对象中拷贝原状态的属性,再局部覆盖同名属性
|
|
注意事项2:地址要变 ⇧
setState(obj)
如果obj
不变,那么React
中数据不变React
只侦测对象地址变化,不侦测同对象的属性变化
|
|
注意事项3 接受函数 ⇧
useState
接受函数:const [x, setX] = useState(()=>{})
const [state, setState] = useState( () => {return initialValue} )
const [state, setState] = useState( () => ({/*initialValue*/}) )
- 该函数返回初始
state
,且只执行一次- 区别于
useState
传对象,JS
每次会解析该对象 useState
传工厂函数,减少内部多余的计算过程
- 区别于
setState
接受函数:setState(()=>{})
setN(i => i + 1)
- 代码示例
|
|
setN(i => i + 1)
并不直接传值,而是传一个函数来操作,形参占位符i
命名随便- 如果需要对
setN
多次操作,更推荐使用函数
2. useReducer
⇧
用来践行
Flux/Redux
的思想
- 代码示例,共分4步走
- 1.创建初始值
initialValue
- 2.创建所有操作
reducer(state, action)
- 3.传给
useReducer(所有操作, 初始数据)
,获取 读 和 写 的API
- 4.调用 写
dispatch({type: '操作类型'})
,不是直接操作state
,而是通过reducer
来操作
- 1.创建初始值
- 总的来说
useReducer
就是useState
的复杂版
|
|
- 不可以直接操作属性
state.n += 1; return state
同useState
的规则 - 对多个状态做整体操作
- 将所有操作聚拢在函数
const reducer = (state, action) => {}
中- 通过
action
的不同属性,来调用对应不同的分支操作 - 通过
action
还可以传额外属性
- 通过
- 每次用户操作更新数据,
App
就重新render
一遍
如何判断使用
useReducer
还是useState
- 事不过三原则:有多个数据可以在一起放入同一个对象里,就适用
useReducer
,对这个对象做整体操作 - 例如表单中的数据,见 用
useReducer
表单代码示例:
|
|
3. 用 useReducer
代替 redux
⇧
辅助函数和基本结构 ⇧
辅助函数,假的
AJAX
获取数据,调用时传入参数url
,返回对应数据
|
|
基本结构
|
|
复杂useReducer
使用步骤 ⇧
- 将数据几种在一个
store
对象中 - 将所有操作集中在
reducer
方法中 - 创建一个
Context
- 创建对数据的读写
API
- 将第四步的内容
{state, dispatch}
放到第三步的上下文数据提供组件中 - 用
Context.Provider
将Context
提供给所有子组件
<Context.Provider value={{state, dispatch}}>{/* 里面是各个子组件 */}</Context.Provider>
- 各个组件用
useContext
获取读写API
:const {state, dispatch} = useContext(Context)
(注意是获取对象使用花括号)
- 请求获取
mock
数据,ajax('/user').then(user => {dispatch({type: 'setUser', user})}
(ES6对象属性简写) - 每次刷新组件时,都会再去请求一次
AJAX
,为了防止每次渲染就请求一次,可以使用useEffect
只在进入页面时请求一次useEffect(() => {}, [])
,第二个参数传一个空数组,那么只会在第一次进入页面时运行,之后永远不会再执行(之后有更好的方式)
|
|
模块化处理 ⇧
目录
|
|
main.jsx
|
|
- 注意使用策略模式(表驱动)重构
reducer
代码。如下,之后可拆分为模块文件,分别导入,方便增加其他接口
|
|
|
|
use_reducer
|
|
books_reducer
|
|
movies_reducer
|
|
User.jsx
|
|
Books.jsx
|
|
Movies.jsx
|
|
Context.js
|
|
ajax.js
|
|
- 小结:使用
useReduce
和useContext/createContext
就可以代替Redux
- 但更复杂的场景,可使用
Redux-Saga
等 - 在自定义hooks中可实现完全代替
4. useContext
⇧
上下文
- 全局变量 是 全局的 上下文
- 上下文 是 局部的 全局变量
使用方法,可分为三步
- 使用
const Ctx = createContext(initialValue)
创建上下文- 初始值
initialValue
可以为null
- 初始值
- 使用
<Context.Provider></Context.Provider>
圈定作用域范围,在标签内部写子组件- 在此标签上添加需要传入的初始属性
value
,一般是一个包含读和写接口的对象 <Context.Provider value={{state, dispatch}}></Context.Provider>
- 也可声明中间变量
const contextValue = { state, dispatch }
直接传这个对象<Context.Provider value={contextValue}></Context.Provider>
- 在此标签上添加需要传入的初始属性
- 在作用域内,使用
useContext(Ctx)
,来使用上下文- 获取读和写的接口
const {state, dispatch} = useContext(Ctx)
- 获取读和写的接口
|
|
useContext
将数据传递给圈定的作用域内所有子孙代组件- 可以配合
useState
或者useReducer
来获取读写接口
使用useContext
注意事项 ⇧
区别于
Vue
,数据更新不是响应式的,而只是一个重新渲染的过程(从上而下逐级通知setN
重新渲染)
- 在一个模块中,将
Ctx
里的值改变(动态Context
) - 在另一个模块不会感知到这个变化
- 在某个子组件中触发
setN
变更数据,通知根组件<App />
变更,然后 自顶向下,逐级 通知使用到数据的组件去更新数据 - 数据流向始终是单向的
5. useEffect
& useLayoutEffect
⇧
useEffect
⇧
副作用(函数式概念)
- 对环境的改变即为 副作用,例如修改
document.title
- 但不一定非要把副作用放在
useEffect
里 - 实际上,称为
useAfterRender
更好,即每次render
后运行
用途
- 模拟组件生命周期的三个钩子函数
- 作为
componentDidMount
使用,将空数组[]
作为第二个参数 - 作为
componentDidUpdate
使用,第二个参数指定依赖[n]
放在数组中 - 作为
componentWillUnmount
使用,通过return () => {}
- 路由中常用,在此钩子中做清理工作,防止内存泄漏
- 对回京做的任何变动,组件卸载时都需要清理
- 作为
- 第二个参数 和 返回的回调 决定了回调函数什么时候再次执行
- 以上三种用途可同时存在
特点
- 如果同时存在多个
useEffect
,会按照 出现次序 执行
|
|
useLayoutEffect
⇧
useEffect
在浏览器渲染完成后执行,代码示例
|
|
- 页面渲染时有一个闪烁,
value
从0
迅速变为1000
useLayoutEffect
称为 “布局副作用”, 在浏览器渲染前执行
|
|
- 页面没有出现闪烁问题
- 有很多
DOM
操作,浏览器会等DOM
操作完了再渲染useEffect
在浏览器渲染改变屏幕像素之后useLayoutEffect
在DOM
操作完了,紧接着执行回调(对Layout
有影响的操作),然后再去渲染(改变屏幕像素之前)
useEffect
V.S. useLayoutEffect
⇧
- 通过时间点来侧面证明,代码示例
|
|
- 全局API
performance.now()
可以记录当前时间 - 从第二次点击开始计算
useEffect
总是比useLayoutEffect
慢,是因为useEffect
在改变外观之后执行,而useLayoutEffect
在改变外观之前执行
特点
useLayoutEffect
总是比useEffect
先执行,代码示例useLayoutEffect
里的回调最好时改变Layout
,否则没有意义
经验
- 虽然可以避免屏幕闪烁,但大部分时候不会去改变
DOM
,所以并不是总是使用useLayoutEffect
更好 - 为了用户体验,优先使用
useEffect
(优先渲染)useLayoutEffect
操作的优先级,但会影响(延长)用户看到画面变化的时间
6. useMemo
⇧
使用useMemo
前,先需要理解 React.memo
⇧
React
默认总是有多余的render
,即使渲染的数据没有发生改变,代码示例
|
|
Child
只依赖了{m}
,而当数据{n}
改变了,重新渲染App
,连带着也会重新渲染Child
,即使{m}
未发生改变- 如果
props
不变,就没有必要再次执行一个函数组件 - 代码中的
Child
用React.memo(Child)
代替,封装一层
变更后的效果,见示例代码
|
|
React.memo(Child)
使得一个组件只有在依赖(例如props
)变化时,才会再次执行渲染
简化代码
|
|
React.memo
的bug
⇧
|
|
- 在监听了函数之后,一秒破功,
Child2
居然又执行了 - 因为
App
每次重新运行时,都会在执行const onClickChild = () => {}
处,生成新的函数 - 新旧函数虽然功能一样,但是内存地址不一样
配合使用useMemo
解决React.memo
的问题
useMemo
可以实现函数的缓存,比对函数内容,相同的话使用之前的缓存的函数useMemo
的返回值就是需要缓存的函数- 使用
useMemo
来返回出需要传入子组件的函数,并给定依赖作为第二个参数
|
|
useMemo
特点
- 第一个参数是
() => value
,见源码useMemo
的TS类型声明 - 第二个参数是依赖
[m, n]
- 只有当依赖变化时,才会计算出新的
value
- 如果依赖不变,那么就重用之前的
value
- 这与
Vue
的computed
相类似,用来缓存依赖一些不变化的数据
useMemo
注意事项
- 如果
value
传的是函数,那么就要写成useMemo(() => (x) => {})
,这是一个返回函数的函数(高阶函数/组件)
形式上复杂难用,于是就有了
useCallback
useCallback
是useMemo
的语法糖
用法
- 使用
useCallback(x => consolog(x), [m])
- 等价于
useMemo(() => x => console.log(x), [m])
7. useRef
⇧
useRef
用途: ⇧
- 如果需要一个值,在组件不断
render
时保持不变,可以使用useRef
- 初始化:
const count = useRef(0)
- 读取值:
count.current
- 初始化:
- 区别于
setN
,n
每次渲染都会变更
需求:要知道组件渲染了几次,使用一个变量记录(全局变量会污染命名环境)
- 使用
useRef
,每次渲染时更新useEffect(() => {count.current += 1})
,得到一个新的count
,而count.value
在内存中的引用不变 - 渲染
count
对象地址不变,其中的current
值被记录在useRef
对应的一个fiberNode
上,示例代码
|
|
useRef
读取值为什么需要.current
? ⇧
- 为了保持新旧组件更新前后两次
useRef
是同一个引用对象(只有引用能做到,保持引用地址不变)
对比三个数据变化的钩子函数
useState/useReducer
:n
每次都变useMemo/useCallback
:在依赖[n]
变更时,对应的函数fn
改变,有条件地变useRef
:n
永远不变
延伸
Vue3
的ref()
- 初始化:
const count = ref(0)
- 读取值:
count.value
- 初始化:
- 不同点:
- 当
count.value
变化时,Vue3
会自动render
,帮你实现了UI
更新 - 而
React
中使用useRef
不会自动更新UI
,代码示例
- 当
|
|
useRef
不能做到变化时自动render
刷新UI
,需要额外步骤 ⇧
- 因为不符合
React
的函数式理念:UI = render(data)
- 每次
data
变化时,使用函数(例如setState
)去渲染成新的虚拟DOM
- 将新的虚拟
DOM
与页面上旧的虚拟DOM
做DOM Diff
,得到一个patch
补丁 - 根据这个
patch
补丁去重渲染DOM
- 每次
- 如果想要变化时自动
render
,可以自定义去实现(使用setN
),React
不管 - 监听
ref
,当ref.current
变化时,再加一个依赖,调用setX
即可,示例代码
|
|
8. useRef
配合forwardRef
用在函数组件中 ⇧
基本用法:让函数组件
<Button2 />
支持ref
,获取其虚拟DOM
的引用
- 代替原生使用
id
,和document.getElementById
来获取元素
函数组件中会警告无法直接使用
useRef
代码示例1:props
无法传递ref
属性
|
|
- 报警告
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
useRef
作为元素的引用是在类组件中使用的,即只有类组件可以直接接受ref
元素引用- 提示函数组件去使用
forwardRef
- 组件内打印
console.log("props", props);
得到object {children: "按钮"}
,只把按钮传过去了,其他属性都忽略了
代码示例2:函数组件中使用
React.forwardRef
包裹一层组件,可声明第二个形参ref
,实现函数组件ref
的组件间传递
|
|
- 函数组件需要接受外部传来的
ref
参数,必须用React.forwardRef
包起来<Button ref={buttonRef}>...</Button>
const Button = React.forwardRef((props, ref) => (<button ref={ref} {...props} />))
代码示例3:两次
ref
传递得到button
的引用,实现可移动的按钮(高阶组件封装)(仍有bug,待修复)
- 功能包装:
const MovableButton = movable(Button2)
- 通过
ref
引用到Button2
里面的按钮:const buttonRef = useRef(null)
,需要做两次传递- 第一次 将
<MovableButton name="email" ref={buttonRef}>
传给movable(Component)
中返回的<Component ref={ref} {...props} />
- 即
movable(Button2)
中返回的<Button2 ref={ref} {...props} />
- 即
- 第二次
Button2
组件声明时传递React.forwardRef((props, ref) => { return <button ref={ref} {...props} />; });
- 第一次 将
|
|
useRef
V.S.forwardRef
- 由于大部分时候不需要,所以
props
不包含ref
- 由于函数组件的参数
props
不包含ref
,所以需要React.forwardRef
包裹一层,才可以传递ref
React.forwardRef
就是在函数组件中用来转发ref
的
9. useImperativeHandle
⇧
Imperative
重要的- 命名太长:其实意思就是
setRef
:用来变更ref
- 作用是自定义
ref
的属性
不用
useImperativeHandle
的 代码示例
|
|
- 如果希望得到的
ref
是一个对button
的自定义封装对象,则需要使用useImperativeHandle
使用
useImperativeHandle
的 代码示例
|
|
10. 自定义 Hook
⇧
- 使用自定义
Hook
来封装数据操作
简单代码示例
- 直接获取处理过的数据
const { list /*, setList */ } = useList();
- 处理数据的逻辑封装在自定义
Hook
中,只暴露读和写的接口 - 初始化状态
const [list, setList] = useState(null)
DidMount
时 请求数据- 返回出读和写的接口
- 处理相关的逻辑都写在一个钩子函数中,运行时暴露出读和写的接口
|
|
useList.js
|
|
功能更全的代码示例
- 不仅提供了读和写的借口,增删改查等其他功能
|
|
useList.js
|
|
分析
- 还可以在自定义
Hook
里使用Context
useState
只说了不能在if
里,没说不能在函数里运行- 只要这个函数在函数组件里运行即可
- 实际业务中,使用良好的组件封装,更抽象的自定义
Hook
封装,来代替页面组件中直接使用useState
、uesEffect
等钩子 - 使用函数组件,就没有必要使用
Redux
了
11. Stale Closure
过时的闭包 ⇧
- 期待打印自增的
count
- 结果每次打印都是相同的
count
- 打印的
count
是过时的闭包,而不是后续操作产生的新的count
|
|
- 把依赖
[count]
放入useEffect
,更新DidUpdate
时同时更新闭包 - 同时卸载
WillUnmount
时(return () => {}
) 清除上一次组件卸载时的id
,来干掉旧的闭包 - 代码中的两处
id
调用时机不同
|
|
- 避免保留下过时的闭包,每次更新前需要重新获取新的闭包
- 或者在返回的函数中 读取最新的闭包,而不是在返回的函数外
题外话
JS
中有许多需要避免的bug
或者反模式- 隐式类型转换,用
===
- 过时的闭包
- 空值判断,用
??
- 异步
- 过长的条件分支,用策略模式(表驱动)
- 隐式类型转换,用
12. 总结 ⇧
|
|
参考文章 ⇧
相关文章 ⇧
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名