useCreation-Hooks-React
1. useCreation
解决了什么问题? #
-
简单看一下它的用法
const [name, setName] = useState('xiao Hong') // useMemo const firstChar = useMemo(() => { return { first: name.charAt(0) } }, [name]) // useCreation const firstChar = useCreation(() => { return { first: name.charAt(0) } }, [name])
const [name, setName] = useState('xiao Hong') // useMemo const firstChar = useMemo(() => { return { first: name.charAt(0) } }, [name]) // useCreation const firstChar = useCreation(() => { return { first: name.charAt(0) } }, [name])
-
可以看到和
useMemo
完全一致,但是useCreation
内部使用了useRef
来维护工厂函数产生的变量,保证了缓存变量不会被意外回收。它可以看成是useMemo
的优化版本,因为官方表示,useMemo 持有的变量并不能保证一定不会被回收,具体可以看 ahooks官方文档 或 react官方文档
2. 如何实现? #
-
首先考虑在 react 中有什么办法可以持久化一个变量,而且不用担心意外的变量回收?答案是,
useRef
,它本身的逻辑就是分配一个脱离 react 声明周期的对象,并在 mount 时初始化一次,之后通过 current 指针来修改或使用。所以useCreation
的核心依赖就是useRef
function useCreation(factory, deps) { // 在这里构建一个基础对象,来维护初始化参数 const { current } = useRef({ initialized: false, data: undefined, deps }) // 有两种情况会导致重新进行初始化赋值 // 1. 如果没有初始化,那么就初始化一次,并且修改初始化标量为 true // 2. 对新旧 deps 比较,如果 deps 发生变化,如新初始化一次 if (!current.initialized || !depsAreSame(current.deps, deps)) { current.data = factory(); current.initialized = true; current.deps = deps; } return current.data } // 这里的对比方法其实还有很多不同实现,这里以官方方法为准 function depsAreSame(oldDeps, deps) { if (oldDeps === deps) return true; for (let i = 0; i < oldDeps.length; i++) { // 和 ===,== 的行为不同,具体看这里 // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is if (!Object.is(oldDeps[i], deps[i])) return false; } return true; }
function useCreation(factory, deps) { // 在这里构建一个基础对象,来维护初始化参数 const { current } = useRef({ initialized: false, data: undefined, deps }) // 有两种情况会导致重新进行初始化赋值 // 1. 如果没有初始化,那么就初始化一次,并且修改初始化标量为 true // 2. 对新旧 deps 比较,如果 deps 发生变化,如新初始化一次 if (!current.initialized || !depsAreSame(current.deps, deps)) { current.data = factory(); current.initialized = true; current.deps = deps; } return current.data } // 这里的对比方法其实还有很多不同实现,这里以官方方法为准 function depsAreSame(oldDeps, deps) { if (oldDeps === deps) return true; for (let i = 0; i < oldDeps.length; i++) { // 和 ===,== 的行为不同,具体看这里 // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is if (!Object.is(oldDeps[i], deps[i])) return false; } return true; }
3. 一些题外话 #
-
个人认为,在大多数情况下,使用优化手段前(如
useMemo
,memo
等方法),应该充分考虑是否真的需要进行优化。往往一次缓存优化的性能代价比重新计算或渲染一次,更耗时。 -
如 memo 的对比函数
memo((prev, next) => true/false)
中,如果对比计算比较复杂,可能计算逻辑的耗时比组件的一次 re-render 更高 -
又比如 useMemo 中,我们也要搞清楚我们优化的目的是什么?是为了避免一次耗时计算,还是仅仅为了保持变量引用不变。如果是后者,完全可以用
useRef
去代替。const memo_0 = useMemo(() => { const data = someExpensiveCalc() return data }, []) // 为了保持引用一致而使用 useMemo const memo_1 = useMemo(() => referenceObj, []) // 代替方案 const memo_2 = useRef(referenceObj)
const memo_0 = useMemo(() => { const data = someExpensiveCalc() return data }, []) // 为了保持引用一致而使用 useMemo const memo_1 = useMemo(() => referenceObj, []) // 代替方案 const memo_2 = useRef(referenceObj)
-
但是这里有个问题,如果上面的
referenceObj
是这种形式:useRef(new Xxx())
。 那么,每次 re-render 都会实例化一个 Xxx 的新对象,虽然这个新对象不会被更新到 useRef.current 上,但构造函数每次都被重复的执行了。这个问题该如何解决呢?如何更精确的控制 Xxx 的执行时机? -
useCreation
通过构建工厂方法,实现了 延迟执行 的特性(在必要时执行工厂方法,构造对象) -
其实可以看一下
useRef
的源码,会对它所做的工作有个更清晰的认识,代码量很小function mountRef(initialValue) { var hook = mountWorkInProgressHook(); // 很简单的初始化操作,构建一个 { current } 对象 const ref = { current: initialValue } hook.memoizedState = ref return ref } // 可以看到 useRef 的更新过程中,参数 initialValue 是没有被用到的 function updateRef(initialValue) { var hook = updateWorkInProgressHook(); return hook.memoizedState }
function mountRef(initialValue) { var hook = mountWorkInProgressHook(); // 很简单的初始化操作,构建一个 { current } 对象 const ref = { current: initialValue } hook.memoizedState = ref return ref } // 可以看到 useRef 的更新过程中,参数 initialValue 是没有被用到的 function updateRef(initialValue) { var hook = updateWorkInProgressHook(); return hook.memoizedState }
所以,initialValue 只在
useRef
初始化时生效,并且可以随时通过 ref.current 来修改 -
useMemo
// 很清晰的初始化逻辑,创建 hook,创建初始值,挂在初始值,返回初始值 function mountMemo(nextCreate, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; var nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } function updateMemo(nextCreate, deps) { var hook = updateWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; var prevState = hook.memoizedState; // 存在缓存对象,并且前后 deps 相同时,使用旧对象 if (prevState !== null && nextDeps !== null) { var prevDeps = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } // 否则通过 factory 函数(这里的 nextCreate)产生新的对象,并且和 nextDeps 一起,构造新的 memoizedState var nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
// 很清晰的初始化逻辑,创建 hook,创建初始值,挂在初始值,返回初始值 function mountMemo(nextCreate, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; var nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } function updateMemo(nextCreate, deps) { var hook = updateWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; var prevState = hook.memoizedState; // 存在缓存对象,并且前后 deps 相同时,使用旧对象 if (prevState !== null && nextDeps !== null) { var prevDeps = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } // 否则通过 factory 函数(这里的 nextCreate)产生新的对象,并且和 nextDeps 一起,构造新的 memoizedState var nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
-
在了解了两个
hook
的代码逻辑后,就可以比较轻松的组合出自己的useCreation
了。
版权属于: vincent
转载时须注明出处及本声明