React Hooks 设计思想

前端大学

共 7266字,需浏览 15分钟

 · 2020-08-26

作者:繁星https://zhuanlan.zhihu.com/p/103692400

聊聊 React 的 class 组件

组件是 React 应用的构建块,自上而下的数据流结合组件可以将 UI 解构为独立且可复用的单元。组件主要做的事情主要有以下三点:

  • 将传入的 props 和 内部 state 渲染到页面上;

  • 管理内部 state,并根据 state 变化渲染出最新的结果;

  • 处理与组件外部的交互;

假如现在有一个新闻列表页面,列表的每一项都包含有标题、概要、详情和缩略图,如图所示:

只是渲染内容。如果不考虑查看详情这个交互,新闻列表的每一项是很纯的,也就是 props 传入什么数据,就能渲染出一一对应的结果:

  1. let NewsItem = (props) => {

  2. return (

  3. <img src={props.imgUrl} />

  4. {props.title}h2>

  5. {props.summary}p>

  6. <p style={{display: 'none'}}>{props.detail}p>

  7. 查看详情a>

  8. div>

  9. li>

  10. )

  11. }

要考虑查看详情这个交互,就必须在 NewsItem 里加入一个 isDetailShow 的 state 来表示新闻摘要与详情的互斥显示。到目前为止,NewsItem 还是很纯的,并没有和外部有交互。

要实现新闻图片的懒加载,只有 NewsItem 进入可视区时才将 img 的 src 替换为真实的 url,这就要求 NewsItem 必须监听浏览器事件,并在组件被卸载时移除这些监听(防止内存泄漏)。此时,NewsItem 便不是一个纯的组件了,因为与外部有了交互,这种与外部的交互被称为副作用(函数式编程里没有任何副作用的函数被称为纯函数)。

组件的副作用是不可避免的,最常见的有 fetch data,订阅事件,进行 DOM 操作,使用其他 JavaScript 库(比如 jQuery,Map 等)。在这个例子中,NewsItem 并没有 fetch data,相关职责由不纯的父组件来承担。

综上,我们的组件需要 state 来存储一定的逻辑状态,并且需要可以访问并更改 state 的方法函数。

class 就是一个很好的表现形式:要渲染的内容(props 或 state)放在类的属性里,那些处理用户交互的回调函数和生命周期函数放在类的方法里。方法与属性通过 class 的形式建立了关联,有能力访问和更改属性。回调函数通过更改对应属性处理用户操作,生命周期函数则给予开发者处理组件与外部的交互能力(处理副作用)。

这样通过 class 组件,ReactDOM 就能做到渲染数据,绑定事件,并在不同的生命周期调用开发者所编写的代码,按需求将数据渲染成 HTML DOM,然后被浏览器渲染展示出来。

将组件渲染粗暴地分为若干个阶段,通过生命周期函数处理副作用会带来一些问题:

重复逻辑,被吐槽最多的例子如下:

  1. async componentDidMount() {

  2. const res = await get(`/users`);

  3. this.setState({ users: res.data });

  4. };


  5. async componentDidUpdate(prevProps) {

  6. if (prevProps.resource !== this.props.resource) {

  7. const res = await get(`/users`);

  8. this.setState({ users: res.data });

  9. }

  10. };

同一职责代码有可能需要被强行分拆到不同的生命周期,例如同一个事件的订阅与取消订阅;

一部分代码被分割到不同生命周期中,会导致组件没有优雅的复用 state 逻辑代码的能力,高阶组件或 render props 等模式引入了嵌套,复杂且不灵活;

越来越多逻辑被放入不同生命周期函数中,这种组织方式导致代码越来越复杂难懂;

除了这些,class 组件中的 this 也常被人们拿出来吐槽。那么,是否有更优雅的设计呢?

闭包为什么在某种程度上能取代 class?

我们的程序在执行的时候主要做了两件事:

为了实现复用,我们将具有特定单一功能的逻辑放在函数里,这样既可以消灭掉重复代码,又可以让我们在思考问题时能够进行合理的分解,降低代码复杂度。

但是只有函数是不够的,函数是一个标准的输入-加工-输出模型,输入和输出的都是变量里所存储的数据,当一个系统的复杂度高到一定程度的时候,将函数与其所操作的数据(环境)关联起来就很有必要了。

注:函数式编程要求把I/O限制到最小,干掉所有不必要的读写行为,保持计算过程的单纯性。

最常见的将变量与函数关联起来方式有:

函数对于其词法环境(lexical environment)的引用共同构成闭包(closure),简单说,一个函数内部能够访问到函数外的变量,如果这 个函数内部引用了其外部的变量,且自身又被别处引用,那这个不会被销毁的函数就和它所引用的外部变量一起构成闭包。例如:

  1. // 模块化下可以将 makeCounter 内部代码放在 makeCounter.js 中,并将 return 改为 export

  2. const makeCounter = () => {

  3. let privateCounter = 0;


  4. function changeBy(val) {

  5. privateCounter += val;

  6. }


  7. return {

  8. increment: function() {

  9. changeBy(1);

  10. },

  11. decrement: function() {

  12. changeBy(-1);

  13. },

  14. value: function() {

  15. return privateCounter;

  16. }

  17. }

  18. };


  19. // 使用 makeCounter

  20. const counter = makeCounter();

  21. console.log(counter.value()); /* logs 0 */

  22. counter.increment();

  23. counter.increment();

  24. console.log(counter.value()); /* logs 2 */

  25. counter.decrement();

  26. console.log(counter.value()); /* logs 1 */

看,我们使用闭包将变量 privateCounter 与几个函数关联了起来,从这点来讲能力与面向对象编程相同。

组件的 API 设计

API 的核心在于表达能力,对于 React 组件来说,就是如何让开发者将需求良好地表达出来,然后被 ReactDOM 识别并渲染。

class 组件和 functional 组件所要表达的内容是是一样的,只是表现形式不同。它们都努力做到了一点:将存储组件状态的 state 与处理这些 state 的方法关联起来。具体一点说就是一下三点:

2 中函数的执行是确定的,用户的操作触发某个事件后就会执行相应的回调函数,更改 state,触发新的渲染。开发者需要有能力控制 3 中的函数执行,确定要不要执行以及在什么时候执行。在 class 组件中,生命周期函数给开发者提供了这种控制能力。

那么,如果我们通过一套 API 设计实现以上三点且避开 class 组件的缺陷,提供更好的分离关注点能力,让代码复用更加简易,是不是一件很值得期待的事情呢?React Hooks 就是满足这些要求的新设计。

React Hooks 原理

先来看一个使用 React Hooks 的例子:

  1. function Counter() {

  2. const [counter, setCounter] = useState(0);


  3. function increment() {

  4. setCounter(counter+1);

  5. }


  6. function decrement() {

  7. setCounter(counter-1);

  8. }


  9. return (

  10. <div className="content">

  11. My Awesome Counter h1>

  12. <hr/>

  13. <h2 className="count">{counter}h2>

  14. <div className="buttons">

  15. <button onClick={increment}>+button>

  16. <button onClick={decrement}>-button>

  17. div>

  18. div>

  19. );

  20. }

是的,你看到了这个例子与闭包例子中的 makeCounter 十分相似。makerCounter 使用程序控制并通过 console 出结果,Counter 通过用户点击控制,输出包含结果且可以被渲染的组件。除了这点不同,其他部分代码原理是完全一致的,只是 Hook 进行了一些封装,让开发者编写代码体验更好。

我们来看下 useState 的简化实现:

  1. // React useState hooks

  2. const React = (function() {

  3. let hooks = [];

  4. let idx = 0;


  5. return {

  6. render(Component) {

  7. const C = Component();

  8. C.render();

  9. idx = 0; // reset for next render

  10. return C;

  11. },

  12. useState(initVal) {

  13. const state = hooks[idx] || initVal;

  14. const _idx = idx;

  15. const setState = newVal => {

  16. hooks[_idx] = newVal;

  17. };

  18. idx++;

  19. return [state, setState];

  20. }

  21. };

  22. })();


  23. // Component which use useState

  24. const { useState, render } = React;

  25. function Counter() {

  26. const [count, setCount] = useState(0);

  27. const [text, setText] = useState('apple');


  28. return {

  29. render() {

  30. console.log(`text: ${text}, count: ${count}`);

  31. },

  32. click() {

  33. setCount(count + 1);

  34. },

  35. type(type) {

  36. setText(type)

  37. }

  38. };

  39. }


  40. // simulate render

  41. const counter = render(Counter); // text: apple, count: 0

  42. counter.click();

  43. render(Counter); // text: apple, count: 1

  44. counter.type("pear");

  45. render(Counter); //text: pear, count: 1

代码很简单,这里不做解读,这里重点说几点:

正是因为 hooks 是这样实现的,我们在调用 hooks 的时候必须要严格保证每一次 render 都能获得一致的执行顺序,所以必须要做到:

到目前为止,我们已经可以通过 hooks 的形式管理 state,并通过调用包含 setState 的回调函数处理用户操作。剩下要解决的便是副作用的问题,useEffect 是 hooks 所提供的方案,下面来看一下 useEffect 的简化实现原理(并不完整):

  1. useEffect(cb, depArray) {

  2. const hasNoDeps = !depArray;

  3. hooks[idx] = hooks[idx] || {};

  4. const {deps, cleanup} = hooks[idx]; // undefined when first render

  5. const hasChanged = deps

  6. ? !depArray.every((el, i) => el === deps[i])

  7. : true;

  8. if (hasNoDeps || hasChanged) {

  9. cleanup && cleanup();

  10. hooks[idx].cleanup = cb();

  11. hooks[idx].deps = depArray;

  12. }

  13. idx++;

  14. }

完整简化代码地址:https://stackblitz.com/edit/behind-react-hook

useEffect 提供了一个函数(上面代码中的 cb)运行的容器,这个容器有以下几个特点:

通过将副作用相关代码放在 useEffect 的 cb 中,并在 cb 返回的函数里移除副作用,我们可以在一个 useEffect 中实现任何想要的生命周期控制:

这种设计最大的好处就是我们可以将单一职责的代码放在一个独立的 useEffect 容器里,而不是粗暴地将它们拆分在各个生命周期函数中。同时也要注意的是,useEffect 的 cb 必须要返回一个 cleanup 函数或者 undefined,所以不可以是 async 函数;

React Hooks 的优点

通过 Hooks 我们可以对 state 逻辑进行良好的封装,轻松做到隔离和复用,优点主要体现在:


本文主要介绍了 React Hooks 设计思想和优点,但 hooks 也是有不少”坑点“的,我们在使用的时候要利用好优点,努力避开”坑点“。后面我会单独写一篇文章来介绍 React Hooks 的实践。

点分享
点点赞
点在看
浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报