React Hooks原理&手动实现

1,453 阅读7分钟

简介

React Hooks自从推出后就一直深受前端开发者的喜爱,优雅的函数组件,让开发变的更加轻量,更关注业务本身。本文将介绍React Hooks中的常用的几个hooks手动实现模拟一下,更好的理解React Hooks的原理。

通过这篇文章,可以更好理解下面这些问题:

1.为什么useState不能在循环、判断和子函数里面使用
2.useEffect如何模拟class中的生命周期
3.如何实现一个自定义hooks

useState

官方定义:

/**
 * Returns a stateful value, and a function to update it.
 */
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
// convenience overload when first argument is omitted
/**
 * Returns a stateful value, and a function to update it.
 */
function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

从以上接口来看,useState接收一个初始值,返回一个被state管理的值和一个更新函数。

useState的作用:React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数

使用方法

让我们先看看使用的例子:

基于create-react-app 创建一个简单的工程

function App() {
  const [count, setCount] = useState(0);

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

这样当点击按钮时,页面会根据点击次数逐步增加:

image.png

那么这个useState干了什么呢?这是官方的一个解释

调用 useState 方法的时候做了什么?  它定义一个 “state 变量”。我们的变量叫 count, 但是我们可以叫他任何名字,比如 name。这是一种在函数调用时保存变量的方式 —— useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

useState 需要哪些参数?  useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0 作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 useState() 两次即可。)

useState 方法的返回值是什么?  返回值为:当前 state 以及更新 state 的函数。这就是我们写 const [count, setCount] = useState() 的原因。这与 class 里面 this.state.count 和 this.setState 类似,唯一区别就是你需要成对的获取它们。

手动实现

下面我们简单的手写一个useState的实现

既需要每次刷新都保留原有值,又能在给新值的时候主动刷新页面

var  _memoizedState // 在全局存储的state
function useState(initialState) {
  var state = _memoizedState||initialState;
  function setState(newState) {
    _memoizedState = newState;
    render();
  }
  return [state, setState];
}
//实现一个基础的render函数来触发刷新
const rootElement = document.getElementById("root");
function render() {
  ReactDOM.render(<App />, rootElement);
}
render();

运行起来看到跟原来实际的useState效果一致,那接下来我们看下useEffect

useEffect

官方定义

/**
 * Accepts a function that contains imperative, possibly effectful code.
 *
 * @param effect Imperative function that can return a cleanup function
 * @param deps If present, effect will only activate if the values in the list change.
 *
 */
 function useEffect(effect: EffectCallback, deps?: DependencyList): void;

可以看到useEffect接收一个callback和一个ReadonlyArray<any>的DependencyList数组,useEffect的官方用法描述为:可以让你在函数组件中执行副作用操作,所谓副作用就是数据获取,设置订阅以及手动更改 React 组件中的 DOM,比如修改document.title或者获取网络数据等等。

使用例子

function App() {
  const [count, setCount] = useState(0);
  useEffect(()=>{
    document.title = `You clicked ${count} times`;
  },[count])
  return (
    <div>
      <div>{count}</div>
      <Button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        点击
      </Button>
    </div>
  );
}

基于上面useState的例子,我们增加一个useEffect,其中第二个参数是[count],这样当count变更的时候页面标题就会变成You clicked ${count} times

image.png

useEffect的用法:

  1. 如果 DependencyList 不存在或者为[],那么 callback 每次 render 都会执行
  2. 如果 DependencyList 存在,只有当数字内部发生了变化, callback 才会执行

官方对useEffect的工作流程解释如下:

useEffect 做了什么?  通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffect  将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗?  是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

手动实现

var _deps; 
function useEffect(callback, deps) {
  /* 如果 deps 不存在,或者 deps 有变化,或者第一次_deps不存在*/
  if (!_deps ||!deps||!deps.every((el, i) => el === _deps[i])) {
    callback();
    _deps = deps;
  }
}

运行后即可看到实际效果,符合预期

至此我们简单的实现了一下useState和useEffect,大家也可能发现一个问题,就是当多个state的时候,比如:

  const [count, setCount] = useState(0);
  const [name, setName] = useState('leo');

我们的useState就失效了,接下来我们做下简单的改造

支持多state改造

let memoizedState = [];
let index = 0; //memoizedState计数
//基础实现useEffect
function useEffect(callback, deps) {
  const hasNoDeps = !deps;
  const _deps = memoizedState[index];
  const hasChangedDeps = _deps
    ? !deps.every((el, i) => el === _deps[i])
    : true;

  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[index] = deps;
  }
  index++;
}

//基础实现useState
function useState(initialState) {
  memoizedState[index] = memoizedState[index] || initialState;
  const currentIndex = index;
  function setState(newState) {
    memoizedState[currentIndex] = newState;
    render();
  }
  return [memoizedState[index++], setState];
}
const rootElement = document.getElementById("root");
function render() {
  index = 0;
  ReactDOM.render(<App />, rootElement);
}

1、初始化时会把所有的useState和所有的useEffect按照顺序存储到memoizedState,同时闭包保存currentIndex

2、渲染时会依次拿出上一次值

这里全局采用一个数组和一个计数来存储state,同时内部采用闭包来记录他存在的index。同时也解释了最上面的问题:

为什么useState不能在循环、判断和子函数里面使用 如果放到循环判断和子函数内,这个index就会错乱,无法再维护原因的memoizedState

回答问题

1.为什么useState不能在循环、判断和子函数里面使用
  答:如果放到循环判断和子函数内,这个index就会错乱,无法再维护原因的memoizedState
2.useEffect如何模拟class中的生命周期
  答: 当useEffect没有第二个参数时,就类似class模式中的componentDidMount 和 componentDidUpdate
      当useEffect第二个参数为[]时,类似class模式中的componentDidMount
      同时useEffect还有个返回函数,用来当组件被卸载的时候做一些取消注册作用
        useEffect(()=>{
            return ()=>{
                  //componentWillUnmount
            }
         })
3.如何实现一个自定义hooks
  答: 方法1:可以实际操作与useState一样的memoizedState数组来实现自己的hooks(需要知道内部实现和变量名,不推荐)
      方法2:内部包装官方的hooks来定制自己的hooks(推荐做法,后续会更新文章讲自定义hooks)

目前官方的实现:

采用了一个单向链表结构,通过next定位下一个

export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
|};

export type Effect = {|
  tag: HookFlags,
  create: () => (() => void) | void,
  destroy: (() => void) | void,
  deps: Array<mixed> | null,
  next: Effect,
|};

总结

至此,React hooks的基本原理已经介绍完了,同时手动实现了一下useState和useEffect。当然官方还有很多常用的hooks,比如useMemo、useCallback、useReducer等,大家可以在官网查看具体使用方式 zh-hans.reactjs.org/docs/hooks-…

后面会更新一些常用的hooks使用和一些自定义hooks,感谢大家阅读,有问题欢迎留言和私信。