React Hook 解析2

183 阅读5分钟

参考: zhuanlan.zhihu.com/p/50597236

参考: zhuanlan.zhihu.com/p/88593858

官方文档: reactjs.org/docs/hooks-…

什么是 React Hooks

React Hooks 是 React 16.7.0-alpha 版本推出的新特性

React Hooks 要解决的问题是状态共享,不会产生 JSX 嵌套地狱问题,这个状态指的是状态逻辑,所以称为状态逻辑复用会更恰当,因为只共享数据处理逻辑,不会共享数据本身。

为了更快理解 React Hooks 是什么,先看下面一段 renderProps 代码:

function App() {
  return (
    <Toggle initial={false}>
      {({ on, toggle }) => (
        <Button type="primary" onClick={toggle}> Open Modal </Button>
        <Modal visible={on} onOk={toggle} onCancel={toggle} />
      )}
    </Toggle>
  )
}

恰巧,React Hooks 解决的也是这个问题:

function App() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button type="primary" onClick={() => setOpen(true)}>
        Open Modal
      </Button>
      <Modal
        visible={open}
        onOk={() => setOpen(false)}
        onCancel={() => setOpen(false)}
      />
    </>
  );
}

可以看到,React Hooks 就像一个内置的打平 renderProps 库,我们可以随时创建一个值,与修改这个值的方法。看上去像 function 形式的 setState,其实这等价于依赖注入,与使用 setState 相比,这个组件是没有状态的。

React Hooks 的特点

React Hooks 带来的好处不仅是 “更 FP,更新粒度更细,代码更清晰”,还有如下三个特性:

  1. 多个状态不会产生嵌套,写法还是平铺的(renderProps 可以通过 compose 解决,可不但使用略为繁琐,而且因为强制封装一个新对象而增加了实体数量)。
  2. Hooks 可以引用其他 Hooks。
  3. 更容易将组件的 UI 与状态分离。

Hooks 带来的约定

Hook 函数必须以 "use" 命名开头,因为这样才方便 eslint 做检查,防止用 condition 判断包裹 useHook 语句。

React Hooks 并不是通过 Proxy 或者 getters 实现的,而是通过数组实现的,每次 useState 都会改变下标,如果 useState被包裹在 condition 中,那每次执行的下标就可能对不上,导致 useState 导出的 setter 更新错数据。

优点

一、更容易复用代码

这点应该是react hooks最大的优点,它通过自定义hooks来复用状态,从而解决了类组件有些时候难以复用逻辑的问题。hooks是怎么解决这个复用的问题呢,具体如下:

每调用useHook一次都会生成一份独立的状态,这个没有什么黑魔法,函数每次调用都会开辟一份独立的内存空间。

虽然状态(from useState)和副作用(useEffect)的存在依赖于组件,但它们可以在组件外部进行定义。这点是class component做不到的,你无法在外部声明state和副作用(如componentDidMount)

二、清爽的代码风格

函数式编程风格,函数式组件、状态保存在运行环境、每个功能都包裹在函数中,整体风格更清爽,更优雅。另外,对比类组件,函数组件里面的unused状态和unused-method更容易被发现。

三、代码量更少

1.向props或状态取值更加方便,函数组件的取值都从当前作用域直接获取变量,而类组件需要先访问实例引用this,再访问其属性或者方法,多了一步。

2.更改状态也变得更加简单, this.setState({ count:xxx })变成 setCount(xxx)。

因为减少了很多模板代码,特别是小组件写起来更加省事,人们更愿意去拆分组件。而组件粒度越细,则被复用的可能性越大。所以,hooks也在不知不觉中改变人们的开发习惯,提高项目的组件复用率。

缺点

一、响应式的useEffect

写函数组件时,你不得不改变一些写法习惯。你必须清楚代码中useEffect和useCallback等api的第二个参数“依赖项数组”的改变时机,并且掌握上下文的useEffect的触发时机。当逻辑较复杂的时候,useEffect触发的次数,可能会被你预想的多。对比componentDidmount和componentDidUpdate,useEffect带来的心智负担更大。

二、状态不同步

这绝对是最大的缺点。函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面,当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的。比如下面的一个例子:

import React, { useState } from "react";
​
const Counter = () => {
  const [counter, setCounter] = useState(0);
​
  const onAlertButtonClick = () => {
    setTimeout(() => {
      alert("Value: " + counter);
    }, 3000);
  };
​
  return (
    <div>
      <p>You clicked {counter} times.</p>
      <button onClick={() => setCounter(counter + 1)}>Click me</button>
      <button onClick={onAlertButtonClick}>
        Show me the value in 3 seconds
      </button>
    </div>
  );
};
​
export default Counter;

当你点击Show me the value in 3 seconds的后,紧接着点击Click me使得counter的值从0变成1。三秒后,定时器触发,但alert出来的是0(旧值),但我们希望的结果是当前的状态1。

这个问题在class component不会出现,因为class component的属性和方法都存放在一个instance上,调用方式是:this.state.xxx和this.method()。因为每次都是从一个不变的instance上进行取值,所以不存在引用是旧的问题。

其实解决这个hooks的问题也可以参照类的instance。用useRef返回的immutable RefObject(current属性是可变的)来保存state,然后取值方式从counter变成了: counterRef.current。如下:

import React, { useState, useRef, useEffect } from "react";
​
const Counter = () => {
  const [counter, setCounter] = useState(0);
  const counterRef = useRef(counter);
​
  const onAlertButtonClick = () => {
    setTimeout(() => {
      alert("Value: " + counterRef.current);
    }, 3000);
  };
​
  useEffect(() => {
    counterRef.current = counter;
  });
​
  return (
    <div>
      <p>You clicked {counter} times.</p>
      <button onClick={() => setCounter(counter + 1)}>Click me</button>
      <button onClick={onAlertButtonClick}>
        Show me the value in 3 seconds
      </button>
    </div>
  );
};
​
export default Counter;

结果如我们所期待,alert的是当前的值1。

我们可以把这个过程封装成一个custom hook,如下:

import { useEffect, useRef, useState } from "react";
​
const useRefState = <T>(
  initialValue: T
): [T, React.MutableRefObject<T>, React.Dispatch<React.SetStateAction<T>>] => {
  const [state, setState] = useState<T>(initialValue);
  const stateRef = useRef(state);
  useEffect(() => {
    stateRef.current = state;
  }, [state]);
  return [state, stateRef, setState];
};
​
export default useRefState;

尽管这个问题被巧妙地解决了,但它不优雅、hack味道浓,且丢失了函数编程风格。

怎么避免react hooks的常见问题

  1. 不要在useEffect里面写太多的依赖项,划分这些依赖项成多个单一功能的useEffect。其实这点是遵循了软件设计的“单一职责模式”。

  2. 如果你碰到状态不同步的问题,可以考虑下手动传递参数到函数。如:

   // showCount的count来自父级作用域 
   const [count,setCount] = useState(xxx); 
   function showCount(){ console.log(count) } 
   
   // showCount的count来自参数 
   const [count,setCount] = useState(xxx); 
   function showCount(c){ console.log(c) }

但这个也只能解决一部分问题,很多时候你不得不使用上述的useRef方案。

  1. 重视eslint-plugin-react-hooks插件的警告。

  2. 复杂业务的时候,使用Component代替hooks。