From React Hooks to Component Class

React 支持通过 classfunction 两种方式定义 Component 。但 React Team 后来推荐大家使用 Hooks 通过 function 来定义组件。认为这是一种新的思维模式(mental Model)。

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.

-- https://reactjs.org/docs/hooks-intro.html

从 class 到 function with hooks 当然有其背后的 Motivation 。

一个有意思的比较方式是,反过来想,假如是从 Function Hooks 转换到 class 会是什么样子。

useState()

简单的计数组件,点击按钮,计数加一。

function Counter(){
  let [count, setCount] = useState(0)

  let increment = function(){
    setCount(count+1);
  }

  return <button click={increment}>{count} +1</button>
}
class Counter extends React.Component {
  constructor(props){
    super(props);
    this.state = {count:0}
    this.increment = this.increment.bind(this);
  }

  increment(){
    this.setState({count: this.state.count);
  }

  render(){
    return <button click={this.increment}>{this.state.count}+1</button>
  }
}

上面的改写对 class 来说有一点时不公平的。

  • function 形式直接访问 count
  • class 却需要通过 this.state 间接访问 count
  • function 形式通过 setCount() 修改状态
  • class 通过统一的 this.setState() 修改状态

State in React Class

React 文档关于 class 的 state 提出了三条注意事项:

  1. Do Not Modify State Directly
  2. State Updates May Be Asynchronous
  3. State Updates are Merged

其中第一条指的就是所有状态都需要通过 this.setState() 来修改。

这条规则背后的意义在于:React 需要监听状态数据的变化。

而监听的实现实际上可以让用户显示地来完成。

从 this.state.count 到 this.count

我们先假设我们可以直接使用单个状态数据。并且显示地通知 React 状态发生了改变。

class Counter extends React.Component {
  constructor(props){
    super(props);
    this.count = 0;
  }

  increment(){
    this.count = this.count + 1;
    this.notifyUpdate({}});    // 通知 React 状态发生了改变
  }

  render(){
    return <button click={this.increment}>{this.count} +1</button>
  }

到这里为止,和前面 function 形式的组件相比,差别主要是

  1. 多了一个 constructor()
  2. 多了一个 this.notifyUpdate()
  3. 少了一个 setCount()
  4. 少了一个 useState()

在 class 里使用 this.useState()

如果添加上少掉的 useState(), class 形式的 Counter 定义如下。

class Counter extends React.Component {
  constructor(props){
    super(props);
    [this.count, this.setCount] = this.useState('count', 0);

    this.increment = this.increment.bind(this);
  }

  increment(){
    this.setCount(this.count + 1);
  }

  render(){
    return <button onClick={this.increment}>{this.count} + 1</button>
  }

这样一来,可以说差别就只是多了 constructor()this 这个指针。

实现 this.useState()

理想情况下,当然时定义在 React.Component 的实现里。

class React.Component {
  useState(name, initValue){
    let $this = this, $state = this.state;
    $state[name] = initValue;
    let setValue = function(newValue){
       $state[name] = newValue;
       $this.setState({});
    }
    return [$state[name], setValue.bind($this)]
  }
}

render(props, state)

函数形式的 React Component 里, props 作为参数每次都参与了“计算”。 class 形式里面则只在 constructor() 时传入了 props, 其他时候隐式地通过类似于 setProps() 的方式改变。

用函数的形式描述,组件的创建和更新类似于 view = render(props, state) 的多次调用。 因此采用 render(props, state) 的函数形式可能更准确,也更方便。

当然,并不是每个 Component 都需要维护一个 state,这种情况可以等价于 render(props, null) 来处理。

class Counter extends React.Component {
  ...

  render(props, state){
    return <button onClick={this.increment}>{state.count} + 1</button>
  }
}

自动绑定 .bind(this)

既然 React 已经选择了通过 JXS 自动生成代码的方式,而且模板里已经清楚地写明了需要调用 this.handleEvent(),那么帮助用户自动绑定 this 指针可以说是“举手之劳”的事情。

注册 Hook 函数的 useEffect()

React Hooks 里面的 useEffect() 的关键点有两处

  1. 合并了 componentDidMount()componentDidUpdate()
  2. 返回一个函数用于在 componentWillUnmount() clean up

这两点功能如果是在 class 里,可以用 this.registerHook() 来实现。

之所以叫做 registerHook(),是因为 useEffect() 定义的函数正是传统意义上的 Hook —— 在特定节点上调用预先注册的代码。

引用外部数据的 useContext()

useContext() 实质上是对外部数据的引用。这个外部数据类似于多个 Component 所共享的“全局”数据。

同时 useContext() 可以告诉框架当前 Component 需要对这个“全局”数据进行 react

useRef() == this.state

React Hooks 模式相当于默认使用 State 的 snapshot 值。需要使用 State 的最新值得话需要借助 useRef() 函数。

而在 class component 里,默认情况下 this.state 就是最新的状态值。

是否需要区分最新值和映像值,要视具体情况而定。很多时候应该是不需要做严格区分的。

在暂且假定使用映像值有利于后期代码优化的假设下,可以给回调函数显示地传入 state 的方式来处理。

这可以通过使用 callback.bind(this, props, state) 实现。

实现 memoized 的 useCallback() 和 useMemo()

useCallback() 和 useMemo() 是辅助函数。和是 class 形式还是 function 形式关系不大。

可能的 React Class 写法

根据上述对比,我们也许可以用下面的形式来定义 React Component

class Counter extends React.Component {
  constructor(props){
    super(props);

    this.defineState('count', 0);          // 定义状态   == useState()
    this.useContext(MyContext);            // 外部状态   == useContext()

    this.registerHook(this.changeTitle);   // 生命周期   == useEffect()
    this.registerHook(model.fetchStatus);  // 重用外部逻辑
  }

  render(props, state){
    return <button onClick={this.increment}>{state.count} + 1</button>
  }

  increment(props, state){   // 有助于暗示这是状态操作,同时传入了旧的状态映像
    this.updateState('count', state.count + 1);  // 更新状态
  }

  changeTitle(){
    document.title = "hello";
    let cleanup = function(){
       // ...
    }
    return cleanup
  }
}

React class 还是 React function ?

总结上面的对比,Function 的写法主要优点是避免了 this 的使用。

而 class 形式的优点在于通过代码拆分,结构实际上更清楚。

对于状态数据的管理,function 形式实际上是把状态数据的管理完全交由框架统一管理。 而 class 则是自己管理,严格来说,这更符合封装的含义。

对并发的处理

React 转向 Hooks 的动机之一是并行的状态更新。

对此,TODO.