31

一、前言

知己知彼,才能百战百胜,所以我们想学好学深一个东西,一定要去了解他的由来,写过react的人大部分都应该了解redux,所以我们今天就来聊聊react hooks;请收下我这个包袱,学习是枯燥的,所以时不时的给自己找点乐子。为什么我要突然加这么一个梗也是有原因的,我记得redux是一位叫Dan的人写的,忘了记没记错,我们就先叫他Dan(诞)总吧,随着redux地位的提升,Dan总也顺其自然的进了facebook,负责react-hooks的开发,你看我这个梗是不是连上了;

二、为什么说redux和react-hooks有些渊源?

其实redux很好理解,基于发布订阅模式,在内部实现一个状态(鸡),然后加一些约定和限制,外部不可以直接改变这个状态,只能通过redux提供的一个代理方法去触发这个状态的改变,使用者可以通过redux提供的订阅方法订阅事件,当状态改变的时候,redux帮助我们发布这个事件,使得各个订阅者获得最新状态,是不是很简单,大道至简~

下面提供了个简版的redux代码,有兴趣的可以看一下

/**
 *
 * @param {*} reducer 处理状态方法
 * @param {*} preloadState 初始状态
 */
function createStore(reducer, preloadState) {
  /** 存储状态 */
  let state = preloadState;

  /** 存储事件 */
  const listeners = [];

  /** 获取状态方法,返回state */
  const getState = () => {
    return state;
  };

  /**
   * 订阅事件,将事件存储到listeners中
   * @param {*} listener 事件
   */
  const subscribe = (listener) => {
    listeners.push(listener);
  };

  /**
   * 派发action,发布事件
   * @param {*} action 动作
   */
  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach((listener) => listener());
  };

  /** 状态机 */
  const store = {
    getState,
    subscribe,
    dispatch,
  };

  return store;
}

export default createStore;

为什么说react-hooks跟redux有渊源,因为Dan总把redux的这种思想带到了react-hooks中,通过创建一个公共hookStates来存储所有的hooks,通过添加一个索引hookIndex来顺序执行每一个hook,所以react-hooks规定hooks不能使用在if语句和for语句中,来保持hooks按顺序执行,在react-hooks中有一个叫useReducer的hooks,正是按redux的这个思想实现的,而我们常用的useState只是useReducer的一个语法糖,有人可能会说这个hookStates会不会很大,所有的hooks都存在里边,毕竟一个项目中有那么多组件,我说的只是简单的实现原理,我们知道react在16的几个版本之后就加入了fiber的概念,在react内部实现中,会把每个组件的hookStates存放到fiber节点上,来维护各个节点的状态,react每次render之后都会把hookIndex置为0,来保证下次hooks执行是从第一个开始执行,每个hooks执行完也会把hookIndex++,下面是useReducer的简单实现:

/** @param hookStates 存储所有hooks */
const hookStates = [];

/** @param hookIndex 索引 */
let hookIndex = 0;

/**
 *
 * useReducer
 * @param {*} reducer 状态处理函数
 * @param {*} initialState 初始状态,可以传入函数实现懒加载
 * @returns
 */
function useReducer(reducer, initialState) {
  hookStates[hookIndex] =
    hookStates[hookIndex] || typeof initialState === "function"
      ? initialState()
      : initialState;

  /**
   *
   * dispatch
   * @param {*} action 动作
   * @returns
   */
  const dispatch = (action) => {
    hookStates[hookIndex] = reducer
      ? reducer(hookStates[hookIndex], action)
      : action;
    /** React内部方法实现重新render */
    scheduleUpdate();
  };
  return [hookStates[hookIndex++], dispatch];
}

前文说过useState是useReducer的语法糖,所以下面是useState的实现

/**
 *
 * useState 直接将reducer传为null,调用useReducer并返回
 * @param {*} initialState 初始化状态
 * @returns
 */
function useState(initialState) {
  return useReducer(null, initialState);
}

二、如何实现useState改变值之后立刻获取最新的状态?

之前铺垫了一堆,终于到正题了,本想直奔主题,一不小心变成了介绍react-hooks的文章,不过多了解一点终究不会亏什么,react-hooks的思想其实是同步逻辑,但是在react的合成事件中状态更新是异步的,看一下下面这个场景,使用useState声明了一个状态state,又分别声明了两个函数setT和func,在setT中,调用setstate更新状态,然后调用func函数,在func中直接打印当前状态state

function App() {
  const [state, setstate] = useState(0);

  const setT = () => {
    setstate(2);
    func();
  };

  const func = () => {
    console.log(state);
  };

  return (
    <div className="App">
      <header className="App-header">
        <button onClick={setT}>set 2</button>
      </header>
    </div>
  );
}

当我们点击set 2 按钮时,控制台打印的还是上一次的值0,而不是最新的2

因为在react合成事件中改变状态是异步的,出于减少render次数,react会收集所有状态变更,然后比对优化,最后做一次变更,在代码中可以看出,func的调用和setstate在同一个宏任务中,这是react还没有render,所以直接使用state获取的肯定是上一次闭包里的值0

有的人可能会说,直接将最新的值当作参数传递给func不就行了吗,对,这也是一种解决办法,但是有时不只是一个状态,可能要传递的参数很多,再有也是出于对react-hooks的深入研究,所以我选择通过自定义hooks实现在useState改变值之后立刻获取到最新的值,我们先看下实现的效果,代码变更如下:

function App() {
  const [state, setstate] = useState(0);

  const setT = () => {
    setstate(2);
    func();
  };
  /** 将func的方法传递给useSyncCallback然后返回一个新的函数 */
  const func = useSyncCallback(() => {
    console.log(state);
  });

  return (
    <div className="App">
      <header className="App-header">
        <button onClick={setT}>set 2</button>
      </header>
    </div>
  );
}

export default App;

效果如下:

可以看出当我们点击set 2按钮之后,控制台输出的直接是2,所以达到了在setstate之后立刻获取了最新状态

三、如何实现useSyncCallback?

先把代码放到这里,大家可以看一下

/*
 * @lastTime: 2021-03-05 15:29:11
 * @Description: 同步hooks
 */

import { useEffect, useState, useCallback } from 'react'

const useSyncCallback = callback => {
    const [proxyState, setProxyState] = useState({ current: false })

    const Func = useCallback(() => {
        setProxyState({ current: true })
    }, [proxyState])

    useEffect(() => {
        if (proxyState.current === true) setProxyState({ current: false })
    }, [proxyState])

    useEffect(() => {
        proxyState.current && callback()
    })

    return Func
}

export default useSyncCallback
/*
 * @lastTime: 2021-02-26 15:29:11
 * @param: callback为回调函数
 * @Description: 用法 const newFunc = useSyncCallback(yourCallback)
 */

在这里还需要介绍一个知识点useEffect,useEffect会在每次函数式组件render之后执行,可以通过传递第二个参数,配置依赖项,当依赖项变更useEffect才会更新,如果第二个参数传递一个空数组[],那么useEffect就会只执行一次,react-hooks正是使用useEffect来模拟componentDidMount和componentDidUpdate两个生命周期,也可以通过在useEffect中return一个函数模拟componentWillUnmount,那么useEffect是如何实现在render之后调用的呢,原理就是在useEffect hooks中会封装一个宏任务,然后把传进来的回调函数放到宏任务中去执行,下面实现了一个简版的useEffect:

/** @param hookStates 存储所有hooks */
const hookStates = [];

/** @param hookIndex 索引 */
let hookIndex = 0;

/**
 *
 * useReducer
 * @param {*} callback 副作用函数
 * @param {*} dependencies 依赖项
 * @returns
 */
function useEffect(callback, dependencies) {
  /** 判断当前索引下hookStates是否存在值 */
  if (hookStates[hookIndex]) {
    /** 如果存在就取出,并结构出destroyFunc销毁函数,和上一次依赖项 */
    const [destroyFunc, lastDep] = hookStates[hookIndex];
    /** 判断当前依赖项中的值和上次依赖项中的值有没有变化 */
    const isSame =
      dependencies &&
      dependencies.every((dep, index) => dep === lastDep[index]);
    if (isSame) {
      /** 如果没有变化就把索引加一,hooks向后遍历 */
      hookIndex++;
    } else {
      /** 如果有变化,并且存在销毁函数,就先调用销毁函数 */
      destroyFunc && destroyFunc();
      /** 创建一个新的宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
      setTimeout(() => {
        const destroyFunction = callback();
        hookStates[hookIndex++] = [destroyFunction, dependencies];
      });
    }
  } else {
    /** 如果hookStates中不存在,证明是首次执行,直接创建一个宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
    setTimeout(() => {
      const destroyFunction = callback();
      hookStates[hookIndex++] = [destroyFunction, dependencies];
    });
  }
}

我已经将useEffect的实现步骤写到了注释里,有兴趣的可以看一下,接下来我们就来使用useEffect实现我们想要的结果,因为useEffect是在react组件render之后才会执行,所以在useEffect获取的状态一定是最新的,所以利用这一点,把我们写的函数放到useEffect执行,函数里获取的状态就一定是最新的

  useEffect(() => {
        proxyState.current && callback()
    })

首先,在useSyncCallback中创建一个标示proxyState,初始的时候会把proxyState的current值赋成false,在callback执行之前会先判断current是否为true,如果为true就允许callback执行,若果为false,就跳过不执行,因为useEffect在组件render之后,只要依赖项有变化就会执行,所以我们无法掌控我们的函数执行,在useSyncCallback中创建一个新的函数Func,并返回,通过这个Func来模拟函数调用,

 const [proxyState, setProxyState] = useState({ current: false })

在这个函数中我们要做的就是变更prxoyState的current值为true,来使得让callback被调用的条件成立,同时触发react组件render这样内部的useEffect就会执行,随后调用callback实现我们想要的效果。

四、小结

因为在工作中遇到了这个场景,所以写了此次分享,通过实现一个自定义hooks来达到给组内人复用,解决类似的问题,如果机智的你发现了我的小错误,还请指正,最后来句鸡汤 “春风得意时布好局,四面楚歌时有退路”,持续学习~


我来人间一趟
74 声望8 粉丝