手写React-hooks核心原理

1,194 阅读5分钟

前言

react-hooks 是 React 16.8 的新增特性。它可以让我们在函数组件中使用 state 、生命周期以及其他 react 特性,而不仅限于 class 组件。这篇文章通过手写 react-hooks 的核心原理来带大家轻松掌握 react-hooks,希望能够打动屏幕面前的你。

useState

useState 会返回一个数组:一个 state,一个更新 state 的函数。

useState 类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并,而是直接替换

需要注意的是:

useState 支持我们在调用的时候直接传入一个值,来指定 state 的默认值,比如这样 useState(0), useState({ a: 1 }), useState([ 1, 2 ]),还支持我们传入一个函数,来通过逻辑计算出默认值

在state需要更新的时候也有两种方式 一个是通过一个新的 state 值更新,一个是通过函数式更新返回新的 state

// 保存状态的数组
let hookStates = [];
// 索引
let hookIndex = 0;

function useState(initState) {

  //判断传入的参数是否是函数
  if (typeof initState === "function") {
    hookStates[hookIndex] = initState();
  } else {
    hookStates[hookIndex] = hookStates[hookIndex] || initState;
  }
  
  // 使用闭包维护函数调用位置
  let currentIndex = hookIndex;
  function setState(newState) {
    // 判断传入的state是否为函数,如果是把prevState传入
    if (typeof newState === "function") {
      // 重新复制给newState
      newState = newState(hookStates[currentIndex]);
    }
    
    // 更新state
    hookStates[currentIndex] = newState;
    
    // 触发视图更新的函数
    render();
  }
  
  // 返回数组形式
  return [hookStates[hookIndex++], setState];
}

useEffect

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。与 componentDidMount 或 componentDidUpdate 不同的是,使用 useEffect 调度的 effect 不会阻塞浏览器更新视图,这让你的应用看起来响应更快。

需要注意的是useEffect中第二个参数的作用:

  • 什么都不传,组件每次 render 之后 useEffect 都会调用
  • 传入一个空数组 [], useEffect 只会调用一次
  • 传入一个非空数组,其中包括变量,只有这些变量变动时,useEffect 才会执行
//保存状态的数组
let hookStates = [];

//索引
let hookIndex = 0;

function useEffect(callback, dependiencies) {
  if (hookStates[hookIndex]) {
    // 非初始调用

    let lastdependiencies = hookStates[hookIndex];

    // 判断传入依赖项跟上一次是否相同
    let same = dependiencies.every((item, i) => {
      //判断item是不是对象
      if (
        typeof item === "object" &&
        typeof item !== "function" &&
        item !== null
      ) {
        return isObjectValueEqual(item, lastdependiencies[i]);
      } else {
        return item === lastdependiencies[i];
      }
    });

    if (same) {
      hookIndex++;
    } else {
      hookStates[hookIndex++] = dependiencies;
      callback();
    }
  } else {
    // 初始调用

    hookStates[hookIndex++] = dependiencies;
    callback();
  }
}

//比较两个对象是否一致
function isObjectValueEqual(a, b) {
  var aProps = Object.getOwnPropertyNames(a);
  var bProps = Object.getOwnPropertyNames(b);
  if (aProps.length != bProps.length) {
    return false;
  }
  for (var i = 0; i < aProps.length; i++) {
    var propName = aProps[i];

    var propA = a[propName];
    var propB = b[propName];

    //b里面没有propName这个key名
    if (!b.hasOwnProperty(propName)) return false;

    // 判断两边都有相同键名
    if (propA instanceof Object) {
      if (this.isObjectValueEqual(propA, propB)) {
        // 这里不能return ,后面的对象还没判断
      } else {
        return false;
      }
    } else if (propA !== propB) {
      return false;
    } else {
    }
  }
  return true;
}

useMemo

useMemo 把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

需要注意的是useMemo中第二个参数的作用:

  • 不传数组,每次更新都会重新计算
  • 空数组,只会计算一次
  • 依赖对应的值,当对应的值发生变化时,才会重新计算
// 保存状态的数组
let hookStates = [];
// 索引
let hookIndex = 0;

function useMemo(factory, dependencies) {
  if (hookStates[hookIndex]) {
    // 非首次调用
    let [lastMemo, lastDependencies] = hookStates[hookIndex];

   // 判断传入依赖项跟上一次是否相同
    let same = dependiencies.every((item, i) => {
      //判断item是不是对象
      if (
        typeof item === "object" &&
        typeof item !== "function" &&
        item !== null
      ) {
        return isObjectValueEqual(item, lastdependiencies[i]);
      } else {
        return item === lastdependiencies[i];
      }
    });
    
    if (same) {
      hookIndex++;
      return lastMemo;
    } else {
      // 只要有一个依赖变量不一样的话
      let newMemo = factory();
      hookStates[hookIndex++] = [newMemo, dependencies];
      return newMemo;
    }
  } else {
    // 首次调用
    let newMemo = factory();
    hookStates[hookIndex++] = [newMemo, dependencies];
    return newMemo;
  }
}

//比较两个对象是否一致
function isObjectValueEqual(a, b) {
    ...
}

useCallback

useCallback 接收一个内联回调函数参数和一个依赖项数组(子组件依赖父组件的状态,即子组件会使用到父组件的值) ,useCallback 会返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新

useCallback 把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

需要注意的是useCallback中第二个参数的作用:

  • 不传数组,每次更新都会重新计算
  • 空数组,只会计算一次
  • 依赖对应的值,当对应的值发生变化时,才会重新计算
// 保存状态的数组
let hookStates = [];
// 索引
let hookIndex = 0;

function useCallback(callback, dependencies) {
  if (hookStates[hookIndex]) {
    // 非首次
    let [lastCallback, lastDependencies] = hookStates[hookIndex];

   // 判断传入依赖项跟上一次是否相同
    let same = dependiencies.every((item, i) => {
      //判断item是不是对象
      if (
        typeof item === "object" &&
        typeof item !== "function" &&
        item !== null
      ) {
        return isObjectValueEqual(item, lastdependiencies[i]);
      } else {
        return item === lastdependiencies[i];
      }
    });
    
    if (same) {
      hookIndex++;
      return lastCallback;
    } else {
      // 只要有一个依赖变量不一样的话
      hookStates[hookIndex++] = [callback, dependencies];
      return callback;
    }
  } else {
    // 首次调用
    hookStates[hookIndex++] = [callback, dependencies];
    return callback;
  }
}

//比较两个对象是否一致
function isObjectValueEqual(a, b) {
    ...
}

memo

memo 类似于PureCompoent 作用是优化组件性能,防止组件触发重渲染, memo 针对 一个组件的渲染是否重复执行

function memo(OldFunComp) {
  return class extends React.PureComponent {
    render() {
      return <OldFunComp {...this.props} />;
    }
  };
}

useContext

useContext 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值useContext(MyContext), 只是让你能够读取 context 的值以及订阅 context 的变化。仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

function useContext(context) {
  return context._currentValue;
}

// 父组件
const CountCtx = React.createContext();
function ParentComp() {
  const [state, setState] = React.useState({ number: 0 });
  return (
    <CountCtx.Provider value={{ state, setState }}>
      <Child />
    </CountCtx.Provider>
  );
}

// 子组件
function Child() {
  let { state, setState } = useContext(CountCtx);
  return (
    <div>
      <p>{state.number}</p>
      <button onClick={() => setState({ number: state.number + 1 })}>
        add
      </button>
    </div>
  );
}

useRef

useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数 useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的 ref 对象都是同一个(注意使用 React.createRef ,每次重新渲染组件都会重新创建 ref)

let lastRef;

function useRef(value) {
  lastRef = lastRef || { current: value };
  return lastRef;
}

useReducer

useReducer 接受类型为 (state, action) => newState 的 reducer,并返回与 dispatch 方法配对的当前状态。useReducer 和 redux 中 reducer 很像 useState 内部就是靠 useReducer 来实现的

// 保存状态的数组
let hookStates = [];
// 索引
let hookIndex = 0;

function useReducer(reducer, initState) {
  hookStates[hookIndex] = hookStates[hookIndex] || initState;

  let currentIndex = hookIndex;
  function dispatch(action) {
    hookStates[currentIndex] = reducer
      ? reducer(hookStates[currentIndex], action)
      : action;
    // 触发视图更新
    render();
  }
  return [hookStates[hookIndex++], dispatch];
}

// useState可以使用useReducer改写
function useState(initState) {
  return useReducer(null, initState);
}

hook 使用规则

使用 Hooks 的时候必须遵守 2 条规则:

  • 只能在代码的第一层调用 Hooks,不能在循环、条件分支或者嵌套函数中调用 Hooks。
  • 只能在Function Component或者自定义 Hook 中调用 Hooks,不能在普通的 JS 函数中调用。