onclick 源码_仿照React源码流程打造90行代码的Hooks

9eac7b0b66287a897f12e7f75d4c54ed.png

作者:苏畅

转发链接: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}

;}

可以将工作分为两部分:

  1. 通过一些途径产生更新,更新会造成组件render。
  2. 组件render时useState返回的num为更新后的结果。

其中步骤1的更新可以分为mount和update:

  1. 调用ReactDOM.render会产生mount的更新,更新内容为useState的initialValue(即0)。
  2. 点击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的区别:

  1. React Hooks没有使用isMount变量,而是在不同时机使用不同的dispatcher。换言之,mount时的useState与update时的useState不是同一个函数。
  2. React Hooks有中途跳过更新的优化手段。
  3. React Hooks有batchedUpdates,当在click中触发三次updateNum,精简React会触发三次更新,而React只会触发一次。
  4. React Hooks的update有优先级概念,可以跳过不高优先的update。

作者:苏畅

转发链接:https://mp.weixin.qq.com/s/YLSD4IojDWTPlov_RQtVAA

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/245129.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java写一个窗体并连接MySQL_大神帮忙写一个简单地java页面,连接MySQL数据库之后能够显示数据库上的数据...

展开全部用jdbc 连接mysql数据库就行了,网上搜下一大把。--记得在classpath下加入mysql 的jdbc驱动包。/*** author :来e68a84e8a2ad3231313335323631343130323136353331333337386636自互联网*/import java.sql.DriverManager;import java.sql.ResultSet…

java break 在if 中使用_java中使用国密SM4算法详解

前言上次总结了一下加密算法的分类(加密算法有集中形式,各有什么不同?),现在我们用java语言实现一下SM4:无线局域网标准的分组数据算法。对称加密,密钥长度和分组长度均为128位。ps:我们既可以基…

移动web前端开发框架_移动前端开发是Web前端开发吗?

移动端开发并不是Web前端开发,但移动前端开发和web前端开发其实都属于前端开发的范围,目前前端发展的趋势就是大前端,可以说是包罗万象。但不论趋势如何发展,目前来看HTML、CSS和JavaScript依然是整个前端开发的三大基石。所以不论…

mtk一键usb驱动_三菱MRJEB驱动器报错,导致报错原因37.1参数设置范围异常?

三菱MR-JE-B驱动器报错,导致报错原因37.1参数设置范围异常?最近海蓝机电工程师们在做一个项目,做的是三菱MR-JE-B驱动器。工程师们在实操这个项目过程中遇到各种问题,其中就像驱动器报错的问题,导致报错的原因显示37,1…

将虚拟主机加入到netskills.net域环境_网站建设阿里云虚拟主机、ECS服务器、企业邮箱选择购买指南...

对于刚接触阿里云的人来说可能看到阿里云的产品介绍页面会比较头晕,各种产品分类,而且同一个产品在不同的分类目录下都能看到,下面简单介绍一下制作网站过程中常用的阿里云的产品。阿里云在网站建设方面常用的服务主要有云虚拟主机、ECS云服务…

linux查看硬盘smart_Linux检测磁盘坏道工具用什么命令

请关注本头条号,每天坚持更新原创干货技术文章。如需学习视频,请在微信搜索公众号“智传网优”直接开始自助视频学习1. badblock命令简介badblock命令用于查找磁盘中损坏的区块。 电脑硬盘出现坏道后,如果不及时更换或进行技术处理&#xff0…

python弹出框_selenium+python学习——弹出框

1、alert警告框 以百度搜索设置为例,在进行搜索设置后点击“保存设置”按钮,弹出alert对话框,如下图所示:实现代码: from selenium import webdriver import time as t from selenium.webdriver.support.select import…

网页打开共享目录_你会做Excel文件目录吗?真的太太太太太简单了!

点击蓝字发送【2020】免费领 100图表模板!本文作者:长小安本文编辑:尔冬哈喽大家好!我是长小安,一名和秋叶一起学了多年 Excel、现在成功出道投稿的同学~让我来猜猜,你的电脑是不是也像我的一样&#xff0c…

centos7搜狐 mysql_基于centOS6.7搭建LAMP(httpd-2.4.18+mysql-5.5.47+php-5.6.16)环境

首先确保系统可以联网。设置IP地址以及虚拟机安装linux在此略过。本文采用centos6.7 64位minimal版、php5.6.16、httpd-2.4.18、mysql-5.5.47版搭建lamp环境。默认设置软件下载目录/usr/local/src,软件安装目录/app/local/下,安装顺序是apache→mysql→p…

c语言 如何创建adt_编程那些事儿:面向对象编程基石之数据抽象(ADT)

数据抽象本文接上一篇《编程那些事儿:为什么说抽象是面向对象编程的根基【1】?》,在了解了面向对象编程抽象的过程抽象后,下面我们继续聊一下数据抽象。数据抽象可以说面向对象编程最重要的基石。而数据抽象的结果就是数据类型(或简单的类型)…

mysql客户端版本太低_windows一机多装mysql,5.5低版本+5.8高版本

安装第一个mysql,压缩版或者安装版,过程省略,目录如下:运行中输入“regedit”进入后,找到 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MySQL其实mysql启动的路径一般为:可执行文件的路径&…

java8 list 行转列_Java14 都来了,你还不会用 Java8吗?

Java 8 于2014年3月18日发布,并且成为主流的 Java,如今,虽然 Java 14 都已经发布了,但是 开发者和公司选择的版本依旧是经久不衰的 Java 8 版本,如果你还不了解这些新特性,是时候学习一下了。Java 8 更新的…

mysql主键用完了怎么办_MySQL 自增 ID 用完了怎么办?

MySQL 自增 ID 用完了怎么办?在MySQL中有很多类型的自增ID,每个自增ID都设置了初始值,然后按照一定的步长增加,只要定义了字节长度,那么就会有上限,如果达到上限再次添加,则会报主键冲突错误&am…

python字典统计_python字典计数

广告关闭 腾讯云11.11云上盛惠 ,精选热门产品助力上云,云服务器首年88元起,买的越多返的越多,最高返5000元!字典?thcollections.counter 计数器? image.png找不到键的时候会调用miss方法如果键不是字符串&#xff0c…

java相册_精致小巧的java相册制作方法

本文实例为大家分享了java相册制作方法,供大家参考,具体内容如下注:1)html上的图片是静态指定的。当更新了新的图片时必须手工更新。所以使用Servlet读取本地images中的所有图片,动态显示给用户。2)如果存在中文名的图片&#xff…

利用python画分形图_使用 Python 绘制 Mandelbrot 分形图

以前收集的关于Mandelbrot分形图的Python脚本,Mandelbrot集合的图像因它洛可可式繁复卷曲华丽的风格而受到大众的欣赏和赞叹,与其他分形图案一起,是许多艺术家和设计师的灵感来源。 是由于今天个人电脑计算能力的强大,才有可能让我…

java sound api_Java Sound API

Java Sound API是javaSE平台提供底层的(low-level)处理声音接口。例外,java也提供了简单的实用的高层媒体接口(higher-level) - JMF(Java Media Framework)。Java Sound API 将需要处理的数字音频分为:simpled-audio和midi,分别提供Package来…

java实现的小程序_Java实现 微信小程序 + 消息推送

实现效果:下面要显示五个字段接下来,参照官方文档,一步步实现:一、官方给出请求示例、返回示例二、根据上面编写实体类(1)请求参数Datapublic class SendTemplateReq {/*** 接收者(用户)的 openid*/private String touser;/*** 所…

vb excel 整行删除_Excel中常用的批量处理都不掌握,那就真的Out了

针对一些有规律,能批量处理的数据,必须采用批量处理的方法,否则对于工作效率就会有很大的影响。今天我们来学习的内容是Excel中的6个批量处理技巧。一、Excel批量技巧:批量求和。目的:按照“季度”和“产品”两个维度计…

exe编辑器_【小功能】Unreal Editor中调用exe

后续文章更新移步→微信公众号“虚幻社区”(mantra-xhsq),您的支持是我创作的动力。在程序界混,哪能碰上不改需求的策划 --Mantra最近遇到了一个奇葩的需求,在Unreal Editor的Toolbar中添加一个快捷键,可以…