onclick 源码_仿照React源码流程打造90行代码的Hooks
作者:苏畅
转发链接:https://mp.weixin.qq.com/s/YLSD4IojDWTPlov_RQtVAA
前言
你可能已经看过其它简易的Hooks实现。那么本文和其它实现有什么区别呢?
本文的实现完全参照React源码的运行流程。学懂本文,去看React源码,你会发现流程基本一致。
这是本实现的在线Demo,建议对照着代码来看本文。
工作原理
对于useState Hook,考虑如下例子:
function App() { const [num, updateNum] = useState(0); return
updateNum(num => num + 1)}>{num}
;}
可以将工作分为两部分:
- 通过一些途径产生更新,更新会造成组件render。
- 组件render时useState返回的num为更新后的结果。
其中步骤1的更新可以分为mount和update:
- 调用ReactDOM.render会产生mount的更新,更新内容为useState的initialValue(即0)。
- 点击p标签触发updateNum会产生一次update的更新,更新内容为num => num + 1。
接下来讲解这两个步骤如何实现。
更新是什么
通过一些途径产生更新,更新会造成组件render。
首先我们要明确更新是什么。
在我们的极简例子中,更新就是如下数据结构:
const update = { // 更新执行的函数 action, // 与同一个Hook的其他更新形成链表 next: null}
对于App来说,点击p标签产生的update的action为num => num + 1。
如果我们改写下App的onClick:
// 之前return
updateNum(num => num + 1)}>{num}
;// 之后return
{ updateNum(num => num + 1); updateNum(num => num + 1); updateNum(num => num + 1);}}>{num}
;
那么点击p标签会产生三个update。
update数据结构
这些update是如何组合在一起呢?
答案是:他们会形成环状单向链表。
调用updateNum实际调用的是dispatchAction.bind(null, hook.queue),我们先来了解下这个函数:
function dispatchAction(queue, action) { // 创建update const update = { action, next: null } // 环状单向链表操作 if (queue.pending === null) { update.next = update; } else { update.next = queue.pending.next; queue.pending.next = update; } queue.pending = update; // 模拟React开始调度更新 schedule();}
环状链表操作不太容易理解,这里我们详细讲解下。
当产生第一个update(我们叫他u0),此时queue.pending === null。
update.next = update;即u0.next = u0,它会和自己首尾相连形成单向环状链表。
然后queue.pending = update;即queue.pending = u0
queue.pending = u0 ---> u0 ^ | | | ---------
当产生第二个update(我们叫它u1),update.next = queue.pending.next;,此时queue.pending.next === u0, 即u1.next = u0。
queue.pending.next = update;,即u0.next = u1。
然后queue.pending = update;即queue.pending = u1
queue.pending = u1 ---> u0 ^ | | | ---------
你可以照着这个例子模拟插入多个update的情况,会发现queue.pending始终指向最后一个插入的update。
这样做的好处是,当我们要遍历update时,queue.pending.next指向第一个插入的update。
状态如何保存
现在我们知道,更新产生的update对象会保存在queue中。
不同于ClassComponent的实例可以存储数据,对于FunctionComponent,queue存储在哪里呢?
答案是:FunctionComponent对应的fiber中。
fiber为React16中组件对应的虚拟DOM
我们使用如下精简的fiber结构:
// App组件对应的fiber对象const fiber = { // 保存该FunctionComponent对应的Hooks链表 memoizedState: null, // 指向App函数 stateNode: App};
Hook数据结构
接下来我们关注fiber.memoizedState中保存的Hook的数据结构。
可以看到,Hook与update类似,都通过链表连接。不过Hook是无环的单向链表。
hook = { // 保存update的queue,即上文介绍的queue queue: { pending: null }, // 保存hook对应的state memoizedState: initialState, // 与下一个Hook连接形成单向无环链表 next: null}
注意区分update与hook的所属关系:
每个useState对应一个hook对象。
调用const [num, updateNum] = useState(0);时updateNum(即上文介绍的dispatchAction)产生的update保存在useState对应的hook.queue中。
模拟React调度更新流程
在上文dispatchAction末尾我们通过schedule方法模拟React调度更新流程。
function dispatchAction(queue, action) { // ...创建update // ...环状单向链表操作 // 模拟React开始调度更新 schedule();}
现在我们来实现他。
我们用isMount变量指代是mount还是update。
// 首次render时是mountisMount = true;function schedule() { // 更新前将workInProgressHook重置为fiber保存的第一个Hook workInProgressHook = fiber.memoizedState; // 触发组件render fiber.stateNode(); // 组件首次render为mount,以后再触发的更新为update isMount = false;}
通过workInProgressHook变量指向当前正在工作的hook。
workInProgressHook = fiber.memoizedState;
在组件render时,每当遇到下一个useState,我们移动workInProgressHook的指针。
workInProgressHook = workInProgressHook.next;
这样,只要每次组件render时useState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象。
到此为止,我们已经完成第一步。
通过一些途径产生更新,更新会造成组件render。
接下来实现第二步。
组件render时useState返回的num为更新后的结果。
计算state
组件render时会调用useState,它的大体逻辑如下:
function useState(initialState) { // 当前useState使用的hook会被赋值该该变量 let hook; if (isMount) { // ...mount时需要生成hook对象 } else { // ...update时从workInProgressHook中取出该useState对应的hook } let baseState = hook.memoizedState; if (hook.queue.pending) { // ...根据queue.pending中保存的update更新state } hook.memoizedState = baseState; return [baseState, dispatchAction.bind(null, hook.queue)];}
我们首先关注如何获取hook对象:
if (isMount) { // mount时为该useState生成hook hook = { queue: { pending: null }, memoizedState: initialState, next: null } // 将hook插入fiber.memoizedState链表末尾 if (!fiber.memoizedState) { fiber.memoizedState = hook; } else { workInProgressHook.next = hook; } // 移动workInProgressHook指针 workInProgressHook = hook;} else { // update时找到对应hook hook = workInProgressHook; // 移动workInProgressHook指针 workInProgressHook = workInProgressHook.next;}
当找到该useState对应的hook后,如果该hook.queue.pending不为空(即存在update),则更新其state。
// update执行前的初始statelet baseState = hook.memoizedState;if (hook.queue.pending) { // 获取update环状单向链表中第一个update let firstUpdate = hook.queue.pending.next; do { // 执行update action const action = firstUpdate.action; baseState = action(baseState); firstUpdate = firstUpdate.next; // 最后一个update执行完后跳出循环 } while (firstUpdate !== hook.queue.pending) // 清空queue.pending hook.queue.pending = null;}// 将update action执行完后的state作为memoizedStatehook.memoizedState = baseState;
完整代码如下:
function useState(initialState) { let hook; if (isMount) { hook = { queue: { pending: null }, memoizedState: initialState, next: null } if (!fiber.memoizedState) { fiber.memoizedState = hook; } else { workInProgressHook.next = hook; } workInProgressHook = hook; } else { hook = workInProgressHook; workInProgressHook = workInProgressHook.next; } let baseState = hook.memoizedState; if (hook.queue.pending) { let firstUpdate = hook.queue.pending.next; do { const action = firstUpdate.action; baseState = action(baseState); firstUpdate = firstUpdate.next; } while (firstUpdate !== hook.queue.pending) hook.queue.pending = null; } hook.memoizedState = baseState; return [baseState, dispatchAction.bind(null, hook.queue)];}
对触发事件进行抽象
最后,让我们抽象一下React的事件触发方式。
通过调用App返回的click方法模拟组件click的行为。
function App() { const [num, updateNum] = useState(0); console.log(`${isMount ? 'mount' : 'update'} num: `, num); return { click() { updateNum(num => num + 1); } }}
在线Demo
至此,我们完成了一个不到100行代码的Hooks。重要的是,它与React的运行逻辑相同。
在线Demo
在Demo中,调用window.app.click()模拟组件点击事件。
你也可以使用多个useState。
function App() { const [num, updateNum] = useState(0); const [num1, updateNum1] = useState(100); console.log(`${isMount ? 'mount' : 'update'} num: `, num); console.log(`${isMount ? 'mount' : 'update'} num1: `, num1); return { click() { updateNum(num => num + 1); }, focus() { updateNum1(num => num + 3); } }}
与React的区别
我们用尽可能少的代码模拟了Hooks的运行,但是相比React Hooks,他还有很多不足。以下是他与React Hooks的区别:
- React Hooks没有使用isMount变量,而是在不同时机使用不同的dispatcher。换言之,mount时的useState与update时的useState不是同一个函数。
- React Hooks有中途跳过更新的优化手段。
- React Hooks有batchedUpdates,当在click中触发三次updateNum,精简React会触发三次更新,而React只会触发一次。
- React Hooks的update有优先级概念,可以跳过不高优先的update。
作者:苏畅
转发链接:https://mp.weixin.qq.com/s/YLSD4IojDWTPlov_RQtVAA
onclick 源码_仿照React源码流程打造90行代码的Hooks相关推荐
- 最新酒桌小游戏喝酒小程序源码_带流量主源码下载
2022最新酒桌小游戏喝酒小程序源码_带流量主 喝酒神器3.6,我修改增加了广告位,根据文档直接替换即可,原版本没有广告位 直接上传源码到开发者端即可 通过后改广告代码,然后关闭广告展示提交,通过后打 ...
- 仿qq空间源码_【每日源码】一个Go语言编写的百度网盘客户端,强力推荐
本月第7个源码推送 仿 Linux shell 文件处理命令的百度网盘命令行客户端. 功能简介: 目录 特色 编译/交叉编译 说明 下载/运行 说明 Windows Linux / macOS And ...
- python编译器源码_编译python源码
广告关闭 回望2020,你在技术之路上,有什么收获和成长么?对于未来,你有什么期待么?云+社区年度征文,各种定制好礼等你! 尝试通过源码自己编译 python,使用的系统是 ubuntu14.04 l ...
- 客户要求提供源码_一对一直播源码定制:如你所愿,得你所想
作者:布谷惠泽/来源:山东布谷鸟网络 移动互联网时代,产品的更新迭代层出不穷,视频直播行业的火爆,催生了不少直播程序开发公司,乘着这股火爆的劲头,一对主播源码也是一路繁华.一对一直播源码开发定制更是做 ...
- java字节码_掌握Java字节码
java字节码 嘿! Happy Advent:D我是ZeroTurnaround的技术布道者Simon Maple( @sjmaple) . 您知道, JRebel伙计们! 由于编写了类似JRebe ...
- MySQL中什么是码_数据库中的码是什么含义?
展开全部 1.超码e68a843231313335323631343130323136353331333365656662: 超码是一个或多个属性的集合,这些属性可以让我们在一个实体集(所谓的实体集就 ...
- java生成冗余校验码_对循环冗余校验码CRC的理解
模2加法 1+1=0, 0+1=1, 1+0=1, 0+0=0 模2减法 1-1=0, 0-1=1, 1-0=1, 0-0=0 相当于二进制中的逻辑异或运算.也就是比较后两者对应位相同则结果为&quo ...
- excel中怎样用公式获取表单控件_挑战高手:用不到 100 行代码,在前端实现 Excel 全部功能...
(点击图片获取专属你的开发工具) SpreadJS是一款基于 HTML5 的纯前端表格控件,兼容 450 种以上的 Excel 公式,具备"高性能.跨平台.与 Excel 高度兼容" ...
- 机器学习 文本分类 代码_无需担心机器学习-如何在少于10行代码中对文本进行分类
机器学习 文本分类 代码 This article builds upon my previous two articles where I share some tips on how to get ...
最新文章
- ERROR in ./node_modules/element-ui/lib/theme-chalk/fonts/element-icons.ttf 1:0 Module parse failed:
- OS / Linux / epoll 各种事件解析
- VS2008中配置 Windows SDK v7
- java redis集群连接池_(08)redis之使用java客户端、spring连接redis、redis集群示例...
- php边框的颜色 怎么设置,网页中table表格如何修改边框颜色
- go 文件服务器 搜索,golang文件服务器,可以访问任何目录
- windows中PLSQL/Developer、Oracle InstantClient的安装与配置
- Response.Redirect和Server.Transfer的区别
- 关于下载 GAE High Replication Datastore 数据[实战篇]上
- redis的其他功能
- 字节跳动的外包,值得去吗?
- 电池单位中的毫安时与能量单位千焦及大卡的换算关系
- SpringBoot整合EasyExcel实现Excel表格的导出功能
- 模拟行走机器人-c语言
- kali自带浏览器上不了网的解决办法
- JS逐页转pdf文件为图片格式
- Origin设置线条颜色和符号颜色一致
- 2021-06-21指针与变量 和字符数组作业。
- Unity 游戏区域性崩溃,深藏的国际化巨坑
- 人工智能开源社区论坛----开源助力多领域AI生态发展| ChinaOSC
热门文章
- struts2多文件动态下载及中文解决方案
- 添加javascript代码:_javascript事件?你又了解多少呢
- python星空画法教程_对比几段代码,看看你是 Python 菜鸟还是老鸟
- css 大于号 标签_web前端教程之怎样学好css?
- [蓝桥杯]算法提高 道路和航路(spfa+deque+快读优化)
- html怎么给边框改样式,html里面怎么设置边框?html边框样式设置方法
- 华为鸿蒙分布式系统2020,鸿蒙2.0来了!华为开发者大会HDC 2020宣布
- linux桌面显示温度,请问ubuntu下有没有什么桌面小控件现实显卡温度cpu使用率之类的...
- 计算机408考研专题(建议收藏)
- 联通研究院处长王志军:Hadoop在电信业大数据的应用