导航
[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() 钩子函数
-
useEffect等价于 ( componentDidMount ) ( componentDidUpdate ) ( componentWillUnmount ) 三个生命周期
- 模拟componentDidMount(只在挂载阶段执行)
useEffect(()=>{}, [])
在依赖数组为空时,如果存在 Cleanup 取消函数,第一次后都不会执行,直到组件销毁时才执行 Cleanup 清除函数
- 模拟componentDidUpate(挂载阶段不执行,在更新阶段执行)
- 用一个标志位,isDidUpdate默认是false,则第一次useEffect不执行,再设置为true,第二次以后就会执行
useEffect(() => { if(isDidUpdate){ // compenntDidUpdate阶段执行的代码 }; setIsDidUpdate(true) })
- 模拟componentDidMount(只在挂载阶段执行)
-
依赖数组是如何比对的?,主要通过 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 对象,强烈不建议这样使用
- useEffect第一个参数函数
(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节点
-
useImperativeHandle
- 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>
<button onClick={delayConsole}>不用useRef时,延时打印count</button>
<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
-
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
<MyContext.Provider value={/* 某个值 */}>
Provider组件:供消费组件订阅context的变化,并提供value给消费组件
当Provider组件的 ( value变化 ) 时,Provider内部的所有 ( 消费组件 ) 都会 ( 重新渲染 ),value变化是通过 Object.is()来浅比较,所以要注意一个value是对象的情况
- 注意事项 ( Provider的value是对象时,将value提升到父节点的state中缓存 )
-
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() 的依赖做对比变化时,对函数做缓存
-
useCallback(fn, deps)
-
useMemo
- 作用:
- 主要用于对 ( 对象类型 ) 的数据进行缓存,淡然任意的类型的值都可以,只是对象用于优化
- 也能实现 useCallback 的功useMemo是useCallback的超集,可以缓存任意类型的数据
useCallback(f1, [a]) 和 useMemo(() => f1, [a]) 相等的,缓存函数
useMemo(() => object, [a])
- 使用场景
- 和useCallback类似
- useMemo 可以缓存一个组件的一部分内容,比 React.memo 做到更小粒度的缓存优化,而不是整个组件!!!
- useMemo更细粒度的缓存 - 具体见这篇博文
- 作用:
-
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>
)
})
image
(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(): 清空当前实例,会移除实例中所有的组件和图表
-
echarts
-
代码
封装
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
image
(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交叉区域的大小
-
callback:可见性变化的回调函数
- 返回值
- 观察器实例
- 开始观察:io.observe(document.getElementById('example'));
- 停止观察:io.unobserve(element);
- 关闭观察器:io.disconnect();
- 兼容性
- 一个polify的库,提高兼容性
- intersection-observer
- 注意点:
- 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
- 这里只考虑
value
和onChage
是因为所有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
- useLocalStorageState源码-仓库地址
- 手写hook调度 - useState实现
- 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;
项目源码
资料
- useCallback()写得太好 https://juejin.im/post/6844904132164190221#heading-0
- useCallback useMemo React.memo 性能优化 https://juejin.im/post/6844904119358980110#heading-0
- useReducer + useContext + React.createContext 实现一个redux https://juejin.im/post/6844904005055807496
- 详解 useCallback & useMemo https://juejin.im/post/6844904101445124110
- useRef使用细节 https://segmentfault.com/a/1190000024536290?utm_source=tag-newest
- 你不知道的 useRef https://zhuanlan.zhihu.com/p/105276393
- 自定义hooks https://juejin.im/post/6844904161511915533#heading-22
- useModal https://blog.csdn.net/jx950915/article/details/107047681
- useDebounce的坑点 https://juejin.cn/post/6844904135091814407
- useIntersectionObserver https://juejin.cn/post/6863624907452841998
网友评论