终于读懂 Hooks
最近在想,如果告诉你 view = f(state),你会如何设计前端框架?
朴素想法
最直接的想法,假如一个页面就一个 state:
const state = { name: 'hello' }
那就实现这样一个 f:
function f(state) {
return (```
<html>
<body>
<p>${state.hello}</p>
<body>
</html>
```)
}
于是就实现了 view = f(state):
const view = f(state)
这样会有两个严重问题:
- 每次都生成新的 DOM,性能损耗严重;
- 如果强制一个复杂页面的只有一个 state,那将完全不可行。
对于问题1,希望 f 返回的不是 DOM,而是一个 DOM 的描述,最好是 js 对象,再由另外一套程序保持 js 对象和 DOM 一致(这其实就是 JSX 和 虚拟 DOM 了);
对于问题2,希望能将 state 进行拆分,小的 state 和小的 f 形成组件,然后将这些组件拼装起来。
class
将 state 和 f 组合起来,即 将数据和方法绑定起来,js 中有且只有3种方式:
- 模块
- 类
- 闭包
先考虑使用类,于是设计组件如下:
class Component {
state = { name: 'hello' }
f () {
// 这里开始使用 jsx
return (
<p>{this.state.name}</p>
)
}
}
但这样的组件没法对外交流,因此需要对外暴露参数,以便外部传入参数和方法:
class Component {
constructor(props) {
// XXX: do sth. with props
//
this.state = { name: 'hello' }
}
f () {
return (
<p>{this.state.name}</p>
)
}
}
将 f 换成 render,猛然发现,这不就是 React class 组件吗?!
但是这样 class 写法带着浓厚的 面向对象 的气息,在一些复杂的类中,已经很难看到最初的 view = f(state) 设计思想。
Hooks
于是考虑用闭包:
function makeComponent(props) {
const state = { name: 'hello' }
return Component() {
// XXX: do sth. with props
//
return (
<p>{state.name}</p>
)
}
}
const Comp = makeComponent()
对于“一个模块使用一个值”,其实模块并不关心值存在哪,它只要能拿到 get/set 方法就可以了,于是可以把状态统一放到模块里,并暴露出 get/set 方法:
import { makeState } from 'xxx';
function Component(props) {
// XXX: do sth. with props
//
const [get, set] = makeState({ name: 'hello' })
return (
<p>{get('name')}</p>
)
}
将 makeState 换成 useState,哈哈,Hooks come!
除了状态,还需要 允许在特定的时机做一些事,这在 class 组件里体现为生命周期方法,在函数组件如何做呢?其实这事的本质是告诉框架要执行哪些方法,那框架提供个函数就行了:
import { makeState, doAtDidMount, doAtWillUnmount } from 'xxx';
function Component(props) {
// XXX: do sth. with props
//
const [get, set] = makeState({ name: 'hello' })
doAtDidMount(() => {
// XXX:
})
doAtWillUnmount(() => {
// XXX:
})
return (
<p>{get('name')}</p>
)
}
将 doAtDidMount 换成 useEffect,又绕回了 Hooks!到此问题就只剩下一个,框架中需要有几个“时机”?
class 组件中分 挂载、更新和卸载 3个阶段好几个时机,而在 Hooks 中,则认为只有“组件渲染到屏幕之后”这一个时机,两种划分方式并无优劣之分。
理论上,Hooks 能实现 class 组件的全部功能,并且看起来更加“函数式”。
动机理解
回头看 React 官方对提出 Hooks 的 动机:
在组件之间复用状态逻辑很难
这个主要指的是自定义 Hooks 复用逻辑的优势,在 class 组件中想要抽离逻辑,则必须剥离 setState 和生命周期,因为它们是和组件实例(即 this)绑定的;但是自定义 Hooks 里,可以使用 useState、useEffect 等其他 Hooks。
复杂组件变得难以理解
这个指的是受制于 class 组件生命周期,相关的逻辑不得不放到不同的生命周期里,典型的是“挂载是申请资源、卸载是释放资源”,但在 Hooks 中,可以返回一个释放资源的函数,于是相关的逻辑就可以组合在一起。
难以理解的 class
这条稍微有点牵强,对于前端开发人员,掌握 class 和判定 this 指向是基本功,没那么“难以理解”,但是,复杂嵌套调用下突然冒出一个 this,也是件烦人的事。使用 Hooks 后可以完全不使用 this(class 都没有,当然可以不用 this),倒也确实能规避掉。