文章用于记录开发中对于react的学习和体会。
仅包含 function components 和 hooks ,摒弃class component。
可能包含 TS+React 相关内容。
Hooks
hooks的灵魂:闭包。
useState
浅层对比
更新时间:2021.4.12
场景:上传图片场景中,删除待上传列表中某张指定图片。
const [data, setData] = useState<number>(0);
const [list, setList] = useState<string []>([]);
复杂类型的踩坑点
React在调用 setData / setList 进行 diff 并 rerender 的过程中,只会对state进行浅层对比;
// list === ["a" , "b"]
list.push("c")
setList(list) // 错误!!!
换句话说,对复杂类型(如上方list)需要特别注意:直接在原 list 上修改,并且使用setList(list)是不会触发render的。
理想做法
使用新对象 / 新数组,或者 [...list] / {...obj} 方式进行setState。
setList([...list, "hello world"]);
独立渲染
更新时间:2021.4.7
每次一渲染都是一次函数执行。
-
拥有独立(相对于上一次渲染和下一次渲染)的state,props。
换句话说,触发渲染的一般方式为:props改变 or setState -
拥有独立的函数声明。
如下函数声明,会导致每次在组件渲染过程中,生成完全不同的handleEvent变量;如果该函数作为props传入子组件,会使子组件做多次跟随父组件的冗余渲染。const Dad = () => { const handleEvent = () => { ... } return < Child handleEvent={handleEvent} /> } const Child = props => { return <div onClick={handleEvent}> button </div> }
解决方法:使用useCallback进行包裹,该函数会作为回调挂在在组件对应的Fiber节点上,在依赖不变的情况下直接取出使用。
const handleEvent = useCallback(()=>{ ... }, [xxx] )
-
每一次渲染都形成了自己的作用域,于是有了形成闭包的可能。
典型场景就是异步任务(ajax,setTimeout)中保存了旧作用域中的变量形成闭包,造成不符合预期的效果。
解决方法:一般使用 useRef 解决。ref 可以摆脱作用域和闭包的影响。
useCallback、useMemo
使用场景:一般用作性能优化
闭包依赖
更新时间:2021.3.24 场景:两个函数之间出现引用关系(A引用B)且B使用useCallback包裹。因为A未将B列入依赖导致运行出错。
错因分析
- 当依赖数组中元素未发生改变时,目标函数不会重新声明。因此导致目标函数中会一直记录旧作用域中的变量(可能为函数变量),形成闭包。
理想做法
- 一般通过ref或添加hooks依赖解决。
- 闭包陷阱参考博客:juejin.cn/post/684490…
何时使用
更新时间:2021.4.12
keyword:性能优化也要付出性能代价。
- 逻辑简单清晰的函数一般不使用useCallback。
- 在父组件中声明,并需要传给子组件的函数,一般要使用useCallback(避免子组件的冗余渲染)。
useEffect
绑定解绑
更新时间:2021.4.13 在useEffect做事件绑定的时候要在return的函数中注销绑定。否则易发生冗余绑定情况 useEffect(() => { on(tapTopRight, handler); return () => { on(tapTopRight, handler); } }, [] )
携参调用
-
错误写法 1
useEffect(() => { on(tapTopRight, () => handler(data) ); return () => { on(tapTopRight, () => handler(data) ); } }, [] )
- 显然匿名函数是无法被简单解绑的,反而会在按钮上多次绑定不合预期的事件。
-
错误写法 2
const someHandler = () => handler(data) useEffect(() => { on(tapTopRight, someHandler ); return () => { on(tapTopRight, someHandler ); } }, [] )
- 在 useEffect 中调用用函数时,最好将该函数在 useEffect 中声明
- 尽量避免将useEffect中调用的函数放到外部声明,然后在 useEffect 中调用 (因为往往容易忘记外部声明的函数有那哪些依赖!!)
-
理想写法
useEffect(() => { const someHandler = () => handler(data) on(tapTopRight, someHandler ); return () => { on(tapTopRight, someHandler ); } }, [data, handler] )
可以清楚看出effect的依赖为 状态data / handler
useEffect 不能接收 async 作为回调函数
更新时间:2021.4.13
async函数的本质
- async函数运行后会返回一个promise对象。可以利用 then / await 取出返回值 useEffect的返回值
- useEffect的返回值应为一个cleanup函数,而async的返回值是一个promise对象这显然是不对的 useEffect(async () => { const result = await axios('url'); setData(result.data); },[]);
理想方式
useEffect(()=>{
const axiosTask = async () => {
const result = await axios('url');
setData(result.data);
}
axiosTask();
},[])
useRef
引用不变
更新时间:2021.4.13
使用方法:
const flagRef = useRef<boolean>(flag)
flagRef 的引用在其整个生命周期不会发生改变,且flagRef.current的改变不会影响渲染。这在某些场景下是非常重要的功能,例如:各种无需渲染的各种flag、异步任务等等。
绑定DOM
更新时间:2021.4.13
使用场景:在父组件中声明ref,传给自定义文本框TextArea并作控制。
一般使用
const inputRef = useRef<HTMLInputElement>(null)
const handleBtnClick = () => { inputRef.current.focus() }
...
return (
<div>
<input ref={inputRef} />
<button onClick={handleBtnClick}> focus </button>
</div>
)
声明的ref可以直接通过ref关键字绑定到指定元素,通过ref.current获取dom对象 跨组件使用ref
- 因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性
- 解决方法是使用 forwardRef,forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上
const Child = forwardRef((props, ref) => { //转发ref
return ( <input type="text" ref={ref}/> )
})
function Parent(){
let [number,setNumber] = useState(0);
const inputRef = useRef(null); //父组件中声明Ref
function getFocus(){
inputRef.current.focus();
}
return ( <>
<Child ref={inputRef}/>
<button onClick={()=>setNumber(i=>i+1})}>+</button>
<button onClick={getFocus}>获得焦点</button>
</> )
}
写码理念及优化
Memo
减少非必要渲染
更新时间:2021.4.12
类比class component的pureComponent。对包裹组件的props做浅层对比,若props未发生改变,则不做更新。