[源码-react02] 手写hook调度-useState实现

805 阅读17分钟

hook-useState.png

导航

[react] Hooks

[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

[源码-react01] ReactDOM.render01
[源码-react02] 手写hook调度-useState实现

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例
[深入24] Fiber

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC
[前端学java12-SpringSecurity] JWT
[前端学java13-SpringCloud] Eureka + RestTemplate + Zuul + Ribbon

(一) 前置知识

(1) 一些单词

raw 未加工的 原始的
process 处理 进程
bootstrap 引导器
survey 调查 调研 
dimension 维度
destructure 解构
passive 消极的 被动的
destructure 解构 // dob dar 结构对象和数组

persist 坚持 持久化 // usePersistFn 持久化一些函数
imperative 命令式 紧急的 // useImperativeHandle + forwardRef
delegation 委托 // 事件代理,事件委托

credentials 凭证 资格证书 // fetch-option-credentials: 'include' 'same-origin' 'omit'
omit 省略 忽略

metrics 指标
interval 间隔

(2) fetch 和 XMLHttpRequest

  • fetch 和 XMLHttpRequest设置cors跨域 - 要携带cookie
    • ajax => xhr.withCredentials = true
    • fetch => option.credentials = 'include' | 'same-origin' | 'omit'
      • 跨域 => 'include'
      • 只想在请求URL与调用脚本位于同一起源处时发送凭据 => 'same-origin'
      • 不包含凭据 => 'omit'
      • credential凭证的意思 omit忽略的意思
  • fetch
    • 语义简洁,基于promise实现,支持 async await,返回值是一个promise对象
  • response对象
    • 属性 header status statusText
    • 类型
      • response.json() -------- json数据
      • response.text() -------- 文本数据,比如请求静态资源是 .md 类型的文件
      • response.blob() -------- 图片
      • response.formDate() ---- 表单

(3) DOM-事件

  • 2021/08/15 复习 事件模型.png
  • EventTarget 接口
    • ( DOM的事件操作 ) 都定义在 ( EventTarget接口 ) 上
    • EventTarget接口主要提供了三个实例方法
      • addEventListener(type, listener[, useCapture])
        • 绑定事件的监听函数
        • 参数
          • type 事件名称 - 大小写敏感
          • listener 监听函数 - 事件触发时调用
          • useCapture 是否在捕获触发(可选参数) - 默认false,表示在冒泡阶段触发
        • 参数注意点
          • ( 如何实现只触发一次事件?once )
          • 第二个参数:除了是监听函数,还可以是 ( 具有handleEvent方法的对象 )
          • 第三个参数:除了 ( useCapture ) 外,还可以是一个 ( 属性配置对象 )
            • capture:表示是否在捕获阶段触发 boolean
            • once:表示监听函数是否只触发一次 boolean
            • passive:表示监听函数不会调用 preventDefault 方法
        • this
          • 监听函数中this,指向 ( 事件所在的那个对象,即事件所绑定的那个节点 )
      • removeEventListener
        • 作用:用来移除 addEventListener 方法添加的事件监听函数
        • 注意点
          • removeEventListener 生效条件
          • 必须和addEventListener绑定 ( 同一个DOM节点 ) 和 ( 相同的参数 )
      • dispatchEvent
        • 在 ( 当前节点 ) 触发 ( 指定的事件 ),从而触发 ( 监听函数 )
        • target.dispatchEvent(event) - 参数是 Event 对象的实例
  • 事件绑定监听函数的三种方法
    • html的 on- 属性
      • on- 属性的值是 ( 将要执行的代码,而不是一个函数,要执行函数需要加() )
      • 只会在冒泡阶段触发
      • 直接设置on-,和通过元素节点的 ( setAttribute ) 方法设置on-属性,效果一样
    • 元素节点的事件属性
      • 只会在冒泡阶段触发
      • 属性对应的是一个 ( 函数名 )
    • addEventListener
  • 三种绑定事件的监听函数的优缺点
    • on-:违背了html于javascript分离的原则,只能在冒泡阶段触发,指定是要执行的代码
    • 元素节点的事件属性:只能绑定一个监听函数,只能在冒泡阶段触发
    • addEventListener:( 同一事件可绑定多个监听函数) ( 能指定触发阶段,设置触发一次 )
  • 事件的传播
    • capturePhase捕获阶段:window->document->html->bodu-> ...目标节点
    • targetPhase目标阶段:在目标节点上触发
    • bubblingPhase冒泡阶段:目标节点->window
  • 事件代理 - delegation
  • Event 对象
    • targetcurrentTarget
      • target:表示 ( 最先触发事件的节点 )
      • currentTarget:表示 ( 绑定事件监听函数的节点 )
    • preventDefault
      • ( Event.preventDefault ) 用来 ( 取消 ) 浏览器 ( 当前事件的默认行为 )
      • 生效的前提 ( cancelable = true )
      • 只是取消默认行为,不会阻止传播
      • 阻止传播是 stopPropagation + stopImmediatePropagation
      • 可以用来做文本框的验证,验证不成功取消默认的文本选中
    • stopPropagation
      • ( Event.stopPropagaton ) 阻止事件在DOM中继续传播
        • stoppropagation()可以防止再触发定义在别的节点上的同一事件的监听函数,但不包括当前节点上的其他监听事件,也不包括同一事件的其他监听函数也会执行
        • 要彻底阻止事件传播则需要使用stopImmediatePropagation()
    • stopImmediatePropagation
      • 阻止同一个事件的其他监听函数被调用,同一个事件的其他监听函数也会被阻止执行
  • CustomEvent 自定义事件接口
    • CustomEvent接口用于生成 ( 自定义的事件实例 )
    • 自定义事件的作用?
      • 浏览器预定义的事件,不能在事件上绑定数据
      • ( 自定义事件 ) 可以在 ( 触发事件的同时 ),传入 ( 指定的数据 )
      • const eventInstance = new CustomEvent(type, options) image.png

(4) 链表

  • 概念
    • ( 链表 ) 通过 ( 指针 ) 将 ( 分散的内存 ) 链接到一起
  • 特点
    • 链表是非连续的,所以不需要一块连续的存储空间
  • 节点
    • 每个内存块就是一个节点
    • 为了将每个节点连接在一起,每个节点不仅( 存储数据 ),还要 ( 记录下一个节点的地址 )
  • 后继指针next
    • 记录下一个节点的指针称为后继指针next
  • 单链表
    • 单链表是单方向顺序的一个线性表
    • 单链表中比较特殊的节点
      • 第一个节点:头节点 - 记录链表的基地址
      • 最后一个节点:尾节点 - 指针指向空地址Null
    • 如果判断一个节点是不是尾节点
      • 可以通过 后继指针next是不是Null来判断是不是节点是不是尾节点
    • 单链表插入和删除的时间复杂度是多少?
      • O(1)
      • 这个是理论值,需要有前提条件
      • 首先遍历寻找的时间复杂度O(n),然后添加删除的时间复杂度是O(1) => O(n)
    • 节点的组成
      • ( 数据域 ) 和 ( 指针域 )
  • 双向链表
    • 双向链表可以双向遍历,灵活度更高
    • 双向链表在 ( 删除,插入 ) 节点时效率更高
  • 循环链表
    • 循环链表是一种 ( 特殊的单链表 )
    • 循环链表与单链表的区别
      • 循环链表的(最后一个节点)的指针指向(头节点)而不是(空地址Null)

(二) js - 实现一个单链表

// js实现 - [单向链表]
// 单向链表, 每一个元素都有一个存储元素自身的节点和一个指向下一个元素引用的节点组成
function LinkedList() {
  // NodeConstructor
  // 链表节点
  const NodeConstructor = function (el) {
    this.el = el; // 存储的数据
    this.next = null; // 指针
  };

  this.head = null; // 单链表的第一个节点,初始值 null,其实head保存的就是整个链表
  this.length = 0; // 单链表的长度,初始值 0


  // 在尾部添加节点
  this.append = (el) => {
    let node = new NodeConstructor(el);
    let current = null; // 指针
    if (!this.head) {
      // -------------- 头部节点不存在,生成一个节点
      this.head = node;
    } else {
      // ----------------------- 头部节点存在,找到最后一个节点并将最后一个节点的next指向要插入的node
      current = this.head; // 缓存头部节点
      while (current.next) {
        current = current.next;
        // 用current不断的指向下一个节点,直到最后一个节点时不满足条件,最后一个节点的 ( lastNode.next = null )
        // 其实就是找到最后一个节点,然后将next指向node,则是在尾部添加节点
      }
      current.next = node; // 将最后一个节点的 next 指向 node,则添加完成,最新添加的node成为单链表的最后一个节点
    }
    this.length++; // 添加节点完成后,将长度+1
  };


  // 插入节点
  // 实现插入节点逻辑首先我们要考虑边界条件,如果插入的位置在头部或者比尾部位置还大,我们就没必要从头遍历一遍处理了,这样可以提高性能
  this.insert = (position, el) => {
    const node = new NodeConstructor(el);
    let prevNode = null;
    let current = this.head;
    let currentIndex = 0;

    if (position >= 0 && position < this.length) {
      if (position === 0) {
        // 插入头部
        node.next = current; // 插入的元素的 next => head
        this.head = node; // 插入后,插入的节点重新成为head
      } else {
        // 这里没有判断是不是最后一个,因为没有最后一个的标志位指针,还是要从头遍历
        while (currentIndex++ < this.position) {
          prevNode = current; // 游标的前一个位置节点
          current = current.next; // 不断向后寻找位置
        }
        node.next = current; // 找到了插入的位置,将 将要插入的节点的 node.next => current
        prevNode.next = node; // 找到了插入的位置,将 前一个节点的 next => node
        this.length++; // 插入完长度+1
        return true; // 表示插入成功
      }
    } else {
      return false;
    }
  };


  // 查询节点所在位置
  // 根据节点的值查询节点位置实现起来比较简单,我们只要从头开始遍历,然后找到对应的值之后记录一下索引即可
  this.indexOf = (el) => {
    let currentIndex = 0;
    let current = this.head;
    while (currentIndex < this.length) {
      if (current.el === el) {
        return currentIndex; // 找到返回节点位置
      }
      current = current.next; // 否则找下一个
      currentIndex++; // index+1
    }
  };


  // 移除指定位置的元素
  // 移除指定位置的节点也需要判断一下边界条件,可插入节点类似,但要注意移除之后一定要将链表长度-1
  this.removeAt = (position) => {
    // position表示位置,是number类型
    // 检测边界条件
    if (position >= 0 && position < this.length) {
      let prevNode = null; // 缓存当前节点,也可以理解为是当前节点的前一个节点
      let current = this.head; // 当前节点
      let currentIndex = 0; // 当前节点位置

      if (position === 0) {
        this.head = current.next; // 如果删除链表的第一个节点,则直接把第二个节点赋值给第第一个节点 ( head = head.next )
      } else {
        // 这里没有直接判断是不是最后一个,因为没有最后一个的标志位指针,还是要从头遍历 ( 即没有做这样的判断 position === length -1 )
        while (currentIndex++ < position) {
          prevNode = current;
          current = current.next; // 不断重头往后遍历,知道找到position所在的节点
        }
        prevNode.next = current.next;
        // 将 ( 要删除的节点的前一个节点的next ) 指向 ( 将要删除的节点的下一个节点 )
        // 这里包含了最后一个节点的删除,当是最后一个节点时,current.next === null,prevNode.next = current.next = null 表达式也是成立的
      }
      this.length--; // 操作完成,长度-1
      return current.el; // 返回删除的节点上的数据el
    } else {
      return null; // 随便返回一个值,表示没做任何操作,因为位置都不在链表上
    }
  };


  // 移除指定节点
  // 移除指定节点实现非常简单,我们只需要利用之前实现好的查找节点先找到节点的位置,然后再用实现过的removeAt即可
  this.remove = (el) => {
    let index = this.indexOf(el);
    this.removeAt(index);
  };


  // 判断链表是否为空
  // 只需要判断 length 是否为0,返回boolean
  this.isEmpty = () => {
    return this.length === 0;
  };


  // 返回链表长度
  this.size = () => {
    return this.length;
  };


  // 将链表转化为数组返回
  this.toArray = () => {
    let current = this.head;
    let resultList = [];
    while (current.next) { // 节点的next存在,就把el添加进数组,直到最后一个节点,最后一个节点的 ( lastNde.next = null )
      resultList.push(current.el);
      current = current.next; // 不断 next
    }
    return resultList;
  };
}


const linkedList = new LinkedList();

// 测试 append
// head(el:100) -> next(el: 200) -> next(null)
linkedList.append(100);
linkedList.append(200);
linkedList.append(300);
linkedList.append(400);
linkedList.append(500);
linkedList.append(600);
linkedList.append(700);

// 测试 indexOf
const index = linkedList.indexOf(300);
console.log(`index`, index);

// 测试 removeAt
linkedList.removeAt(1);

// 测试 remove
linkedList.remove(600);

// 测试 isEmpty
const isEmpty = linkedList.isEmpty()
console.log(`isEmpty`, isEmpty)

// 测试 size
const size = linkedList.size()
console.log(`size`, size)

// 测试 toArray
const arr = linkedList.toArray()
console.log(`arr`, arr)

(三) 手写 hook-useState实现

hook-useState.png

// 手写hooks源码 - useState

// (一)
// useState工作原理
// 1. 产生了更新,更新的内容保存在update对象中,更新会让组件重新render
// 2. 组件render时,useState调用后返回值数组,数组的第0项count为更新后最新的值
// 3. 更新分为两种
//    - mount: 初始化mount阶段时,count是useSate的参数即 initialState
//    - update: 点击事件等触发dispatcher函数,即useState的返回值数组的第二个成员函数,导致count更新为 count => count + 1

// (二)
// hooks中的一些概念
// 1. 链表
//    hook.memoizedState => 单向链表 - (hook对象,fiber对象) => hooks函数本的值身会保存在链表结构中,比如 [count, setCount] = useState(0) 中的 count
//    hook.queue =========> 环状链表 - (update对象) =========> 多个useState返回数组中的第二个成员setter函数( 更新执行的函数 ),也会保存在链表中,是一个环状链表,环状链表是特殊的单链表

let workInProgressHook = null;
// workInProgressHook
// 1. 第一种情况: workInProgressHook 指向当前正在计算的hook,相当于一个指针,时刻修改指向
// 2. 第二种情况: 执行到 schedule() 时,workInProgressHook 指向保存在 fiber.memoizedState 中的整个单向链表
// 3. 总结:
//    - workInProgressHook: 可能指向最新的hook对象,也有可能指向整个hook对象组成的整个单向链表
//    - fiber.memoizedState: 始终指向整个hook对象组成的单向链表

let isMount = true; // 标志位,boolean,表示是否是 ( mount ) 阶段,对应的有 ( mount update ) 两个阶段

// fiber节点
const fiber = {
  stateNode: App, // fiber.stateNode指向真实的DOM,即指向fiberRoot
  memoizedState: null, // hooks链表,memoizedState保存的是kook对象,用来保存function函数组件中的state数据,比如setState返回数组的第一个成员的值,本例中是count的值
};

// schedule
// 调度
function schedule() {
  workInProgressHook = fiber.memoizedState;
  // workInProgressHook
  // 1. 调度方法中的更新方法执行前,将workInProgressHook重置为fiber中的第一个hook,这样才能保证mount周期后,update周期遍历链表是从头往后遍历的
  // 2. 为什么是第一个hook,因为 fiber.memoizedState 是一整条链表,本身就是链表的第一个节点,叫做头节点

  const app = fiber.stateNode(); // 触发组件更新,即执行functionComponent
  isMount = false; // 第一次调度后,不再是mount节点,以后将是render阶段
  return app;
}

// dispatchAction
// 1. dispatchAction 就是 useState 的 setter 函数
const dispatchAction = (queue, action) => {
  const update = {
    action,
    next: null,
  };

  if (queue.pending === null) {
    update.next = update; // 初始化时,环状链表只有一个节点,所以 u0 -> u0
    // 1. 第一次
    // 1. 环状链表是 u0 -> u0
    // 1. 对应代码是 update.next = update
    // 2. 接下来代码 queue.pending = update;
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
    // 1. 非第一次
    // 1. 非第一次链表已经存在,因为第一次初始化得到的链表是u0,现在插入节点u1
    // 2. 环状链表是 u1 -> u0 -> u1
    // 2. u1->u0 对应的代码是 ( u1的update.next = u0的update.next还是u0,因为第一次u0->u0 ) 而 ( queue.pending = 上一次的update ) 所以 ( update.next = queue.pending.next )
    // 2. u0->u1 对应的代码是 ( u0的update.next = u1的update ) 即 ( queue.pending.next = update)
    // 3. 接下来代码的代码是 queue.pending = update; 即将 queue.pending指针指向最新的update,这里是u1,在下一次开始时成为旧的update,也就是环状链表的最后一个节点
  }

  queue.pending = update;
  // queue.pending
  // 1. 初始化时,queue.pending = update,即 queue.pending = update.next = update -> u0
  // 2. 本次dispatchAction调用update是最新的update,在下一次新的dispatchAction调用时,update将成为旧的update

  schedule(); // dispatch执行后,执行调度函数schedule,更新组件
};



function useState(initialState) {
  let hook;

  if (isMount) {
    // ----------------------------------------- mount
    hook = {
      memoizedState: initialState, // 保存初始值,即 useState 的参数
      queue: {
        pending: null, // 环形链表,用来保存update组成的链表数据,update对象上有action属性,该属性就是 const [count, setCount] = useState(0) 的 setCount 函数
      },
      next: null,
    };
    if (!fiber.memoizedState) {
      // mount阶段时,即初始化时 fiber.memoizedState = null
      fiber.memoizedState = hook; // 将 fiber.memoizedState 指向最新的 hook
    } else {
      //  fiber.memoizedState存在,说明在mount阶段中有多个 useState
      workInProgressHook.next = hook; // 将上一个hook的next指向当前的hook,即 prevHook.next -> hook,所以单链表中新节点是在旧节点之后
      // 1. mount阶段,第一个hook,第一次未进入时: workInProgressHook = fiber.memoizedState = hook
      // 2. mount阶段,第二个hook,会进入else:  workInProgressHook.next === fiber.memoizedState.next
    }
    workInProgressHook = hook; // 移动workInProgressHook的指针,从新将 workInProgressHook 指向最新的 hook,即 workInProgressHook 始终指向最新的 hook
  } else {
    // ----------------------------------------- update
    // 1. 更新时,hook对象已经存在,而 workInProgressHook 上保存的整个hook对象组成的链表
    // 2. 问题:为什么是整个链表?
    //    回答:因为更新执行 setCount => 触发 schedule() => 在更新 schedule() 调度开始时,都要把指针重新指向hook节点组成的单向链表
    // 3. 注意点:workInProgressHook 是一个指针,是会变的,可能指向当前最新的正在执行hook函数的hook对象节点,也可能指向这个那个单向链表

    hook = workInProgressHook; // ---------------------1.完整的hook链表
    workInProgressHook = workInProgressHook.next; // --2.注意这里workInProgressHook又会马指向最新的hook对象,每调用一次setCount,就往后移动一个节点从而找到当前的hook对象,从而知道现在是在处理哪个hook,即不断往后遍历,用于下一次遍历时就行赋值给hook ( 即hook = workInProgressHook )
  }

  let baseState = hook.memoizedState; // 当前,正在计算的hook的state数据,注意,此时还没有进行 dispatchAction
  if (hook.queue.pending) {
    let firstUpdate = hook.queue.pending.next; // 找到环形链表中的第一个update对象节点

    do {
      const action = firstUpdate.action;
      baseState = action(baseState); // 执行update对象上的action方法
      // action
      // 1. setCount((count) => count + 1) 对应 dispatchAction.bind(null, hook.queue) === dispatchAction = (queue, action) { ...}
      //    - dispatchAction.bind(null, hook.queue) 返回的新的函数就是 setCount,并且dispatchAction的第一个参数被固定成了hook.queue,setCount只需要传入第二个参数即可
      //    - dispatchAction(queue, action)
      //    - dispatchAction(hook.queue, (count) => count + 1) 所以 update.action 就是 (count) => count + 1

      // 2. baseState = action(baseState)
      //    - 就的state传入函数,返回值在赋值给 baseState,成为最新的state的值

      firstUpdate = firstUpdate.next; // 得到最新计算的state后,指针后移一位,继续执行下一个 setState
    } while (firstUpdate !== hook.queue.pending); // 最后一个update执行完后跳出循环

    hook.queue.pending = null; // 遍历完所有的 setCount 之后,清空环形链表
  }

  hook.memoizedState = baseState; // 再保存最新的state
  // ( 最新的state ) 赋值给 ( hook.memoizedState )
  // baseState 可能是新计算的值,也有可能是旧值,取决于上面的是否进入上面的 hook.queue.pending 的if语句中

  return [baseState, dispatchAction.bind(null, hook.queue)];
}

function App() {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(2);

  console.log(`isMount => `, isMount);
  console.log(`count,count2 =>`, count, count2);

  return {
    click: () => {
      setCount((count) => count + 1)
      setCount((count) => count + 2)
    },
  };
}

window.app = schedule();

(四) useLocalStorageState 自定义hook-ahooks源码

// 1 useLocalStorageState
// 一个可以将状态持久化存储在 localStorage 中的 Hook 。
const useLocalStorageState = createUseStorageState(
  typeof window === 'object' ? window.localStorage : null,
  // window存在,传入 window.localStorage
  // 否则传入 null
);

export default useLocalStorageState;
  • createUseStorageState

//createUseStorageState
export function createUseStorageState(nullishStorage: Storage | null) {
  function useStorageState<T = undefined>(key: string): StorageStateResult<T>; // 只有一个参数的情况
  function useStorageState<T>( // 两个参数的情况
    key: string,
    defaultValue: T | IFuncUpdater<T>,
  ): StorageStateResultHasDefaultValue<T>;

  function useStorageState<T>(key: string, defaultValue?: T | IFuncUpdater<T>) {
    const storage = nullishStorage as Storage;
    const [state, setState] = useState<T | undefined>(() => getStoredValue());

    useUpdateEffect(() => {
      setState(getStoredValue());
    }, [key]);
    // useUpdateEffect - 首次加载不运行,之后只在依赖更新时运行

    // const useUpdateEffect: typeof useEffect = (effect, deps) => {
    //   const isMounted = useRef(false);
    //   useEffect(() => {
    //     if (!isMounted.current) {
    //       isMounted.current = true;
    //     } else {
    //       return effect();
    //     }
    //   }, deps);
    // };

    // getStoredValue
    // 1. raw存在,转成对象返回
    // 2. row不存在
    //    - 1. defaultValue 是一个函数,调用并返回执行结果
    //    - 2. defaultValue 不是一个函数,直接返回
    function getStoredValue() {
      const raw = storage.getItem(key); // raw:未加工

      if (raw) {
        try {
          return JSON.parse(raw); // storage中存在key对应的数据,parse 并返回
        } catch (e) {}
      }

      if (isFunction<IFuncUpdater<T>>(defaultValue)) {
        // 1
        // if
        // - 如果 defalut 是一个函数,调用函数,返回调用结果值
        // 2
        // defaultValue
        // - useLocalStorageState() 的第二个参数,表示初始化默认值
        return defaultValue();
      }

      return defaultValue;
    }

    const updateState = useCallback(
      (value?: T | IFuncUpdater<T>) => {
        if (typeof value === 'undefined') {
          // 1. undefined
          // - storage 清除 // updateState() 或者 updateState(unfined)
          // - state undefined
          storage.removeItem(key);
          setState(undefined);
        } else if (isFunction<IFuncUpdater<T>>(value)) {
          // value = (prevState: T) => T
          // 2. function
          // - storage 存入新值 - 新值是 value(previousState) 函数调用的返回值
          // - state
          const previousState = getStoredValue();
          const currentState = value(previousState);
          storage.setItem(key, JSON.stringify(currentState));
          setState(currentState);
        } else {
          // 3. 非 undefined 和 function
          // - storage 存入新值
          // - state value
          storage.setItem(key, JSON.stringify(value));
          setState(value);
        }
      },
      [key],
    );

    return [state, updateState];
  }

  if (!nullishStorage) {
    // localStorage不存在时熔断处理
    return function (_: string, defaultValue: any) {
      return [
        isFunction<IFuncUpdater<any>>(defaultValue) ? defaultValue() : defaultValue,
        () => {},
      ];
    } as typeof useStorageState;
  }

  return useStorageState;
}
  • useUpdateEffect
// useUpdateEffect
// - 模拟 componentDidUpdate,当不存在依赖项时
const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isMounted = useRef(false);

  useEffect(() => {
    if (!isMounted.current) {
      // 1
      // ref.current
      // ref.current 的值在组件的整个生命周期中保持不变,相当于classComponent中的一个属性,因为属性挂载到原型链上的
      // 2
      // react源码中 ref 对象通过 Object.seal() 密封了,不能添加删除,只能修改
      isMounted.current = true; // 初始化时,进入if,false => true;之后不再进入
    } else {
      return effect();
      // 1. update => 第一次不执行effect(),只有也只会在依赖更新时执行即除了第一次,以后和useEffect行为保持一致
      // 2. 如果没有依赖项 deps,则和 ( compoenntDidMount ) 行为保持一致
    }
  }, deps);
};

export default useUpdateEffect;

资料