React.js学习-hooks原理简析

118 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情

让我们从实现两个简单的hook入手来探究hooks原理

示例代码

useState

useState用于在函数式组件中声明并保存一个变量,useState的使用是这样的:

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

console.log(count);

setCount(1);

setCount((pre) + pre + 1);

有几个特点:

  1. 接受一个函数或值作为变量的初始值
  2. 返回一个数组(元组),第一个参数是变量值,第二个参数是一个函数,可用来更新变量值
  3. 返回的更新函数支持传入一个函数,改函数的参数是当前的变量值

据此,可以实现一版简单的useState

function useState(initialState) {
    // 没有考虑传入一个函数的情况
    let state = initialState;
    
    const setState = (newState) => {
        state = newState;
    }
    
    return [state, setState];
}

但在使用的时候会发现,当调用setCount的时候,count 并不会变化,这是因为我们没有存储state,导致每次渲染组件的时候,state都会重新设置

为解决这个问题,会自然而然地想到,把state提取出来,存在useState外面:

let _state;

function useState(initialState) {
  _state = _state || initialState;

  const setState = (newState) => {
    _state = typeof newState === 'function' ? newState(_state) : newState;
  };

  return [
    _state,
    setState,
  ];
}

测试用例

至此,实现了一个简单的useState,后边会进一步完善

useEffect

useEffect的使用是这样的

useEffect(() => {
    // do something
});

useEffect(() => {
    // do something
}, [])

useEffect(() => {
    // do something
}, [deps])

useEffect的使用有几个特点:

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

根据使用方法和特点,可以做一个简单地实现:

let _deps;

function useEffect(callback, deps) {
  const hasNoDeps = !deps;
  const hasChangeDeps = _deps
    ? !deps?.every((dep, index) => _deps[index] === dep)
    : true;

  if (hasNoDeps || hasChangeDeps) {
    callback();
    _deps = deps;
  }
}

测试用例

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

优化

我们上边实现的两个简单的hook存在一个致命缺点,在一个组件内只能使用一次,对此,我们可以将_state_deps保存至一个全局数组memoizedState 中,并用一个变量存储当前memoizedState下标

let memoizedStates = []; // hooks存放在这个数组
let cursor = 0; // 当前memoizedState下标

function useState(initialState) {
  memoizedStates[cursor] = memoizedStates[cursor] || initialState;

  const currentCursor = cursor;
  const setState = (newState) => {
    memoizedStates[currentCursor] = typeof newState === 'function'
      ? newState(memoizedStates[currentCursor])
      : newState;
  };

  const res = [memoizedStates[cursor], setState];
  cursor += 1;

  return res;
}

function useEffect(callback, deps) {
  const hasNoDeps = deps === undefined;
  const preDeps = memoizedStates[cursor];
  const hasChangedDeps = preDeps
    ? !deps.every((dep, index) => dep === preDeps[index])
    : true;

  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedStates[cursor] = deps;
  }

  cursor += 1;
}

function resetCursor() {
  cursor = 0;
}

function resetMemoizedStates() {
  memoizedStates = [];
}

Not Magic, just Arrays

测试用例

真正的React实现

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

  1. React中是通过类似单链表的形式来代替数组的。通过next按顺序串联所有的hook
  2. memoizedStatecursor是存在哪里的?如何和每个函数组件一一对应的?

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

type Hooks = {
    // others
    memoizedState: any, // useState中 保存 state 信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和 deps | useRef 中保存的是 ref 对象
    next: Hook | null, // link 到下一个 hooks,通过 next 串联每一个hooks
}

解惑

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

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

Q. 为什么useEffect第二个参数是空数组,在组件更新时回调只会执行一次?

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

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

A. 共享同一个memoizedState,共享同一个顺序

Q. Capture Value 特性是如何产生的?

A. 每一次rerender的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。即每次渲染(执行),都有它自己的xxx

参考

React Hooks 原理