react 16.8 以后加上了 react hook,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。16.8之前,react组件可以分为类组件和函数组件。
- 函数组件一定是无状态组件,展示型组件(渲染组件)一般是无状态组件;
- 类组件既可以是有状态组件,又可以是无状态组件。类组件也可以叫容器组件,一般有交互逻辑和业务逻辑,而容器型组件一般是有状态组件。
我们为什么要拥抱react hook?由于类组件存在以下几点问题:
- 组件变得复杂和难以维护,业务变得复杂之后,组件之间共享状态变得频繁,此时组件将变得非常难以理解和维护,复用状态逻辑更是难上加难。
- 满天class导致的热重载和性能问题,class自生具有的复杂度和组件嵌套过深props层级传递。
- 函数式组件没有状态
下面逐一介绍官方提供的hook API。
1.useState()
作用:返回一个状态以及能修改这个状态的setter,在其他语言称为元组(tuple),一旦mount之后只能通过这个setter修改这个状态。
2.useEffect(callback, arr)
作用:处理函数组件中的副作用,如异步操作、延迟操作等。useEffect有两个参数,callback和数组依赖项,无arr时相当于componentDidMount生命周期,有arr时相当componentDidMount和componentDidUpdata生命周期。如果callback中有return,则相当于componentWillUnmount。
3.useContext
作用:跨组件共享数据钩子,使用可分为三步:
- 首先使用React.createContext API创建Context,由于支持在组件外部调用,因此可以实现状态共享
export const MyContext = React.createContext(null);
- 使用Context.Provider API在上层组件挂载状态
<MyContext.Provider value={value}>
<childComponent />
</MyContext.Provider>
- 获取上层组件中距离当前组件最近的<MyContext.Provider> 的 value
const value = useContext(MyContext) // MyContext 为 context 对象(React.createContext 的返回值)
useContext和传统的props传参分别适用于那些场景
useContext的应用场景:
1.全局状态的定义,即可以被不同层级的组件所需要。
2.多个组件之间传参(他们之间可能是跨多层级即祖孙关系传参)时。
传统props的应用场景:
如果普通的父子组件之间传参,即父子组只有单纯的一层时,用props传参更省事
4.useReducer
语法:const [state, dispatch] = useReducer(reducer, initialArg, init);
作用:用于管理复杂的数据结构(useState一般用于管理扁平结构的状态),基本实现了redux的核心功能。useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state
以及与其配套的 dispatch
方法。
const initialState = {count: 0};
const reducer = (state, action)=> {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
5.useMemo、useCallback
这俩个Api与性能优化有关。react中,性能的优化点在于:
- 调用setState,就会触发组件的重新渲染,无论前后的state是否不同
- 父组件更新,子组件也会自动的更新
基于上面的两点,我们通常的解决方案是:使用immutable进行比较,在不相等的时候调用setState;在shouldComponentUpdate中判断前后的props和state,如果没有变化,则返回false来阻止更新。
在hooks出来之后,我们能够使用function的形式来创建包含内部state的组件。但是,使用function的形式,失去了上面的shouldComponentUpdate,我们无法通过判断前后状态来决定是否更新。而且,在函数组件中,react不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。因此useMemo 和useCallback就是解决性能问题的杀手锏。
useCallback和useMemo的参数跟useEffect一致。useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
语法:
useMemo:const A = useCallback(fnB, [a])调用fnB函数并返回其结果
useCallback:const fnA = useCallback(fnB, [a])返回fnB函数
通过一个例子来看useMemo的作用:
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = () => {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return <>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</>;
不使用useMemo,无论count还是val改变都会执行expensive()
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 <>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</>;
使用useMemo,只有在count发生改变使才会会执行expensive()
useCallback跟useMemo比较类似,但是使用场景不同:比如有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
console.log('callback执行');
return count;
}, [count]);
return <>
<h4>{count}</h4>
<Child callback={callback} />
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)} />
</div>
</>
const Child = (props: any) => {
const [count, setCount] = useState(() => props.callback());
useEffect(() => {
setCount(props.callback());
}, [props.callback]);
return <div>{count}</div>;
};
6.useRef
语法:const refContainer = useRef(initialValue);
useRef是一个方法,返回一个可变的 ref 对象;其 .current 属性被初始化为传入的参数(initialValue);可以保存任何类型的值:dom、对象等任何可变值;返回的 ref 对象在组件的整个生命周期内保持不变;修改 ref 的值是不会引发组件的重新 render 。
useRef非常常用的一个操作,访问DOM节点,对DOM进行操作,监听事件等等,如下:
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
除了传统的用法之外,它还可以“跨渲染周期”保存数据。
const likeRef = useRef(1);
const [like, setLike] = useState(1);
const onButtonClick = () => {
setTimeout(() => {
console.log(likeRef.current); //11
console.log(like); // 1
}, 2000);
};
return (
<>
<button
onClick={() => {
likeRef.current++;
setLike(like + 1);
}}>
{like}
</button>
<button onClick={onButtonClick}>打印like</button>
</>
)
在上面的例子中,点击了打印like
按钮后,连续点击数字按钮,会发现2s后likeRef.current
打印出11,而like
打印出1。
因为,在任意一次渲染中,props和 state 是始终保持不变的,如果props和state在任意不同渲染中是相互独立的话,那么使用到他们的任何值也是独立的。所以onButtonClick时拿到的时未点击数字按钮时的like
值。
而ref 在所有 render 都保持着唯一的引用,因此所有的对 ref 的赋值或者取值拿到的都是一个最终的状态,而不会存在隔离。
7.useImperativeHandle
语法:useImperativeHandle(ref, createHandle, [deps])
当userRef用于一个组件时,useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值;如果不使用,父组件的ref(chidlRef)访问不到任何值(childRef.current==null);且useImperativeHandle
应当与 forwardRef
一起使用。
const RefDemo= ()=>{
const childRef = useRef<any>(null);
const onButtonClick = () => {
childRef.current?.say();
};
return (
<div>
<Child ref={childRef} />
<button onClick={onButtonClick}>say123</button>
</div>
)
};
export default RefDemo;
const Child = React.forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
say: () => {
console.log('123');
},
}));
return (
<>
<h4>子组件</h4>
</>
);
});
- React.forwardRef会创建一个React组件,这个组件能够将其接受的ref属性转发到其组件树下的另一个组件中。
- React.forwardRef接受渲染函数作为参数,React将使用prop和ref作为参数来调用此函数。
8.useLayoutEffect
用法与useEffect 相同,但它会在所有的 DOM 变更之后同步调用(立即执行)。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。由于会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制。
使用场景:当useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现闪屏问题。
9.useDebugValue
useDebugValue 用于在 React 开发者工具中显示 自定义 Hook 的标签。
useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。