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

2021-09-28  本文已影响0人  woow_wu7

导航

[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 )

(1) useState

(2) useEffect

(1-2) 总结useState和useEffect

(3) 神奇的 useRef

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
image

(4) useReducer,useContext,React.createContext 实现一个简单的 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 做性能优化

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)
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>
  )
})
image

(6) 链判断运算符 ?.

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 区别如下


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

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

(8) 数组的 - 非数字键

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中的封装

封装

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

定义:
// 注意点: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

image

(3) useFetch

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

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

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自定义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

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

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

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)
  }
}
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 实现图片懒加载

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
}

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

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
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
// - 模拟 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


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

项目源码

资料

上一篇 下一篇

猜你喜欢

热点阅读