• 来来来,手摸手写一个hook


    来来来,手摸手写一个hook

    hello,这里是潇晨,今天就带着大家一起来手写一个迷你版的hooks,方便大家理解hook在源码中的运行机制,配有图解,保姆级的教程,只求同学一个小小的,。

    第一步:引入React和ReactDOM

    因为我们要将jsx转变为virtual-dom,这一步分工作就交给babel吧,而jsxbabel进行词法解析之后会形成React.createElement()的调用,而React.createElement()执行之后的返回结果就是jsx对象或者叫virtual-dom

    又因为我们要将我们的demo渲染到dom上,所以我们引入ReactDOM

    import React from "react";
    import ReactDOM from "react-dom";
    

    第二步:我们来写一个小demo

    我们定义两个状态countage,在点击的时候触发更新,让它们的值加1。

    在源码中useState是保存在一个Dispatcher对象上面的,并且在mountupdate的时候取到的是不同的hooks,所以我们先暂时从Dispatcher上拿到useState,等下在来定义Dispatcher

    接下来定义一个schedule函数,每次调用的时候会重新渲染组件。

    function App() {
      let [count, setCount] = Dispatcher.useState(1);
      let [age, setAge] = Dispatcher.useState(10);
      return (
        <>
          <p>Clicked {count} times</p>
          <button onClick={() => setCount(() => count + 1)}> Add count</button>
          <p>Age is {age}</p>
          <button onClick={() => setAge(() => age + 1)}> Add age</button>
        </>
      );
    }
    
    function schedule() {	//每次调用会重新渲染组件
      ReactDOM.render(<App />, document.querySelector("#root"));
    }
    
    schedule();
    

    第三步:定义Dispatcher

    在看这部分前,先来捋清楚fiberhookupdate的关系,看图:

    image-20211129105128673

    Dispatcher是什么:Dispatcher在源码中就是一个对象,上面存放着各种各样的hooks,在mountupdate的时候会使用过不同的Dispatcher,来看看在源码中Dispatcher是什么样子:

    在调用useState之后,会调用一个resolveDispatcher的函数,这个函数调用之后会返回一个dispatcher对象,这个对象上就有useState等钩子。

    image-20211126164214374

    那我们来看看这个函数做了啥事情,这个函数比较简单,直接从ReactCurrentDispatcher对象上拿到current,然后返回出来的这个current就是dispatcher,那这个ReactCurrentDispatcher又是个啥?别急,继续在源码中来找一下。

    image-20211126164903336

    在源码中有这样一段代码,如果是在正式环境中,分为两种情况

    1. 如果满足 current === null || current.memoizedState === null,说明我们处于首次渲染的时候,也就是mount的时候,其中current就是我们fiber节点,memoizedState保存了fiberhook,也就是说在应用首次渲染的时候,current fiber是不存在的,我们还没有创造出任何fiber节点,或者存在某些fiber,但是上面没有构建相应的hook,这个时候就可以认为是处于首次渲染的时候,我们取到的是HooksDispatcherOnMount
    2. 如果不满足 current === null || current.memoizedState === null,就说明我们处于更新阶段,也就是update的时候,我们取到的是HooksDispatcherOnUpdate
    if (__DEV__) {
        if (current !== null && current.memoizedState !== null) {
          ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
        } else if (hookTypesDev !== null) {
          ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
        } else {
          ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
        }
      } else {
        ReactCurrentDispatcher.current =
          current === null || current.memoizedState === null
            ? HooksDispatcherOnMount
            : HooksDispatcherOnUpdate;
      }
    

    那我们就来看一下这个HooksDispatcherOnMountHooksDispatcherOnUpdate是个什么,好家伙,原来你包含了所有的hooks啊。

    const HooksDispatcherOnMount: Dispatcher = {
      readContext,
    
      useCallback: mountCallback,
      useContext: readContext,
      useEffect: mountEffect,
      useImperativeHandle: mountImperativeHandle,
      useLayoutEffect: mountLayoutEffect,
      useMemo: mountMemo,
      useReducer: mountReducer,
      useRef: mountRef,
      useState: mountState,
      useDebugValue: mountDebugValue,
      useDeferredValue: mountDeferredValue,
      useTransition: mountTransition,
      useMutableSource: mountMutableSource,
      useOpaqueIdentifier: mountOpaqueIdentifier,
    
      unstable_isNewReconciler: enableNewReconciler,
    };
    
    const HooksDispatcherOnUpdate: Dispatcher = {
      readContext,
    
      useCallback: updateCallback,
      useContext: readContext,
      useEffect: updateEffect,
      useImperativeHandle: updateImperativeHandle,
      useLayoutEffect: updateLayoutEffect,
      useMemo: updateMemo,
      useReducer: updateReducer,
      useRef: updateRef,
      useState: updateState,
      useDebugValue: updateDebugValue,
      useDeferredValue: updateDeferredValue,
      useTransition: updateTransition,
      useMutableSource: updateMutableSource,
      useOpaqueIdentifier: updateOpaqueIdentifier,
    
      unstable_isNewReconciler: enableNewReconciler,
    };
    

    所以dispatcher就是个对象,里面包含了所有的hooks,在首次渲染和更新的时候拿到的是不同的dispatcher,在调用hooks的时候就会调用到不同的函数,比如如果使用了useState,在mount的时候调用到的就是mountState,在update的时候调用到的就是updateState

    image-20211126170906166

    现在我们来手写一下dispatcherdispatcher是个对象,对象上存在useState,我们用一个自执行函数来表示,此外还需要用到两个变量和一个常量fiber

    • workInProgressHook表示遍历到的hook(因为hook会保存在链表上,需要遍历链表计算hook上保存的状态)
    • 为了简单起见,定义一个isMount=true表示mount的时候,在update的时候将它设置成false
    • 为简单起见,fiber就定义成一个对象,memoizedState表示这个fiber节点上存放的hook链表,stateNode就是第二步的demo。
    let workInProgressHook;//当前工作中的hook
    let isMount = true;//是否时mount时
    
    const fiber = {//fiber节点
      memoizedState: null,//hook链表
      stateNode: App
    };
    
    const Dispatcher = (() => {//Dispatcher对象
      function useState(){
        //。。。
      }
    
      return {
        useState
      };
    })();
    

    在定义useState之前,首先来看看hookupdate的数据结构

    hook:
    • queue:上面有pending属性,pending也是一条环状链表,上面存放了未被更新的update,也就是说这些update会以next指针连接成环状链表。
    • memoizedState表示当前的状态
    • next:指向下一个hook,形成一条链表
     const hook = {//构建hook
       queue: {
         pending: null//未执行的update链表
       },
       memoizedState: null,//当前state
       next: null//下一个hook
     };
    
    update:
    • action:是出发更新的函数
    • next:连接下一个update,形成一条环状链表
     const update = {//构建update
        action,
        next: null
      };
    

    那接下来定义useState吧,分三个部分:

    • 创建hook或取到hook
      1. mount的时候:调用mountWorkInProgressHook创建一个初始的hook,赋值useState传进来的初始值initialState
      2. update的时候:调用updateWorkInProgressHook,拿到当前正在工作的hook
    • 计算hook上未更新的状态:遍历hook上的pending链表,调用链表节点上的action函数,生成一个新的状态,然后更新hook上的状态。
    • 返回新的状态和dispatchAction传入queue参数
    function useState(initialState) {
      	//第1步:创建hook或取到hook
        let hook;
        if (isMount) {
          hook = mountWorkInProgressHook();
          hook.memoizedState = initialState;//初始状态
        } else {
          hook = updateWorkInProgressHook();
        }
    		//第2步:计算hook上未更新的状态
        let baseState = hook.memoizedState;//初始状态
        if (hook.queue.pending) {
          let firstUpdate = hook.queue.pending.next;//第一个update
    
          do {
            const action = firstUpdate.action;
            baseState = action(baseState);//调用action计算新的状态
            firstUpdate = firstUpdate.next;//通过update的action计算state
          } while (firstUpdate !== hook.queue.pending);//当链表还没遍历完时 进行循环
    
          hook.queue.pending = null;//重置update链表
        }
        hook.memoizedState = baseState;//赋值新的state
      
    		//第3步:返回新的状态和dispatchAction传入queue参数
        return [baseState, dispatchAction.bind(null, hook.queue)];//useState的返回
      }
    

    接下来定义mountWorkInProgressHookupdateWorkInProgressHook这两个函数

    • mountWorkInProgressHook:在mount的时候调用,新创建一个hook对象,
      1. 如果当前fiber不存在memoizedState,那当前hook就是这个fiber上的第一个hook,将hook赋值给fiber.memoizedState
      2. 如果当前fiber存在memoizedState,那将当前hook接在workInProgressHook.next后面。
      3. 将当前hook赋值给workInProgressHook
    • updateWorkInProgressHook:在update的时候调用,返回当前的hook,也就是workInProgressHook,并且将workInProgressHook指向hook链表的下一个。
    function mountWorkInProgressHook() {//mount时调用
        const hook = {//构建hook
          queue: {
            pending: null//未执行的update链表
          },
          memoizedState: null,//当前state
          next: null//下一个hook
        };
        if (!fiber.memoizedState) {
          fiber.memoizedState = hook;//第一个hook的话直接赋值给fiber.memoizedState
        } else {
          workInProgressHook.next = hook;//不是第一个的话就加在上一个hook的后面,形成链表
        }
        workInProgressHook = hook;//记录当前工作的hook
        return workInProgressHook;
      }
    
    function updateWorkInProgressHook() {//update时调用
      let curHook = workInProgressHook;
      workInProgressHook = workInProgressHook.next;//下一个hook
      return curHook;
    }
    

    第四步:定义dispatchAction

    • 创建update,挂载载queue.pending

      1. 如果之前queue.pending不存在,那创建的这个update就是第一个,则update.next = update
      2. 如果之前queue.pending存在,则将创建的这个update加入queue.pending的环状链表中

      1

    • isMount=false,并且赋值workInProgressHook,调用schedule进行更新渲染

    function dispatchAction(queue, action) {//触发更新
      const update = {//构建update
        action,
        next: null
      };
      if (queue.pending === null) {
        update.next = update;//update的环状链表
      } else {
        update.next = queue.pending.next;//新的update的next指向前一个update
        queue.pending.next = update;//前一个update的next指向新的update
      }
      queue.pending = update;//更新queue.pending
    
      isMount = false;//标志mount结束
      workInProgressHook = fiber.memoizedState;//更新workInProgressHook
      schedule();//调度更新
    }
    

    最终代码

    import React from "react";
    import ReactDOM from "react-dom";
    
    let workInProgressHook;//当前工作中的hook
    let isMount = true;//是否时mount时
    
    const fiber = {//fiber节点
      memoizedState: null,//hook链表
      stateNode: App//dom
    };
    
    const Dispatcher = (() => {//Dispatcher对象
      function mountWorkInProgressHook() {//mount时调用
        const hook = {//构建hook
          queue: {
            pending: null//未执行的update链表
          },
          memoizedState: null,//当前state
          next: null//下一个hook
        };
        if (!fiber.memoizedState) {
          fiber.memoizedState = hook;//第一个hook的话直接赋值给fiber.memoizedState
        } else {
          workInProgressHook.next = hook;//不是第一个的话就加在上一个hook的后面,形成链表
        }
        workInProgressHook = hook;//记录当前工作的hook
        return workInProgressHook;
      }
      function updateWorkInProgressHook() {//update时调用
        let curHook = workInProgressHook;
        workInProgressHook = workInProgressHook.next;//下一个hook
        return curHook;
      }
      function useState(initialState) {
        let hook;
        if (isMount) {
          hook = mountWorkInProgressHook();
          hook.memoizedState = initialState;//初始状态
        } else {
          hook = updateWorkInProgressHook();
        }
    
        let baseState = hook.memoizedState;//初始状态
        if (hook.queue.pending) {
          let firstUpdate = hook.queue.pending.next;//第一个update
    
          do {
            const action = firstUpdate.action;
            baseState = action(baseState);
            firstUpdate = firstUpdate.next;//循环update链表
          } while (firstUpdate !== hook.queue.pending);//通过update的action计算state
    
          hook.queue.pending = null;//重置update链表
        }
        hook.memoizedState = baseState;//赋值新的state
    
        return [baseState, dispatchAction.bind(null, hook.queue)];//useState的返回
      }
    
      return {
        useState
      };
    })();
    
    function dispatchAction(queue, action) {//触发更新
      const update = {//构建update
        action,
        next: null
      };
      if (queue.pending === null) {
        update.next = update;//update的环状链表
      } else {
        update.next = queue.pending.next;//新的update的next指向前一个update
        queue.pending.next = update;//前一个update的next指向新的update
      }
      queue.pending = update;//更新queue.pending
    
      isMount = false;//标志mount结束
      workInProgressHook = fiber.memoizedState;//更新workInProgressHook
      schedule();//调度更新
    }
    
    function App() {
      let [count, setCount] = Dispatcher.useState(1);
      let [age, setAge] = Dispatcher.useState(10);
      return (
        <>
          <p>Clicked {count} times</p>
          <button onClick={() => setCount(() => count + 1)}> Add count</button>
          <p>Age is {age}</p>
          <button onClick={() => setAge(() => age + 1)}> Add age</button>
        </>
      );
    }
    
    function schedule() {
      ReactDOM.render(<App />, document.querySelector("#root"));
    }
    
    schedule();
    

    预览效果https://codesandbox.io/s/custom-hook-tyf19?file=/src/index.js

    视频讲解(高效学习):点击学习

    往期react源码解析文章:

    1.开篇介绍和面试题

    2.react的设计理念

    3.react源码架构

    4.源码目录结构和调试

    5.jsx&核心api

    6.legacy和concurrent模式入口函数

    7.Fiber架构

    8.render阶段

    9.diff算法

    10.commit阶段

    11.生命周期

    12.状态更新流程

    13.hooks源码

    14.手写hooks

    15.scheduler&Lane

    16.concurrent模式

    17.context

    18事件系统

    19.手写迷你版react

    20.总结&第一章的面试题解答

  • 相关阅读:
    Nginx与Apache的对比
    gc buffer busy waits(ZT)
    Brocade SAN Switch Change Domain ID (ZT)
    Oracle异机恢复时报错ora19870 ora19507
    row cache lock (ZT)
    can a select block a truncate (ZT)
    NBU常用命令
    the RRD does not contain an RRA matching the chosen C
    Solaris10 x64安装64bit perl
    Solaris and Oracle 32bit Linking Error "fatal: symbol 'ntcontab'
  • 原文地址:https://www.cnblogs.com/xiaochen1024/p/15665412.html
Copyright © 2020-2023  润新知