过去,React 中的函数组件都被称为无状态函数式组件(stateless functional component),这是因为函数组件没有办法拥有自己的状态,只能根据 Props 来渲染 UI ,其性质就相当于是类组件中的 render 函数,虽然结构简单明了,但是作用有限。
但自从 React Hooks 横空出世,函数组件也拥有了保存状态的能力,而且也逐渐能够覆盖到类组件的应用场景,因此可以说 React Hooks 就是未来 React 发展的方向。
我们知道组件化的思想就是将一个复杂的页面/大组件,按照不同层次,逐渐抽象并拆分成功能更纯粹的小组件,这样一方面可以减少代码耦合,另外一方面也可以更好地复用代码;但实际上,在使用 React 的类组件时,往往难以进一步分拆复杂的组件,这是因为逻辑是有状态的,如果强行分拆,会令代码复杂性急剧上升;如使用 HOC 和 Render Props 等设计模式,这会形成“嵌套地狱”,使我们的代码变得晦涩难懂。
这其实也是上一点的延续:要给一个拥有众多状态逻辑的组件写单元测试,无疑是一件令人崩溃的事情,因为需要编写大量的测试用例来覆盖代码执行路径。
对于类组件,我们需要在组件提供的生命周期钩子中处理状态的初始化、数据获取、数据更新等操作,处理起来本身逻辑就比较复杂,而且各种“副作用”混在一起也使人头晕目眩,另外还很可能忘记在组件状态变更/组件销毁时消除副作用。
我认为 React Hooks 的亮点不在于 React 官方提供的那些 API ,那些 API 只是一些基础的能力;其亮点还是在于自定义 Hooks —— 一种封装复用的设计模式。
例如,一个页面上往往有很多状态,这些状态分别有各自的处理逻辑,如果用类组件的话,这些状态和逻辑都会混在一起,不够直观:
class Com extends React.Component { state = { a: 1, b: 2, c: 3, } componentDidMount() { handleA() handleB() handleC() } }
而使用 React Hooks 后,我们可以把状态和逻辑关联起来,分拆成多个自定义 Hooks ,代码结构就会更清晰:
function useA() { const [a, setA] = useState(1) useEffect(() => { handleA() }, []) return a } function useB() { const [b, setB] = useState(2) useEffect(() => { handleB() }, []) return b } function useC() { const [c, setC] = useState(3) useEffect(() => { handleC() }, []) return c } function Com() { const a = useA() const b = useB() const c = useC() }
我们除了可以利用自定义 Hooks 来拆分业务逻辑外,还可以拆分成复用价值更高的通用逻辑,比如说目前比较流行的 Hooks 库:react-use;另外,React 生态中原来的很多库,也开始提供 Hooks API ,如 react-router 。
React 提供了大量的组件生命周期钩子,虽然在日常业务开发中,用到的不多,但光是 componentDidUpdate 和 componentWillUnmount 就让人很头痛了,一不留神就忘记处理 props 更新和组件销毁需要处理副作用的场景,这不仅会留下肉眼可见的 bug ,还会留下一些内存泄露的隐患。
类 MVVM 框架讲究的是数据驱动,而生命周期这种设计模式,就明显更偏向于传统的事件驱动模型;当我们引入 React Hooks 后,数据驱动的特性能够变得更纯粹。
下面我们以一个非常典型的列表页面来举个例子:
class List extends Component { state = { data: [] } fetchData = (id, authorId) => { // 请求接口 } componentDidMount() { this.fetchData(this.props.id, this.props.authorId) // ...其它不相关的初始化逻辑 } componentDidUpdate(prevProps) { if ( this.props.id !== prevProps.id || this.props.authorId !== prevProps.authorId // 别漏了! ) { this.fetchData(this.props.id, this.props.authorId) } // ...其它不相关的更新逻辑 } render() { // ... } }
上面这段代码有3个问题:
如果改成用 React Hooks 来实现,问题就能得到很大程度上的解决了:
function List({ id, authorId }) { const [data, SetData] = useState([]) const fetchData = (id, authorId) => {} useEffect(() => { fetchData(id, authorId) }, [id, authorId]) }
改用 React Hooks 后:
最常见的副作用莫过于绑定 DOM 事件:
class List extends React.Component { handleFunc = () => {} componentDidMount() { window.addEventListener('scroll', this.handleFunc) } componentWillUnmount() { window.removeEventListener('scroll', this.handleFunc) } }
这块也还是会有上述说的,影响高内聚的问题,改成 React Hooks :
function List() { useEffect(() => { window.addEventListener('scroll', this.handleFunc) }, () => { window.removeEventListener('scroll', this.handleFunc) }) }
而且比较绝的是,除了在组件销毁的时候会触发外,在依赖项变化的时候,也会执行清除上一轮的副作用。
在使用类组件的时候,我们需要利用 componentShouldUpdate 这个生命周期钩子来判断当前是否需要重新渲染,而改用 React Hooks 后,我们可以利用 useMemo 来判断是否需要重新渲染,达到局部性能优化的效果:
function List(props) => { useEffect(() => { fetchData(props.id) }, [props.id]) return useMemo(() => ( // ... ), [props.id]) }
在上面这段代码中,我们看到最终渲染的内容是依赖于props.id
,那么只要props.id
不变,即便其它 props 再怎么办,该组件也不会重新渲染。
在我们刚开始使用 React Hooks 的时候,经常会遇到这样的场景:在某个事件回调中,需要根据当前状态值来决定下一步执行什么操作;但我们发现事件回调中拿到的总是旧的状态值,而不是最新状态值,这是怎么回事呢?
function Counter() { const [count, setCount] = useState(0); const log = () => { setCount(count + 1); setTimeout(() => { console.log(count); }, 3000); }; return ( <div> <p>You clicked {count} times</p> <button onClick={log}>Click me</button> </div> ); } /* 如果我们在三秒内连续点击三次,那么count的值最终会变成 3,而随之而来的输出结果是? 0 1 2 */
“这是 feature 不是 bug ”,哈哈哈,说是 feature 可能也不太准确,因为这不正是 javascript 闭包的特性吗?当我们每次往setTimeout
里传入回调函数时,这个回调函数都会引用下当前函数作用域(此时 count 的值还未被更新),所以在执行的时候打印出来的就会是旧的状态值。
那为啥类组件中,每次都能取到最新的状态值呢?这是因为我们在类组件中取状态值都是从this.state
里取的,这相当于是类组件的一个执行上下文,永远都是保持最新的。
通过useRef
创建的对象,其值只有一份,而且在所有 Rerender 之间共享。
听上去,这 useRef 其实跟 this.state 很相似嘛,都是一个可以一直维持的值,那我们就可以用它来维护我们的状态了:
function Counter() { const count = useRef(0); const log = () => { count.current++; setTimeout(() => { console.log(count.current); }, 3000); }; return ( <div> <p>You clicked {count.current} times</p> <button onClick={log}>Click me</button> </div> ); } /* 3 3 3 */