Hooks API 从 2018 年开始进入开发者视野至今已经将近两年了,但目前还有很多同学对 Hooks 有很多的不理解和困惑。
我们也不难发现社区上已经有很多关于 Hooks API 的教程,甚至的其源码的分析学习。但当我们实际地到 React 的源码中学习 Hooks API 的实现方式的时候,会被其“神奇”的实现方式搞得更加一头雾水。那么我们究竟还能从什么角度去学习和理解 Hooks API 的设计模式呢?
本文包含以下内容:
本文将带你用另外一种角度探索 Hooks API,帮助你对 Hooks API 有更好的理解。
本文不会教导你如何使用 Hooks API,也不能直接帮助你如何更好地使用 Hooks API。
嗯,居然还想继续往下看吗?那就请不要跳过任何一句话哦,不然会严重影响阅读体验的。
实际上 Hooks API 的核心是函数式编程(Functional Programming)的开发模式,绝大部分的 API 表达都通过函数的形式提供。其实函数式编程在 React 这个框架中是从一开始便存在的。
这个是理解 React 的 Immutable(不可修改的)开发模式的一个核心“口诀”。JSX 实际上是 React 用于将 State 转换为 Virtual DOM 的函数表达方式,开发者通过 JSX 来定义好 State -> View 中间的转换关系以后,只需通过 setState
来改变组件的状态,React 变会通过各个组件的视图表达函数(Render)对所有的 State 进行转换和组合,最终得到实际的页面结构。
而在 React 的开发生态中,状态管理工具经过了多次迭代后,同样是以函数式编程作为核心的 Redux 成为了 React 开发社区中最流行的选择。在 Redux 的设计模式中,函数式的概念更是体现地更加彻底。Redux Store 中的 State 进行变更需要通过 Action 来触发 Reducer。而最核心的 Reducer 的基本概念就是:
interface State {
count: number
}
const state: State = {
count: 0
}
const INCR = (state: State, payload: number = 1): State => {
return {
count: state.count + payload
}
}
const DECR = (state: State, payload: number = 1): State => {
return {
count: state.count - payload
}
}
Redux 的状态管理实际上是通过使用不断地组合 Reducer+Action 得到的“记录”结果,我们在 Redux Devtools 中便可以看到按时间顺序执行的 Action 列表,甚至可以在 Action 队列中进行“时间穿梭”。但实际这种开发模式对不熟悉函数式编程或者数学功底比较一般的同学来说,理解成本会相对较高。
让我们回归到 Hooks API 的函数式编程本身。虽然 React 和 Redux 本身对函数式编程的模式的支持程度都很高了,为何带状态的组件还是只能以面向对象的形式开发呢?而且实际上 React 的 Class API 开发模式是建立在状态机模式之上的,组件中通过修改组件实例中的 this.state
来让组件的视图进行响应。当然这种模式从组件开发上并没有什么问题,但当出现需要对组件中的某些逻辑进行抽象并复用时,Class API 的问题就暴露出来了。
假设有一个需要抽象的计数器逻辑,在 Class API 中有两种抽象方式:Mixin 和 HOC(Higher Order Component)。
是一种非常“原始”的逻辑复用方式,它通过 React.createClass
的方式将一个抽象逻辑混合到业务组件中,从而能够在组件中复用抽象逻辑。但这种模式限制非常大,而且很容易出现冲突。
import React from "react"
import ReactDOM from "react-dom"
import { Row, Col, Button } from "antd"
const counterMixin = {
getInitialState() {
return {
count: 0
}
},
incr(increment = 1) {
this.setState({
count: this.state.count + increment
})
},
decr(decrement = 1) {
this.setState({
count: this.state.count - decrement
})
}
}
// Deprecated since React 15.5.0
const Counter = React.createClass({
mixins: [counterMixin],
render() {
return (
<Row style={{ width: 150, textAlign: "center" }}>
<Col span={24}>
<h3>{this.state.count}</h3>
</Col>
<Col span={12}>
<Button onClick={() => this.incr()}>INCR</Button>
</Col>
<Col span={12}>
<Button onClick={() => this.decr()}>DECR</Button>
</Col>
</Row>
)
}
})
DEMO:https://codesandbox.io/s/counter-with-mixins-t4two?file=/src/index.js
createClass
从 React 15.5.0 版本开始就被移除了,而 HOC 这种拓展方式则相对更灵活一些。HOC 的核心逻辑是对业务组件进行包装,并将封装的逻辑通过参数的方式传给业务组件。
import React, { Component } from "react"
import ReactDOM from "react-dom"
import { Row, Col, Button } from "antd"
function CounterComponent(WrappedComponent) {
return class extends Component {
state = {
count: 0
}
incr(increment = 1) {
this.setState({
count: this.state.count + increment
})
}
decr(decrement = 1) {
this.setState({
count: this.state.count - decrement
})
}
render() {
const countProps = {
count: this.state.count,
incr: (increment) => this.incr(increment),
decr: (decrement) => this.decr(decrement)
}
return <WrappedComponent {...this.props} {...countProps} />
}
}
}
const Counter = CounterComponent((props) => {
const { count, incr, decr } = props
return (
<Row style={{ width: 150, textAlign: "center" }}>
<Col span={24}>
<h3>{count}</h3>
</Col>
<Col span={12}>
<Button onClick={() => incr && incr()}>INCR</Button>
</Col>
<Col span={12}>
<Button onClick={() => decr && decr()}>DECR</Button>
</Col>
</Row>
)})
DEMO:https://codesandbox.io/s/counter-with-hoc-o6mcm?file=/src/index.js
这种方法看似简单,但如果需要在业务组件中添加状态的话就比较麻烦。如果需要传入更复杂的组件逻辑的话,则需要使用 connect
之类的方法来实现了。
那么这个需求在 Hooks API 需要怎么实现呢?
Hooks API 的 React 可以让原本只能充当 Stateless Component 的函数组件也具备了 Stateful 的能力,而 Hooks 本身也是通过函数来实现的。
import React, { useState } from "react"
import ReactDOM from "react-dom"
import { Row, Col, Button } from "antd"
function useCount(initCount = 0) {
const [count, setCount] = useState(initCount)
const incr = () => setCount(count + 1)
const decr = () => setCount(count - 1)
return {
count,
incr,
decr
}
}
function Counter() {
const { count, incr, decr } = useCount()
return (
<Row style={{ width: 150, textAlign: "center" }}>
<Col span={24}>
<h3>{count}</h3>
</Col>
<Col span={12}>
<Button onClick={() => incr()}>INCR</Button>
</Col>
<Col span={12}>
<Button onClick={() => decr()}>DECR</Button>
</Col>
</Row>
)
}
DEMO:https://codesandbox.io/s/counter-with-hooks-wqyq8
嗯,看上去简单多了。但当我们像使用 Class API + Async/Await 那样对修改后的状态进行读取的时候,奇怪的事情发生了。比如我们需要在执行 incr
之后打印出新的 count
,在 Class API 中只要在执行 this.setState
的时候使用 await
进行异步执行,便可以在后面的代码中取得正确的新数值。而在 Hooks API 中,哪怕使用了 Async/Await 也无法取得正确的值。
import React, { useState } from "react"
import ReactDOM from "react-dom"
import { Row, Col, Button, Collapse, Input } from "antd"
const { TextArea } = Input
const { Panel } = Collapse
class CounterA extends React.Component {
state = {
count: 0
}
async incr(increment = 1) {
await this.setState({
count: this.state.count + increment
})
}
async incrAndLog(increment = 1) {
await this.incr(increment)
this.props.log("Class API", this.state.count) // Here will log the new value
}
decr(decrement = 1) {
this.setState({
count: this.state.count - decrement
})
}
render() {
return (
<Row style={{ width: 300, textAlign: "center" }}>
<Col span={24}>
<h3>{this.state.count}</h3>
</Col>
<Col span={8}>
<Button onClick={() => this.incr()}>INCR</Button>
</Col>
<Col span={8}>
<Button onClick={() => this.incrAndLog()}>INCR&LOG</Button>
</Col>
<Col span={8}>
<Button onClick={() => this.decr()}>DECR</Button>
</Col>
</Row>
)
}
}
function useCount(initCount = 0) {
const [count, setCount] = useState(initCount)
const incr = async () => await setCount(count + 1)
const decr = async () => await setCount(count - 1)
return {
count,
incr,
decr
}
}
function CounterB({ log }) {
const { count, incr, decr } = useCount()
const incrAndLog = async (increment = 1) => {
await incr(increment)
log("Hooks API", count) // Here will log the previous value, WHY?
}
return (
<Row style={{ width: 300, textAlign: "center" }}>
<Col span={24}>
<h3>{count}</h3>
</Col>
<Col span={8}>
<Button onClick={() => incr()}>INCR</Button>
</Col>
<Col span={8}>
<Button onClick={() => incrAndLog()}>INCR&LOG</Button>
</Col>
<Col span={8}>
<Button onClick={() => decr()}>DECR</Button>
</Col>
</Row>
)
}
function App() {
const [logText, setLogText] = useState("[LOGGING HERE]")
const log = (domain, msg) => {
setLogText(logText + "\n" + `[${domain}] ${msg}`)
}
return (
<Row gutter={15}>
<Col span={12}>
<h4>DEMO</h4>
<Collapse defaultActiveKey={[1]}>
<Panel header="Counter with Class API" key={1}>
<CounterA log={log} />
</Panel>
<Panel header="Counter with Hooks API" key={2}>
<CounterB log={log} />
</Panel>
</Collapse>
</Col>
<Col span={12}>
<h4>Log</h4>
<TextArea rows={8} value={logText} />
</Col>
</Row>
)
}
DEMO:https://codesandbox.io/s/qiguaide-hooks-xgbsz?file=/src/index.js
是否感觉虽然 Hooks API 在使用时让代码看上去比较干净,但奇奇怪怪的心智成本增加了很多?是的,这一点我十分认同,Hooks API 引入了不少比较抽象且晦涩,导致其初期的学习曲线比较陡。
但不要害怕,让我来慢慢地讲讲如何基于 TypeScript 来帮助理解和学习函数式编程和 Hooks API 吧。
首先,我们先认识两个基本的函数类型:
简单地解释一下便是,第一种是函数的出参和入参为同一类型;而第二种则是函数的入参经过函数的执行后,出参的类型与入参不一致,可以理解为进行了一次映射。
有了这两种基本类型以后,我们就可以开始对它们进行变形了。但在这之前我们来看看来自工业聚大大的两句话:
我不知道的数据,都是参数;
我不知道的行为,都是参数;
这两句话基本上涵盖了 90% 以上在 Web 开发语境中的函数式编程场景,那么我们便可以从以下一系列的函数变形中理解这两句话的含义。
这一句话很好理解,参考 JavaScript 开发社区中最流行的“瑞士军刀” Lodash,里面这么多的数据处理方法便可以看作是
或者
的体现。
函数中虽然我不知道我要处理的数据是什么,甚至连具体类型都是未知的,但需要执行的逻辑是确定的。
DEMO:https://codesandbox.io/s/lodash-data-flow-f9o5z
在 Web 开发中,我们经常需要对业务逻辑中的一些自定义行为进行管控,比如:
发送 Ajax 请求需要知道以下信息:useRequest
该请求是否正在等待返回:加载中状态
该请求是否成功返回:成功状态
该请求是否需要手动触发:触发条件
若该请求为自动触发,则触发条件有哪些:依赖刷新
该请求是否需要被缓存:缓存条件、缓存状态
有一系列涉及到异步操作的行为,为了降低系统的并发量,需要将它们自动进入队列并进行串行执行:runTrack
在行为定义的地方希望能做到无感知
执行方也不需要感知到队列的存在
……
我们以第一个发送 Ajax 请求的场景作为例子,我们希望能有一个工具能够把实际的业务行为进行包装,并且包装过后的出参在使用方式上跟作为入参的行为是一致的。
const { run: fetchPosts, loading } = wrapRequest((authorId: number) => fetchPostsService(authorId))
fetchPosts(123)
// ...
const statusText = loading ? "Loading..." : "Done"
其实我们还有一种我们非常熟悉的“我不知道的行为”的场景,就是组件进行组合(Compose)和渲染(Render)的过程,接下来我们还需要引入另外一种概念 —— codata。
不要害怕,codata 实际上并不是什么新鲜玩意。我们在学习小学数学中解方程的时候,就已经开始接触一个名为代数(Algebra)的概念了。什么?代数太抽象不好跟编程作类比?没关系,我们一步步来。
还记得圆形的面积计算公式吗:
其中:
S为圆的面积,也就是这条公式需要求的值;
π 为圆周率,作为一个常用无理数,它在不同的场景中会有不同的精度近似值,比如我们在学习的时候一般会使用 3.14 作为一个便于手算的近似值使用,而在对精度要求更高的场景中则可能需要用到小数点后 N 位;
然而实际上我们应该把 π 看做圆周率计算算法在不同的近似精度下的结果
r 为圆的半径,而对于这条公式本身来说,r 也是一个未知数。
在这么简单的一条公式中,包括公式本身就已经存在了三个代数了。在数学中我们可以把未知数、算法看做是代数,代数本身没有特定的值,但我们却知道它们该如何使用。
有点抽象?没关系我们还是以 Web 开发的语境来举例子,假设有以下 PRD 需要进行实现:
在这个 PRD 中,包含了以下几个元素:
使用实际的 React Hooks 来实现 PRD 中的元素的话,需要定义下面的几个内容(伪代码):
import { useState, useMemo } from 'react'
function App() {
const [r, setR] = useState(10)
const [PI, setPI] = useState(3.14)
const S = useMemo(() => PI * Math.pow(r, 2), [PI, r])
const circleCSS = useMemo(() => ({
width: r * 2,
height: r * 2,
}), [r])
}
在这段代码中定义了四个实体,分别对应了上面需求中的四个元素,这四个元素已经可以满足 PRD 中所有的只读部分的开发了。但是在 PRD 中,圆的半径 r 和圆周率π这两个最重要的元素都是可以被改变的,所以在实际计算之前这四个元素的实际值都是未知的。只有当这个组件被实际渲染时,React 框架才会真正地从内存中读取当前由用户选定的、具有时效性的值进行计算。
嗯,是否有些体感了?我们可以把 codata 的使用过程分为两个部分:组合(Compose)和观测(Visit)。假设说我们同样把这个函数组件以数学语言的方式表达出来,就可以是这样的形式:
其中 S 和 circleCSS 又可以被改写成 F<π,r> 和 F
这样看是不是感觉更加简单明了?这个渲染函数实际上是一个关于半径 r 和圆周率 π且在 Web 开发领域中这个函数是可以分别对这两个元进行求偏导的。忘了什么是偏导?没关系,简单理解就是这两个元素的任意变动,所引起对整个组件整体所产生的变动都是可推测的。有没有回想起些什么?对,这也是 Redux 的理念。
DEMO: https://codesandbox.io/s/yuandemianji-8yr0n
我们在构建渲染函数:
的过程实际上就是一个组合的过程,在这个过程中每一个未知元实际上都不需要使用到它们的实际值,而仅仅是使用了他们的概念本身。
只有当函数被实际观测,也就是渲染的时候,里面的实际值才会被观测并用于计算每一层函数的值,最终得到渲染函数的实际结果。
在通过编写自定义 Hooks 的方式来复用逻辑时,实际上就是把这个自定义 Hooks 作为业务组件中的参数,这便是一个组合的过程。
这样的好处是组件本身天然就是对惰性计算(Lazy Compute)更加地友好,对性能的调优也有了更大的空间。在 React Hooks 中,还提供了 useMemo
和 useCallback
这些可以减少观测过程中的不必要计算的方法,对性能能做进一步的优化。
不过现实比较残酷,绝大部分组件都无法完美地改写成可导的函数,因为我们经常需要通过一些副作用(Side Effects)来实现我们更多的复杂需求,比如 React Hooks 中也提供了 useEffect
这样的方法。
让我们再次回到函数类型定义本身,渲染函数实际上也是 F
当然 codata 还有相比于 Algebra 来说更多的特性,但在这里我们就不展开了,后续有可能会单独写一篇来详细讲讲这个熟悉又陌生的概念。
我们知道 Hooks API 在 React 率先发布以后,另外一个流行的 Web 框架 Vue 也公布了它的 3.0 版本。其中 Vue 3.0 与 2.x 最大的区别便是 3.0 版本中同样也引入了 Hooks API 的概念,并有着另外一个名字 —— Composition API。
相比于 React Hooks 的函数式组件,Vue Composition API 依然保留着 Vue 中独特的传统优势 —— Single File Component。相比于 React 中使用 JSX 作为视图层的表达方式,Vue 依然保留了更加原始且更易于管理的模板引擎。
import Vue from "vue"
import VueCompositionAPI, { ref } from "@vue/composition-api"
import Antd from "ant-design-vue"
Vue.use(VueCompositionAPI)
Vue.use(Antd)
const template = `
<a-row id="app">
<a-col :span="24">
<h3>{{count}}</h3>
</a-col>
<a-col :span="12">
<a-button @click="() => incr()">INCR</a-button>
</a-col>
<a-col :span="12">
<a-button @click="() => decr()">DECR</a-button>
</a-col>
</a-row>
`
new Vue({
name: "App",
template,
setup(props) {
const count = ref(0)
const incr = (increment = 1) => (count.value += increment)
const decr = (decrement = 1) => (count.value -= decrement)
return {
count,
incr,
decr
}
}
}).$mount("#app")
DEMO:https://codesandbox.io/s/vue-composition-api-tdrg3
不难发现同样是组合和观测的过程,在 Vue Composition API 中是被分开成两个部分的分别对应 setup
方法和模板渲染(当然也可以是基于 JSX 的 render
函数),这相比于 React Hooks 来说更加贴近 codata 的概念。而且从实现原理上看,Vue Composition API 的 Compose 过程是只执行一次的与 React Hooks 的每次有任何参数发生变化都会重新执行组合相比更加利于管理,对性能来说也更加容易进行调优。
另外 Vue Composition API 在设计上也更加照顾原本使用 Vue 2.x API 的开发者,将 React Hooks 中
的模型压缩为
将大部分复杂的心智成本由框架内部消化,大大降低了 Hooks API 的入门门槛。
以至于现在竟然还出现了将两者融合起来的项目 antfu/reactivue,不失为一种好玩的思路。
import React from "react"
import { defineComponent, ref } from "reactivue"
import { Row, Col, Button } from "antd"
const Counter = defineComponent(
// Setup
() => {
const count = ref(0)
const incr = (increment = 1) => (count.value += increment)
const decr = (decrement = 1) => (count.value -= decrement)
return {
count,
incr,
decr
}
},
// Render
({ count, incr, decr }: any) => (
<Row style={{ width: 150, textAlign: "center" }} className="App">
<Col span={24}>
<h3>{count}</h3>
</Col>
<Col span={12}>
<Button onClick={() => incr()}>INCR</Button>
</Col>
<Col span={12}>
<Button onClick={() => decr()}>DECR</Button>
</Col>
</Row>
)
)
DEMO:https://codesandbox.io/s/blue-fast-b5hui?file=/src/App.tsx
感谢您能花费这么长时间看一篇没有任何教程的文章,如果本文能够为你带来更多的新思路,重新思考 Hooks API 以及 React 应用的开发,就是本文最大的成就。
我们在学习一种新技术的时候,往往会先考虑是否要通过阅读其源码来进行“深入的学习”,但其实有的时候源码远远没有设计思想要重要,而 Hooks API 就是一个非常典型的例子。Hooks 的实现方式事实上十分取巧,导致如果只从源码角度来学习 Hooks 的话,只会导致久久无法越过 Hooks 最初的学习陡坡,而逐渐丧失对其的兴趣。
余之拙见,且望指教。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/KjEgK-vD1AKR1_v2T3qT9w
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。