内容简介:这个标题可能不太好,但此文章确实不是一篇使用教程,而且也不会覆盖太多点,建议时间充裕的还是应该完整地看下React Hooks 对于部分人来说可能还是陌生的,但还是阻止不了它成为了当前 React 社区里「最」热门的一个词汇。
这个标题可能不太好,但此文章确实不是一篇使用教程,而且也不会覆盖太多点,建议时间充裕的还是应该完整地看下 官网文档 。
React Hooks 对于部分人来说可能还是陌生的,但还是阻止不了它成为了当前 React 社区里「最」热门的一个词汇。
一开始了解到这个还是 Dan Abramov 在十月底的时候发了一个推,是一篇文章 Making Sense of React Hooks ,建议没看过的先看下。看完第一感受就是:React 本就应该是这样的啊!
看完这篇文章,希望你可以从整体上对 Hooks 有个认识,并对其设计哲学有一些理解,希望看的过程不要急,跟着我的思路走。
如果你想自己跟着文章一起练手,需要把 react
和 react-dom
更新到 16.7.0-alpha
及以上,如果配置了 ESLint,记得添加对应的 Plugin 。
插曲
长期以来很多人会把 Stateless Component
和 Functional Component
混为一谈,我会试着跟他们解释这不是一回事(不是一个维度),但当时 Functional Component
里确实无法使用 state,我无论怎么解释都会显得很无力。难道这冥冥之中都预示着会有类似 React Hooks 的东西出现?
React Hooks 的本质
稍微复杂点的项目肯定是充斥着大量的 React 生命周期函数(注意,即使你使用了状态管理库也避免不了这个),每个生命周期里几乎都承担着某个业务逻辑的一部分,或者说某个业务逻辑是分散在各个生命周期里的。
而 Hooks 的出现本质是把这种 面向生命周期编程 变成了 面向业务逻辑编程 ,你不用再去关心本不该关心的生命周期。
一个 Hooks 演变
我们先假想一个常见的需求,一个 Modal 里需要展示一些信息,这些信息需要通过 API 获取且跟 Modal 强业务相关,要求我们:
- 因为业务简单,没有引入额外状态管理库
- 因为业务强相关,并不想把数据跟组件分开放
- API 数据会随机变动,因此需要每次打开 Modal 才获取最新数据
- 为了后期优化,不可以有额外的组件创建和销毁
我们可能的实现如下:
class RandomUserModal extends React.Component { constructor(props) { super(props); this.state = { user: {}, loading: false, }; this.fetchData = this.fetchData.bind(this); } componentDidMount() { if (this.props.visible) { this.fetchData(); } } componentDidUpdate(prevProps) { if (!prevProps.visible && this.props.visible) { this.fetchData(); } } fetchData() { this.setState({ loading: true }); fetch('https://randomuser.me/api/') .then(res => res.json()) .then(json => this.setState({ user: json.results[0], loading: false, })); } render() { const user = this.state.user; return ( <ReactModal isOpen={this.props.visible} > <button onClick={this.props.handleCloseModal}>Close Modal</button> {this.state.loading ? <div>loading...</div> : <ul> <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li> <li>Gender: {user.gender}</li> <li>Phone: {user.phone}</li> </ul> } </ReactModal> ) } }
我们抽象了一个包含业务逻辑的 RandomUserModal
,该 Modal 的展示与否由父组件控制,因此会传入参数 visible
和 handleCloseModal
(用于 Modal 关闭自己)。
为了实现在 Modal 打开的时候才进行数据获取,我们需要同时在 componentDidMount
和 componentDidUpdate
两个生命周期里实现数据获取的逻辑,而且 constructor
里的一些初始化操作也少不了。
其实我们的要求很简单:在合适的时候通过 API 获取新的信息,这就是我们抽象出来的一个 业务逻辑 ,为了这个业务逻辑能在 React 里正确工作,我们需要将其 按照 React 组件生命周期进行拆解 。这种拆解除了 代码冗余 ,还 很难复用 。
下面我们看看采用 Hooks 改造后会是什么样:
function RandomUserModal(props) { const [user, setUser] = React.useState({}); const [loading, setLoading] = React.useState(false); React.useEffect(() => { if (!props.visible) return; setLoading(true); fetch('https://randomuser.me/api/').then(res => res.json()).then(json => { setUser(json.results[0]); setLoading(false); }); }, [props.visible]); return ( // View 部分几乎与上面相同 ); }
很明显地可以看到我们把 Class 形式变成了 Function 形式,使用了两个 State Hook 进行数据管理(类比 constructor
),之前 cDM
和 cDU
两个生命周期里干的事我们直接在一个 Effect Hook 里做了(如果有读取或修改 DOM 的需求可以看 这里 )。做了这些,最大的优势是 代码精简 ,业务逻辑变的紧凑,代码行数也从 50+ 行减少到 30+ 行。
Hooks 的强大之处还不仅仅是这个,最重要的是这些业务逻辑可以随意地的的抽离出去,跟普通的函数没什么区别(仅仅是看起来没区别),于是就变成了可以 复用 的自定义 Hook。具体可以看下面的进一步改造:
// 自定义 Hook function useFetchUser(visible) { const [user, setUser] = React.useState({}); const [loading, setLoading] = React.useState(false); React.useEffect(() => { if (!visible) return; setLoading(true); fetch('https://randomuser.me/api/').then(res => res.json()).then(json => { setUser(json.results[0]); setLoading(false); }); }, [visible]); return { user, loading }; } function RandomUserModal(props) { const { user, loading } = useFetchUser(props.visible); return ( // 与上面相同 ); }
这里的 useFetchUser
为自定义 Hook,它的地位跟自带的 useState
等比也没什么区别,你可以在其它组件里使用,甚至在这个组件里使用两次,它们会天然地隔离开。
业务逻辑复用
这里说的业务逻辑复用主要是需要跨生命周期的业务逻辑。单单按照组件堆积的形式组织代码虽然也可以达到各种复用的目的,但是会导致组件非常复杂,数据流也会很乱。组件堆积适合 UI 布局,但是不适合逻辑组织。为了解决这些问题,在 React 发展过程中,产生了很多解决方案,我认知里常见的有以下几种:
Mixins
坏处远远大于带来的好处,因为现在已经不再支持,不多说,可以看看这篇文章: Mixins Considered Harmful 。
Class Inheritance
官方 很不推荐此做法,实际上我也没真的看到有人这么做。
High-Order Components (HOC)
React 高阶组件 在封装业务组件上简直是屡试不爽,它的实现是把自己作为一个函数,接受一个组件,再返回一个组件,这样它可以统一处理掉一些业务逻辑并达到复用目的。
比较常见的一个就是 react-redux
里的 connect
函数:
(图片来自 这里 )
但是它也被很多人吐槽嵌套问题:
(图片来自 这里 )
Render Props
Render Props 其实很常见,比如 React Context API :
class App extends React.Component { render() { return ( <ThemeProvider> <ThemeContext.Consumer> {val => <div>{val}</div>} </ThemeContext.Consumer> </ThemeProvider> ) } }
它的实现思路很简单,把原来该放「组件」的地方,换成了回调,这样当前组件里就可以拿到子组件的状态并使用。
但是,同样这会产生 Wrapper Hell 问题:
(图片来自 这里 )
Hooks
Hooks 本质上面说了,是把 面向生命周期编程 变成了 面向业务逻辑编程 ,写法上带来的优化只是顺带的。
这里,做一个类比, await/async
本质是把 JS 里异步编程思维变成了同步思维,写法上表现出来的特点就是原来的 Callback Hell 被打平了。
总结对比:
await/async
这里不得不客观地说,HOC 和 Render Props 还是有存在的必要,一方面是支持 React Class,另一方面,它们不光适用于纯逻辑封装,很多时候也适合逻辑 + 组件的封装场景,虽然此时使用 Hooks 也可以,但是会显得啰嗦点。另外,上面诟病的最大的问题 Wrapper Hell,我个人觉得使用 Fragment 也可以基本解决。
状态盒子
首先,React Hooks 的设计是反直觉的,为什么这样说呢?可以先试着问自己:为什么 Hooks 只能在其它 Hooks 的函数或者 React Function 组件里?
在我们的认知里,React 社区一直推崇函数式、纯函数等思想,引入 Hooks 概念后的 Functional Component
变的不再纯了, useXxx
与其说是一条执行语句,不如说是一个 声明 。声明这里放了一个「状态盒子」,盒子有输入和输出,剩下的内部实现就一无所知,重要的是,盒子是有 记忆 的,下次执行到此位置时,它有之前上下文信息。
类比「代码」和「程序」的区别,前者是死的,后者是活的。表达式 c = a + b
表示把 a
和 b
累加后的值赋值给 c
,但是如果写成 c := a + b
就表示 c
的值由 a
和 b
相加得到。看起来表述差不多,但实际上,后者隐藏着一个时间的维度,它表示的是一种 联系 ,而不单单是个运算。这在 RxJS 等库中被大量使用。
这种声明目前是通过很弱的 use
前缀标识的(但是设计上会简洁很多),为了不弄错每个盒子和状态的对应关系,书写的时候 Hooks 需要 use
开头且放在顶层作用域,即不可以包裹 if/switch/when/try
等。如果你按文章开头引入了那个 ESLint Plugin 就不用担心会弄错了。
总结
这篇文章可能并没有一个很条理的目录结构,大多是一些个人理解和相关思考。因此,这不能替代你去看真正的文档了解更多。如果你看完后还是觉得废话太多,不知所云,那我希望你至少可以在下面几点上跟作者达成共鸣:
- Hooks 本质是把 面向生命周期编程 变成了 面向业务逻辑编程 ;
- Hooks 使用上是一个逻辑状态盒子,输入输出表示的是一种联系;
- Hooks 是 React 的未来,但还是无法完全替代原始的 Class。
参考
文章可随意转载,但请保留此 原文链接 。
非常欢迎有激情的你加入 ES2049 Studio ,简历请发送至 caijun.hcj(at)alibaba-inc.com 。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- 深入理解 Java 函数式编程,第 5 部分: 深入解析 Monad
- 深入理解 HTTPS
- 深入理解 HTTPS
- 深入浅出Disruptor
- 深入了解 JSONP
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。