⚓ React Hooks 理解
Banner from React Hooks
!本篇文章过于久远,其中观点和内容可能已经不准确,请见谅!~
想分享的是对 React-Hooks 这个新功能的思考,不仅仅是新的接口,重要的是这种开发模式代表的逻辑的拆分,状态和副作用的思维方式,而不是生命周期~~
Hooks 概念刚出来的时候就看了发布会的 Presentation,当时感觉思想特别好,能够在一个函数里面就可以对数据和生命周期进行管理。
而且对于从 prototype 过来的原型继承,一直觉得虽然 Class 很好用,但是太重量了,一个很简单的组件只要涉及到内部状态就要有 Class,所以 Hooks 发布之后很长一段时间我都是以为要替代 Class 组件,至少大部分场景可以用 Hooks 重写。
在使用了两三个月之后,不用 Hooks 不舒服斯基也踩了很多坑,我现在才顿悟过来,其实 Hooks 不仅仅是看上去这么简单,也不是仅仅用来取代 Class 的,而是一个不一样的组件思想。
我想要开发一个简单的关注组件
这样一个组件不可能是简单的展示,必定需要一些交互,但是 props 的单向更新没办法实现灵活的状态管理,所以必须要将这个函数式组件改成 class 的,同时考虑到业务复杂性,添加了数据的初始化、订阅、更新等生命周期等,最终实现:
现在我们就有一个可交互组件了,逻辑算是很清晰。也很明显的得出结论,如果需要内容展示,使用函数式组件,如果想要交互,就必须改写成一个 class 组件。
- function 组件简单纯粹,只有一个输入输出,无状态的开发模式,确定的输入一定有确定的输出,当时也被称为 SFC(stateless functional component)。所以上面的例子如果使用函数式,除非父组件改变输入,否则一定没办法实现界面改变。
- class 的组件有自己的生命周期,可以维护自己的状态,父组件的输入无法唯一确定组件的结果,组件的功能更独立灵活,可以使用状态、方法和事件来完成用户的交互,对于父组件内部逻辑都是黑盒。
以上就是 React 开发者在一段时间后都会有的思考。
几乎所有的最佳实践都在说组件上需要有明确的状态和干净的渲染,数据和渲染分离,获取、处理、更新数据之后再调用纯渲染组件展示。这也是数据管理、状态管理层功能在 react 甚至所有中大型应用的开发过程中有很大分量的原因,也能从逻辑上更好的抽象和复用代码。
但是优秀的团队没那么多,更没那么多高质量的优雅实现,数据的流转和组件设计并没有那么追求完美,现状是很多业务中组件的实践很多变成了:
状态管理 从组件的模式来说,class 和 function 代表着两种不同的开发体验,按照我的理解就是带复杂状态管理的组件,和纯展示型组件。
React 将状态管理的方案扔给了社区,社区诞生了非常多的优秀方案,比如 Redux、Mobx 等,状态共享和分发让状态管理足以撑起大型应用的开发。
理想很丰满,但是现实非常骨干,有强迫症的开发者没那么多,但是着急催人命的老板很多,
所以很多的交互功能、数据获取功能都一股脑的放到 class 组件里,生命周期分割开的各种逻辑变成了一团麻,没有时间或者初期没法去实现最佳实践,后期更没法抽象更细粒度的代码重用,围绕着框架的生命周期、state的处理代码到处都是。即使 function 组件简单纯粹,只有一个输入输出,但是一旦需求变更,或者需要交互,很快就必须改写成 class 类型了。
代码复用 从代码状态逻辑复用上来说,HoC 高阶组件和 Render props 技术开发模式一般用的比较多。
只有 class 和 function 的使用体验,很多时候嫌弃 class 笨重、讨厌 function 没用的体验,但是毕竟这几种开发模式都有独特的场景需求。
但是大部分开发人员没办法做到渲染和状态的分离,工作环境是业务而不是框架,所以快速实现业务的时候,很难做好业务的梳理,就出现了大量使用 class 组件,在生命周期中做很多逻辑处理。
用得多了后才发现很多地方也并不尽如人意,可能也是能力问题或者没有时间来抽象,比如 class 组建中严格的生命周期经常把逻辑代码分割开,每个阶段的状态逻辑变更,复杂组件会变得很混乱很难拆分。function 组件经常做简单组件,但是一旦需求变更,或者需要交互,很快就必须改写成 class 类型了。
其实上面的不友好的地方也不是那么夸张,但是 Hooks 出现确实提供了一个可能更好的方案,业务层有了一个灵活还又强大的模式。
Hooks 的出现让 function 组件从数据输入到视图输出的模式变成了有状态逻辑的组件。
无状态表现是输入相同,输出就相同。原本的 function 组件,输入可以指 props 输出是 视图 UI。
现在增加 Hooks,实际上组件内部维护了一个状态,可能 props 不修改,视图 UI 也会变动,但是每次使用相同的 props 渲染出来的视图UI表现和状态都是能确定的
Hooks 的出现一个是增加函数组件中状态管理的可能性,而且还能提供一个更细粒度的逻辑复用。
再次将上面的例子封装出来的结果非常清爽,不用关心生命周期,只需要关心状态和改变状态的行为:
在 Hooks 发布第一时间之后,我觉得这个形式是编程习惯上的问题,爱用 Class 就用,爱用 hooks 也可以,只要业务中统一即可。所以大概拿了几个 class 组件转成 hooks 之后,也没觉得有什么大不了的,甚至还觉得那些 use-* 有点不知所云。
但是用的多了之后发现,好像并不是这样的,hooks 不仅仅是实现了功能,还悄默默的把我对组件的认知改变了。
之前 我以为的组件是状态加生命周期的修改状态,有什么状态渲染什么界面,然后在生命周期中对状态进行改变。
现在 关心哪些是状态,有哪些是副作用,有哪些是计算和渲染,不需要关心我什么时候才能做哪些事。
理解了 hooks 之后,对那些以 use-* 模式的方法名也是喜欢得紧,简直传神,需要什么模式、功能、扩展、数据,封装一个 use 方法,直接调用即可,状态和修改状态的方法能够抽象的很干净,各种逻辑都集成在一起而不是拆分到各个生命周期。
比如最简单直接的就是在需要简单数据交互的地方使用 useState 了:
const View = () => {let [count, setCount] = React.useState(0)return <button onClick={() => setCount(count + 1)}>{count}</button>}
简单两行的函数,就可以封装成原本需要 class 才能完成的功能了,而且很好的封装了逻辑实现,很好用,比如使用我自己封装的状态管理框架 novus:
状态和改变状态的行为,还是非常有助于程序的抽象逻辑的。
先看一个小栗子:
小朋友你是否有很多问号?为什么界面的值一直没有更新呢?
ps: 上面的实例仅用来演示为什么计算能够被缓存,useCallback 依赖于每次变更的状态的话,也没什么意义,徒增耗时。
这个原因在于,effect / callback 执行的时候,会创建一个闭包,内部对 state 的访问依赖于闭包中初始的值 0。deps 参数表示每次 render 的时候需要判断与上次的值进行浅比较,如果没有变动就会缓存上次的函数,也包括这个闭包内部的传值引用。如果 deps 为空,表示不会更新这个闭包,导致每次执行函数中的值会永远是当时初始化的值。
正确的做法应该是:
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const id = setInterval(() => {setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量}, 1000);return () => clearInterval(id);}, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量,表示只会执行一次return <h1>{count}</h1>;}
所以 hooks 在每次更细你都会执行的函数体内怎么保存状态和函数的呢?答案是使用了闭包。下面用非常简单的实现来演示下一个闭包:
也就是闭包在获取值的时候已经在上下文环境中缓存了这个值,再多次调用也没办法更新,这也是 hooks 用来缓存计算提升性能的一个方案。
更多的 hooks 性能和 react 性能调优可以在另一篇文章中了解:📊 React 实践技巧和性能优化
const useSomeHook = () => {// 一个简单的状态const [status, setStatus] = useState(false)// 封装的计算逻辑,里面会修改状态const someFunc = useCallback(() => {setStatus(!status)// 虽然上面有更新 state 值,但是因为此处的值引用是闭包中上次的值,所以没办法获得立即的更新值// 必须等再次调用 someFunc = useCallback(...) 才能运行新生成的包含更新值的闭包函数console.log('status => ', status) // falseif(status) console.log('hello world!')else console.log('hello react!') // √}, [status])useEffect(() => {document.body.addEventListener('click', someFunc, false)return () => {document.body.removeEventListener('click', someFunc, false)}}, []) // 此处没有依赖,所以点击事件永远使用的是第一次的 someFunc,所以可能永远也没办法拿到正确的 state,导致内部使用的 state 和 return 输出的 state 不一致的情况useEffect(() => {console.log(status) // falsesetStatus(true)console.log(status) // false// class 组件中,因为 setState 是异步的,一般 setTimeout 在下一个 EventLoop 中能够获取到更新值// 但是这里的 status 因为一直引用的是闭包的值,是不会在 1s 后更新的setTimeout(() => {console.log(status) // false}, 1000);}, [status])const loopFunc = useCallback(() => {for (let i = 0; i < 5; i++) {// 此处虽然 useCallback 依赖了 someFunc,但是每次循环内部都是使用的闭包值,第一次循环中 setState 也没办法再第二次循环就看到更新后的值someFunc()}}, [someFunc])// !!!!! 如果一个函数本身就需要依赖于频繁变动的状态,那么使用 use-* 优化性能本身也没办法做到,最好是把不依赖状态的计算部分剥离先return [status, someFunc, loopFunc]}
hooks 的思想是开发者不再需要去理清每一个生命周期函数的触发时机,以及在里面处理逻辑会有哪些影响。而是更关注去思考哪些是状态,哪些是副作用,哪些是需要缓存的复杂计算和不必要的渲染。
class 更偏向的是清晰完整的生命周期,一个经典的面向对象思想,强调的是方法和属性。
感谢您的阅读,本文由 Ubug 版权所有。如若转载,请注明出处:Ubug(https://ubug.io/blog/react-hooks-vs-class)