[React 从零实践03-后台] 自定义hooks

2,513 阅读28分钟

导航

[react] Hooks

[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

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

[深入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] 算法 - 查找和排序

前置知识

(1) 一些单词

phenomenon:现象

converter:转换器
(比如:对原始数据的转换,过滤等)

memory:记忆
(比如:函数式编程中的 记忆函数 )

deprecated:已弃用
( vscode-tslint (deprecated) )

mutable:可变的
( interface MutableRefObject<T> { current: T;} )

associate:关联
(
  // 表关联的字段
  User.associate = function() {
    // 一对多
    app.model.User.hasMany(app.model.Diary, { foreignKey: 'user_id', targetKey: 'id'})
  }
)

legend:图例

imperative:命令式,强制,迫切
( useImperativeHandle )
  • 2021/01/08 更新过

(1) useState

  • useState函数签名
    • function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]
      • type Dispatch<A> = (value: A) => void
      • type SetStateAction<S> = S | ((prevState: S) => S)
  • 上面签名的意思是
    • [state, setter] = useState(initialState)
    • initialState
      • ( 是一个值 ) 或者 ( 一个函数,该函数会返回一个值 )
    • setter
      • setter是一个函数,没有返回值,setter函数的参数是一个值或者一个函数,setter函数的参数函数的参数是上一次的state,并且返回值和参数类型要一致
  • useState的神奇之处!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    • setter 函数可以触发重新渲染
    • state 是单独的作用域,重新渲染不会重新声明赋值,而是存在于闭包中
    • state 每次渲染具有独立的值,即 capture value
      • 扩展:每次渲染时,( state, 事件监听函数 ) 等都是独立的,即只属于那一次渲染

(2) useEffect

  • useEffect函数签名
    • function useEffect(effect: EffectCallback, deps?: DependencyList): void
      • type EffectCallback = () => (void | (() => void | undefined))
      • type DependencyList = ReadonlyArray<any>
  • 上面签名的意思是
    • useEffect(() => { return Cleanup(){...} }, [...])
    • 第一个参数:是一个函数,该函数还可以返回一个 Cleanup 清除函数
    • 第二个参数:是一个依赖数组
  • useEffect的神奇之处!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    • useEffect第一个参数函数
      • useEffect 在渲染完成之后执行,因此不会阻塞渲染
        • 渲染前执行 ( useLayoutEffect同步钩子 )
          • 那要在渲染执行执行一些任务时怎么办? 可以使用 useLayoutEffect
        • 渲染后执行 ( useEffecct异步钩子 )
          • useEffect在渲染之后执行,不阻塞渲染,提高性能
      • useEffect 执行前,会先执行上一次useEffect的 Cleanup 清除函数 ( 如果有的话)
        • useEffect只执行一次就没有上一次
        • useEffect第一次执行也不会执行 Cleanup 函数,第二次才会执行上一次的
        • 组件卸载时,执行最有一次的 useEffect 的 Cleanup 函数
      • 当组件销毁时,会执行最后一次useEffect的 Cleanup 清除函数
    • useEffect的第二个参数 - 依赖数组
      • 依赖数组用来控制是否触发 useEffect() 钩子函数
      • useEffect等价于 ( componentDidMount ) ( componentDidUpdate ) ( componentWillUnmount ) 三个生命周期
        • 模拟componentDidMount(只在挂载阶段执行)
          • useEffect(()=>{}, [])
          • 在依赖数组为空时,如果存在 Cleanup 取消函数,第一次后都不会执行,直到组件销毁时才执行 Cleanup 清除函数
        • 模拟componentDidUpate(挂载阶段不执行,在更新阶段执行)
          • 用一个标志位,isDidUpdate默认是false,则第一次useEffect不执行,再设置为true,第二次以后就会执行
          • useEffect(() => { if(isDidUpdate){ // compenntDidUpdate阶段执行的代码 }; setIsDidUpdate(true) })
      • 依赖数组是如何比对的?,主要通过 Object.is 来做对比
        • 所以,如果依赖数组成员是( 数组,对象,函数 )等引用类型的数据时,需要使用useMemo()和useCallbak()来处理
        • useMemo(() => object, [依赖数组])
        • useCallback(fn, [依赖数组])
        • useMemo用来缓存任意类型的数据,包括函数
        • useMemo(() => fn, []) 等价于 useCallback(fn, [])
      • 为什么在 useEffect(() -> { setter() }, [依赖]) 中调用 setter 函数时,没有把state的setter函数作为依赖项???
        • 因为react内部已经把setter函数做了memoizaton缓存处理,即每次的setter函数都是同一个函数,所以就不用手动通过useMemo或者useCallback来做memoization了
    • 还有一个注意点
      • 案例:useEffect( async() => {}, [依赖]) 这样的写法超级不好
      • 因为:由useEffect的函数签名可知,第一个参数函数要么没有返回值,要么返回一个 Cleanup 清除函数,而 aysnc函数返回的却是 promise 对象,强烈不建议这样使用

(1-2) 总结useState和useEffect

  • useState和useEffect每次调用都被添加到 Hook ( 链表 ) 中
  • useEffect还会额外在一个 ( 队列 ) 中添加一个等待执行的回调函数,即 useEffect的第一个参数函数,在渲染完成后,依次调用队列中的Effect回调函数
  • 关于为什么hooks必须放在组件头部?不能在循环,嵌套,条件语句中使用?
    • 主要是保证每次执行函数组件时,调用hooks的顺序保持一致
    • 因为 循环,嵌套,条件语句都是动态语句,可能会导致每次函数组件调用hooks的顺序不能保持一致,导致链表记录的数据失效

(3) 神奇的 useRef

  • const refContainer = useRef(initialValue);
    • 函数签名:function useRef<T>(initialValue: T|null): RefObject<T>;
    • 如果报错current是常量的话,需要这样传入类型变量 const countRef = useRef<类型|null>(null)
  • 相关方法:
    • useImperativeHandle
      • useImperativeHandle(ref, createHandle, [deps]) 可以让你在使用 ref 时,自定义暴露给父组件的实例值
      • useImperative应该与React.forwardRef一起使用,并尽量避免这样的命令式操作
    • React.forwardRef
      • React.forwardRef(fnComponent(props, ref)) 可以接收父组件传递过来的 ref 对象,并且返回一个react节点
  • useRef返回一个 ( 可变的ref对象 ),( ref.current ) 被初始化为 initialValue
  • 返回的 ref 对象在整个组件生命周期内 ( 保持不变 ),即每次渲染返回的都是同一个ref对象
  • 注意点
    • useRef返回的ref对象,在组件整个生命周期保持不变,即相当于class组件中的实例属性
    • ref对象的值更新后,不会重新渲染
    • ref.current发生变化应该作为Side Effect(因为它会影响下次渲染),所以不应该在render阶段更新current属性
    • ref.current 不可以作为其他 hooks(useMemo, useCallback, useEffect)依赖项
    • useState每次返回的state都是快照,每次渲染相互独立;而useRef不是快照,而是同一个对象的引用
  • 使用场景
    • 1. 绑定DOM
    • 2. 父组件调用子组件中的方法
    • 3. 保存任意可变值,类似于class中的实例属性
    • 4. 利用 ( useRef+setTimeout ) 可以实现修改state后,立即获取修改后的值 - 注意的是如果立即修改state后,只利用setTimeout去获取state,也不是最新的,因为获取的是state的快照,即使延时打印
  • useRef,useImperativeHandle,React.forwardRef综合案例:
import React, { useRef, useState, useEffect, useImperativeHandle } from 'react'

interface IGetmessage {
  getMessage: () => void
}

const Father = () => {
  const [count, setCount] = useState(0)
  const inputRef = useRef<HTMLInputElement>(null)
  const childRef = useRef<IGetmessage>(null)
  const countRef = useRef<number | null>(null)

  useEffect(() => {
    countRef.current = count
    // 每次渲染后,都更新ref.current
  })

  const getFocus = () => {
    inputRef.current && inputRef.current.focus()
    console.log('useRef绑定DOM节点');
  }

  const add = () => {
    setCount(prevCount => prevCount + 1)
    console.log(count, 'setState后,立即打印state的值,是上一次的state值,因为此时回调并没有执行,如果要拿到的话,可以使用setTimeout宏任务+sueRef实现,在更新后打印')
    setTimeout(() => {
      console.log(countRef.current, '这是在setState后利用 ( setTimeout+useRef ) 获取的最新的state的值')
    }, 1000)
    setTimeout(() => {
      console.log(count, '注意:如果直接在setState后,利用setTimeout中打印state,任然不是更新后的state,因为state保存的是快照')
    }, 1000)
  }
  const delayConsole = () => {
    setTimeout(() => {
      console.log('不用useRef延时打印cout,是打印的快照count,即打印的是当时add得到的count,而不是现在add得到的count :>> ', count);
    }, 3000)
  }


  const delayConsoleUseRef = () => {
    setTimeout(() => {
      console.log('用useRef保存count的值,每次渲染后更新ref.current,延时打印的不是快照,而是本次渲染的count :>> ', countRef.current);
    }, 3000)
  }

  const getChildMehod = () => {
    childRef.current && childRef.current.getMessage() // 调用子组件传递的方法
  }

  return (
    <div style={{ background: '#fff', margin: '10px 0', padding: '10px', border: '1px solid black' }}>
      <p>父组件</p>
      <p style={{
        margin: '10px', padding: '14px 24px', background: '#e8eaff',
        border: '1px solid #345bf9', display: 'inline-block',
      }}> useRef </p>

      <div>
        <input type="text" ref={inputRef} />
        <button onClick={getFocus}>获取焦点</button>
      </div>

      <br />

      <div style={{ background: '#bcffb7', padding: '10px', margin: '10px 0' }}>
        <p>count: {count}</p>
        <button onClick={add}>add</button> &nbsp;
        <button onClick={delayConsole}>不用useRef时,延时打印count</button> &nbsp;
        <button onClick={delayConsoleUseRef}>用useRef保存count的值,延时打印count</button>
      </div>

      <br />
      <button onClick={getChildMehod}>useRef+useImperativeHandle实现父组件调用子组件的方法</button>
      <Child ref={childRef} />
    </div>
  )
}

const Child = React.forwardRef((props: any, ref: any) => { // react.ForwardRef() 获取父组件传递的ref作为子组件props
  useImperativeHandle(ref, () => ({ // useImperativeHandle() 设置允许子组件暴露给父组件的方法
    getMessage: () => {
      console.log('打印子组件方法的内容');
    }
  }))

  return (
    <div style={{ margin: '10px', border: '1px solid red', padding: '4px' }}>
      <p>子组件</p>
    </div>
  )
})

export default Father

(4) useReducer,useContext,React.createContext 实现一个简单的 redux

  • reducer函数
    • 概念:
      • (state, action) => newState
    • 特点:
      • 必须是一个纯函数
      • Array.prototype.reduce(reducerFunction)
        • 回调函数reducerFunction就是一个reducer函数,接受一个旧的结果值,返回新的结果值并赋值给旧的结果值,然后继续迭代
  • useReducer
    • const [state, dispatch] = useReducer(reducer, initialArg, init);
  • useContext
    • const value = useContext(context) 参数是context实例,返回值是context当前的值,即最近的Provider组件的value值
    • useContext(context) 相当于 context.Consumer
    • useContext(context) 的作用是:读取context 的值,和 订阅context的变化在上层还是需要 context.Provider提供context的值
  • React.createContext()
    • const MyContext = React.createContext(defaultValue) 生成一个context实例
    • context.Provider
    • context.Consumer
      • <MyContext.Consumer>{value => React节点元素}</MyContext.Consumer>
      • value是最近的Provider提供的value,如果不存在Provider,则使用React.createContext(defaultValue)的value值
  • 实现一个redux
import React, { useReducer, useContext } from 'react';
import ReactDOM from 'react-dom';

// Store => 用React.createContext()生成一个context实例
const Store = React.createContext()

// initialState => 初始化reducer的state
const initialState = {
  count: 0
}

// reducer函数 => (state, action) => newState
const reducer = (state, action) => {
  switch (action.type) { 
    case 'ADD':
      return { ...state, count: action.payload }
    default:
      return { ...state }
  }
}

// Provider组件 => 提供 context 给子组件消费
const Provider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState) // useReducer
  return <Store.Provider value={{ state, dispatch }}>
    {children}
  </Store.Provider>
}

// App组件
const App = () => {
  const { state, dispatch } = useContext(Store)
  const { count } = state
  return <> 
          <p> count: {count}</p>
          <button onClick={() => dispatch({ type: 'ADD', payload: count + 1 })}>add</button>
        </>
}

// 挂载
ReactDOM.render(
  <Provider>
    <App />
  </Provider>,
  document.getElementById('root')
);

(5) useCallback,useMemo,React.Memo 做性能优化

  • useCallback
    • useCallback(fn, deps)
      • fn:需要缓存的函数
      • deps:依赖数组
    • 作用
      • 对函数做缓存,在依赖项没变化时,直接返回缓存的函数,类似函数记忆,只有当依赖项变化时才从新返会新的函数
      • 概括来说,useCallback就是用来解决 ( 函数引用相等 )
      • 大多数情况下,const memoFn = useCallback(fn, []) 都是传一个空的依赖数组,这样就能保证每次useCallback返回的是同一个函数fn,再传入useEffect(() => {}; [memoFn]) 时useEffect就不会重新执行参数函数,因为通过Object.is返回的永远是true,memoFn每次都是同一个函数
    • 使用场景
      • 1. 用于父组件向子组件传递函数属性时,来缓存传递的函数,然后子组件通过 ( React.memo ) 对props做浅比较,如果useCallback的依赖项不变,那么传递的函数属性会直接复用,浅比较的函数属性就不会变化,从而可以用来做性能优化
      • 2. 用于 useEffect() 的依赖做对比变化时,对函数做缓存
  • useMemo
    • 作用:
      • 主要用于对 ( 对象类型 ) 的数据进行缓存,淡然任意的类型的值都可以,只是对象用于优化
      • 也能实现 useCallback 的功useMemo是useCallback的超集,可以缓存任意类型的数据
      • useCallback(f1, [a]) 和 useMemo(() => f1, [a]) 相等的,缓存函数
      • useMemo(() => object, [a])
    • 使用场景
  • React.memo
    • React.memo(functionComponent, areEqual)
      • 第一个参数:functionComponent需要缓存的组件;
      • 第二个参数:areEqual缓存的条件函数,返回true就缓存,返回false就重新渲染
    • React.memo() 用于函数式组件,默认只是对props做浅比较,如果要做进一步的比较使用第二个参数函数
React.memo(functionComponent, areEqual) 
1. 第一个参数是需要缓存的functionComponet
2. 第二个参数是具体比较规则函数areEqual,返回ture不重新渲染,返回false才会重新渲染
3. React.memo用于函数式组件,默认只是对props做浅比较,如果要做进一步的比较使用第二个参数函数
React.memo优化的例子


// 父组件
const Father = () => {
  const [count1, setCount1] = useState({ number: 1 })
  const [count2, setCount2] = useState(2)
  const addCount1 = () => {
    setCount1(prev => ({ ...prev, number: prev.number + 1 }))
  }
  const addCount2 = () => {
    setCount2(prev => prev + 1)
  }
  return <MemoryChild count1={count1} addCount1={addCount1} count2={count2} addCount2={addCount2} />
}

// 子组件
const Child = ({ count1, count2, addCount1, addCount2 }) => {
  return (
    <>
      <p>count1: {count1.number}</p>
      <p>count2: {count2}</p>
      <button onClick={addCount1}>add - count1</button>
      <button onClick={addCount2}>add - count2</button>
    </>
  )
}
const areEqual = (prev, next) => {
  // 1. 当count2变化时,Child是不重新渲染的
  // 2. 只有当count.number变化时,才会重新渲染
  return prev.count1.number === next.count1.number
}
const MemoryChild = React.memo(Child, areEqual) // ------------- React.memo(functionComponent, areEqual)
  • useCallback,useMemo,React.Memo 做性能优化
const Father = () => {
  const [count, setCount] = useState(0)
  const [number, setNumber] = useState(1)

  const add = () => {
    setCount(prevCount => prevCount + 1)
  }
  const memoryAdd = useCallback(add, [])

  const obj = { age: 20 }
  const memoryObj = useMemo(() => obj, [])

  return (
    <>
      <button onClick={() => setNumber(number => number + 1)}>点击 - 改变number</button>
      <Child count={count}>用callback</Child>
      <NotMemoryFnChild count={count} add={add} />
      <MemoryFnChild count={count} memoryAdd={memoryAdd} />
      <NotMemoryObjChild obj={obj} />
      <MemoryObjChild memoryObj={memoryObj} />
    </>
  )
}

const Child = () => {
  return (
    <div style={{ margin: '10px', border: '1px solid red', padding: '4px' }}>
      <div>纯函数组件 - 父组件重新渲染,该子组件就会重新渲染</div>
      <div>{Math.random()}</div>
    </div>
  )
}

const NotMemoryFnChild = React.memo(({ count, memoryAdd }) => {
  return (
    <div style={{ margin: '10px', border: '1px solid red', padding: '10px' }}>
      <div>不用 ( useCallback ) 只用React.memo做对props浅比较,props中有函数时,每次钱比较的结果都是变化,子组件会重新渲染</div>
      <div>{Math.random()}</div>
    </div>
  )
})

const MemoryFnChild = React.memo(({ count, add }) => {
  return (
    <div style={{ margin: '10px', border: '1px solid red', padding: '10px', background: 'yellow' }}>
      <div>用 useCallback() 缓存子组件的props中的函数,并用React.memo做浅比较,props没变,子组件不重新渲染</div>
      <div>{Math.random()}</div>
    </div>
  )
})

const NotMemoryObjChild = React.memo(({ obj }) => {
  return (
    <div style={{ margin: '10px', border: '1px solid red', padding: '10px' }}>
      <div>不用 useMemo() 缓存 props中的对象属性,即使在React.Memo() 做浅比较,因为有对象props,每次都是一个新对象,导致浅比较的结果是props变化,子组件更新</div>
      <div>{Math.random()}</div>
    </div>
  )
})

const MemoryObjChild = React.memo(({ memoryObj }) => {
  console.log(memoryObj, 'memoryObj');
  return (
    <div style={{ margin: '10px', border: '1px solid red', padding: '10px', background: 'yellow' }}>
      <div>用 useMemo() 缓存 props中的对象属性,在React.Memo() 做浅比较,因为对象props做了缓存,props做浅比较时没有变化,子组件不更新</div>https://juejin.im/editor/drafts/6882614048181993479
      <div>{Math.random()}</div>
    </div>
  )
})

(6) 链判断运算符 ?.

  • ?.在链式调用的时候判断,左侧的对象是否为null或undefined,是的,就不再往下运算,而是返回undefined
  • 链判断运算符有三种用法
    • obj?.attribute 对象属性
    • obj?[attribute] 对象属性
    • function?.(params) 函数/方法的调用
const b = {fn: x}
function x(params) {console.log(params) }


b.fn?.(1111111)
// b.fn存在,就调用fn(1111111)
// b.fn不存在,返回 undefined
// ---------- 链判断运算符,判断左边对象是否为null或undefined,是返回undefined,不是就继续往下判断

(7) === 和 Object.is 的区别

  • ===
    • ( 数据类型 ) 和 ( ) 都一样,才严格===相等
    • 对象仅与自身严格相等 即使两个对象属性完全一样,也不相等
  • Object.is
    • 和严格相等运算符相似
  • 问题
    • 在useEffect等hooks的依赖项对比时,使用的是Object.is(),所以当依赖是对象(对象,数组,函数等)类型时,总是不相等,失去意义
  • 解决方案:
    • 如果是函数,可以用 useCallback 做缓存,这样在useEffect的依赖项有函数时,保证每次函数固定不变
=== 和 object.is 区别如下


NaN === NaN // false
Object.is(NaN, NaN) // true

+0 === -0 // true
Object.is(+0, -0) // false

(8) 数组的 - 非数字键

  • for in 可以遍历数组的 ( 数字键 ) 和 ( 非数字键 )
  • 所以:不建议用 for in 循环遍历数组,因为会遍历到非数字键,其他的比如 for, forEach 都只会遍历到数字键
  • for in 可以遍历数组和对象
const arr = [1, 2, 3]
arr.four = 4
arr.five = 5
// arr =>  [1, 2, 3, four: 4, five: 5]


for(let i in arr) { 
  console.log(i) 
}
// 1 2 3 four five

(9) 伪类 :nth-child

选中前5个 ------------------------------ :nth-child(-n+5)
选中第5个元素开始的后面的孩子 ----------- :nth-child(n+5)
选中第 5-10 个孩子 ---------------------- :nth-child(n+5):nth-child(-n+9)

(10) echarts在react-hooks中的封装

  • 封装要实现的功能

    • 传入参数:
      • options: object echarts数据配置对象
      • isResize: boolean 是否自适应窗口的变化
      • showLoading: boolean 是否显示加载数据动画
      • events: object 事件对象,可以传出需要监听的事件
        • key:是事件名称
        • value:是回调函数,回调参数是events对象echarts实例
      • wrapStyle: object 传入的样式
      • className:string 可以具体传入class名
  • 需要用到的 echarts 的相关属性和事件

    • echarts
      • echarts.init(dom, theme, opts)
      • echarts.getInstanceByDom(dom)
    • echartsInstance :echarts.init()生成的实例
      • echartsInstance.setOption():echarts图标的数据项
      • echartsInstance.on:绑定事件的处理函数
      • echartsInstance.resize():改变图表尺寸,在容器大小发生改变时需要手动调用
      • echartsInstance.showLoading():显示加载动画
      • echartsInstance.hideLoading():关闭加载动画
      • echartsInstance.clear(): 清空当前实例,会移除实例中所有的组件和图表
  • 代码

封装

import React, { useEffect, useRef, useState } from 'react'
import echarts from 'echarts'
interface Ioption {
  option: IAny; // 配置对象
  wrapStyle?: IAny; // 样式
  className?: string; // 自定义class,为了不影响全局,最好加上唯一的前缀
  theme?: string; // 主题
  events?: IAny; // 事件的配置对象,key事件名,value事件的回调,回调有events和echarts实例两个参数
  isResize?: boolean; // 是否自适应窗口变化
  showLoading?: boolean; // 是否显示loading
}
interface IAny {
  [propName: string]: any
}


const HocEcharts = ({
  option, // 配置对象
  wrapStyle = { width: '400px', height: '400px', background: '#fff' }, // 样式
  className,// 自定义class,为了不影响全局,最好加上唯一的前缀
  theme = 'vintage', // 主题
  showLoading = true, // 是否显示loading
  isResize = true, // 是否自适应窗口变化
  events, // 事件的配置对象,key事件名,value事件的回调,回调有events和echarts实例两个参数
}: Ioption) => {
  const ref = useRef<HTMLDivElement|any>(null)

  let instance: echarts.ECharts

  // getInstance 创建或获取实例
  const getInstance = async () => {
    instance = await echarts.getInstanceByDom(ref.current) || await echarts.init(ref.current, theme)
    instance.clear() // 清除实例
  }

  // setOption 设置配置项
  const setOption = async () => {
    showLoading && instance.showLoading('default') // loading动画开始
    await new Promise(resolve => {
      setTimeout(() => {
        instance && instance.setOption(option) // 模拟异步
        resolve()
      }, 1000)
    })
    showLoading && instance.hideLoading() // loading动画开始
  }

  const bindEvent = () => {
    if (instance && events) {
      for (let i in events) {
        instance.on(i, events[i].query, (e: any) => events[i].callback(e, instance))
      }
    }
  }

  const init = async () => {
    await getInstance() // 生成或者获取echart实例
    await setOption() // 设置echarts配置项
    await bindEvent() // 绑定事件
  }

  const resizeEcharts = () => {
    instance && instance.resize()
  }

  useEffect(() => {
    init()
  }, [])

  useEffect(() => { // 监听窗口变化,echarts自适应
    if (isResize) {
      window.addEventListener('resize', resizeEcharts)
      return () => window.removeEventListener('resize', resizeEcharts) // 移除监听
    }
  }, [])

  return (
    <div ref={ref} style={wrapStyle} className={className} />
  )
}

export default HocEcharts 
调用

<HocEcharts
  option={barOption2}
  className="custom-echarts-bar"
  theme={theme}
  isResize={true}
  showLoading={true}
  events={Events}
/>

(二) 自定义hooks

(1) usePrevious

  • 用来获取上一个值
import { useEffect, useRef } from 'react';

interface IusePrevious {
  <T>(state: T): T
}

export const usePrevious: IusePrevious = (state) => {
  const ref = useRef(state)
  useEffect(() => {
    ref.current = state
  })
  return ref.current
}

(2) useModal

  • const { CustomModal, toggle } = useModal(title)
  • 参数:传入modoal的title,content从children中获取
  • 返回值:返回一个CustomModal组件,和切换显示隐藏的回调
  • 定义
定义:
// 注意点:useModal和返回的CustomModal都接收了props


import React, { useState } from 'react'
import { Modal } from 'antd'

interface IformInstance {
  submit: () => void // form实例上的属性,这里只写了submit方法,其他业务逻辑type自行添加
}

interface ICustomModalProps {
  formInstance?: IformInstance;
  children: any; // 必传属性
}

const useModal = (title: string) => {
  const [visible, setVisible] = useState(false)
  const toggle = () => {
    setVisible(prevVisible => !prevVisible)
  }

  const CustomModal = (props: ICustomModalProps) => { // 返回CustomModal组件给业务方使用
    const {formInstance, children} = props
    const handleOk = () => {
      formInstance && formInstance.submit() // 如果child是form实例,就提交form,具体逻辑请自定义
      setVisible(false)
    }
    const handleCancel = () => {
      setVisible(false)
    }
    return (
      <Modal
        title={title}
        visible={visible}
        onOk={handleOk}
        onCancel={handleCancel}
      >
        {children}
      </Modal>
    )
  }

  return { CustomModal, toggle }
}

export {
  useModal
}
  • 定义:
使用

import { usePrevious } from '@/utils/hooks/use-previous'
import { useModal } from '@/utils/hooks/use-modal' // --------------------------- 引入
import { Button, Form, Input } from 'antd'
import React, { useState } from 'react'


const CustomHooks = () => {
  const { CustomModal, toggle } = useModal('USEMODAL')
  const swtichModal = () => {
    toggle()
  }

  // --------------------------------------------------------------------------- 调用
  return (
        <CustomModal formInstance={form}> 
          <Form
            name="basic"
            initialValues={{ remember: true }}
            form={form} 
          >
            <Form.Item
              label="Username"
              name="username"
            >
              <Input />
            </Form.Item>
          </Form>
          <div>其他内容</div>
        </CustomModal>
  )
}

export default CustomHooks

(3) useFetch

  • 参数: fetch fetchParmas
  • 返回值: data doFetch loading params
useFetch
----


import { useState, useEffect, useCallback } from "react";

type Tfetch = (...rest: any[]) => any; // 当函数参数接收任意数量,任意类型的参数时,可以用rest转成any[]类型
interface IfnParams {
  current?: number;
  pageSize?: number;
  total?: number;
  [propNmae: string]: any;
}
interface Iconverter {
  (data: any): any;
}

type TuseFetch = (
  fetch: Tfetch, // 请求函数
  fetchParams?: IfnParams, // 请求函数的参数
  isInitRun?: boolean | 'initRun' | 'initNotRun', // 初始化时,是否执行请求函数,接受boolean,和两个字符串 'initRun' 'initNotRun'
  converter?: Iconverter, // 转换函数
) => ({
  data: any;
  doFetch: Tfetch;
  loading: boolean;
  params: IfnParams;
});

const useFetch: TuseFetch = (
  fetch,
  fetchParams = {
    current: 1,
    pageSize: 8,
    total: 10,
  },
  isInitRun = true, // 初始化时,是否执行请求函数,接受boolean,和两个字符串 'initRun' 'initNotRun',默认值true
  converter = (data) => data
) => {
  const [params, setParams] = useState(() => ({ current: 1,  pageSize: 8, ...fetchParams }));
  const [data, setData] = useState<any>(null);
  const [loading, setLoading] = useState(false); // loading有两个作用,一个是防止重复点击,一个是loading动画
  const [isGoRun, setIsGoRun] = useState(isInitRun) // !!!!! 这里需要用state去替代传入的参数isInitRun,因为传入的参数isInitRun永远不变,这请求函数fech永远不会执行,而我们要在调用doFetch时,重新执行fetch函数

  const memoryFetch = useCallback(fetch, []);
  const memoryconverter = useCallback(converter, []);
  // const memoryParams = useMemo(() => params, [params])
  // 这里注意:
  // 1. params是引用类型,不需要在 useEffect中缓存的,因为state本身就做了缓存
  // 2. 但是:如果是常量 aaaaa 是引用类型,在useEffect中就必须用useMemo做缓存,Object.is()永远是false,死循环
  // const aaaaa = {a: 1111}
  // useEffect(() => console.log(aaaaa), [aaaaa])

  useEffect(() => {
    if ((typeof isGoRun === 'boolean' && !isGoRun) || isGoRun === 'initNotRun') {
      return
    }
    const fetchData = async () => {
      setLoading(true);
      try {
        const res = await memoryFetch(params);
        setLoading(false);
        if (res.data) {
          setData(() => memoryconverter(res.data));
        }
      } catch (err) {
        setLoading(false);
        console.error(err);
      }
    };

    fetchData();
  }, [memoryFetch, params, memoryconverter, isInitRun, isGoRun]);

  // doFetch() 用于按钮等重新请求数据
  const doFetch = (fetchParams: IfnParams): void => {
    setIsGoRun(true) // 设置之后,才会进入到fetch函数
    setParams(prevState => ({...prevState, ...fetchParams}));
  };

  return { data, doFetch, loading, params };
  // 返回
  // data: 数据
  // doFetch:请求函数
  // loading: 比如用于table的loading
  // params: 比如用于table的分页参数
};

export { useFetch };

(4) useOnce

  • 参数:delay 延时多少秒改变,不传就在下次渲染时改变
  • 返回:once 布尔值
import { useState, useEffect } from 'react';

const useOnce = (delay?: number) => {
  const [once, setOnce] = useState(false)
  useEffect(() => {
    delay 
      ? setTimeout(() => {
          setOnce(() => true)
        }, delay)
      : setOnce(() => true)
  }, [delay])
  return once
}
export { useOnce }

(5) useViewport

  • 功能:实时获取HTML的宽度和高度
  • 参数:
    • doSomething:当视口变化时,需要执行的函数
  • 返回值:
    • width
    • height
  • 优化:可以进一步优化的话,自己实现一个
  • useViewport 定义
useViewport自定义hooks的定义
------

import { useEffect, useReducer, useCallback } from 'react'

interface IViewportState {
  width?: number;
  height?: number;
}

interface IViewportActionn {
  type: string;
  payload: any;
}

// constant
const actionType = {
  CHANGE_VIEWPORT: 'CHANGE_VIEWPORT'
}

// reducer
const viewPortReducer = (state: IViewportState, action: IViewportActionn) => {
  switch (action.type) {
    case actionType.CHANGE_VIEWPORT:
      return {
        ...state,
        width: action.payload.width,
        height: action.payload.height,
      }
    default:
      return {
        ...state
      }
  }
}

// initialState
const initialState: IViewportState = {
  width: 0,
  height: 0,
}

// -------------------- reducer 版本 --------------------
/**
 * useViewport
 * @desc 实时获取视口的宽高
 * @param { function } doSomething 在窗口变化时,需要执行的函数
 */
export const useViewport = (doSomething?: () => void) => {
  const [state, dispatch] = useReducer(viewPortReducer, initialState)

  const changeViewport = () => {
    const HTML_DOM = document.documentElement
    const width = HTML_DOM.clientWidth
    const height = HTML_DOM.clientHeight
    dispatch({
      type: actionType.CHANGE_VIEWPORT,
      payload: { width, height }
    })
    if (doSomething) {
      doSomething()
    }
  }
  const memoryChangeViewPort = useCallback(changeViewport, [])

  useEffect(() => {
    memoryChangeViewPort()
    window.addEventListener('resize', memoryChangeViewPort, false) // 监听 resize
    return () => { window.addEventListener('resize', memoryChangeViewPort, false) }
  }, [memoryChangeViewPort])

  return {
    width: state.width,
    height: state.height,
  }
}

// -------------------- state 版本 --------------------
// export const useViewport = () => {
//   const HTML_DOM = document.documentElement
//   const [width, setWidth] = React.useState(HTML_DOM.clientWidth)
//   const [height, setHeight] = useState(HTML_DOM.clientHeight)
//   const changeWindowSize = () => {
//     const HTML_DOM_CURRENT = document.documentElement
//     setWidth(v => HTML_DOM_CURRENT.clientWidth)
//     setHeight(v => HTML_DOM_CURRENT.clientHeight)
//   }
//   useEffect(() => {
//     window.addEventListener('resize', changeWindowSize, false)
//     return () => {
//       window.removeEventListener('resize', changeWindowSize, false)
//     }
//   }, [])
//   return { width, height }
// }
  • useViewport 使用
useViewport自定义hooks的使用
------


import React, { useRef } from 'react'
import { useViewport } from '@/utils/hooks/use-viewport'
import './smart-viewport.scss'

const SmartViewport = () => {
  const ref = useRef<HTMLDivElement>(null)
  const timerRef = useRef<any>(0)
  const { width, height } = useViewport(doAnimate)

  const debounce = () => {
    if (timerRef.current) {
      window.clearTimeout(timerRef.current)
    }
    timerRef.current = window.setTimeout(() => {
      if( ref.current) {
        ref.current.style.display = 'none'
      }
    }, 2000)
  }

  function doAnimate() {
    if (ref.current) {
      ref.current.style.display = 'block'
    }
    debounce()
  }

  return (
    <div className="smart-viewport" ref={ref}>
      <span>wdith: {`${width}px`}</span>
      <span>height: {`${height}px`}</span>
    </div>
  )
}

export default SmartViewport

(6) useDebounce

  • 注意点:不能用闭包变量来给timer赋值,不然其他state更新会重新执行debounce函数,timer会重新被赋值为初始值
  • useDebounce定义
import { useRef } from "react";

interface IuseDebounce {
  (fn: Ifn, delay?: number, immediate?: boolean): IClosure;
}

interface Ifn {
  (...rest: any[]): any;
}

interface IClosure {
  (e: any, ...rest: any[]): any;
}

/**
 * @desc debounce 防抖函数
 * @param {function} fn 需要执行的函数
 * @param {number} delay 延时执行的时间段
 * @param {boolean} immediate 是否立即执行
 */
export const useDebounce: IuseDebounce = (
  fn: any,
  delay = 1000,
  immediate = false
) => {
  const refTimer = useRef(0); // 相当于class中的实例属性

  return (e, ...rest) => {
    if (immediate && !refTimer.current) {
      fn.call(rest);
      refTimer.current = 1; // 除了第一次进入,后面都不会在进入该函数
      return; // 第一次不往下执行
    }
    if (refTimer.current) {
      window.clearTimeout(refTimer.current);
    }

    refTimer.current = window.setTimeout(() => {
      fn.call(rest);
    }, delay);
    
    // 取消debounce
    // useDebounce.cancel = function() {
    //   if(refTimer.current) {
    //     window.clearTimeout(refTimer.current)
    //   }
    // }
  };
};

// -------------------- 变量 timer 版本 --------------------
// 问题:当 UseDebounce 组件中有其他 state 更新时,useDebounce是新的函数重新执行了,timer又会被重新赋值
// 如何验证:useDebounce在UseDebounce组件有其他state更新时重新执行了:在useDebounce中 console.log() 打印即可
// 如何解决:使用 useRef 固定数据,类似class中的实例变量
// export const useDebounce: IuseDebounce = (fn: any, delay = 1000, immediate = false) => {
//   let timer = 0;
//   return (e, ...rest) => {
//     if (immediate && !timer) {
//       fn.call(rest);
//       timer = 1;
//       return;
//     }
//     if (timer) {
//       window.clearTimeout(timer);
//     }
//     timer = window.setTimeout(() => {
//       fn.call(rest);
//     }, delay);
//   };
// };

  • useDebounce 调用
import React, { useEffect, useState, useRef } from 'react'
import { useDebounce } from '@/utils/hooks/use-debounce'

const UseDebounce = () => {
  const [count, setCount] = useState(0)
  const refInterval = useRef(0)
  const doSomething = () => {
    console.log('debounce');
  }
  useEffect(() => {
    refInterval.current = window.setInterval(() => {
      setCount(v => v + 1)
    }, 1000)
    return () => window.clearInterval(refInterval.current)
  }, [])
  return (
    <div
      style={{
        background: '#fff',
        margin: '10px 0',
        padding: '10px',
        border: '1px solid black'
      }}
    >
      <br/>
      <p>useDebounce</p><br/>
      <div> {count}</div><br/>
      <button onClick={useDebounce(doSomething, 1000, false)}>
        点击测试 - debounce 函数 看console
      </button>
    </div>
  )
}
export default UseDebounce

(7) useThrottle

  • useThrottle定义
import { useRef } from 'react'

interface IuseThrottle {
  (fn: Ifn, delay: number): IClosure;
}

interface Ifn {
  (...rest: any[]): any
}

interface IClosure {
  (e: any, ...rest: any[]): any
}

export const useThrottle: IuseThrottle = (fn, delay) => {
  const refGoRun = useRef(true)
  const refTimer = useRef(1)

  return (e, ...args) => {
    if (!refGoRun.current) {
      return
    }
    refGoRun.current = false

    refTimer.current = window.setTimeout(() => {
      fn.call(args)
      refGoRun.current = true // 执行完后标志位改为true,可再次进入
      window.clearTimeout(refTimer.current) // 每次执行完,清除定时器
    }, delay)
  }
}
  • useThrottle使用
import React, { useEffect, useState, useRef } from 'react'
import { useThrottle } from '@/utils/hooks/use-throttle'

const UseThrottle = () => {
  const [count, setCount] = useState(0)
  const refCount = useRef(0)

  useEffect(() => {
    refCount.current = window.setInterval(() => setCount(count => count + 1), 1000)
    return () => window.clearInterval(refCount.current)
  }, [])

  const doSomething = () => {
    console.log('throttle');
  }

  return (
    <div style={{
      background: '#fff',
      margin: '10px 0',
      padding: '10px',
      border: '1px solid black'
    }}>
      <p style={{
        margin: '10px', padding: '14px 24px', background: '#fdf2ff',
        border: '1px solid #e821ff', display: 'inline-block',
      }}>useThrottle</p>

      <br />
      <br />
      <div>{count}</div>
      <br />
      <br />
      <button onClick={useThrottle(doSomething, 1000)}>
        点击测试 - throttle 函数 看console
      </button>
    </div>
  )
}

export default UseThrottle

(8) useIntersectionObserver 实现图片懒加载

  • useIntersectionObserver(doms, option)
  • 前置知识:
  • const io = new IntersectionObserver(callback, option)
  • 参数
    • callback:可见性变化的回调函数
      • callback一般会触发 ( 两次 ),进入视口和离开视口,即开始可见和开始不可见两次
      • callback的 ( 参数 ),是一个 ( IntersectionObserverEntry ) 对象组成的 ( 数组 )
        • 如果有两个可观测对象的可见性发生变化,数组就有两个成员
        • IntersectionObserverEntry 的 6 个属性
          • time:发生可见性变化的时间,是一个单位为毫秒的时间戳
          • target被观察的目标元素,是一个DOM节点
          • rootBounds:根元素的矩形区域信息,是getBoundingClientRect()的返回值,没有根元素返回null
          • boundingClientRect:目标元素的矩形区域信息
          • intersectionRect:目标元素与根元素的交叉区域信息
          • intersectionRatio目标元素的可见比列,intersectionRect/boundingClientRect,完全可见=1,完全不可见<=0
    • option:配置参数对象
      • threshold:一个数组,默认值[0],即交叉比例到达0时触发回调
      • root:指定根元素
      • rootMargin:用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小
  • 返回值
    • 观察器实例
    • 开始观察:io.observe(document.getElementById('example'));
    • 停止观察:io.unobserve(element);
    • 关闭观察器:io.disconnect();
  • 兼容性
  • 注意点:
    • IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发
  • 总结
    • option.root:指的是 ( 容器节点 )
    • target:io.observer(target) 中的target指的是需要观测的 ( 目标节点 )
    • ( 目标节点 ) 必须是 ( 容器节点 ) 的 ( 子节点 )
    • 当目标节点和容器节点有交集时,触发回调,进入和离开都会触发,还可以通过option.threshold指定多个交叉比例
  • useIntersectionObserver 定义
import { useRef, useEffect } from 'react';

type TuseIntersectionObserver = (doms: any[], option: IOption) => void

interface IOption {
  root?: any;
  rootMargin?: string;
  threshold?: number[];
}

export const useIntersectionObserver: TuseIntersectionObserver = (doms, option) => {
  const refIo = useRef<any>(null)

  useEffect(() => {
    if (!doms.length) return; // doms可能是空数组
    refIo.current = new IntersectionObserver((entrys) => {
      entrys.forEach((item) => {
        if (item.intersectionRatio > 0) { // entry.intersectionRadio 交叉比例,即>0时root和target存在交集
          item.target.setAttribute('src', `${item.target.getAttribute('data-src')}`) // entry.target 表示观察的DOM
        }
      })
    }, option)
    doms.forEach(imageItem => refIo.current.observe(imageItem)) // io.observe(target) 开始观察

    return () => refIo.current.disconnect() // 关闭观察器
  }) // 注意:这里不需要依赖数组,因为img的首次渲染数据也可能是异步获取的,每个item可以包含src和data-src
}

  • useIntersectionObserver 使用
import React, { useEffect, useState, useRef } from 'react'
import './test-hooks.scss'
import { useIntersectionObserver } from '@/utils/hooks/use-intersectionObserver'

interface IimagesData {
  tempSrc: string;
  src: string;
}

const imagesData = [{
  tempSrc: require('@/assets/iamges/lazy-temp.png'),
  src: 'https://cdn.seovx.com/?mom=302'
}, {
  tempSrc: require('@/assets/iamges/lazy-temp.png'),
  src: 'https://cdn.seovx.com/d/?mom=302'
}, {
  tempSrc: require('@/assets/iamges/lazy-temp.png'),
  src: 'https://cdn.seovx.com/ha/?mom=302'
}]

const UseIntersectionObserver = () => {
  const [images, setImages] = useState<IimagesData[]>([])
  const refImg = useRef<any[]>([])

  useIntersectionObserver(refImg.current, {
    root: document.getElementById('use-observer-root'),
    rootMargin: '0px',
    threshold: [0]
  })
  
  useEffect(() => {
    setTimeout(() => {
      setImages(() => imagesData)
    }, 500)
  }, [])




  // ----------------------- 未 hooks 化的 IntersectionObserver -----------------------
  useEffect(() => {
    // 未 hooks 化的 IntersectionObserver
    const io = new IntersectionObserver(([entry]) => {
      console.log(entry);
    }, {
      root: document.getElementById('observer-root'),
      rootMargin: '10px',
      threshold: [0]
    })
    const target = document.getElementById('observer-target')
    if (target) {
      io.observe(target)
    }
    return () => { }
  }, [])

  const renderImages = (item: IimagesData, index: number) => {
    return (
      <img
        src={item.tempSrc}
        data-src={item.src}
        alt={"images"}
        key={index + +new Date()}
        ref={el => refImg.current[index] = el}
      />
    )
  }

  return (
    <div
      style={{
        background: '#fff',
        margin: '10px 0',
        padding: '10px',
        border: '1px solid black'
      }}>
      <p
        style={{
          margin: '10px', padding: '14px 24px', background: '#edfffb',
          border: '1px solid #00b792', display: 'inline-block',
        }}
      >
        useIntersectionObserver
      </p>

      <br /><br /><p>滚动下面的滚动条,查看console,当有交集时就能触发指定的回调</p><br /><br />

      {/* IntersectionObserver未hooks版本 */}
      <div id="observer-root">
        <div>这是IntersectionObserver指定的root节点DOM - 绿</div>
        <div>
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
        </div>
        <div id="observer-target">
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
          <p>这是IntersectionObserver观察的节点DOM - 红</p><br /><br />
        </div>
        <div>
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
        </div>
      </div>

      <br /><br /><br /><br /><br />
      <div>IntersectionObserver图片懒加载应用</div><br />
      <div id="use-observer-root">
        <div>
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
        </div>
        {
          images.map(renderImages)
        }
        <div>
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
          <p>填充</p><br /><br /><br />
        </div>
      </div>
    </div>
  )
}

export default UseIntersectionObserver

(9) useInputBind

  • 这里只考虑 valueonChage 是因为所有input都具有的属性
  • 没有加入 onSearch 是因为只有 Input.Search 才具有,需要考虑最小粒度的封装,然后利用函数组合实现复杂功能
import { useState } from 'react'

type TUseBind = () => ({
  value: string,
  onChange:  (e: React.ChangeEvent<HTMLInputElement>) => void
})

const useInputBind: TUseBind = () => {
  const [value, setValue] = useState('')

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(() => e.target.value)
  }

  return {
    value,
    onChange,
  }
}

export {
  useInputBind
}

(10) useLocalStorageState

// 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;

(11) useFullscreen

/* eslint no-empty: 0 */

import { useCallback, useRef, useState } from 'react';
import screenfull from 'screenfull'; // 使用到了 screenfull 第三方库
import useUnmount from '../useUnmount';
import { BasicTarget, getTargetElement } from '../utils/dom';

export interface Options {
  onExitFull?: () => void;
  onFull?: () => void;
}

// screenfull
// 这里首先需要了解几个 screenfull 的 api

// isEnabled
//  - isEnabled: boolean,当前环境是否允支持全屏功能

// isFullscreen
//  - isFullscreen: boolean,当前是否在全屏

// request()
// - request(target, options?) 让元素全全屏,实现全屏操作,传入需要全屏的元素

// off()
// - 删除之前注册过的事件监听函数,这里限制在 change 和 error 两个事件

// on()
// - 绑定事件的监听函数,同样是 change 和 error

// exit()
// - 退出全屏,返回一个promse,resolve状态时抛出的是绑定的需要全屏的元素


export default (target: BasicTarget, options?: Options) => {
  const { onExitFull, onFull } = options || {};

  const onExitFullRef = useRef(onExitFull);
  onExitFullRef.current = onExitFull;

  const onFullRef = useRef(onFull);
  onFullRef.current = onFull;

  const [state, setState] = useState(false); // 是否全屏的标志位


  const onChange = useCallback(() => {
    if (screenfull.isEnabled) { // 当前环境是否允支持全屏功能
      const { isFullscreen } = screenfull; // 是否全屏
      if (isFullscreen) {
        // 全屏
        onFullRef.current && onFullRef.current();
      } else {
        // 非全屏
        screenfull.off('change', onChange);
        // 清除change事件监听函数
        // 这里注意
        // 1. 取消事件的监听函数后,本次执行的函数还是会执行,换言之,setIsFull还是会执行
        // 2. 取消事件的监听函数,只是下次在触发事件,不会在监听该事件了,换言之,就是不再执行监听函数了
        onExitFullRef.current && onExitFullRef.current();
      }
      setState(isFullscreen); // 更新是否全屏的状态
    }
  }, []);


  // setFull 全屏
  const setFull = useCallback(() => {
    const el = getTargetElement(target); // 需要全屏的元素
    if (!el) { // el不存在
      return;
    }
    if (screenfull.isEnabled) { // el存在
      try {
        screenfull.request(el as HTMLElement); // 全屏
        screenfull.on('change', onChange); // 监听 change 事件
      } catch (error) {}
    }
  }, [target, onChange]);


  // exitFull 退出全屏
  const exitFull = useCallback(() => {
    if (!state) { // 如果当前不是全屏状态,直接返回,即不需要退出全屏
      return;
    }
    if (screenfull.isEnabled) {
      screenfull.exit(); // 退出全屏
    }
  }, [state]);


  // toggleFull 切换全屏
  const toggleFull = useCallback(() => {
    if (state) {
      exitFull();
    } else {
      setFull();
    }
  }, [state, setFull, exitFull]);


  // unmount
  // 利用useEffect一个函数函数返回一个函数时,就是mount钩子
  useUnmount(() => {
    if (screenfull.isEnabled) {
      screenfull.off('change', onChange); // mount时,清除事件监听
    }
  });


  return [    state,    {      setFull, // 全屏      exitFull, // 退出全屏      toggleFull, // 两者切换    },  ] as const;
};

(12) useUpdateEffect

  • 模拟 componentDidUpdate
  • 类型和useEffect一样,只是第一次mount不会执行

// useUpdateEffect
// 1
// - 模拟 componentDidUpdate,当不存在依赖项时
// 2
// useUpdateEffect(
//   effect: () => (void | (() => void | undefined)),
//   deps?: deps,
// )
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,则和 ( componentDidMount ) 行为保持一致

      // 注意:
      // 1. 这里的 return 是为了完全模拟 useEffect,因为 useEffect 可以还有清除函数
      // 2. effect函数签名是:effect: () => (void | (() => void | undefined)) 说明可以返回一个清除函数
    }
  }, deps);
};

export default useUpdateEffect;

项目源码

资料