共计 6452 个字符,预计需要花费 17 分钟才能阅读完成。
今年五月份就开始接触 react hook,六月份也在组内分享过一次,但由于太忙所以现在才抽出时间写这篇关于 hook 的学习手册。
前沿:
react hook 相信对于前端开发者来说也不会陌生,就是基于 react16.8 之后推出的解决函数式组件没办法处理内部状态持久化的一种解决方式,它能大大的提高了函数式组件的在 react 之中的地位。本文主要介绍 react hook 钩子的一些使用方式,至于原理性的东西后续补上。
背景:
在介绍 react hook 之前,还是先介绍一下为啥会出现 react hook,使用它之后的函数式组件与类组件之间相比存在哪些优点。
- class 组件存在的问题:
1)多个类组件之间的逻辑较难复用:
如果多个组件之间存在相同的逻辑或者相类似的逻辑,一般会采取两种方式来进行复用,一种是通过高阶组件 HOC 来处理,另一种则是采用了 render prop 的方式。
例如一个场景为 A 组件是点击按钮,然后出现一个弹窗;B 组件是文本,鼠标移入后出现一个弹窗;
从上面可以看出 handle 要被复用,还要再绕一圈通过 HOC,而且还要装饰器,才能复用 handle,代码的复杂度上升且代码量也多了。
2)复杂组件很难理解:
很多时候类组件一开始比较简单,而几个月后组件 ’ 变胖 ’ 了,render 主体越来越多,绑定和调用的方法也越来越多,越来越多生命周期被调用而且生命周期内部的逻辑越来越复杂,有时候如监听函数的绑定和解除需要拆分成两部分放在两个生命周期等,原来组件才一百多行代码,一下子变成上千行代码。
3)class 组件的生命周期使用时的坑:
相信很多开发者在使用类组件的时候,会经常在调用生命周期时踩过不少的坑,这里我大概总结了一下生命周期的坑:
- class 组件和函数式组件的对比:
如表中所示,因为函数式组件功能单一,以至于在 hooks 出来之前,一般不常用函数式组件。
- hooks 钩子能解决什么:
先对比一下类组件和运用了 react hook 的函数式组件,从图中可以清晰的看到类组件该有的东西,函数式组件运用了钩子之后才拥有了。
再用一个例子简单的对比一下,表格中列举了两个组件,然后分别用普通方式,HOC 方式,react hook 方式,可以看出代码的可读性,简洁性以及复用性钩子会更优异一点。
所以简单的总结了 react hook 的几个适用场景:
1)让对于复杂庞大的组件,可以拆分成颗粒度更小的函数式组件,每个小组件各自控制自身的状态;
2)将一段业务代码封装成复用,提供给每个组件;
常用 api:
前面阐述了这么多,主要时让大家知道 react hook 的存在意义以及和类组件之间的对比,接下来要重点介绍一下 react hook 常用的几个钩子。
- useState:
作用:可以让函数式组件中加入简单的状态管理,而且可以使用多次 useState,但每个状态之间是相互独立,即更新一个状态,另一个状态不会随着也更新,但要注意只能在函数式组件中使用。
语法:
[状态,状态更新函数] = useState(状态默认值)
这里的状态和状态更新函数没有具体的变量声明,所以可以采用各种各样的声明变量。
注意点:
1)每更新一次状态,函数式组件就会重新在渲染一次;
2)利用状态改变函数改变了状态后,该状态不会立即同步,还是原来的值,只有 rerender 时,该状态才会最新;
3)状态改变函数是直接将新的值代替旧的值,而不像 setState 那样能局部更新,然后将局部更新的状态合并到整个状态之中;
const [apple, setApple] = useState({a: {a1: 1}, b: 2});
setApple({a: {a1: 3}}) // 此时 apple 的值是 {a1: 3},而不会是 {a: {a1: 3}, b: 2}
state = {a: {a1: 1}, b: 2};
setState({a: {a1: 3}});
{a: {a1: 3}, b: 2}
useState 的 demo:
function FunCom(props) {const [apple, setApple] = useState('apple');
useEffect(() => { document.title = `${apple}`; });
return (
<div>
<p onClick={(e) => {setApple('banana'); console.log(apple); }}>
函数式组件 -{apple}
</p>
</div>
)
}
- useEffect:
对于 react 来说,当渲染后(不管是首次渲染还是重复渲染),还会存在其他一些操作:数据获取,操作 dom 节点,设置订阅或发布等操作;
1) 类组件:一般该副作用会放置在 componentDidMount,componentDidUpdate 或者 componentWillUnMount;
2) 函数式组件:一般副作用放置在钩子里面执行;
语法:useEffect(callback);
其中:
1) callback 是一个回调函数,它里面可以访问到最新的状态,但不能直接调用同步方式下的更新状态的函数;
2) useEffect 函数在首次和之后的每次渲染后,销毁前都会调用(理解为 componentDidMount,componentDidUpdate 或者 componentWillUnMount 时刻下的调用)。
3) 该钩子是异步触发;
effect 调用过程:
性能优化:useEffect(callback, [callback 内部的状态或者属性])
注意:
- 如果是 [],则该钩子只会在 mount 和 unmount 发生反应,在 update 的时候不会发生反应;
- 如果依赖项值不变,useEffect 返回的清除函数就不会再下一次渲染中被执行,只有当依赖项的值变化,在渲染的时候,清除函数才会执行然后再执行 useEffect;
- useContext:
语法:const value = React.useContext(Context);
其中 Context 是指 React.createContext 对象,value 是指该对象所传递下去给后代组件的值;
作用:不管函数值组件是不是 Context.Provider 组件的子孙组件,都可以获取 Context 组件将要透传下去的值;
注意点:
- 该组件可以放在 Context.Provider 里面,也可以放在外面,所以与 Context.Provider 的关系无关;
- 如果 Provider 组件的值改变,该钩子也会让该函数式组件重新渲染;
const Context = React.createContext('hello world');
<Context.Provider value="asd"/>
<Middle/>
</Context.Provider>
<FunCom/>
function Middle() {
return (
<div>
<Bottom/>
<FunCom/>
<div>
)
}
function Bottom(props) {
return <Context.Consumer>
{
val => {return <div>{value}-jiji</div>
}
}
</Context.Consumer>
}
function FunCom(props) {const context = useContext(Context);
console.log('context:', context);
return (
<div>
<p>context 上下文 </p>
</div>
)
}
- useReducer:
语法:const {state, dispatch} = useReducer(reducer, initState);
其中 state 就是当前的状态值,dispatch 就是用来触发 reducer 跟新状态的方法,reducer 是纯函数,initState 为 state 的初始值;
useState 和 useReducer 的区别:
前者状态更新是的本质是代替,更新前后的数据结构有可能不一样;而后者则是状态的合并,类似于 setState,将局部状态合并到当前状态;对于简单的数据结构,可以用前者,对于复杂的数据结构,则需要用到后者;
例子:状态为 {a: {a1: 1}, b: 2},先更新 a1 为 3
demo:
- useCallback 钩子和 useMemo 钩子:
背景:由于函数式组件没有 scu,只要父组件或者内部的状态发生变化,都会直接让函数式组件 rerender,此时如果函数式组件中存在:
1) 某个值需要依赖某个状态进行比较复杂的计算和循环计算等;
2) 含有子组件,并且子组件的属性指向某个状态;
此时不管该状态有没有发生变化,都会进行重新的渲染和计算,从而造成资源的浪费;
function A() {const [a1, setA1] = useState(10);
const [a2, setA2] = useState(20);
const value = () => {
let sum = 0;
for(let i = 0; i < a1; i++) {sum = sum + i;}
return sum;
}
return (
<div>
<p>{value()}</p>
<B cb={value}/>
<button onClick={() => { setA2(21); }}/>
</div>
)
作用:
useCallback 和 useMemo 这两个钩子,主要是作用缓存作用,只要输入源的值没变,他们就会直接采用前一次计算好的缓存,减少没必要的计算和重复渲染,一旦输入源的值发生变化,他们就会重新计算,抛弃缓存;这样做可以优化了组件的性能。
钩子说明:
1.useMemo 钩子:
语法:const value = useMemo(fn, input);
其中,fn 就是含有计算相关的回调函数,input 就是输入源,value 就是 fn 计算出来的值;
说明:input 就是 fn 当中的依赖项,可以是属性或者内部状态,一开始时会执行 fn,并将值返回给 value,当二次渲染时,此时如果 input 当中的依赖项的值没有发生变化,则会采用上次执行的结果返回给 value,如果 input 的值发生变化,则会重新执行 fn,得到新的 value 值;
使用场景:如果 A 组件中有个值需要依赖 a 状态进行复杂的计算后得到,并且 A 组件中的子组件 B 某个属性又和该值产生联系,此时可以采用 useMemo,这样可以减少无畏的计算和没必要的渲染。
function WithMemo() {const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {sum += i;}
return sum;
}, [count]);
return <div>
<h4>{count}-{expensive}</h4>
{val}
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
<B val={expensive}/>
</div>
<div>
}
2.useCallback 钩子:
语法:const fn2 = useCallback(fn1, input),
其中 fn1 是回调函数,input 是输入源,fn2 是返回的函数;
说明:首次渲染时,会将 fn1 返回给 fn2,然后执行 fn2 获取对应的值;当二次渲染时,input 的值没有发生变化,则 fn2 还是上一次的 fn2,此时 fn2 虽然还是函数,但前一次和后一次都是指向同一个指针,因此是同一个函数,若 input 的值发生变化,此时会返回一个新的 fn1 给 fn2,这时候的 fn2 就会和上一次的 fn2 不一样,虽然函数的内容是一样,但指向不一样。
使用场景:多数使用在当该函数作为子组件的属性函数传到子组件。
demo:
useEffect,useMemo,useCallback 的区别:
useEffect(fn, input) 主要是针对副作用,返回的是一个清除函数,而且没有缓存作用,只有在组件更完成新且 input 的值发生变化时才会触发,他只能减少没必要的副作用执行。
useMemo,useCallback 主要是针对复杂的计算以及子组件的优化(配合子组件的 scu),减少没必要复杂计算和子组件的渲染。
- useRef:
语法:const curRef = useRef(initState);
说明:要区别组件的 ref,因为 useRef 返回的对象更像一个容器,容器的形式为 {current: initState};该容器可以储存很多东西,其中包括类组件(与组件的 ref 绑定),标签,变量等等,而且该容器会一直存在该函数式组件的整个生命周期,即只有在组件被销毁的时候才消失;
使用场合:
- 用来获取类组件,原生组件,标签的内部元素,只要和他们的 ref 绑定,就可以在函数式组件中直接访问(其实就是存储了一份目标的 dom 结构);
- 由于它的返回值存在组件整个生命周期,所以可以用来存储一些变量或者函数(例如定时器返回值等);
注意:
- 不能存储函数式组件。
- 修改 curRef.current 的值不会引起组件的重新渲染;
function ShowRef(props) {const [a, setA] = useState(1);
const mount = useRef({isMount: false});
const dom = useRef(null);
useEffect(() => {if (!mount.current.isMount) {console.log('again');
mount.current.isMount = true;
}
console.log('Mount?', mount.current.isMount);
console.log('dom', dom.current);
});
const click = function(e) {setA(a + 1);
dom.current.style.color = 'blue';
};
return (<div ref={dom}>
<div onClick={click}>ref1</div>
</div>
)
- useImperativeMethods:
语法:useImperativeMethods(ref, createInstance, [input]);
说明:就是让父组件能访问函数式组件内部成分,相当于函数式组件的 ref;其中 ref 是函数式组件传过来的参数,createInstance 是给父组件访问的属性和方法实例;
使用场合:就是父组件访问子组件(函数式组件)的时候用;
demo:
function FancyInput(props, ref) {const inputRef = useRef();
useImperativeMethods(ref, () => ({focus: () => {inputRef.current.focus(); }
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
父组件:<FancyInput ref = {fancyInputRef} />
调用:fancyInputRef.current.focus()
- useLayoutEffect:
语法:useLayoutEffect(fn)
作用:主要使用在 dom 新更后,用来访问 dom 节点布局方面的操作,例如宽高等;这个是用在处理 DOM 的时候, 当你的 useEffect 里面的操作需要处理 DOM, 并且会改变页面的样式, 就需要用这个, 否则可能会出现出现闪屏问题, useLayoutEffect 里面的 callback 函数会在 DOM 更新完成后立即执行, 但是会在浏览器进行任何绘制之前运行完成, 阻塞了浏览器的绘制。
以上就是我对 react hook 使用上的一些理解和总结,如有不对请指出。