IT学徒、技术民工、斜杠青年,机器人爱好者、摄影爱好 PS、PR、LR、达芬奇潜在学习者
共 243 篇文章
前端框架,快速开发页面,函数式编程,与后端api快速搭建
当使用component时,父组件的props或者state更新时,无论子组件的state、props是否更新,都会触发子组件的更新,这会造成很多没必要的render,浪费很多性能。pureComponent的优点在于,在shouldcomponentUpdate只进行浅层比较,只要外层对象没有变化,就不会触发render,也就是不需要开发者使用shouldComponentUpdate就可使用简单的判断来提升性能
缺点:
由于进行的时浅比较,可能由于深层的数据不一致导致产生错误的否定判断,从而导致页面得不到更新
不适合用于在含有多层嵌套对象的state和props中,一般是作为展示组件来使用。因为对于数组和对象等引用类型,需要引用不同才会渲染
尤其是当遇到复杂组件时,可以将一个组件拆分成多个pureComponent,以这种方式来实现复杂数据结构,以期达到节省不必要渲染的目的,如表单、复杂列表、文本域等
如果props和state每次都会变,建议使用Component
父组件是pureComponent时,子组件无论是purecomponent或者component都不影响,因为父组件不会重新渲染,
父组件是Component时,子组件是component时每次都会重新渲染,子组件是purecomponent时,props不变时不会重新渲染
reacr.memo控制函数组件的重新渲染,reacr.purecomponent控制类组件的重新渲染
使用时将函数组件传递给react.memo函数就可以
实例
const Funcomponent = () =>{
return (
<div>
hiya!i am a functional component!
</div>
)
}
const MemoFuncComponent = React.memo(Funcomponent)
React.memo返回纯组件MemoFuncComponent,jsx中将标记次组件,每当组件的props和state发生变化时,react会检查上一个props和state与下一个pros和state是否相等,不相等重新渲染,相等则不会重新渲染
React.memo可以传递第二个参数,自定义比较函数,返回false时更新
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);
类组件中即成purecomponent实现
import React from 'react'
class TestC extends React.PureComponent{
constructor(props){
super(props);
this.state = {
conut: 0
}
}
render(){
return(
<div>
{this.state.count}
<button onClick={()=>this.setState({count:1})}>
click me
</button>
</div>
)
}
}
使用immutable属性。
Https://juejin.cn/post/6844903985338400782
Hook是react16.8新增的特性,可以在不编写class 的情况下使用state和其他react特性,reactnative从0.59版本开始支持hook。
https://juejin.cn/post/6844903498299998222
Class component 劣势
Hooks不能在class中使用,只能在函数组件中,为函数组件勾入react state及生命周期等函数
react内置的hook有以下
基础hook:useState、useEffect、useContext
额外的hook:useReducer、useCallback、useMemo、useRef、useLayoutEffect、useDebugValue、useImperativeHandle
class组件中,setState之后要对比整个虚拟dom的状态。对于一个复杂页面,几十个状态要对比消耗性能。而hook阶段只对比一个值即可,性能更佳
闭包很多,值捕获现象严重,要尤其注意hook的依赖
大量的内联函数、函数嵌套,垃圾回收压力大。函数式组件每次渲染就像调用纯函数一样,调用之后产生一个作用域,并开辟对应的内容空间存储该作用域下的变量,函数返回结束后该作用域会被销毁,该作用域下的变量如果没有被作用域外的东西引用,在作用域销毁之后就需要在下一次GC时被回收。因此相对于Class组件额外的开销会多很多。因为Class组件所有的东西都是承载在一个对象上的,都是在这个对象上,每次更新组件,这个对象上的属性、方法和对象本身都不会被销毁,即不会出现频繁的开辟和回收内存空间。
constructor:函数组件不需要构造函数,可以直接调用useState来初始化State,如果代价比较昂贵可以穿一个函数给useState
getDerivedStateFromProps:改为在渲染时安排一次更新
shouldComponentUpdate:使用React.memo替代
使用react.memo包裹一个组件对props进行浅比较
const Button = React.memo((props)=>{
// component
})
react.memo不比较state,因为没有单一的state对象进行比较,可以用usememo优化子节点
render:函数组件本身就有
componentDidMount、componentDidUpdate、componentWillUnmount:通过使用UseEffect的不同方式可以分别表达这些生命周期
getSnapshotBeforeUpdate、ComponentDidCatch、getDerivedFromError:目前还没有这些方法的等价写法
函数式组件与class组件的心智模型不同。函数式组件捕获了渲染所用的值
hooks的最好的心智规则是"写代码时要认为任何值都可以随时更改"。
在react中,默认props在组件中是不可变的,这是在函数组件和类组件中都适用
但是,在类组件中,props要通过this取得,而this是永远可变的。这也是类组件 this
存在的意义:能在渲染方法以及生命周期方法中得到最新的实例。
class ProfilePage extends React.Component {
showMessage = () => alert('Followed ' + this.props.user);
handleClick = () => setTimeout(this.showMessage, 3000);
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
所以如果在请求已经发出的情况下我们的组件进行了重新渲染, this.props
将会改变。 showMessage
方法从一个"过于新"的 props
中得到了 user
。
从 this 中读取数据的这种行为,调用一个回调函数读取 this.props
的 timeout 会让 showMessage
回调并没有与任何一个特定的渲染"绑定"在一起,所以它"失去"了正确的 props。。
解决上面的问题有两种方式
1.在调用事件之前读取 this.props
,然后显式地传递到timeout回调函数中
class ProfilePage extends React.Component {
showMessage = (user) => alert('Followed ' + user);
handleClick = () => {
const {user} = this.props;
setTimeout(() => this.showMessage(user), 3000);
};
render() {
return <button onClick={this.handleClick}>Followbutton>;
}
}
2.利用JavaScript的闭包
在render函数中获取渲染所用的props
class ProfilePage extends React.Component {
render() {
const props = this.props;
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
而在函数
props
仍旧被捕获了 —— React将它们作为参数传递。 不同于 this
, props
对象本身永远不会被React改变
捕获props和state通常是更好的默认值。 然而,在处理类似于intervals和订阅这样的命令式API时,ref会十分便利。你可以像这样跟踪 任何值 —— 一个prop,一个state变量,整个props对象,或者甚至一个函数
React Hook 是一种特殊的函数,其本质可以是函数式组件(返回 Dom 或 Dom 及 State ),也可以只是一个工具函数(传入配置项返回封装后的数据处理逻辑)
Hooks 组件的目标并不是取代类组件,而是增加函数式组件的使用率,明确通用工具函数与业务工具函数的边界,鼓励开发者将业务通用的逻辑封装成 React Hooks 而不是工具函数
实例
import React,{useState} from 'react';
function Example() {
const [count,setCount] = useState(0);
return (
<div>
<p>you click {count} times</p>
<button onClick={()=> setCount(count + 1)}>
click
</button>
</div>
)
}
上述useState方法定义了一个state变量count,并给他初始化的值0。通过setCount方法更新当前count的值。
调用count时不需要绑定this直接调用,更新count时也直接调用setCount方法
usestate定义state时返回一个有两个值的数组,第一个是当前state,第二个是更新state的函数,
count与setCount与class中的this.state.count和this.setstate类似,唯一的区别是需要成对地获取他们。
如果初始化state时需要复杂计算,可以调用函数,此函数只在初次渲染时被调用
const [state,setState] = useState(() => {
const initialState= someExpensiveComputation(props);
return initialState;
})
count与setCount与class中的this.state.count和this.setstate类似,唯一的区别是需要成对地获取他们。
可以同时声明多个state变量
function ExamplewithManyStates(){
const [age,setAge] = usestate(42);
const [fruit,setFruit] = usestate('banana');
const [todos,setTodos] = usestate([{text:'学习'}]);
}
Hook只能在函数最外层调用,不要在循环、条件判断或者子函数中调用
useState更新数组时,必须用浅拷贝之后的新数组,如果只是简单赋值无法更新页面。因为react中如果数组引用地址不变,是不触发渲染的,但是值是设置进去的
// 无法更新
const [lists, setLists] = useState([]);
const arr = lists;
arr.splice(index,1);
setLists(arr);
//扩展运算符浅拷贝
const [lists, setLists] = useState([]);
const arr = lists; //const arr = [...lists]
arr.splice(index,1);
setLists([...arr]);
setState会自动合并,不同的useState不会
import { useState, useCallback } from 'react'
export const useForceRender = () => {
const [, updateState] = useState<any>();
return useCallback(() => updateState({}),[])
}
useState初始化时不能直接使用props。因为props变化时并不会通知useState方法,所以使用props初始化state时一直是undefined,可以使用useEffect在组件初始化时手动set一次
import React, { useState, useEffect } from 'react';
const FC = (initContent) => {
const [content, setContent] = useState<Partial<ContentType[]>>([]);
useEffect(() => {
initContent?.length && setContent(initContent);
}, [initContent]);
}
手动批量更新
hook中的useState和this.setState已经做了批量更新了
但是 批量更新在某些情况下会失效,造成多次渲染 ,例如 setTimeout,Promise.then (fn), fetch 回调,xhr 网络回调等,如下例子会渲染 3 次
为了避免此类情况发生,我们需要引入 unstable_batchedUpdates 进行手动合并,如下
handerClick=()=>{
setTimeout(() => {
unstable_batchedUpdates(()=>{
this.setState({ a:a+1 })
this.setState({ b:b+1 })
this.setState({ c:c+1 })
})
}, 0)
}
const handerClick = () => {
Promise.resolve().then(()=>{
unstable_batchedUpdates(()=>{
setB( { ...b } )
setC( c+1 )
setA( a+1 )
})
})
}
对于class中的生命周期函数,为了能在函数组件中使用类似功能,使用useEffect方法,它相当于componentDidMount、componentDidupdate、componentWillUnmount三个函数的组合
useEffect默认情况下会在第一次渲染之后和每次更新之后都会执行。
useEffect在全部渲染完毕后才会执行,而useLayoutEffect会在浏览器layout之后,painting之前执行
为了用户体验,一般先使用useEffect
useLayoutEffect与原componentDidMount和componentDidUpdate相同,会堵塞渲染,useEffect不会。
使用步骤:
[]
const BlinkyRender = () => {
const [value, setValue] = useState(0);
useEffect(() => {
document.querySelector('#x').innerText = `value: 1000`
}, [value]);
return (
<div id="x" onClick={() => setValue(0)}>value: {value}</div>
);
};
ReactDOM.render(
<BlinkyRender />,
document.querySelector("#root")
);
清除effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect
函数需返回一个清除函数。
为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。
需要注意的是,如果包括多个副作用,应该调用多个useEffect,而不能够合并在一起。
//错误
useEffect(()=>{
const timeoutA = setTimeout(()=> setVarA(varA + 1), 1000)
const timeoutB = setTimeout(()=> setVarB(varB + 1), 2000)
return () => {
clearTimeout(timeoutA);
clearTimeout(timeoutB);
}
},[varA, varB])
//正确
useEffect(()=>{
const timeout = setTimeout(()=> setVarA(varA + 1), 1000)
return () => { clearTimeout(timeout)};
},[varA])
useEffect(()=>{
const timeout = setTimeout(()=> setVarB(varB + 1), 1000)
return () => { clearTimeout(timeout)};
},[varB])
useLayoutEffect
和 useEffect
的相同点是:
区别:
1.useEffect
是异步执行的,而useLayoutEffect
是同步执行的。useLayoutEffect
会阻塞paint流程,而useEffect
不会阻塞paint流程。useLayoutEffect
callback 里面的「状态更新是批量」, 而 useLayou tEffect
callback 里面的「状态更新是非批量的」(也就是说,会分配到不同的渲染帧里面)。
如果useLayoutEffect
的callback函数里面对状态请求了多次更新,那么这些更新请求会合并成一个 paint 请求,浏览器更新一次 UI 界面;同样的情况如果发生在useEffect
的callback函数里面,那么更新请求不会被合并,有多少次状态更新请求,就会有多少次 paint 请求, 浏览
2.useEffect
的执行时机是浏览器完成渲染之后,而 useLayoutEffect
的执行时机是浏览器把内容真正渲染到界面之前,和 componentDidMount
等价。也就是,useLayoutEffect
比 useEffect
先执行
也就是说,对于 useEffect
来说,执行的时机是完成所有的 DOM 变更并让浏览器渲染页面后,而 useLayoutEffect
和 class 组件中 componentDidMount
, componentDidUpdate
一致——在 React 完成 DOM 更新后马上同步调用,会阻塞页面渲染。
最好把操作 dom 、动画的相关操作放到 useLayouteEffect
中去,避免导致闪烁
比如把useState分别放在useEffect和useLayoutEffect的if语句中,useEffect会闪烁,useLayoutEffect不会
import React, { useState, useLayoutEffect, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const BlinkyRender = () => {
const [value, setValue] = useState(0);
useEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
console.log("render", value);
return <div onClick={() => setValue(0)}>value: {value}</div>;
};
ReactDOM.render(<BlinkyRender />, document.querySelector("#root"));
useState内部就是靠useReducer实现的。
useReducer可以理解为是用来代替 Redux 的,或者说,是一个加强版的 useState
。
使用步骤:
1.创建初始值initialState
2.创建所有操作reduce(state,action)
3.传给useReducer,得到读和写api
4.调用,写({type: '操作类型'})
const initial = {
n: 0
};
const reducer = (state, action) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multi") {
return { n: state.n * 2 };
} else {
throw new Error("unknown type");
}
};
function App() {
const [state, dispatch] = useReducer(reducer, initial);
const { n } = state;
const onClick = () => {
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "add", number: 2 });
};
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
</div>
);
}
useReducer 与js 数组的reduce方法这两者之间有巨大的相似之处。
reduce in JavaScript | useReducer in React |
---|---|
array.reduce(reducer, initialValue) |
useReducer(reducer, initialState) |
singleValue = reducer(accumulator, itemValue) |
newState = reducer(currentState, action) |
reduce method returns a single value | useReducer returns a pair of values. [newState, dispatch] |
如果 state 的类型为 Number, String, Boolean 建议使用 useState,如果 state 的类型 为 Object 或 Array,建议使用 useReducer
如果 state 变化非常多,也是建议使用 useReducer,集中管理 state 变化,便于维护
如果 state 关联变化,建议使用 useReducer
业务逻辑如果很复杂,也建议使用 useReducer
如果 state 只想用在 组件内部,建议使用 useState,如果想维护全局 state 建议使用 useReducer
Scenario | useState | useReducer |
---|---|---|
Type of state | Number, String, Boolean | Object or Array |
Number of state transitions | 1 or 2 | Too many |
Related state transitions | No | Yes |
Business logic | No business logic | Complex business logic |
local vs global | local | global |
在react hooks的源码中,useState就是给useReducer的函数中传入一个固定的dispatch,其他与useReducer相同
// packages/react-reconciler/src/ReactFiberHooks.new.js
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, A> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
// useState的处理函数
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
接受一个context对象,并返回该context的当前值,用于在函数组件之间共享状态
使用方法:
1.使用C=createContext(initial)创建上下文
2.使用<C.provider>圈定作用域
3.在作用域内使用 useContext(C)
来使用上下文
const C = createContext(null);
function App() {
console.log("App 执行了");
const [n, setN] = useState(0);
return (
<C.Provider value={{ n, setN }}>
<div className="App">
<Baba />
</div>
</C.Provider>
);
}
function Baba() {
const { n, setN } = useContext(C);
return (
<div>
我是爸爸 n: {n} <Child />
</div>
);
}
function Child() {
const { n, setN } = useContext(C);
const onClick = () => {
setN(i => i + 1);
};
return (
<div>
我是儿子 我得到的 n: {n}
<button onClick={onClick}>+1</button>
</div>
);
}
useContext(MyContext)
相当于 class 组件中的 static contextType = MyContext
或者 <MyContext.Consumer>
。
useContext(MyContext)
只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context。
useMemo
返回一个缓存值。
useMemo是一种缓存机制提速,当他的依赖未发生改变时就不会触发重新计算,与vue中computed类似
使用语法:useMemo(()=> fn,deps)
把创建函数和依赖项数组作为参数传入useMemo,它只会在某个依赖项改变时才重新计算memoized值。如果没有提供依赖项数组,useMemo在每次渲染时都会计算新的值
const myValue: any = useMemo(() => {
const item = allCrowdData[value as number] || {
name: undefined,
};
if (value && item.name) {
return {
value: value,
label: item.name,
} as LabeledValue;
}
return undefined;
}, [allCrowdData, value]);
useCallback
返回一个缓存函数。把内联回调函数及依赖数组作为参数传入useCallback,它将返回该回调函数u的memorized版本,该回调函数仅在某个依赖项改变时才会更新,在组件中使用usecallback可以避免非必要渲染
useCallback(fn,dep) 与useCallback( ()=>fn,dep)等效
useCallback(fn,dep)相当于useMemo( () => fn,deps)
useMemo与usecallback的区别:useMemo
可以缓存所有对象,useCallback
只能缓存函数。
useCallback(x => log(x), [m])
等价于 useMemo(() => x => log(x), [m])
const handleInputChange = useCallback(
(e) => {
handleUpdateSearchWord(e.target.value);
},
[handleUpdateSearchWord],
);
普通的函数组件本身就是高内聚的,所以一般没有useCallback的事,比如
// 用于记录 getData 调用次数
let count = 0;
function App() {
const [val, setVal] = useState("");
function getData() {
setTimeout(()=>{
setVal('new data '+count);
count++;
}, 500)
}
useEffect(()=>{
getData();
}, []);
return (
<div>{val}</div>
);
}
实际上,在普通组件中使用useCallback可能性能会更差,比如普通的onChange函数
https://codesandbox.io/s/test-usecallback-xqvuc?file=/src/index.js
function App() {
const [val, setVal] = useState("");
const onChange = (evt) => {
setVal(evt.target.value);
};
// const onChange = useCallback(evt => {
// setVal(evt.target.value);
// }, []);
//可以看到onChange的定义是省不了的,而且额外还要加上调用useCallback产生的开销,性能怎么可能会更好
return <input val={val} onChange={onChange} />;
}
useCallback能够改善性能的场景有两种:
定义
时需要进行大量运算, 这种场景极少useEffect
,又或者是配合React.Memo
使用:const Child = React.memo(function({val, onChange}) {
console.log('render...');
return <input value={val} onChange={onChange} />;
});
function App() {
const [val1, setVal1] = useState('');
const [val2, setVal2] = useState('');
const onChange1 = useCallback( evt => {
setVal1(evt.target.value);
}, []);
const onChange2 = useCallback( evt => {
setVal2(evt.target.value);
}, []);
return (
<>
<Child val={val1} onChange={onChange1}/>
<Child val={val2} onChange={onChange2}/>
</>
);
}
在useEffect中的监听函数
// 用于记录 getData 调用次数
let count = 0;
function App() {
const [val, setVal] = useState("");
function getData() {
setTimeout(() => {
setVal("new data " + count);
count++;
}, 500);
}
return <Child val={val} getData={getData} />;
}
function Child({val, getData}) {
useEffect(() => {
getData();
}, [getData]);
return <div>{val}</div>;
}
上面的代码中,是一个死循环,分析下代码的执行过程:
App
渲染Child
,将val
和getData
传进去Child
使用useEffect
获取数据。因为对getData
有依赖,于是将其加入依赖列表getData
执行时,调用setVal
,导致App
重新渲染App
重新渲染时生成新的getData
方法,传给Child
Child
发现getData
的引用变了,又会执行getData
把函数用useCallback缓存就可以
const getData = useCallback(() => {
setTimeout(() => {
setVal("new data " + count);
count++;
}, 500);
}, []);
还有一种情况是在getData
中需要用到val
( useState 中的值),就需要将其加入依赖列表,这样的话又会导致每次getData
的引用都不一样,死循环又出现了
如果我们希望无论val
怎么变,getData
的引用都保持不变,同时又能取到val
最新的值,可以通过自定义 hook 实现。注意这里不能简单的把val
从依赖列表中去掉,否则getData
中的val
永远都只会是初始值(闭包)
function useRefCallback(fn, dependencies) {
const ref = useRef(fn);
// 每次调用的时候,fn 都是一个全新的函数,函数中的变量有自己的作用域
// 当依赖改变的时候,传入的 fn 中的依赖值也会更新,这时更新 ref 的指向为新传入的 fn
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
// 调用
const getData = useRefCallback(() => {
console.log(val);
setTimeout(() => {
setVal("new data " + count);
count++;
}, 500);
}, [val]);
useRef
主要作用是创建一个数据的引用,并让这个数据在 render 过程中始终保持不变。修改ref对象不会像修改state那样触发重绘。
Refs为我们提供了一种绕过状态更新和重新渲染访问元素的方法(获取某个元素的实例),但不能作为props 和 state 的替代方法
基本语法: const count = useRef(0)
,读取用 count.current
export function ReactEcharts(props) {
const {option, loading} = props
const container = useRef(null)
const chart = useRef(null)
useEffect(() => {
const width = document.documentElement.clientWidth
const c = container.current
console.log(c)
c.style.width = `${width - 20}px`
c.style.height = `${(width - 20) * 1.2}px`
chart.current = echarts.init(c, 'dark')
}, []) // [] - mounted on first time
useEffect(() => {
chart.current.setOption(option)
}, [option]) // when option change 类似 vue 的 watch
useEffect(() => {
if (loading) chart.current.showLoading()
else chart.current.hideLoading()
}, [loading])
return (
<div ref={container}/>
)
}
useRef使用时报错不能将类型“MutableRefObject<HTMLDivElement | undefined>”分配给类型“LegacyRef | undefined”。
原因:
1.没赋初值
import React, { useRef, createRef, useState } from "react";
import ReactDOM from "react-dom";
function App() {
const [renderIndex, setRenderIndex] = useState(1);
const refFromUseRef = useRef();
const refFromCreateRef = createRef();
if (!refFromUseRef.current) {
// 赋值操作,赋值给useRef
refFromUseRef.current = renderIndex;
}
if (!refFromCreateRef.current) {
// 赋值操作,赋值给createRef
refFromCreateRef.current = renderIndex;
}
return (
<div className="App">
Current render index: {renderIndex}
<br />
在refFromUseRef.current中记住的第一个渲染索引:
{refFromUseRef.current}
<br />
在refFromCreateRef.current中未能成功记住第一个渲染索引:
{refFromCreateRef.current}
<br />
<button onClick={() => setRenderIndex(prev => prev + 1)}>
数值 + 1
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
上面的案例中无论如何点击按钮 refFromUseRef.current
将始终为 1
,而 renderIndex
和 refFromCreateRef.current
会伴随点击事件改变
当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用 callback ref
来实现
因为非受控组件将真实数据储存在 DOM 节点中,所以再使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件
基于 React 的音乐或视频播放器可以利用 Refs 来管理其当前状态(播放/暂停),或管理播放进度等。这些更新不需要进行状态管理。
如果要在元素上触发过强制动画时,可以使用 Refs 来执行此操作。
默认情况下,你不能在函数组件上使用 ref
属性,因为它们没有实例
如果要在函数组件中使用 ref
,你可以使用 forwardRef
(可与 useImperativeHandle
结合使用),或者可以将该组件转化为 class 组件。
你可以在函数组件内部使用 ref
属性,只要它指向一个 DOM 元素或 class 组件
React 也支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除。
不同于传递 createRef()
创建的 ref
属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// 使用原生 DOM API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// 组件挂载后,让文本框自动获得焦点
this.focusTextInput();
}
render() {
// 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
// 实例上(比如 this.textInput)
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
React 将在组件挂载时,会调用 ref
回调函数并传入 DOM 元素,当卸载时调用它并传入 null
。在 componentDidMount
或 componentDidUpdate
触发前,React 会保证 refs 一定是最新的
useImperativeHandle
useImperativeHandle可以让你在使用转发ref时自定义暴露给父组件的实例值。通过useImperativeHandle
减少暴露给父组件获取的DOM元素属性,只暴露特定的操作,从而提升性能
在大多数情况下,应当避免使用ref这样的命令式代码,useImperativeHandle应当与forwardRef一起使用
useImperativeHandle的语法:useImperativeHandle(ref, createHandle, [deps])
ref
需要被赋值的ref
对象。createHandle
:
createHandle
函数的返回值作为ref.current
的值。[deps]
依赖数组,依赖发生变化会重新执行createHandle
函数。import React, { useRef, forwardRef, useImperativeHandle } from 'react'
const JMInput = forwardRef((props, ref) => {
const inputRef = useRef()
// 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
// 参数1: 父组件传递的ref属性
// 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
}))
return <input type="text" ref={inputRef} />
})
export default function ImperativeHandleDemo() {
// useImperativeHandle 主要作用:用于减少父组件中通过forward+useRef获取子组件DOM元素暴露的属性过多
// 为什么使用: 因为使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了
// 解决: 使用uesImperativeHandle解决,在子函数式组件中定义父组件需要进行DOM操作,减少获取DOM暴露的属性过多
const inputRef = useRef()
return (
<div>
<button onClick={() => inputRef.current.focus()}>聚焦</button>
<JMInput ref={inputRef} />
</div>
)
}
useImperativeHandle更常用的写法是
type Props = {
content?: string;
};
type Instance = {
insertParam: (text: string, value: string) => void;
};
const Setting = React.forwardRef<
Instance,
Props
>(function ContentSetting({ content }, ref) {
const itemRefs = React.useRef<(RichTextInputInstance | null)[]>([]);
useImperativeHandle(ref, () => ({
insertParam(text, value) {
const focusRef = itemRefs?.current?.[focusIndex as any];
focusRef?.insertParam(text, value);
},
}));
return (
<RichTextInput
placeholder={placeholder}
readOnly={readOnly}
ref={(inputRef) => (itemRefs.current[index] = inputRef)}
onFocus={() => handleFocus(index)}
className={classNames({ [styles.formReadOnly]: readOnly })}
singleLine={singleLine}
/>)
}
可以把不同的hook按照实际的需求混合起来,封装成一个新的函数使用
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
const useList = () => {
const [list, setList] = useState(null);
useEffect(() => {
ajax("/list").then(list => {
setList(list);
});
}, []); // [] 确保只在第一次运行
return {
list: list,
setList: setList
};
};
export default useList;
抽象useInput hooks
import { useState } from 'react'
function useInput(initialValue: string) {
const [value, setValue] = useState(initialValue)
const reset = () => {
setValue(initialValue)
}
const bind = {
value,
onChange(e: any) {
setValue(e.target.value)
}
}
return [value, bind, reset]
}
export default useInput
使用useInput Hook
import React, { FormEvent } from 'react'
import useInput from './hooks/useInput'
function UserForm() {
const [firstName, bindFirstName, resetFirstName] = useInput('')
const [lastName, bindLastName, resetLastName] = useInput('')
const submitHandler = (e: FormEvent) => {
e.preventDefault()
console.log(`Hello ${firstName} ${lastName}`)
// @ts-ignore
resetFirstName()
// @ts-ignore
resetLastName()
}
return (
<div>
<form onSubmit={submitHandler}>
<div>
<label htmlFor="">First name</label>
<input
type="text"
{...bindFirstName}
/>
</div>
<div>
<label htmlFor="">Last name</label>
<input
type="text"
{...bindLastName}
/>
</div>
<button>submit</button>
</form>
</div>
)
}
export default UserForm
返回 DOM 其实和最基本的 Hook 逻辑是相同的,只是在返回的数据内容上有一些差异
import React, { useState } from 'react';
import { Modal } from 'antd';
function useModal() {
const [visible, changeVisible] = useState(false);
const toggleModalVisible = () => {
changeVisible(!visible);
};
return [(
<Modal
visible={visible}
onOk={toggleModalVisible}
onCancel={toggleModalVisible}
>
弹窗内容
</Modal>
), toggleModalVisible];
}
export default function HookDemo() {
const [modal, toggleModal] = useModal();
return (
<div>
{modal}
<button onClick={toggleModal}>打开弹窗</button>
</div>
);
}
注意性能优化
someExpensiveComputation
是一个相对耗时的操作。如果我们直接采用
const initialState = someExpensiveComputation(props);
const [state, setState] = useState(initialState);
注意,虽然 initialState
只在初始化时有其存在的价值,但是 someExpensiveComputation
在每一帧都被调用了。只有当使用惰性初始化的方法
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
因 someExpensiveComputation
运行在一个匿名函数下,该函数当且仅当初始化时被调用,从而优化性能。
useMemo 的目的其实是尽量使用缓存的值。
对于函数,其作为另外一个 useEffect 的 deps 时,减少函数的重新生成,就能减少该 Effect 的调用,甚至避免一些死循环的产生;
对于对象和数组,如果某个子组件使用了它作为 props,减少它的重新生成,就能避免子组件不必要的重复渲染,提升性能。
// 未优化代码
const data = { id };
return <Child data={data}>;
// 优化代码
const data = useMemo(() => ({ id }), [id]);
return <Child data={data}>;
未优化之前,每当父组件需要 render 时,子组件也会执行 render。如果使用 useMemo
对 data 进行优化:
当父组件 render 时,只要满足 id 不变,data 的值也不会发生变化,子组件也将避免 render。
对于组件返回的 React Elements,我们可以选择性地提取其中一部分 elements,通过 useMemo 进行缓存,也能避免这一部分的重复渲染。
但同时,也要避免滥用useMemos
使用 useMemo 当 deps 不变时,直接返回上一次计算的结果,从而使子组件跳过渲染。
但是当返回的是原始数据类型(如字符串、数字、布尔值)。即使参与了计算,只要 deps 依赖的内容不变,返回结果也很可能是不变的。此时就需要权衡这个计算的时间成本和 useMemo 额外带来的空间成本(缓存上一次的结果)了。
此外,如果 useMemo 的 deps 依赖数组为空,这样做说明你只是希望存储一个值,这个值在重新 render 时永远不会变。此时用useMemo不是最佳选择
// 未优化代码
const Comp = () => {
const data = useMemo(() => ({ type: 'xxx' }), []);
return <Child data={data}>;
}
// 优化1
const Comp = () => {
const { current: data } = useRef({ type: 'xxx' });
return <Child data={data}>;
}
// 优化2
const data = { type: 'xxx' };
const Comp = () => {
return <Child data={data}>;
}
此外,如果 deps 频繁变动,我们也要思考,使用 useMemo 是否有必要。因为 useMemo 占用了额外的空间,还需要在每次 render 时检查 deps 是否变动,反而比不使用 useMemo 开销更大。
一个函数执行完毕之后,就会从函数调用栈中被弹出,里面的内存也会被回收。因此,即使在函数内部创建了多个函数,执行完毕之后,这些创建的函数也都会被释放掉。函数式组件的性能是非常快的。相比class,函数更轻量,也避免了使用高阶组件、renderProps等会造成额外层级的技术。使用合理的情况下,性能几乎不会有什么问题。
而当我们使用useMemo/useCallback
时,由于新增了对于闭包的使用,新增了对于依赖项的比较逻辑,因此,盲目使用它们,甚至可能会让你的组件变得更慢。
大多数情况下,这样的交换,并不划算,或者赚得不多。你的组件可能并不需要使用useMemo/useCallback来优化。
记忆函数并非完全没有代价,我们需要创建闭包,占用更多的内存,用以解决计算上的冗余。
通常情况下,当函数体或者结果的计算过程非常复杂时,我们才会考虑优先使用useCallback/useMemo。
如果组件中有派生state,可以像这样
useSomething = (inputCount) => {
const [count, setCount] = setState(inputCount);
};
外部传入的 inputCount
属性发生了变化时,默认不会更新,因为 useState 参数代表的是初始值,仅在 useSomething
初始时赋值给了 count
state。后续 count
的状态将与 inputCount
无关。这种外部无法直接控制 state 的方式,我们称为非受控。
如果想被外部传入的 props 始终控制,比如在这个例子中,useSomething
内部,count
这一 state 的值需要从 inputCount
进行同步,
useSomething = (inputCount) => {
const [ count, setCount ] = setState(inputCount);
setCount(inputCount);
};
setCount
后,React 会立即退出当前的 render 并用更新后的 state 重新运行 render 函数。
在这种的机制下,state 由外界同步的同时,内部又有可能通过 setState 来修改 state,可能引发新的问题。例如 useSomething
初始时,count 为 0,后续内部通过 setCount
修改了 count
为 1。当外部函数组件的 render 函数重新调用,也会再一次调用 useSomething
,此时传入的 inputCount
依然是 0,就会把 count
变回 0
遇到这样的问题,建议将 inputCount 的当前值与上一次的值进行比较,只有确定发生变化时执行 setCount(inputCount) 。