共计 4648 个字符,预计需要花费 12 分钟才能阅读完成。
前言
hooks 是 react16.8 新增的特性。关于为什么要新增 hooks,是因为 class 的组件会存在以下的一些问题。
- 在组件之间复用状态逻辑很难
- 复杂组件变得难以理解
- 难以理解的 class
这些点就不详细赘述了,这篇文章的重点是介绍 hooks。
useState
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
useState 是用来取代 class 组件中的 setState。useState 接受一个值作为 state 的初始值,返回一个数组,数组第一个值是 state,第二个值是一个用于修改 state 的函数。useState 可以多次使用。栗子如下:
import React, {useState} from 'react';
const App = function() {const [count, setCount] = useState(0);
// 另一个没有使用的 state
const [other, setOther] = useState('hello');
return (<button onClick={() => setCount(count+1)}>{count}</button>
);
}
它对应的 class 组件的代码如下:
import React, {Component} from 'react';
class App extends Component {constructor(props) {super(props);
this.state = {
count: 0,
other: 'hello'
};
}
setCount(count: number) {
this.setState({count});
}
setOther(other: string) {
this.setState({other});
}
render() {
return (<button onClick={() => this.setCount(this.state.count + 1)}>
{this.state.count}
</button>
);
}
}
useReducer
useReducer 和 useState 的用处是一样的,不同的是 useReducer 处理的是更加复杂的场景,例如三级联动选择框,需要同时对多个 state 进行联动处理。可以看做是一个小的 redux,使用方式和 redux 也基本一致。
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 App = function() {const [state, dispatch] = useReducer(reducer, { count: 0});
return (
<>
{state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
useEffect
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
type EffectCallback = () => (void | (() => void | undefined));
Effect Hook 可以让你在函数组件中执行副作用操作。何为副作用操作呢?例如:请求接口、操作 dom、定时器等等。我们可以使用 useEffect 模拟 React 中的一些生命周期函数。但需要注意的是:请不要把这种对应划上等号。
从他的定义中就可以知道,useEffect 有两个参数,第一个参数是一个回调函数(可以返回一个函数或者不返回内容),第二个参数是依赖项。
模拟 componentDidMount:
// 在 didMount 中请求一个接口
useEffect(() => {fetch('http://xxx.com/api/list');
}, []);
模拟 componentDidUpdate
// 不传入依赖时,会在每次渲染之后执行
useEffect(() => {console.log('update');
});
模拟 componentWillUnmount: useEffect 的回调函数返回一个函数。
// 向 document 添加一个 click 事件,并在下次重新渲染前将其卸载。useEffect(() => {const handler = () => {console.log('click'); }
document.addEventListener('click', handler);
return () => {document.removeEventListener('click', handler);
}
}, []);
模拟 shouldComponentUpdate: 这个我觉得应该和 vue 的 watch 更加相近。
// 在第一个加载和 count 发生变化时才会触发
useEffect(() => {// ...}, [count]);
useMemo
如果你对 vue 比较熟悉的话,useMemo 可以看做是 computed
,可以看做是需要手动添加依赖的计算属性,在依赖的值不发生改变时,返回的值是不变的。
const App = function() {const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const name = useMemo(() => {return firstName + ' ' + lastName}, [firstName, lastName]);
return (
<>
姓:<input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
名:<input value={lastName} onChange={(e) => setLastName(e.target.value)} />
姓名:{name}
</>
);
}
useCallback
我们知道,函数组件中 props 或者 state 改变时,都会重新执行当前函数,那么如果我们直接在函数组件中定义处理函数的话,属性更改会触发修改,那么每次修改都会导致处理函数的重新定义,这样会造成极大的性能耗损。
useCallback 便是用来处理这个问题,在依赖项不改变的情况下,函数不会重新定义。
const App = function() {const change = useCallback((e) => {console.log(e);
}, []);
return (
<>
<input onChange={change} />
</>
);
}
useRef
ref 的作用我想很多前端小伙伴应该并不陌生,可以用来操作 dom 或者实例。而 useRef 除了用于操作 DOM 之外,在一些其他的方面也很有用,例如我们需要把一个定时器的值全局保存,但又不希望这个值的变化触发 render,就像是我们在使用 class 组件时的this.timer = setInterval(...)
。
// 例如我们设置了定时获取数据的 interval,在点击某个按钮之后就停止定时器
const App = () => {const inputEl = useRef(null); // 这个用来获取 dom,绑定到 input 上之后,就可以通过 inputEl.current 进行访问
const timer = useRef(null);
useEffect(() => {timer.current = setInterval(...);
return () => {clearInterval(timer.current);
}
}, []);
return (
<div>
<button onClick={() => clearInterval(timer.current)}>Stop</button>
<input ref={inputEl} />
</div>
);
}
useContext
从字面上也可以看出来,useContext 就是为了方便使用 context(一般用于祖孙组件的数据通信
) 的。需要注意的是调用了 useContext 的组件总会在 context 值变化时重新渲染。
const themes = {
light: {
color: '#fff',
background: '#f12'
}
};
const ThemeContext = createContext(themes.light);
const Child = () => {const theme = useContext(ThemeContext);
return (<div style={{color: theme.color, background: theme.background}}>Child</div>
);
}
const App = () => {
return (
<div>
<ThemeContext.Provider value={themes.light}>
<Child />
</ThemeContext.Provider>
</div>
);
}
自定义 Hook
自定义 hook 一般用来 HOC 做比较,他们都是用来对组件的逻辑进行复用。hook 与 HOC 不同的是:
- 由于 HOC 机制的原因(包裹一层组件)会改变原组件的 HTML 结构,hook 则不会;
- 使用过 HOC 的小伙伴应该都体验过使用了多个 HOC 之后,完全不知道某个 prop 是来自于哪里,也就我们常说的 props 来源不明。不方便调试;
- HOC 可能会覆盖固有的 props。
自定义 hooks 或使用 hooks 时需要 注意 的是:
- 自定义 hook 必须以
use
开头,这有利于 react 对自定义 hook 的规则进行检查; - 不要在循环,条件或嵌套函数中调用 Hook;
- 不要在普通的 JavaScript 函数中调用 Hook。
栗子:在多个组件中需要实时获取鼠标的位置
// 定义 hook
const useMousePosition = () => {const [location, setLocation] = useState({x: 0, y: 0});
useEffect(() => {function move(e) {setLocation({ x: e.screenX, y: e.screenY});
}
document.addEventListener('mousemove', move);
return () => {document.removeEventListener('mousemove', move);
};
}, []);
return location;
}
// 使用 hook
const App = () => {const position = useMousePosition();
return (<div>{ position.x} {position.y}</div>);
}
可以看到,我们的就对代码进行了复用,也避免了上述关于 HOC 的问题。
小结
hooks 的引入让我们很方便的使用函数组件来编写代码,不过关于 OOP 和 FP 的争议也一直存在,所以是否使用 hooks 也需要童鞋们好好考量。
参考:React Hook API 索引