2

1、react hooks出现的动机?

1.1、在组件之间复用状态逻辑很难

React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如render props高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。

你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。这使得在组件间或社区内共享 Hook 变得更便捷。

在官网上有一个聊天程序例子,该程序中FriendStatus组件用于显示好友状态

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);       
  useEffect(() => {    
    function handleStatusChange(status) {      
        setIsOnline(status.isOnline);    
    }    
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    
    return () => {      
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    
    };  
  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

现在我们假设聊天应用中有一个联系人列表,当用户在线时需要把名字设置为绿色。我们可以把上面类似的逻辑复制并粘贴到FriendListItem组件中来,但这并不是理想的解决方案:

import React, { useState, useEffect } from 'react';

function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

相反,我们希望在FriendStatusFriendListItem之间共享逻辑。
目前为止,在 React 中有两种流行的方式来共享组件之间的状态逻辑:render props(指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术)和高阶组件(高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。高阶组件是参数为组件,返回值为新组件的函数),现在让我们来看看 Hook 是如何在让你不增加组件的情况下解决相同问题的。

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。例如,下面的useFriendStatus是我们第一个自定义的 Hook:

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

此处并未包含任何新的内容——逻辑是从上述组件拷贝来的。与组件中一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。

与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以use开头,这样可以一眼看出其符合Hook 的规则

此处useFriendStatus的 Hook 目的是订阅某个好友的在线状态。这就是我们需要将friendID作为参数,并返回这位好友的在线状态的原因。

现在我们已经把这个逻辑提取到useFriendStatus的自定义 Hook 中,然后就可以_使用它了:_

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

这段代码等价于原来的示例代码吗?等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。

自定义 Hook 必须以 “use” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了Hook 的规则

在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用_状态逻辑_的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

自定义 Hook 如何获取独立的 state?每次_调用_Hook,它都会获取独立的 state。由于我们直接调用了useFriendStatus,从 React 的角度来看,我们的组件只是调用了useStateuseEffect。 正如我们在之前章节了解到的一样,我们可以在一个组件中多次调用useStateuseEffect,它们是完全独立的。

1.2、复杂组件变得难以理解

我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在componentDidMountcomponentDidUpdate中获取数据。但是,同一个componentDidMount中可能也包含很多其它的逻辑,如设置事件监听,而之后需在componentWillUnmount中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。

为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

在显示好友是否在线的FriendStatus组件中,从 class 中 props 读取friend.id,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

但是当组件已经显示在屏幕上时,friend prop 发生变化时会发生什么?我们的组件将继续展示原来的好友状态。这是一个 bug。而且我们还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。

在 class 组件中,我们需要添加componentDidUpdate来解决这个问题:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {    
      // 取消订阅之前的 friend.id    
      ChatAPI.unsubscribeFromFriendStatus(      
          prevProps.friend.id,      
          this.handleStatusChange    
      );    
      // 订阅新的 friend.id    
      ChatAPI.subscribeToFriendStatus(      
          this.props.friend.id,      
          this.handleStatusChange    
      );  
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

忘记正确地处理componentDidUpdate是 React 应用中常见的 bug 来源。

现在看一下使用 Hook 的版本:

function FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

它并不会受到此 bug 影响。(虽然我们没有对它做任何改动。)

并不需要特定的代码来处理更新逻辑,因为useEffect_默认_就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 运行第一个 effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 运行下一个 effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 运行下一个 effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect

此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。

1.3、难以理解的 class

除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中this的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。

export default App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            count: 0
        }
    }
    
    handle() {
        this.setState((preState) => ({
           count: preState.count + 1 
        }))
    }
    
    render() {
        return <button onClick={this.handle.bind(this)}></button>
    }
}

使用函数组件就不用理会那些this指针了

const App = props => {
    const [count, setCount] = useState(0)
    
    return <button onClick={() => {setCount(count + 1)}}></button>
}

2、react hooks怎么用?

官网介绍非常详细,链接:https://react.docschina.org/d...

3、理解useState、useEffect

3.1、useState

1.最简单的 useState 用法是这样的:
demo1:https://codesandbox.io/s/v0nqm309q3

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

  return (
    <div>
      <div>{count}</div>
      <Button onClick={() => { setCount(count + 1); }}>
        点击
      </Button>
    </div>
  );
}

2.基于 useState 的用法,我们尝试着自己实现一个 useState:
demo2:https://codesandbox.io/s/myy5qvoxpp

function useState(initialValue) {
  var state = initialValue;
  function setState(newState) {
    state = newState;
    render();
  }
  return [state, setState];
}

3.这时我们发现,点击 Button 的时候,count 并不会变化,为什么呢?我们没有存储 state,每次渲染 Counter 组件的时候,state 都是新重置的。

自然我们就能想到,把 state 提取出来,存在 useState 外面。

demo3:https://codesandbox.io/s/q9wq6w5k3w

var _state; // 把 state 存储在外面

function useState(initialValue) {
  _state = _state || initialValue; // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
  function setState(newState) {
    _state = newState;
    render();
  }
  return [_state, setState];
}

到目前为止,我们实现了一个可以工作的 useState,至少现在来看没啥问题。

接下来,让我们看看 useEffect 是怎么实现的。

3.2、useEffect

useEffect 是另外一个基础的 Hook,用来处理副作用,最简单的用法是这样的:

demo4:https://codesandbox.io/s/93jp55qyp4

useEffect(() => {
    console.log(count);
}, [count]);

我们知道 useEffect 有几个特点:

  1. 有两个参数 callback 和 dependencies 数组
  2. 如果 dependencies 不存在,那么 callback 每次 render 都会执行
  3. 如果 dependencies 存在,只有当它发生了变化, callback 才会执行

我们来实现一个 useEffect

demo5:https://codesandbox.io/s/3kv3zlvzl1

let _deps; // _deps 记录 useEffect 上一次的 依赖

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray; // 如果 dependencies 不存在
  const hasChangedDeps = _deps
    ? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
    : true;
  /* 如果 dependencies 不存在,或者 dependencies 有变化*/
  if (hasNoDeps || hasChangedDeps) {
    callback();
    _deps = depArray;
  }
}

到这里,我们又实现了一个可以工作的 useEffect,似乎没有那么难。

此时我们应该可以解答一个问题:

Q:为什么第二个参数是空数组,相当于componentDidMount

A:因为依赖一直不变化,callback 不会二次执行。

3.3、Not Magic, just Arrays

到现在为止,我们已经实现了可以工作的 useState 和 useEffect。但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state 和 一个 _deps。比如

const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');

count 和 username 永远是相等的,因为他们共用了一个 _state,并没有地方能分别存储两个值。我们需要可以存储多个 _state 和 _deps。

如 《React hooks: not magic, just arrays》所写,我们可以使用数组,来解决 Hooks 的复用问题。

demo6:https://codesandbox.io/s/50ww35vkzl

代码关键在于:

  1. 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
  2. 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
  3. 如果还是不清楚,可以看下面的图。
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memoizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}

我们用图来描述 memoizedState 及 cursor 变化的过程。

3.3.1、 初始化


1

3.3.2、 初次渲染

2

3.3.3、 事件触发

341995505-f8d92cba61b2d29b_articlex (1).png

3.3.4、 Re Render

4

到这里,我们实现了一个可以任意复用的 useState 和 useEffect。

同时,也可以解答几个问题:

Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。

A:memoizedState 数组是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。

Q:自定义的 Hook 是如何影响使用它的函数组件的?

A:共享同一个 memoizedState,共享同一个顺序。

Q:“Capture Value” 特性是如何产生的?

A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

3.4、真正的 React 实现

虽然我们用数组基本实现了一个可用的 Hooks,了解了 Hooks 的原理,但在 React 中,实现方式却有一些差异的。

  • React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。
  type Hooks = {
      memoizedState: any, // 指向当前渲染节点 Fiber
      baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
      baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
      queue: UpdateQueue<any> | null,// UpdateQueue 通过
      next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
    }
     
    type Effect = {
      tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
      create: () => mixed, // 初始化 callback
      destroy: (() => mixed) | null, // 卸载 callback
      deps: Array<mixed> | null,
      next: Effect, // 同上 
    };
  • memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?

    我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

5

参考:
https://react.docschina.org/d...
https://github.com/brickspert...
https://juejin.im/post/5e53d9...
https://juejin.im/post/5be3ea...


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。