完全理解React Hooks

看完这篇文章,希望你可以从整体上对 Hooks 有个认识,并对其设计哲学有一些理解

React组件设计理论

React以一种全新的编程范式定义了前端开发约束,它为视图开发带来了一种全新的心智模型:

  • React认为,UI视图是数据的一种视觉映射,即UI = F(DATA),这里的F需要负责对输入数据进行加工、并对数据的变更做出响应
  • 公式里的F在React里抽象成组件,React是以组件(Component-Based)为粒度编排应用的,组件是代码复用的最小单元
  • 在设计上,React采用props属性来接收外部的数据,使用state属性来管理组件自身产生的数据(状态),而为了实现(运行时)对数据变更做出响应需要,React采用基于类(Class)的组件设计!

除此之外,React认为组件是有生命周期的,因此开创性地将生命周期的概念引入到了组件设计,从组件的create到destory提供了一系列的API供开发者使用
这就是React组件设计的理论基础,我们最熟悉的React组件一般长这样:

// React基于Class设计组件
class MyConponent extends React.Component {
  // 组件自身产生的数据
  state = {
    counts: 0
  }

  // 响应数据变更
  clickHandle = () => {
    this.setState({ counts: this.state.counts++ });
    if (this.props.onClick) this.props.onClick();
  }

  // lifecycle API
  componentWillUnmount() {
    console.log('Will mouned!');
  }

    // lifecycle API
  componentDidMount() {
    console.log('Did mouned!');
  }

  // 接收外来数据(或加工处理),并编排数据在视觉上的呈现
  render(props) {
    return (
      <>
        
Input content: {props.content}, btn click counts: {this.state.counts}
); } }

Class Component的问题

组件复用困局

组件并不是单纯的信息孤岛,组件之间是可能会产生联系的,一方面是数据的共享,另一个是功能的复用:

  • 对于组件之间的数据共享问题,React官方采用单向数据流(Flux)来解决
  • 对于(有状态)组件的复用,React团队给出过许多的方案,早期使用CreateClass + Mixins,在使用Class Component取代CreateClass之后又设计了Render Props和Higher Order Component,直到再后来的Function Component+ Hooks设计,React团队对于组件复用的探索一直没有停止

HOC使用(老生常谈)的问题

  • 嵌套地狱,每一次HOC调用都会产生一个组件实例
  • 可以使用类装饰器缓解组件嵌套带来的可维护性问题,但装饰器本质上还是HOC
  • 包裹太多层级之后,可能会带来props属性的覆盖问题

Render Props

  • 数据流向更直观了,子孙组件可以很明确地看到数据来源
  • 但本质上Render Props是基于闭包实现的,大量地用于组件的复用将不可避免地引入了callback hell问题
  • 丢失了组件的上下文,因此没有this.props属性,不能像HOC那样访问this.props.children

Javascript Class的缺陷

this的指向(语言缺陷)

class People extends Component {
  state = {
    name: 'dm',
    age: 18,
  }

  handleClick(e) {
    // 报错!
    console.log(this.state);
  }

  render() {
    const { name, age } = this.state;
    return (
My name is {name}, i am {age} years old.
); } }

createClass不需要处理this的指向,到了Class Component稍微不慎就会出现因this的指向报错。

编译大小(还有性能)问题

// Class Component
class App extends Component {
  state = {
    count: 0
  }

  componentDidMount() {
    console.log('Did mount!');
  }

  increaseCount = () => {
    this.setState({ count: this.state.count + 1 });
  }

  decreaseCount = () => {
    this.setState({ count: this.state.count - 1 });
  }

  render() {
    return (
      <>
        

Counter

Current count: {this.state.count}

); } } // Function Component function App() { const [ count, setCount ] = useState(0); const increaseCount = () => setCount(count + 1); const decreaseCount = () => setCount(count - 1); useEffect(() => { console.log('Did mount!'); }, []); return ( <>

Counter

Current count: {count}

); }

Class Component编译结果(Webpack):

var App_App = function (_Component) {
  Object(inherits["a"])(App, _Component);

  function App() {
    var _getPrototypeOf2;
    var _this;
    Object(classCallCheck["a"])(this, App);
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }
    _this = Object(possibleConstructorReturn["a"])(this, (_getPrototypeOf2 = Object(getPrototypeOf["a"])(App)).call.apply(_getPrototypeOf2, [this].concat(args)));
    _this.state = {
      count: 0
    };
    _this.increaseCount = function () {
      _this.setState({
        count: _this.state.count + 1
      });
    };
    _this.decreaseCount = function () {
      _this.setState({
        count: _this.state.count - 1
      });
    };
    return _this;
  }
  Object(createClass["a"])(App, [{
    key: "componentDidMount",
    value: function componentDidMount() {
      console.log('Did mount!');
    }
  }, {
    key: "render",
    value: function render() {
      return react_default.a.createElement(/*...*/);
    }
  }]);
  return App;
}(react["Component"]);

Function Component编译结果(Webpack):

function App() {
  var _useState = Object(react["useState"])(0),
    _useState2 = Object(slicedToArray["a" /* default */ ])(_useState, 2),
    count = _useState2[0],
    setCount = _useState2[1];
  var increaseCount = function increaseCount() {
    return setCount(count + 1);
  };
  var decreaseCount = function decreaseCount() {
    return setCount(count - 1);
  };
  Object(react["useEffect"])(function () {
    console.log('Did mount!');
  }, []);
  return react_default.a.createElement();
}
  • Javascript实现的类本身比较鸡肋,没有类似Java/C++多继承的概念,类的逻辑复用是个问题
  • Class Component在React内部是当做Javascript Function类来处理的
  • Function Component编译后就是一个普通的function,function对js引擎是友好的

Function Component缺失的功能

不是所有组件都需要处理生命周期,在React发布之初Function Component被设计了出来,用于简化只有render时Class Component的写法。

Function Component是纯函数,利于组件复用和测试
Function Component的问题是只是单纯地接收props、绑定事件、返回jsx,本身是无状态的组件,依赖props传入的handle来响应数据(状态)的变更,所以Function Component不能脱离Class Comnent来存在!

function Child(props) {
  const handleClick = () => {
    this.props.setCounts(this.props.counts);
  };

  // UI的变更只能通过Parent Component更新props来做到!!
  return (
    <>
      
{this.props.counts}
); } class Parent extends Component() { // 状态管理还是得依赖Class Component counts = 0 render () { const counts = this.state.counts; return ( <>
sth...
this.setState({counts: counts++})} /> ); } }

所以,Function Comonent是否能脱离Class Component独立存在,关键在于让Function Comonent自身具备状态处理能力,即在组件首次render之后,“组件自身能够通过某种机制再触发状态的变更并且引起re-render”,而这种“机制”就是Hooks

Hooks的出现弥补了Function Component相对于Class Component的不足,让Function Component取代Class Component成为可能。

Function Component + Hooks组合

1、功能相对独立、和render无关的部分,可以直接抽离到hook实现,比如请求库、登录态、用户核身、埋点等等,理论上装饰器都可以改用hook实现(如react-use,提供了大量从UI、动画、事件等常用功能的hook实现)。

case:Popup组件依赖视窗宽度适配自身显示宽度、相册组件依赖视窗宽度做单/多栏布局适配

function useWinSize() {
  const html = document.documentElement;
  const [ size, setSize ] = useState({ width: html.clientWidth, height: html.clientHeight });

  useEffect(() => {
    const onSize = e => {
      setSize({ width: html.clientWidth, height: html.clientHeight });
    };

    window.addEventListener('resize', onSize);

    return () => {
      window.removeEventListener('resize', onSize);
    };
  }, [ html ]);

  return size;
}

// 依赖win宽度,适配图片布局
function Article(props) {
  const { width } = useWinSize();
  const cls = `layout-${width >= 540 ? 'muti' : 'single'}`;

  return (
    <>
      
{props.content}
recommended thumb list
); } // 弹层宽度根据win宽高做适配 function Popup(props) { const { width, height } = useWinSize(); const style = { width: width - 200, height: height - 300, }; return (
{props.content}
); }

2、有render相关的也可以对UI和功能(状态)做分离,将功能放到hook实现,将状态和UI分离

case:表单验证

function App() {
  const { waiting, errText, name, onChange } = useName();
  const handleSubmit = e => {
    console.log(`current name: ${name}`);
  };

  return (
    
<> Name: {waiting ? "waiting..." : errText || ""}

); }

React Hooks 的本质

稍微复杂点的项目肯定是充斥着大量的 React 生命周期函数(注意,即使你使用了状态管理库也避免不了这个),每个生命周期里几乎都承担着某个业务逻辑的一部分,或者说某个业务逻辑是分散在各个生命周期里的。


image.png

而 Hooks 的出现本质是把这种面向生命周期编程变成了面向业务逻辑编程,你不用再去关心本不该关心的生命周期。

image.png

一个 Hooks 演变

我们先假想一个常见的需求,一个 Modal 里需要展示一些信息,这些信息需要通过 API 获取且跟 Modal 强业务相关,要求我们:

  • 因为业务简单,没有引入额外状态管理库
  • 因为业务强相关,并不想把数据跟组件分开放
  • API 数据会随机变动,因此需要每次打开 Modal 才获取最新数据
  • 为了后期优化,不可以有额外的组件创建和销毁
    我们可能的实现如下:
class RandomUserModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: {},
      loading: false,
    };
    this.fetchData = this.fetchData.bind(this);
  }

  componentDidMount() {
    if (this.props.visible) {
      this.fetchData();
    }
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.visible && this.props.visible) {
      this.fetchData();
    }
  }

  fetchData() {
    this.setState({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(res => res.json())
      .then(json => this.setState({
        user: json.results[0],
        loading: false,
      }));
  }

  render() {
    const user = this.state.user;
    return (
      
        
        {this.state.loading ?
          
loading...
:
  • Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}
  • Gender: {user.gender}
  • Phone: {user.phone}
}
) } }

我们抽象了一个包含业务逻辑的 RandomUserModal,该 Modal 的展示与否由父组件控制,因此会传入参数 visible 和 handleCloseModal(用于 Modal 关闭自己)。

为了实现在 Modal 打开的时候才进行数据获取,我们需要同时在 componentDidMount 和 componentDidUpdate 两个生命周期里实现数据获取的逻辑,而且 constructor 里的一些初始化操作也少不了。

其实我们的要求很简单:在合适的时候通过 API 获取新的信息,这就是我们抽象出来的一个业务逻辑,为了这个业务逻辑能在 React 里正确工作,我们需要将其按照 React 组件生命周期进行拆解。这种拆解除了代码冗余,还很难复用。

下面我们看看采用 Hooks 改造后会是什么样:

function RandomUserModal(props) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() => {
    if (!props.visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [props.visible]);
  
  return (
    // View 部分几乎与上面相同
  );
}

很明显地可以看到我们把 Class 形式变成了 Function 形式,使用了两个 State Hook 进行数据管理(类比 constructor),之前 cDMcDU 两个生命周期里干的事我们直接在一个 Effect Hook 里做了(如果有读取或修改 DOM 的需求可以看 这里)。做了这些,最大的优势是代码精简,业务逻辑变的紧凑,代码行数也从 50+ 行减少到 30+ 行。

Hooks 的强大之处还不仅仅是这个,最重要的是这些业务逻辑可以随意地的的抽离出去,跟普通的函数没什么区别(仅仅是看起来没区别),于是就变成了可以复用的自定义 Hook。具体可以看下面的进一步改造:

// 自定义 Hook
function useFetchUser(visible) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);
  
  React.useEffect(() => {
    if (!visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [visible]);
  return { user, loading };
}

function RandomUserModal(props) {
  const { user, loading } = useFetchUser(props.visible);
  
  return (
    // 与上面相同
  );
}

这里的 useFetchUser 为自定义 Hook,它的地位跟自带的 useState 等比也没什么区别,你可以在其它组件里使用,甚至在这个组件里使用两次,它们会天然地隔离开。

业务逻辑复用

这里说的业务逻辑复用主要是需要跨生命周期的业务逻辑。单单按照组件堆积的形式组织代码虽然也可以达到各种复用的目的,但是会导致组件非常复杂,数据流也会很乱。组件堆积适合 UI 布局,但是不适合逻辑组织。为了解决这些问题,在 React 发展过程中,产生了很多解决方案,我认知里常见的有以下几种:

Mixins

坏处远远大于带来的好处,因为现在已经不再支持,不多说,可以看看这篇文章:Mixins Considered Harmful。

Class Inheritance

官方 很不推荐此做法,实际上我也没真的看到有人这么做。

High-Order Components (HOC)

React 高阶组件 在封装业务组件上简直是屡试不爽,它的实现是把自己作为一个函数,接受一个组件,再返回一个组件,这样它可以统一处理掉一些业务逻辑并达到复用目的。

比较常见的一个就是 react-redux 里的 connect 函数:

image.png

但是它也被很多人吐槽嵌套问题:


image.png

Render Props

Render Props 其实很常见,比如 React Context API:

class App extends React.Component {
  render() {
    return (
      
        
          {val => 
{val}
}
) } }

它的实现思路很简单,把原来该放「组件」的地方,换成了回调,这样当前组件里就可以拿到子组件的状态并使用。

但是,同样这会产生 Wrapper Hell 问题:


image.png

Hooks

Hooks 本质上面说了,是把面向生命周期编程变成了面向业务逻辑编程,写法上带来的优化只是顺带的。

这里,做一个类比,await/async 本质是把 JS 里异步编程思维变成了同步思维,写法上表现出来的特点就是原来的 Callback Hell 被打平了。

总结对比:

  • await/async 把 Callback Hell 干掉了,异步编程思维变成了同步编程思维
  • Hooks 把 Wrapper Hell 干掉了,面向生命周期编程变成了面向业务逻辑编程

这里不得不客观地说,HOC 和 Render Props 还是有存在的必要,一方面是支持 React Class,另一方面,它们不光适用于纯逻辑封装,很多时候也适合逻辑 + 组件的封装场景,虽然此时使用 Hooks 也可以,但是会显得啰嗦点。另外,上面诟病的最大的问题 Wrapper Hell,我个人觉得使用 Fragment 也可以基本解决。

状态盒子

首先,React Hooks 的设计是反直觉的,为什么这样说呢?可以先试着问自己:为什么 Hooks 只能在其它 Hooks 的函数或者 React Function 组件里?

在我们的认知里,React 社区一直推崇函数式、纯函数等思想,引入 Hooks 概念后的 Functional Component 变的不再纯了,useXxx 与其说是一条执行语句,不如说是一个声明。声明这里放了一个「状态盒子」,盒子有输入和输出,剩下的内部实现就一无所知,重要的是,盒子是有记忆的,下次执行到此位置时,它有之前上下文信息。

类比「代码」和「程序」的区别,前者是死的,后者是活的。表达式 c = a + b 表示把 ab 累加后的值赋值给 c,但是如果写成 c := a + b 就表示 c 的值由 ab 相加得到。看起来表述差不多,但实际上,后者隐藏着一个时间的维度,它表示的是一种联系,而不单单是个运算。这在 RxJS 等库中被大量使用。

image.png

这种声明目前是通过很弱的 use 前缀标识的(但是设计上会简洁很多),为了不弄错每个盒子和状态的对应关系,书写的时候 Hooks 需要 use 开头且放在顶层作用域,即不可以包裹 if/switch/when/try 等。如果你按文章开头引入了那个 ESLint Plugin 就不用担心会弄错了。

总结

这篇文章可能并没有一个很条理的目录结构,大多是一些个人理解和相关思考。因此,这不能替代你去看真正的文档了解更多。如果你看完后还是觉得废话太多,不知所云,那我希望你至少可以在下面几点上跟作者达成共鸣:

  • Hooks 本质是把面向生命周期编程变成了面向业务逻辑编程;
  • Hooks 使用上是一个逻辑状态盒子,输入输出表示的是一种联系;
  • Hooks 是 React 的未来,但还是无法完全替代原始的 Class。

参考:
https://zhuanlan.zhihu.com/p/92211533
https://segmentfault.com/a/1190000017182184

你可能感兴趣的:(完全理解React Hooks)