likes
comments
collection
share

让我们来手写一个简易版react hooks

作者站长头像
站长
· 阅读数 59

前言

今天开始和大家一起深入的学习和了解react相关知识;从useState简易版源码开始

了解react源码我们能学到什么

  • 性能相关:例如;对代码进行优化再也不会无从下手,不使用相关优化API也可以写出性能很好的代码,chrome dev tools performance需要对源码流程熟悉才可以顺利的定位问题;
  • 使用子库:例如,有用到时间分片相关的功能可以看下scheduler这个库
  • 设计模式:对框架设计模式有更深入的认识,在前端中我们所说的MVC是 Model-View-Controller,严格说这三个加起来以后才是三层架构中的UI层,Model代表的是数据的变化也就是state的变化,V就是视图,Controller负责调度 model 和 view,在react中我们的C实际上是react中schedule这个阶段,通过setState触发schedule阶段进而触发视图的更新
  • 面试:面试会问
  • 解决开发中的疑惑,例;为什么引入我们没有使用的createElement,为什么会有hook规则

hooks理念

hooks的实现就是为了践行代数效应,将副作用从我们的函数组件中进行抽离 ,使我们的函数组件变的纯粹

function getTotalUser(tenantIdA, tenantIdB) {
  const num1 = getUser(user1);
  const num2 = getUser(user2); 
  
  return num1 + num2;
}
getTotalUser(xiangrong,xiangwang)
// 因为getUser是异步请求,为了保证getTotalUser的调用方式不变,我们想到了async await,但它是具有传染性的


function TotalUser(tenantIdA, tenantIdB) {
  const num1 = useGetUser(user1);
  const num2 = useGetUser(user2); 
  
  return num1 + num2;
}
function useGetUser(id) {
  const [num, setNum] = useState(0);
  
  useEffect(() => {
    request(id).then((res)=>{
      setNum(res)
    })
  }, [id]);
  
  return num;
}
// 我们通过hooks实现TotalUser,发现TotalUser并不区分是同步或是异步的

由于react的视图与数据不是一一对应的,所以要遵守Hook 规则,同样的vue与solid中的hooks就可以在条件判断中使用,Hook规则是react的问题,我们可以这么理解,vue比起react更好的践行了代数效应;

useState有什么特性

function fn() {
  const [num, setNum] = useState(0);
  // useState是一个方法,接收一个任意形式的参数,并返回一个数组
  // 不能在条件判断或方法内执行useState方法
  const [num1, setNum1] = useState(10);
  
  window.updateNum = setNum;
  // 调用window.updateNum(1)可以更新num

  
  useEffect(() => {
    request(id).then((res)=>{
      setNum(res)
      setNum(res+1)
      setNum(num=>num+1)
      setNum1(res)
    // 同一个setState可以多次调用
    })
    setTimeout(()=>{
      setNum(num+1) // 一秒中点击五次的结果实际还是1,换成setNum(num=>num+1)就会达成我们想要的效果
    },1000)
  }, []);
}

hooks如何保存数据

FunctionComponent的 render本身只是函数调用。那么在render内部调用的hook是如何获取到对应数据呢?

每个组件有个对应的fiber节点(可以理解为虚拟DOM),用于保存组件相关信息,每次render时,全局变量currentlyRenderingFiber都会被赋值为该FunctionComponent对应的fiber节点。所以,hook内部其实是从currentlyRenderingFiber中获取状态信息的。

useState简易实现

大体思路:首次render之后,在我们调用setState时函数组件App将会重新渲染,即重新调用了App函数,两次调用函数组件App不同之处是,第一次属于挂载(mount),第二次则属于更新(update),也就是说两次调用useState的含义也是不同的,说白了就是有两个大分支,第一次走mount,后面都走update,update时会调用schedule及其以后的阶段;

let isMount = true; // 模拟组件生命周期
let workInprogressHook = null; // 工作的任务链表,指针指向当前处理的hook

const fiber = {
  stateNode: App, // 保存实例本身
  memoizedState: null, // 保存的是一个链表,保存的是每一个hook对应的数据,当前头节点指向null
};

/** 阅读顺序四  start */
// 调度-->把不同优先级的更新进行排序
function schedule() {
  workInprogressHook = fiber.memoizedState; // 这部分是把workInprogressHook运行的指针重新指向第一个memoizedState
  const app = fiber.stateNode(); // 对应render阶段
  // todo render  commit
  isMount = false; // 首次调用之后变为update模式,源码中hooks调用时,mount阶段和update阶段调用的是两个方法
  return app;
}
/** 阅读顺序四  end */
// 计算一个新的状态,并返回一个改变当前状态的方法  return [state, setState]
/** 阅读顺序一  start */
function useState(initialState) {
  let hook;
  // 在mount时每一个useState都会调用这个方法创建一个hook,通过next指针把所有useState关联起来形成一个链表
  if (isMount) {
    // 首次渲染的时候fiber.memoizedState的指针指向null ,因此我们需要创建一个hook
    hook = {
      memoizedState: initialState, // 链表的头节点是我们传入的参数
      next: null, // 可以理解为链表的下一个指针指向null
      queue: {
        // 保存的是改变的状态,可能一个setState调用多次,所以用链表保存,
        pending: null, // 当前头节点指向null
      },
    };
    // 如果指针指向null,证明当前fiber.memoizedState对象里面没有hook节点
    if (fiber.memoizedState === null) {
      fiber.memoizedState = hook; // 那么就把我们创建的hook当作头节点
      workInprogressHook = hook; // 当前处理的hook就是我们定义的hook,workInprogressHook在这边被初始化为一个链表
    } else {
      // 如果不是第一个调用用的hook,就把下一个任务存到我们的工作任务链表内
      workInprogressHook.next = hook;
      // 此时workInprogressHook的结构是这样的
      /*workInprogressHook={
        memoizedState: initialState,
        next: {
            memoizedState: initialState,
            next: {...},
            queue: {
              pending: null,
            },
          }, 
        queue: {
          pending: null,
        },
      }*/
    }
  } else {
    // 此时update阶段我们已经有一个hook的链表,把工作链表赋值给hook,然后让当前的workInprogressHook指向下一个工作任务
    hook = workInprogressHook;
    workInprogressHook = workInprogressHook.next; // 这步就相当于清除头节点,把最外层对象干掉,因为执行完了就不再保留当前任务,所以指向下一个任务
  }
  /** 阅读顺序一  end */

  /** 阅读顺序三  start */
  // 当前任务的基本状态,如果是第一次的时候,baseState就是我们传的参数
  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    // 找到第一次的update
    let firstUpdate = hook.queue.pending.next;

    do {// while是先判断条件再执行循环体,do while是先执行循环体再判断条件
      const action = firstUpdate.action;
      baseState = action(baseState);  // 此时是调用完一次setState后的值了
      firstUpdate = firstUpdate.next; // 当前状态更新完,把指针指向下一个setState,用于setState的链式调用,比如setNumber(1);setNumber(2);
    } while (firstUpdate !== hook.queue.pending.next); // 遍历完整个链表,hook.queue.pending.next此时指向空
    // 将链表清空
    hook.queue.pending = null;
  }
  // 更改当前状态
  hook.memoizedState = baseState;
  return [baseState, dispatchAction.bind(null, hook.queue)];
  /** 阅读顺序三  end */
}
// 就是setState方法,源码中的useState实际上调用的useReducer,只不过useState会给useReducer一个默认的reducer,而调用useReducer需要我们自己创建
/** 阅读顺序二  start */
function dispatchAction(queue, action) {
   // 考虑到优先级的问题所以update这边是个环状列表
  const update = {
    action, // 就是我们自己写的updateNum的参数
    next: null,
  };
  // 代表当前没有能触发的更新,那么我们创建的update就是将要触发的第一个更新
  if (queue.pending === null) {
    // 自己指向自己,形成一个环状链表  head->head
    // update = {
    //   action,
    //   next: {
    //     action,
    //     next: {...},
    //   },
    // };
    update.next = update;
  } else {
    // 插入链表的操作queue.pending是最后一个update,所以他的next指向第一个update
    // head->update->head  插入节点形成环状列表
    // 假设第一次走完if
    // queue.pending = {
    //   action:oldAction,
    //   next: {
    //     action:oldAction,
    //     next: null,
    //   },
    // };
    // 此时的update = {
    //          action,
    //          next: null,
    //      };
    update.next = queue.pending.next;
    // 此时update = {
    //          action:newAction,
    //          next: {
    //              action:oldAction,  上一次的更新
    //              next: null,
    //              },
    //           };
    queue.pending.next = update;
    // 此时queue.pending = {
    //          action:oldAction,
    //          next: {
    //              action:newAction,
    //              next: {
    //                  action:oldAction,  上一次的更新
    //                  next: null,
    //                  },
    //              };
    //           };
    // 这样就构成了一个双向链表
  }
  // 每次这个方法创建的update都是这个环状链表的最后一个更新
  // 此时queue.pending = {
  //          action:newAction,
  //          next: {
  //              action:oldAction,  上一次的更新
  //              next: null,
  //              },
  //           };
  queue.pending = update;
  /** 阅读顺序二  end */

  schedule();
}
function App() {
  const [num, updateNum] = useState(0);
  const [num1, updateNum1] = useState(10);

  return {
    onClick() {
      console.log(1)
      updateNum((num) => num + 1);
    },
    onClick1() {
      updateNum1((num) => num + 10);
    },
  };
}
window.app = schedule();

转载自:https://juejin.cn/post/7078270117808701470
评论
请登录