React Hooks 开发实践
写在前面
忘掉react生命周期,方能成就hooks精深功力!!!
忘记了就学会了 ---- 张三丰
为什么会出现hooks
在hooks出现之前,原来的 class 组件,还能正常的使用,并且很稳定。所以,为什么react要发明hooks?解决了什么问题?
react 组件的本质
react组件的职责其实非常直观 就是 model(state、props) 到 view 的映射。 所以 UI 的展示,其实可以表示成 fn(model) => view 而react需要保证的,就是每当数据发生变化时,函数重新执行,生成新的DOM,并更新到浏览器。 所以,貌似函数组件更适合这样的描述。
react的组件并不会相互继承,扩展,我们也不会去调用class的静态方法,所以也无法用到class的重要特性,这样使用class,看起来是比较牵强的。
hooks的出现
react在之前一直也是有函数组建的,只是函数是无法保存状态的,所以更多的是用作展示组件。 所以,我们只需要提供一种方式,让函数在多次执行间,能保持一个相同的状态。
react是怎么做的,它提供了一种机制,能够把一个外部的数据绑定到函数的执行。并且当数据变化时,自动重新执行函数,任何会影响UI的外部数据都可以通过这么一个机制绑定到函数组件。 这个机制就叫做 hooks
hooks解决的问题
让函数组件这种更适合描述 state => view 的组件发挥作用。
1. 逻辑复用
- hooks的作用
hooks把某个目标结果钩到某个可能会变化的数据源或者事件源上。那么当被钩到的数据或者事件源产生变化时,产生目标结果的代码重新执行,生成新的结果。
这个被钩的对象,不仅可以是某个独立的数据,也可以是另外的hooks执行产生的结果。这样我们就可以利用这个特性完成 逻辑复用
在以前使用react的时候,最复杂的其实就是逻辑复用,可以通过 高阶组件 等方式去实现,但是明显没有hooks的方式更加灵活方便。
2. 关注分离
因为第二点提到的特性,我们可以很轻松的把业务逻辑隔离开,让代码的逻辑更加容易理解和维护。甚至可以把处理逻辑和UI代码完全分成两个文件去开发。
hooks思想(注意点)
每一次渲染(执行)拥有独立的变量
function App() {
const [number, setNum] = useState(0)
const timer = useRef(null)
useEffect(() => {
timer.current = setInterval(() => setNum(n => n + 1), 1000)
return () => clearInterval(timer.current)
}, [])
return (<Comp number={number} />)
}
function Comp(props) {
const { number } = props
const handleClick = () => {
setTimeout(() => console.log(number), 2000)
}
return (
<>
<div>{number}</div>
<button onClick={handleClick}>点击</button>
</>
)
}
当number为1时,点击按钮,会输出多少?为什么?
每一次渲染(执行)拥有独立的状态
function Comp() {
const [number, setNum] = useState(0)
const handleClick = () => {
setTimeout(() => setNum(number + 1), 2000)
}
return (
<>
<p>{number}</p>
<button onClick={() => setNum(number + 1)}>点击</button>
<button onClick={handleClick}>延迟</button>
</>
)
}
先点击延迟,再点击普通按钮,会出现什么情况?为什么?
合并更新
function Child({onChange}) {
const handleClick = async () => {
const res = await Promise.resolve([1,2,3,4]) // Promise.all执行的返回结果
if (Array.isArray(res)) {
res.forEach((item) => onChange(item))
} else {
onChange(item)
}
}
return <button onClick={handleClick}>点击</button>
}
function Comp() {
const [value, setVal] = useState([])
const handleChange = (val) => setVal(value.concat(val))
return (<Child onChange={handleChange} />)
}
在 Comp 中 value 会是什么?为什么?
事实上,组件内的每一个函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获定义它们的那次渲染中的props和state。所以,函数组件每一次执行,拥有独立的所有...
hooks实践
useState
-
作用: 让函数组件在多次渲染之间,共享state,具有维持状态的能力。
-
注意一条原则: 永远不要在state中保存可以通过计算得到的值,如 props、 cookie、 storage、 URL中得到的值
-
state聚合,若注册了多个state A B C,当 A 变化时 BC 必然(经常)会变化,应考虑将A B C作为一个 Object 的state
-
惰性初始值,复杂的值初始化
const [value, setVal] = useState(() => {
const initialVal = complicated() // 某种复杂的初始化值操作
return initailVal
})
useEffect
执行副作用: 执行一段和当前执行结果无关的代码,useEffect的执行是不影响当前渲染UI的
执行时机: 在每次组件render完成后判断依赖,再是否执行callback
依赖项解释:略
返回函数解释:略
- tips
值得注意的是,useEffect所需要的回调函数,是不允许async关键字的。因为react在 mountEffect 之后,将effect挂载到fiber的 effect链表 上,而在卸载的时候,会遍历这个链表,并去执行callback返回的destroy函数,而async关键字会返回一个 Promise 对象,是不能被执行的。
但是我们经常需要在useEffect中发起异步请求,如果都用promise的链式写法,难受。但是我们可以传入一个立即执行函数,让立即执行函数去处理异步的事情
useEffect(() => {
(
async () => {
...
}
)()
},[])
缓存
众所周知,函数组件,会在每一次渲染都重新执行一遍,其内部所声明的函数、变量等,也都会重新声明一遍。而这个机制的存在,也容易引起一些问题:
- 这些变量或者函数,作为依赖项被useEffect使用,会引起useEffect多次无谓的执行,因为依赖项变化了。
- 作为子组件的props,react检测到props变化,会重新渲染子组件,这样会引起不必要的渲染。
react当然也提供了方式,来解决这种问题: useCallback useMemo
- useCallback
根据依赖项决定是否返回一个新的函数。
直接声明一个函数:
function Comp() {
const [number, setNum] = useState(0)
// 。。。。其他state
const handleClick = () => setNum(number + 1)
// ... 其他function
return <Button onClick={handleClick}>点击</Button>
}
handleClick这个函数包含了number的闭包,以保证每一次执行都能够得到正确结果。可以看到影响handleClick的其实只有number,但是当Comp重新执行时,handleClick必然会重新声明,这样虽然不会影响结果正确性, 却会增加更多的开销,最重要的是会引起 Button 组件的 重新渲染。
useCallback可以做到根据依赖项来决定是否返回一个新的函数,在这里就只需要根据number是否变化,来决定是否生成新的handleClick
const handleClick = useCallback(() => setNum(number + 1), [number])
- useMemo
useCallback主要用于缓存函数,而useMemo则主要用于缓存计算值。
如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算
function Comp() {
const [number, setNum] = useState(0)
// 。。。。其他state
const calcNum = complicated(number) // 复杂的计算
return <Child calcNum={calcNum}>点击</Child>
}
如果像以上所示,每一次Comp组件由于其他state改变而重新渲染时,这个 calcNum 都会重新计算一次,从而增加不比要的计算量。 优化则只需要用useMemo包裹一下就好:
const calcNum = useMemo(complicated(number), [number])
其实根据这个特性,useCallback也可以用useMemo来实现
const fn = useMemo(() => {
const fn = () => {
...
}
return fn
}, [dep])
- React.memo
使用memo包裹子组件,和useMemo useCallback配合可以有效的避免子组件的无谓重复渲染。
const Child = () => {
// 复杂的组件
return (
<div>
。。。
</div>
)
}
function Comp() {
const [numebr, setNum] = useState(0)
return (
<>
<Child />
<Input value={number} onChange={(val) => setNum(val)} />
</>
)
}
此时,每一次输入事件都会引起Comp组件的重新渲染,但是Child组件与number的变化无关,也重新渲染了,并且由于其计算量大,还有可能造成卡顿,这是非常不好的。
- 使用memo来优化
const Child = React.memo(() => {
// 复杂的组件
return (
<div>
。。。
</div>
)
})
- 使用useMemo
在某些场景下,为了避免组件被过多的拆分,其实也可以使用useMemo来缓存组件,使得逻辑更加易读
function Comp() {
const [numebr, setNum] = useState(0)
const child = useMemo(() => (
// 复杂的组件
<div>
。。。
</div>
), [])
return (
<>
{child}
<Input value={number} onChange={(val) => setNum(val)} />
</>
)
}
- 避免滥用缓存
为了避免子组件重复渲染,为了避免useEffect多次触发,我们经常会写很多useCallback useMemo,但是到处都有这些东西,其实是很笨拙的事情。
由于每一次组件重新执行,都会对依赖项进行比对,这其实也是一部分花销,并且它们还会使用另外的空间去缓存函数,计算结果。如果过分应用useCallback useMemo ,可能会造成不必要的开销和性能问题。
- 值并不会经常改变,如果缓存的依赖项为空,说明这个值在重新render时,并不会改变,只是做一个存储使用
const obj = useMemo(() => {...}, [])
// useRef
const {current: obj} = useRef({...})
- 依赖项会经常改变,导致useMemo 或者 useCallback 的开销是不必要的,因为依赖项变了,必然会新生成值,此时就应考虑不使用缓存,而是直接新创建。
function Comp() {
const [number, setNum] = useState(0)
const handleClick = useCallback(() => setNum(number + 1), [number])
return <Button onClick={handleClick}>点击</Button>
}
共享变量
在react中,也避免不了需要处理dom的情况,如input的focus事件,但是input这个DOM对象,必须要在DOM真正渲染之后才能获取,属于未来渲染中的变量。 对于useState来说,它所获取的是当前渲染的常量。 此时 useRef 是最方便的获取方式。
useRef 通过另一个空间,保存一个对象,在ref.current中去保存变量。因为是对象,它的值随时可以变更,而不会引起组件重新渲染。同样也可以在任意次渲染中获取ref中保存的变量。
const inpRef = useRef<any>(null);
inpRef.current.focus()
<input ref={inpRef} />
- 避免滥用refs
当 useEffect 的依赖频繁变化,有人会想到把频繁变化的值用 ref 保存起来,我在项目中看到了这样的代码。然而,useReducer 可能是更好的解决方式:使用 dispatch 消除对一些状态的依赖
- useEffect 对于函数依赖,尝试将该函数放置在 effect 内,或者使用 useCallback 包裹;
- useEffect/useCallback/useMemo,对于 state 或者其他属性的依赖,根据 eslint 的提示填入 deps;
- 如果不直接使用 state,只是想修改 state,用 setState 的函数入参方式(setState(c => c + 1))代替;
- 如果修改 state 的过程依赖了其他属性,尝试将 state 和属性聚合,改写成 useReducer 的形式。当这些方法都不奏效,使用 ref,但是依然要谨慎操作。