React hooks 简易版实现

233 阅读5分钟

前言

hook是React16.8的新特性,他可以让你在不编写class的情况下去使用state以及其他特性

最后附完整代码

准备阶段

我们先在 App 组件返回的一个对象去模拟用户点击 App().onClick()

function App() {
  const [num, updateNum] = useState(0);
  console.log('num: ' + num)
  return {
    onClick() {
      updateNum(pre => pre + 1);
    },
  }
}

准备这三个全局变量


let isMount = true;
let workInProgressHook = null;  // 代表着当前工作的节点
const fiber = {
  stateNode: App,  //当前这个节点
  memoizedState: null  // 初始值
}

  • isMount 用来判断当前是否为初始化阶段

  • 因为 fiber 是个链表结构,所以 workInProgressHook 用来记录当前工作到某个节点的标志

  • fiber 中的 stateNode 表示当前的组件 也就是 App , memoizedState 会存着当前这个组件上的状态

image.png

如果说第一次执行是 nhook 。第二次执行是 n-1hook (也就是说第二次少了一个hook,造成的原因可能是在 if 里写 hook ) 这种情况 react 就会报错误,原理也就是他是 .next 一直访问下一次hook 通过链表的形式记录每一次是哪个 hook,这样会出现 hook 的值错乱的情况

hooks 当中存在着挂载阶段和更新阶段

我们用 mountWorkInProgressHook 表示挂载阶段需要做的事

// 用来获取初始化阶段的hook 同时将workInprogressHook 指针指向当前的hook
function mountWorkInProgressHook() {
 let hook;
 hook = {
   next: null, // 代表的下一个 hook
   queue: { // 是一个队列的原因是因为 我们有可能在一次更新调用多次更新hook
     pending: null,  // 保存改变的状态
   }
 }
 if (!fiber.memoizedState) {
   // 如果是第一个hook
   fiber.memoizedState = hook; //将fiber的初始值设置为当前的hook
 } else {
   // 如果不是第一个hook    此时workInProgress代表上一次的hook
   workInProgressHook.next = hook // 给当前节点的 next 添加本次的 hook    ps: 因为存在着互相引用的关系 这里给workInProgressHook添加next  根fiber会更改
 }
 workInProgressHook = hook  // 将当前工作的节点更改为现在的hook

 return hook
}

这个函数主要做的任务就是 生成 hook 对象 同时将 workInprogressHook 指针指向当前的 hook

  1. 因为会有多个 hook 调用, 如果当前调用的是第一个 hook 也就是 fiber.memoizedStatenull , 创建 hook 节点,将 hook 节点赋值给 fiber.memoizedState
  2. 如果不是第一个 hook 则将当前的 hook 插入到上一次的节点后 也就是 workInProgressHook.next = hook

我们 updateWorkInProgressHook 表示更新阶段需要做的事


// 用来获取更新阶段的hook 同时将workInprogressHook 指针指向当前的hook
function updateWorkInProgressHook() {
  let hook;
  hook = workInProgressHook; // 更新阶段可以直接从workInprogressHook里取当前的hook
  workInProgressHook = workInProgressHook.next // 当前工作节点hook后移

  return hook
}

在这个函数里我们吧 workInProgressHook 赋值给当前的 hook 并且指针后移, 返回 hook

schedule 来模拟更新组件 ,更新完成,改变 isMount = false

// 用来模拟更新组件
function schedule() {
  workInProgressHook = fiber.memoizedState; // 每次重新执行将当前的工作进度指向 根 fiber 的 memoizedState
  const app = fiber.stateNode(); // 模拟组件重新执行
  isMount = false
  return app
}

window.app = schedule()

因为更新组件需要将 workInProgressHook 指向根 state 所以 workInProgressHook = fiber.memoizedState;

为了方便调用 在这里将 App 调用后的结果赋值给 app 返回 并且 我们可以在控制台用 app.onClick() 去模拟组件点击

useState


function useState(initialState) {
  let hook;
  if (isMount) {
    // 如果是初始化阶段, 初始化 当前这个 hook
    hook = mountWorkInProgressHook()
    hook.memoizedState = initialState;  // 设置初始值

  } else {
    // 更新阶段 这里只是将最新的hook
    hook = updateWorkInProgressHook()
  }

  let baseState = hook.memoizedState; // 当前的状态

  if (hook.queue.pending) {
    // 表示是需要更新 state

    let firstUpdate = hook.queue.pending;  // 这个变量表示当前移动到的action位置
    do {
      const action = firstUpdate.action; // 取到当前的action逻辑
      if (typeof action !== 'function') {
        baseState = action  // 如果传递过来的是值 则直接赋值给baseState
      } else {
        baseState = action(baseState)  // action 传入上一次的值 计算出新的值
      }
      firstUpdate = firstUpdate.next  // 移动当前action指向的位置
    } while (firstUpdate !== hook.queue.pending); // 如果当前的update是指向的第一个update 则表示遍历完毕 (用到了下面创建update为环状链表)

    hook.queue.pending = null; //  清除本次调用后存储的更新

  }
  hook.memoizedState = baseState;  //更改本次值为更新后的值

  return [
    baseState,
    dispatchAction.bind(null, hook.queue)
  ]
}


// setState
function dispatchAction(queue, action) {
  // 本次更新
  const update = {
    action,  // 状态改变的action
    next: null
  }

  // 这里存储为环状链表 会在上面用到判断
  if (queue.pending === null) {
    // 表示在本次更新 当前这个state是第一次更新

    update.next = update;  // 环状链表 u0.next -> u0
  } else {
    // u1 -> u0 -> u1
    // 不是第一次更新 就将本次的更新插入到队列的最前面,此时update是收集了之前更新后最新的 链表
    update.next = queue.pending.next;
    queue.pending.next = update; // 将队列指向最新的 update
  }

  // 将当前队列中的 pendding 指向本次更新创建的update
  queue.pending = update

  // 触发一次更新
  schedule()
}

  1. 先看 setState,也就是 dispatchAction 在这里创建了 update 也是一个链表结构的 因为有可能在一次更新中会调用多次更新,这里用链表收集

  2. useState 中我们用了 baseState 去记录当前的状态,并且利用当前节点 queue.queue.pending 有无值来判断当前是需要更新还是...

image.png

调用 onClick 后 可以看到 pending 用来收集更新的 action 依次调用,结构为环状链表

image.png

useRef

function useRef(initialValue) {
  let hook;
  if (isMount) {
    hook = mountWorkInProgressHook();
    hook.memoizedState = {
      current: initialValue
    }
  } else {
    hook = updateWorkInProgressHook()
  }

  return hook.memoizedState
}

useRef 的代码很简单:如果当前是mount阶段 创建{current: initValue},否则 取到内存中的 这块对象

因为每个useRef只会创建一个相同的对象 也就是 {current: initValue} 所以说ref在组件的生命周期内引用不变

image.png

useMemo

// 用来判断前后依赖性是否一样
function areHookInputsEqual(nextDeps, prevDeps) {

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    /**
     * 如果说两个数组长度不一样 则只会去判断共有的长度的数组值
     *
     * 也就是说 第一次为 [1, 2] 第二次为 [1] 则不会去更新
     *
     * 第一次为[1, 2] 第二次为[2]会去更新
     */

    if (Object.is(nextDeps[i], prevDeps[i])) {
      // 如果两个值是一样的 ,跳出本次循环
      continue;
    }
    // 如果不一样返回false
    return false;
  }
  // 遍历完还没跳出函数 则两个deps为一样的 返回true
  return true;
}

function useMemo(nextCreate, deps) {
  let hook;
  const nextDeps = deps === undefined ? null : deps; //默认不传递第二个参数会当null处理
  if (isMount) {
    hook = mountWorkInProgressHook();
  } else {
    hook = updateWorkInProgressHook();
    const prevState = hook.memoizedState;
    if (prevState[0] !== null && nextDeps !== null) {
      // 如果上一次的值和这一次的依赖项不是null
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果一样,返回上一次的值
        return prevState[0];
      }
    }
  }
  // 如果是mount 阶段或者是 更新阶段值发生变化了 则调用本次的fn 同时更新fiber中存储的状态和依赖
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

useCallback

// useCallback和useMemo的区别就是一个会吧函数调用之后的结果存下来  一个会存函数本身
function useCallback(nextCreate, deps) {
  let hook;
  const nextDeps = deps === undefined ? null : deps; //默认不传递第二个参数会当null处理
  if (isMount) {
    hook = mountWorkInProgressHook();
  } else {
    hook = updateWorkInProgressHook();
    const prevState = hook.memoizedState;
    if (prevState[0] !== null && nextDeps !== null) {
      // 如果上一次的值和这一次的依赖项不是null
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果一样,返回上一次的值
        return prevState[0];
      }
    }
  }
  // 如果是mount 阶段或者是 更新阶段值发生变化了 则调用本次的fn 同时更新fiber中存储的状态和依赖
  hook.memoizedState = [nextCreate, nextDeps];
  return hook.memoizedState[0];
}

完整代码

有兴趣的可以去自己试一试写一写

let isMount = true;
let workInProgressHook = null;  // 代表着当前工作的节点
const fiber = {
  stateNode: App,  //当前这个节点
  memoizedState: null  // 初始值
}
// 用来获取初始化阶段的hook 同时将workInprogressHook 指针指向当前的hook
function mountWorkInProgressHook() {
  let hook;
  hook = {
    next: null, // 代表的下一个 hook
    queue: { // 是一个队列的原因是因为 我们有可能在一次更新调用多次更新hook操作
      pending: null,  // 保存改变的状态
    }
  }
  if (!fiber.memoizedState) {
    // 如果是第一个hook
    fiber.memoizedState = hook; //将fiber的初始值设置为当前的hook
  } else {
    // 如果不是第一个hook    此时workInProgress代表上一次的hook
    workInProgressHook.next = hook // 给当前节点的 next 添加本次的 hook    ps: 因为存在着互相引用的关系 这里给workInProgressHook添加next  根fiber会更改
  }
  workInProgressHook = hook  // 将当前工作的节点更改为现在的hook

  return hook
}

// 用来获取更新阶段的hook 同时将workInprogressHook 指针指向当前的hook
function updateWorkInProgressHook() {
  let hook;
  hook = workInProgressHook; // 更新阶段可以直接从workInprogressHook里取当前的hook
  workInProgressHook = workInProgressHook.next // 当前工作节点hook后移

  return hook
}

// 用来模拟更新组件
function schedule() {
  workInProgressHook = fiber.memoizedState; // 每次重新执行将当前的工作进度指向 根 fiber 的 memoizedState
  const app = fiber.stateNode(); // 模拟组件重新执行
  isMount = false
  return app
}

function useState(initialState) {
  let hook;
  if (isMount) {
    // 如果是初始化阶段, 初始化 当前这个 hook
    hook = mountWorkInProgressHook()
    hook.memoizedState = initialState;  // 设置初始值

  } else {
    // 更新阶段 这里只是将最新的hook
    hook = updateWorkInProgressHook()
  }

  let baseState = hook.memoizedState; // 当前的状态
s
  if (hook.queue.pending) {
    // 表示是需要更新 state

    let firstUpdate = hook.queue.pending;  // 这个变量表示当前移动到的action位置
    do {
      const action = firstUpdate.action; // 取到当前的action逻辑
      if (typeof action !== 'function') {
        baseState = action  // 如果传递过来的是值 则直接赋值给baseState
      } else {
        baseState = action(baseState)  // action 传入上一次的值 计算出新的值
      }
      firstUpdate = firstUpdate.next  // 移动当前action指向的位置
    } while (firstUpdate !== hook.queue.pending); // 如果当前的update是指向的第一个update 则表示遍历完毕 (用到了下面创建update为环状链表)

    hook.queue.pending = null; //  清除本次调用后存储的更新

  }
  hook.memoizedState = baseState;  //更改本次值为更新后的值

  return [
    baseState,
    dispatchAction.bind(null, hook.queue)
  ]
}

// setState
function dispatchAction(queue, action) {
  // 本次更新
  const update = {
    action,  // 状态改变的action
    next: null
  }

  // 这里存储为环状链表 会在上面用到判断
  if (queue.pending === null) {
    // 表示在本次更新 当前这个state是第一次更新

    update.next = update;  // 环状链表 u0.next -> u0
  } else {
    // u1 -> u0 -> u1
    // 不是第一次更新 就将本次的更新插入到队列的最前面,此时update是收集了之前更新后最新的 链表
    update.next = queue.pending.next;
    queue.pending.next = update; // 将队列指向最新的 update
  }

  // 将当前队列中的 pendding 指向本次更新创建的update
  queue.pending = update

  // 触发一次更新
  schedule()
}

function useRef(initialValue) {
  let hook;
  if (isMount) {
    hook = mountWorkInProgressHook();
    hook.memoizedState = { 
      current: initialValue
    }
  } else {
    hook = updateWorkInProgressHook()
  }

  return hook.memoizedState
}

// 用来判断前后依赖性是否一样
function areHookInputsEqual(nextDeps, prevDeps) {

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    /**
     * 如果说两个数组长度不一样 则只会去判断共有的长度的数组值
     *
     * 也就是说 第一次为 [1, 2] 第二次为 [1] 则不会去更新
     *
     * 第一次为[1, 2] 第二次为[2]会去更新
     */

    if (Object.is(nextDeps[i], prevDeps[i])) {
      // 如果两个值是一样的 ,跳出本次循环
      continue;
    }
    // 如果不一样返回false
    return false;
  }
  // 遍历完还没跳出函数 则两个deps为一样的 返回true
  return true;
}

function useMemo(nextCreate, deps) {
  let hook;
  const nextDeps = deps === undefined ? null : deps; //默认不传递第二个参数会当null处理
  if (isMount) {
    hook = mountWorkInProgressHook();
  } else {
    hook = updateWorkInProgressHook();
    const prevState = hook.memoizedState;
    if (prevState[0] !== null && nextDeps !== null) {
      // 如果上一次的值和这一次的依赖项不是null
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果一样,返回上一次的值
        return prevState[0];
      }
    }
  }
  // 如果是mount 阶段或者是 更新阶段值发生变化了 则调用本次的fn 同时更新fiber中存储的状态和依赖
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
// useCallback和useMemo的区别就是一个会吧函数调用之后的结果存下来  一个会存函数本身
function useCallback(nextCreate, deps) {
  let hook;
  const nextDeps = deps === undefined ? null : deps; //默认不传递第二个参数会当null处理
  if (isMount) {
    hook = mountWorkInProgressHook();
  } else {
    hook = updateWorkInProgressHook();
    const prevState = hook.memoizedState;
    if (prevState[0] !== null && nextDeps !== null) {
      // 如果上一次的值和这一次的依赖项不是null
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果一样,返回上一次的值
        return prevState[0];
      }
    }
  }
  // 如果是mount 阶段或者是 更新阶段值发生变化了 则调用本次的fn 同时更新fiber中存储的状态和依赖
  hook.memoizedState = [nextCreate, nextDeps];
  return hook.memoizedState[0];
}

function App() {
  const [num, updateNum] = useState(0);
  console.log('num: ' + num)
  return {
    onClick() {
      updateNum(pre => pre + 1);
    },
  }
}
window.app = schedule()

缺陷

  • 上面手动实现 hook 不能达到批量更新的效果。当多次调用 updateNnm 会多次 render
  • 没有优先级的情况,不能中断渲染

参考资料

React技术揭秘