来来来,手摸手写一个hook
hello,这里是潇晨,今天就带着大家一起来手写一个迷你版的hooks,方便大家理解hook在源码中的运行机制,配有图解,保姆级的教程,只求同学一个小小的👍,🐶。
第一步:引入React和ReactDOM
因为我们要将jsx转变为virtual-dom,这一步分工作就交给babel吧,而jsx被babel进行词法解析之后会形成React.createElement()的调用,而React.createElement()执行之后的返回结果就是jsx对象或者叫virtual-dom。
又因为我们要将我们的demo渲染到dom上,所以我们引入ReactDOM。
import React from "react";
import ReactDOM from "react-dom";
第二步:我们来写一个小demo
我们定义两个状态count和age,在点击的时候触发更新,让它们的值加1。
在源码中useState是保存在一个Dispatcher对象上面的,并且在mount和update的时候取到的是不同的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
在看这部分前,先来捋清楚fiber、hook、update的关系,看图:

Dispatcher是什么:Dispatcher在源码中就是一个对象,上面存放着各种各样的hooks,在mount和update的时候会使用过不同的Dispatcher,来看看在源码中Dispatcher是什么样子:
在调用useState之后,会调用一个resolveDispatcher的函数,这个函数调用之后会返回一个dispatcher对象,这个对象上就有useState等钩子。

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

在源码中有这样一段代码,如果是在正式环境中,分为两种情况
- 如果满足
current === null || current.memoizedState === null,说明我们处于首次渲染的时候,也就是mount的时候,其中current就是我们fiber节点,memoizedState保存了fiber上hook,也就是说在应用首次渲染的时候,current fiber是不存在的,我们还没有创造出任何fiber节点,或者存在某些fiber,但是上面没有构建相应的hook,这个时候就可以认为是处于首次渲染的时候,我们取到的是HooksDispatcherOnMount - 如果不满足
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;}
那我们就来看一下这个HooksDispatcherOnMount和HooksDispatcherOnUpdate是个什么,好家伙,原来你包含了所有的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。

现在我们来手写一下dispatcher,dispatcher是个对象,对象上存在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之前,首先来看看hook和update的数据结构
hook:
queue:上面有pending属性,pending也是一条环状链表,上面存放了未被更新的update,也就是说这些update会以next指针连接成环状链表。memoizedState表示当前的状态next:指向下一个hook,形成一条链表
const hook = {//构建hookqueue: {pending: null//未执行的update链表},memoizedState: null,//当前statenext: null//下一个hook};
update:
action:是出发更新的函数next:连接下一个update,形成一条环状链表
const update = {//构建updateaction,next: null};
那接下来定义useState吧,分三个部分:
- 创建
hook或取到hook:- 在
mount的时候:调用mountWorkInProgressHook创建一个初始的hook,赋值useState传进来的初始值initialState - 在
update的时候:调用updateWorkInProgressHook,拿到当前正在工作的hook
- 在
- 计算
hook上未更新的状态:遍历hook上的pending链表,调用链表节点上的action函数,生成一个新的状态,然后更新hook上的状态。 - 返回新的状态和
dispatchAction传入queue参数
function useState(initialState) {//第1步:创建hook或取到hooklet 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;//第一个updatedo {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的返回}
接下来定义mountWorkInProgressHook和updateWorkInProgressHook这两个函数
mountWorkInProgressHook:在mount的时候调用,新创建一个hook对象,- 如果当前
fiber不存在memoizedState,那当前hook就是这个fiber上的第一个hook,将hook赋值给fiber.memoizedState - 如果当前
fiber存在memoizedState,那将当前hook接在workInProgressHook.next后面。 - 将当前
hook赋值给workInProgressHook
- 如果当前
updateWorkInProgressHook:在update的时候调用,返回当前的hook,也就是workInProgressHook,并且将workInProgressHook指向hook链表的下一个。
function mountWorkInProgressHook() {//mount时调用const hook = {//构建hookqueue: {pending: null//未执行的update链表},memoizedState: null,//当前statenext: null//下一个hook};if (!fiber.memoizedState) {fiber.memoizedState = hook;//第一个hook的话直接赋值给fiber.memoizedState} else {workInProgressHook.next = hook;//不是第一个的话就加在上一个hook的后面,形成链表}workInProgressHook = hook;//记录当前工作的hookreturn workInProgressHook;}function updateWorkInProgressHook() {//update时调用let curHook = workInProgressHook;workInProgressHook = workInProgressHook.next;//下一个hookreturn curHook;
}
第四步:定义dispatchAction
-
创建
update,挂载载queue.pending上- 如果之前
queue.pending不存在,那创建的这个update就是第一个,则update.next = update - 如果之前
queue.pending存在,则将创建的这个update加入queue.pending的环状链表中

- 如果之前
-
让
isMount=false,并且赋值workInProgressHook,调用schedule进行更新渲染
function dispatchAction(queue, action) {//触发更新const update = {//构建updateaction,next: null};if (queue.pending === null) {update.next = update;//update的环状链表} else {update.next = queue.pending.next;//新的update的next指向前一个updatequeue.pending.next = update;//前一个update的next指向新的update}queue.pending = update;//更新queue.pendingisMount = false;//标志mount结束workInProgressHook = fiber.memoizedState;//更新workInProgressHookschedule();//调度更新
}
最终代码
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 = {//构建hookqueue: {pending: null//未执行的update链表},memoizedState: null,//当前statenext: null//下一个hook};if (!fiber.memoizedState) {fiber.memoizedState = hook;//第一个hook的话直接赋值给fiber.memoizedState} else {workInProgressHook.next = hook;//不是第一个的话就加在上一个hook的后面,形成链表}workInProgressHook = hook;//记录当前工作的hookreturn workInProgressHook;}function updateWorkInProgressHook() {//update时调用let curHook = workInProgressHook;workInProgressHook = workInProgressHook.next;//下一个hookreturn 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;//第一个updatedo {const action = firstUpdate.action;baseState = action(baseState);firstUpdate = firstUpdate.next;//循环update链表} while (firstUpdate !== hook.queue.pending);//通过update的action计算statehook.queue.pending = null;//重置update链表}hook.memoizedState = baseState;//赋值新的statereturn [baseState, dispatchAction.bind(null, hook.queue)];//useState的返回}return {useState};
})();function dispatchAction(queue, action) {//触发更新const update = {//构建updateaction,next: null};if (queue.pending === null) {update.next = update;//update的环状链表} else {update.next = queue.pending.next;//新的update的next指向前一个updatequeue.pending.next = update;//前一个update的next指向新的update}queue.pending = update;//更新queue.pendingisMount = false;//标志mount结束workInProgressHook = fiber.memoizedState;//更新workInProgressHookschedule();//调度更新
}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
相关文章:
来来来,手摸手写一个hook
hello,这里是潇晨,今天就带着大家一起来手写一个迷你版的hooks,方便大家理解hook在源码中的运行机制,配有图解,保姆级的教程,只求同学一个小小的👍,🐶。 第一步…...
【C++】AVL树
目录 1 简介 2 实现 2.1 框架构建 2.2 插入操作 2.2.1 平衡因子的更新 2.2.2 平衡因子异常时树的调整 3 检验 1 简介 AVL树基于二叉搜索树之上,又对其提出了平衡的要求,即:当向二叉搜索树插入新节点后,保证每个节点的左右…...
Mybatis源码(2) - SqlSessionTemplate的介绍及创建过程
0. 前言1. Spring对SqlSessionTemplate的管理1.1. SqlSessionTemplate的创建:1.2. MapperProxy中sqlSession的来源:2. SqlSessionInterceptor中的getSqlSession0. 前言 众所周知😏:MyBatis通过SqlSessionFactory 创建SqlSession去调用Executo…...
女生做大数据有发展前景吗?
当前大数据发展前景非常不错,且大数据领域对于人才类型的需求比较多元化,女生学习大数据也会有比较多的工作机会。大数据是一个交叉学科涉及到的知识量比较大学习有一定的难度,女生比较适合大数据采集和大数据分析方向的工作岗位。 大数据采…...
Git实用指令记录
config 用例:对git最先要做的一个操作就是配置用户名和邮箱,否则无法commit查看所有可以config的条目,非常之多$ git config --list core.symlinksfalse core.autocrlftrue core.fscachetrue color.interactivetrue color.uiauto help.forma…...
复杂美公链技术重要特色:平行公链架构
复杂美公链技术Chain33从11月开源至今,获得众多合作方的认可,其中首创的平行公链架构被百度、阿里、360等机构认可并跟进研究,这也说明了平行公链或许是区块链普及应用的重要解决方案之一。 平行公链(以下简称平行链)是…...
Java——进制转换的一些内容
Java——进制转换的一些内容1.16进制字符串String转字节数组byte[]2.16进制字符串String转10进制数字int3.字节数组byte[]转字符串String4.16进制字符串String-->byte[]-->String(使用ByteBuffer转换)5.字节数组byte[]转字符数组char[]6.字节byte转…...
使用 Nodejs、Express、Postgres、Docker 在 JavaScript 中构建 CRUD Rest API
让我们在 JavaScript 中创建一个 CRUD rest API,使用:节点.js表达续集Postgres码头工人码头工人组成介绍这是我们将要创建的应用程序架构的架构:我们将为基本的 CRUD 操作创建 5 个端点:创造阅读全部读一个更新删除我们将使用以下…...
电子招标采购系统源码之什么是电子招投标系统?
随着互联网时代的到来,各行业都受到不同的影响,其中招投标行业也不例外。为了顺应互联网潮流的发展,电子招投标逐渐取代传统的纸质的招投标方式,给招标方、投标方、招标代理等各方也带来了前所未有的机遇与挑战。那么什么是电子招…...
匹配文件名称模块glob和fnmatch
匹配文件名称模块glob 1.概述 glob模式规则与re模块的正则表达式规则不大相同,glob模块遵循标准的UNIX路径扩展规则。 fnmatch模块用于根据glob模式比较文件名 2.glob表达式匹配文件名 2.1.测试文件 介绍glob配置规则前,先使用下面的代码创建测试文…...
day12_oop
今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、作业 二、继承 三、重写 四、this和super 五、访问修饰符 零、 复习昨日 局部变量和成员变量什么区别 位置,作用域,初始值,内存位置,生命周期 构造方法…...
在 Flutter 中使用 webview_flutter 4.0 | js 交互
大家好,我是 17。 已经有很多关于 Flutter WebView 的文章了,为什么还要写一篇。两个原因: Flutter WebView 是 Flutter 开发的必备技能现有的文章都是关于老版本的,新版本 4.x 有了重要变化,基于 3.x 的代码很多要重…...
嵌入式ARM工业边缘计算机BL302的CAN总线接口如何设置?
CAN 接口如图所示,输入如下命令: ifconfig -a //查看所有网卡 如果 FlexCAN 驱动工作正常的话就会看到 CAN 对应的网卡接口,如图。从图中可 以看出,有一个名为“can0”的网卡,这个就是 BL302 板上的 CAN1 接口对应的 c…...
Win11系统如何安装Ubuntu20.04(WSL版本)并安装docker
终于还是下定决心去换电脑了……这次采用轻量级的WSL,发现虽然没有占内存的GUI界面,但是编码和阅读文档还是非常nice的 1、首先开启Win11的虚拟机服务 2、下载你期望的Ubuntu服务器(这里以20.04为例) 安装成功后,发现…...
Elasticsearch和Solr的区别
背景:它们都是基于Lucene搜索服务器基础之上开发,一款优秀的,高性能的企业级搜索服务器。(是因为他们都是基于分词技术构建的倒排索引的方式进行查询)开发语言:java语言开发诞生时间:Solr2004年…...
如何在北京买房
首先我陈述一点,如果为了买房后再卖掉赚取差价,我这篇文章也许不适合,我这篇文章为整体愿景的发展而设计,为可操作房产的买卖而操作。 买房的愿景: 首先,我们要以一种心态来买房。那就是以始为终的态度&am…...
使用Proxifier+burp抓包总结
一、微信小程序&网页抓包 1. Proxifier简介 Proxifier是一款功能非常强大的socks5客户端,可以让不支持通过代理服务器工作的网络程序能通过HTTPS或SOCKS代理或代理链。 2. 使用Proxifier代理抓包 原理:让微信相关流量先走127.0.0.1:80到burp。具体…...
安装华为aab包的处理方式
1、转换 aab包 为 apks 说明: 1、bundletool-all-1.11.2.jar 转换文件的工具 2、a.aab aab源文件 3、xxx.apks 导入的文件以及路径(例如:D:\Android\xxx.apks) 4、–ksxxxx.jks 该aab打包所需的jsk文件 5、三条命令为 jsk打包所…...
Word处理控件Aspose.Words功能演示:使用 C++ 将 RTF 文档转换为 PDF
Aspose.Words 是一种高级Word文档处理API,用于执行各种文档管理和操作任务。API支持生成,修改,转换,呈现和打印文档,而无需在跨平台应用程序中直接使用Microsoft Word。此外,API支持所有流行的Word处理文件…...
【Java|多线程与高并发】进程与线程的区别与联系
文章目录什么是进程什么是线程上下文切换多线程一定比串行执行快吗进程与线程的区别与联系什么是进程 进程的定义:进程是正在运行的程序实体,并且包括这个运行的程序中占据的所有系统资源,比如说CPU(寄存器),IO,内存&a…...
深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
vscode(仍待补充)
写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh? debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具
文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲
文章目录 前言第一部分:体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分:体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...
Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
华为OD机试-最短木板长度-二分法(A卷,100分)
此题是一个最大化最小值的典型例题, 因为搜索范围是有界的,上界最大木板长度补充的全部木料长度,下界最小木板长度; 即left0,right10^6; 我们可以设置一个候选值x(mid),将木板的长度全部都补充到x,如果成功…...
CSS 工具对比:UnoCSS vs Tailwind CSS,谁是你的菜?
在现代前端开发中,Utility-First (功能优先) CSS 框架已经成为主流。其中,Tailwind CSS 无疑是市场的领导者和标杆。然而,一个名为 UnoCSS 的新星正以其惊人的性能和极致的灵活性迅速崛起。 这篇文章将深入探讨这两款工具的核心理念、技术差…...
