首发于前端美学
终于读懂 Hooks

终于读懂 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)

这样会有两个严重问题:

  1. 每次都生成新的 DOM,性能损耗严重;
  2. 如果强制一个复杂页面的只有一个 state,那将完全不可行。

对于问题1,希望 f 返回的不是 DOM,而是一个 DOM 的描述,最好是 js 对象,再由另外一套程序保持 js 对象和 DOM 一致(这其实就是 JSX 和 虚拟 DOM 了);

对于问题2,希望能将 state 进行拆分,小的 state 和小的 f 形成组件,然后将这些组件拼装起来。

class

将 state 和 f 组合起来,即 将数据和方法绑定起来,js 中有且只有3种方式:

  1. 模块
  2. 闭包

先考虑使用类,于是设计组件如下:

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),倒也确实能规避掉。

编辑于 2020-11-09 18:54