React的Hook规则其实很简单的,只要保证use打头即可。但是如何理解它的挂载,闭包行为, 异步带来的问题,以及useRef容易滥用,手动管理依赖等等,还真的是有点费力,一般人真架不住它的水这么深!
举个例子,你很高兴的以为使用useCallback,或useMemo缓存了某个值或函数,然后传递给子组件的props中去,就以为避免了一部分子组件的更新,但可能子组件还需要用 memo(子组件) 这么包裹一下。于是你就memo(子组件)一下,那么现在子组件一定会少了更新吗? 文档上又写了
一句,见下图,这不让人为难了吗? 到底避免不避免子组件更新,还是依赖代码测试的实际结果,否则你任何优化可能只是一厢情愿,甚至有反作用!
继续,当你的useCallback 中,有一个异步请求,请求5秒后完成,再setXXX, 或dispatch更新组件,那么这5秒的时间中,组件可能已经reRender多次了,根据react的函数性,useCallback的函数操作的state, 已经不是当前页面的state了!我刚刚代码测试了!
我说了2遍代码测试,是的,即使我这么自信的老鸟,仅通过文档无法确切Hook的具体行为的,在用Hook时,颤颤巍巍,处处 log,如履薄冰。一方面怕被人说写法不地道,为什么不优化,一方面又怕自己玩着花样写,自己给自己埋雷,无法控制它的行为!
虽说如此,我还是写了一些Hook的,简单记录一下这些Hook:
一、生命周期Hook
虽然useEffect万能的可以模拟很多生命周期,但我无法忍受代码中有太多的useEffect,我还必须找它的参数,才能知道它的行为,这不很怪异吗?我参考网上代码,实现了create,mount,update,unMount的生命周期钩子,以及相关概念的验证,不多解释了,直接看代码上的注释吧!
尤其注意的时,mount事件和第一次update事件,由于都是使用useEffect来模拟的,所以它们出现的时机依赖于你在组件内调用的顺序!父子组件的生命周期的执行顺序是最有意义的事情,希望每个人都要深刻理解。
import { useEffect, useRef } from "react";
// useRef, 保证created早于 mounted 因为mounted使用useEffect,所以它是在update之后的事件
export const useCreated = (fn) => {
const init = useRef(true)
init.current && (fn() || (init.current = false))
}
export const useMount = (fn) => useEffect(fn, [])
export const useUnMount = (fn) => useEffect(() => fn, [])
export const useUpdate = (fn) => useEffect(fn)
/** 用下面两个组件,测试父子组件的生命周期.
* 父组件<Todo>
* 子组件<Foo>
*
* 加载时:父--子--父。
* todo created
foo created
foo mounted
foo Update 由于 使用useEffect,这是加载即第一次
todo mounted
todo Update 由于 使用useEffect,这是加载即第一次
*
更新时:先子后父
* foo Update
todo Update
卸载时: 先父后子
todo UnMount
foo UnMount
*/
二、useMediaQuery
由于最近的任务是自适应页面,看到了其它库有这个函数,比如ahook, @chakra-ui 。ahook库中,名字叫useResponsive,底层使用window.resize来实现的,滑天下之大稽。 公司项目是使用@chakra-ui框架,它的useMeidaQuery用的是标准window.matchMedia,但它的使用需要手写media query表达式,所以我写了一个更简单的Hook, 直接传入指定的断点,返回相应的变量状态即可!
// 第一个参数是断点, 3个断点则有4个区间
// 第二个参数是:onChange函数。 每次区间跳动时,触发一次
// 组件内 使用方法如下:
let matches = useMediaQuery([500, 1000, 1500], () => {
console.log("change media query:", matches)
})
// JSX绑定值:
<div>500以内: {matches[0]?"是":"否"}</div>
<div>1000以内: {matches[1]?"是":"否"}</div>
<div>1500以内: {matches[2]?"是":"否"}</div>
<div>1500以上: {matches[3]?"是":"否"}</div>
效果是:
useMediaQuery 的源码如下:
/**
* 输入一组有序断点,返回一组状态值
* @example 3个断点 生成4个区域
* let matches = useMediaQuery([500, 1000, 1500], () => {
console.log("change media query:", matches)
})
* @param {Array<Number>} breakpoints 数字数组, 由小到大。
* @param {Function} onChange onChange回调。
*/
export default function useMediaQuery(breakpoints, onChange) {
let [matches, setMatches] = useState([])
useEffect(() => {
// 生成 query 表达式
let start = 1, querys = []
breakpoints.forEach(bp => {
querys.push(`(min-width:${start}px) and (max-width:${bp-1}px)`);
start = bp;
})
querys.push(`(min-width:${start}px)`)
let mqlList = querys.map(q => window.matchMedia(q));
//添加所有监听, 通过Idx追踪位置
mqlList.forEach((mql, idx) => {
matches[idx] = mql.matches
mql.addEventListener("change", mql.fn = function (ev) {
matches[idx] = ev.matches
if (ev.matches) {
setTimeout(() => {
setMatches([...matches])
onChange()
}, 0);
}
})
})
setMatches([...matches]) //更新一下
return () => {
mqlList.map(mql => mql.removeEventListener("change", mql.fn))
mqlList = null
}
}, [])
return matches
}
最后,在使用useMediaQuery时,很多人是用它配合 CSS IN JS 这样的框架,去定义不同宽度时的样式的。 个人以为这是不合理的用法,因为每次窗口变化,引起状态变化,必然带来一次reRender,带来运行时的消耗。如果只是控制样式,还是要直接写到css 文件中去,才是性能最高的,浏览器一次加载css,执行css的行为。 只有当需求是:不同宽度时,应用的逻辑不一样了,比如大窗口弹窗,小窗口toast一下, 这时才有必要使用useMediaQuery 这样的 Hook。 总之不要以为用上Hook就比用CSS牛逼这样的想法。
在编写动画上也是这样,能写css动画,理论上就要比js动画性能更好,其道理大致一样!