Hooks解决的问题
- 以前组件间复用状态用的是render props 和 高阶组件
- 复杂组件变得难以理解,Hook将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
- useMemo useCallback useRef,本质上都是为了缓存。
- 这些东西在没有hooks之前。在以前我们都类组件,调用setState的时候让类组件更新
- 类组件有实例,类的实例是同一个。 一旦创建就不会主动销毁 ,上面的属性也会一直存在。
- 但是现在我们hooks,hooks只能用在函数组件里。函数组件没有this,就没有实例,没有办法在实例 上挂属性状态
- 现在就要靠useMemo、useCallback、useRef实现缓存
注意事项
- 只能在函数最外层使用hooks,不要在条件判断或者循环中使用
因为React的函数组件多次渲染的时候,里面的hoos的数量需要保持一致
组件每一次渲染,都会从上到下把所有的hooks变成一个链表,然后多次渲染,会把这个链表和上一次渲染出来的链表进行一一比较,如果放在条件判断里面,这一次的链表就会跟上一次数量不一样
1:useState
1、每次渲染都是独立的闭包
-
通过在函数组件里调用他来给组件添加一些内部state,React会在重复渲染时保留这个state
-
每一次渲染都有自己的Props 和 State
-
每一次渲染都有自己的事件处理函数
-
alert会捕获 点击按钮时候的状态
- 点击的时候是几,无论你延时多久都是几
-
组件里面的函数每次渲染都会重新创建,然后被调用,每一次调用useState的值都属于当前执行的这个函数本身,可以理解为每次调用useState的值都是常量,并且它被赋予了当前渲染中的状态值
-
在单次渲染的范围内,props 和 state 始终保持不变
2、函数式更新
function Count3() {
let [count, setCount] = useState({number: 0});
// 1、普通更新
function lazy(){
//如果先点+,再点lazy:此处拿到的count.number就是+后的值
//如果先点lazy,再点+:此处拿到的count.number就永远是点lazy的时候的值
// - 先点一下lazy,再点两下+ :0 =>1 =>2(两下+) =>1(lazy生效,取初始的0+1)
setTimeout(() => {
setCount({number: count.number + 1})
}, 1000);
}
// 2、函数式更新
// - 先点一下lazyFunction,再点两下+ :0 =>1 =>2(两下+) =>4(lazyFunction生效,取最新的2+1+1)
function lazyFunction() {
setTimeout(() => {
setState(state => ({ number: state.number + 1 }));
setState(state => ({ number: state.number + 1 }));
}, 3000);
}
return (<div>
<p>{count.number}</p>
<!--视图里面可以拿到最新的number-->
<button onClick={()=>setCount({number: count.number+1})}>+</button>
<!--如果先点+,再点lazy-->
<button onClick={lazy}>lazy点击</button>
<button onClick={lazyFunction}>lazyFunction</button>
</div>)
}
3、惰性初始state
- initState参数只会在组件的初始渲染中起作用,后续渲染时会被忽略
- 如果初始state需要通过复杂计算获得,可以传入一个函数,在函数中计算并返回初始的state,此函数旨在初始渲染的时候被调用
- 与class组件中的setState不同,useState不会自动合并更新对象,可以用函数式的setState结合展开运算符来达到合并对象的效果
function Count4() {
let [count, setCount] = useState(function(){
console.log('初始状态'); // 只会执行一次
return {number: 0, name: 'offgun'}
});
return (<div>
<p>{count.name}:{count.number}</p>
// count里面有两个对象,setCount的值就是完整的值
<button onClick={()=>setCount({number: count.number+1})}>+</button>
</div>)
}
2:useCallback
(1)不加依赖:相当于没用useCallback
(2)空依赖:只有第一次会生成函数
let [number, setNumber] = useState(0);
// useCallback产生闭包,空依赖导致x永远都是0
const addClick = useCallback(()=>setNumber(number+1), []);
// 函数式更新:点击按钮可以按照预想的+1
const addClick = useCallback(()=>setNumber(x => x+1), []);
<button onClick={addClick}>+</button>
性能优化
4.1、Object.is
- 调用State Hook的更新函数并传入当前的state时, React将跳过子组件的渲染以及effect的执行。(React使用Object.is比较算法来比较state)setCount(state),传进来的state更新才会重新渲染组件,没更新就不渲染
function Count5() {
let [count, setCount] = useState(function(){
return {number: 0, name: 'offgun'}
});
console.log('Counter5 render')
return (<div>
<p>{count.name}:{count.number}</p>
<button onClick={()=>setCount({...state, number: count.number+1})}>+</button>
<!--点击上面的按钮state变化了才会打印Counter5 render,下面没变不打印-->
<button onClick={()=>setCount(state)}>+</button>
</div>)
}
4.2、减少渲染次数
- 把内联回调函数及依赖项数组作为参数传入
useCallback,它将返回该回调函数的 memoized版本,该回调函数仅是在某个依赖项改变时才会更新
函数组件中定义的函数会在组件每次渲染的时候都生成一个新的函数
3:useMemo
- 把创建函数和依赖项数组作为参数传入
useMemo,它仅会在某个依赖项改变时才重新计算 memorized 值,这种优化有助于每次渲染时都进行计算
memo
memo可以让函数组件拥有了记忆功能,只有当组件的属性(props传过来的值)发生变更的时候才会刷新,否则不刷新
import React, {useState, memo, useCallback} from 'react';
// 不使用memo的话,App里面的number和name变化,Child组件都会重新渲染
// Child是没有状态的纯函数,应该只有属性发生变化时才刷新
function Child(props) {
console.log('render.Child');
return <button onClick={props.addClick}>{props.data.number}</button>
}
// memo可以让函数组件拥有了记忆功能,只有当组件的属性(props传过来的值)发生变更的时候才会刷新,否则不刷新
Child = memo(child);
function App() {
let [number, setNumber] = useState(0);
let [name, setName] = useState('gungun');
const addClick = () => setNumber(x => x+1);
const data = {number};
return (
<div>
<input value={name} onChange={e=>setName(e.target.value)}/>
<Child addClick={addClick} data={data}/>
</div>
)
}
// 此时虽然使用了memo,但是由于每次input输入的时候,App组件都会重新渲染,
// addClick方法会重新生成一遍,data对象也会每次都会在内存中重新生成一个引用空间
// 导致Child所依赖的addClick函数变更了,然后重新刷新
// 使用useCallback
const addClick = useCallback(()=>setNumber(x => x+1), [number])
useMemo
let lastData;
function App() {
let [number, setNumber] = useState(0);
let [name, setName] = useState('gungun');
const addClick = () => setNumber(x => x+1);
// useMemo会把值缓存起来,deps没变就会一直用缓存的值,只有deps变化的时候才重新走一遍,得到新的值
const data = useMemo(()=>({number}), [number]);
console.log(lastData === data);
lastData = data;
return (
<div>
<input value={name} onChange={e=>setName(e.target.value)}/>
<Child addClick={addClick} data={data}/>
</div>
)
}
4:useReducer
- useState的替代方案,接收一个形如(state,action) => newState的reducer,并返回当前的state以及与其配套的dispatch方法
- state逻辑复杂或者包含多个子值,或者下一个state依赖与之前的state,useReducer就比useState更适用
使用useReducer自定义useState
function useState1(initState) {
// 没有case,直接就是payload传过来什么,reducer就返回什么
const reducer = useCallback((state, action) => state.payload)
const [state, dispatch] = useReducer(reducer, initState);
// dispatch({type:'', payload})
function setState(payload){
dispatch({ payload })
return newState
}
return [state, setState]
}
5:useContext
1、接收一个context对象(React.createContext的返回值),并返回该context的当前值
let MyContext = React.createContext();
// 以前获取context中的值的写法
function Counter(){
return (
<div>
<MyContext.Consumer>
{
value => (<div>
<p> {value.state.number}</p>
<button onClick={()=>
value.setState({number: value.state.number})
}>+</button>
</div>)
}
</MyContext.Consumer>
</div>
)
}
// useContext
function Counter(){
let value = useContext(MyContext);
return (
<div>
<p> {value.state.number}</p>
<button onClick={()=>
value.setState({number: value.state.number})
}>+</button>
</div>
)
}
function App(){
const [state, setState] = useState({number: 0});
return (
<MyContext.Provider value={{ state, setState}}>
<Counter />
</MyContext.Provider>
)
}
2、当前的context值由上层组件中距离当前组件最近的<MyContext.Provider>的value prop决定
3、当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重渲染,并使用最新传递给MyContext.Provider的 context value值
4、useContext(MyContext)相当于class组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
5、useContext(MyContext)只是让你能够读取context的值以及订阅context的变化,仍然需要在上层组件树中使用<MyContext.Provider>来为下层组件提供
6:useEffect
- 在函数组件主体内(这里值在React渲染阶段)改变DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这有可能会产生bug和破坏UI的一致性
- 赋值给useEffect的函数会在组件渲染到页面之后(挂载后、更新后)执行,每次渲染都会产生新的effect
- 不能在函数组件里面直接操作DOM
useEffect(() => {
console.log('开始一个定时器');
// 如果没有[]每次都会调用,每次都生成一个新的定时器,就会乱掉
let timer = setInterval(() => {
setState(x => ({ number: x.number + 1 }));
}, 1000);
//useEffect会返回一个清理函数,当组件将要卸载的时候会执行清理函数
return () => {
console.log('销毁一个定时器');
clearInterval(timer);
}
}, []);
如果定时器不销毁,点击hide按钮就会报如下错:不能在一个卸载的组件上进行状态更新
memory leak:内存泄露
//Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
function Counter2() {
let [state, setState] = useState({ number: 0 });
useEffect(() => {
console.log('开始一个定时器');
let timer = setInterval(() => {
setState(x => ({ number: x.number + 1 }));
}, 1000);
}, []);
return (
<button onClick={() => setState({ number: state.number + 1 })}>+</button>
)
}
function App() {
let [visible, setVisible] = useState(true);
return (
<div>
<button onClick={() => setVisible(false)}>hide</button>
{visible && <Counter2 />}
</div>
)
}
7:useRef
- 返回一个可变的ref对象,其.curret 属性被初始化为传入的参数(initialValue)
- 返回的ref对象在组件的整个生命周期内保持不变
// 以前的写法
let refObj = React.createRef(); // refObj = { current: 要引用的组件}
// current指向输入框渲染到页面以后input的真实DOM
<input ref={refObj} />
<button onClick={()=>{ refObj.current.focus() }}>获取焦点</button>
lastRef === refObj一直都是false;说明每次创建的refObj都是新对象
let lastRef;
function Child(props) {
let refObj = React.createRef();//refObj={current:要引用的组件}
console.log('lastRef === refObj', lastRef === refObj);
lastRef = refObj;
return (
<div>
<input ref={refObj}></input>
<button onClick={() => refObj.current.focus()}>获得焦点</button>
</div>
)
}
function Parent() {
let [number, setNumber] = useState(0);
return (
<div>
<Child />
<button onClick={() => setNumber(x => x + 1)}>+</button>
</div>
) }
- 使用useRef:
lastRef === refObj就会变成true;说明是复用了,每次拿到的都是同一个refObj
let refObj = React.createRef(); // 改成
let refObj = useRef();
useRef原理
第一次创建refObj,就缓存起来,以后每次创建都返回缓存的那个
实现
let currentRefObj
function useRef () {
if (!currentRefObj) {
currentRefObj = { current: null }
} else {
return currentRefObj
}
}
// 主要是靠组件的ref属性,会把组件的真实dom给current
forwardRef
-
想要给函数组件增加ref属性,需要用forward属性包裹
-
想在父组件里面拿到子组件中输入框的DOM元素
-
在parent中拿到ref,那么如何把refObj传给Child的input框
-
写法1:可以成功的拿到,但是一般不会这样写
// 父组件引用子组件 <Child xx={refObj} /> // 子组件接口 <input ref={props.xx} /> -
直接给子组件绑ref
<Child ref={refObj} /> <input ref={props.ref} /> /** * ref is not a props不能用props.ref取 * Function components cannot be given refs. * Attempts to access this ref will fail. * Did you mean to use React.forwardRef()? * 函数组件也不能直接给ref * 直接这样传递的话破坏了封装的原则,很危险! */ -
使用forwardRef
-
function Child(props,ref) {
return (
<div><input ref={ref}></input></div>
)
}
let ForwardChild = React.forwardRef(Child);
// 函数组件用forwardRef包住以后,会多一个参数ref
function Parent() {
let refObj = useRef(); // 在parent中拿到ref,那么如何把refObj传给Child的input框
function getFocus() {
refObj.current.focus();
}
return (
<div>
<ForwardChild ref={refObj}/>
<button onClick={() => refObj.current.focus()}>获得焦点</button>
</div>
) }
useImperativeHandle
-
forwardRef带来的问题:父组件可以通过
refObj.current拿到子组件中的某个DOM 元素,直接这样传递破坏了封装的原则 -
useImperativeHandle:可以在使用ref的时候,自定义暴露给父组件的实例值
- 参数一:ref,子组件被forwardRef包裹后会有个和props同级的ref
- 参数二: 函数,返回一个对象,对象里面可以
-
在大多数情况下useImperativeHandle和forwardRef应当一起使用
- useRef依旧放在子组件内部
function Child(props, ref) {
let refObj = useRef();
useImperativeHandle(ref, () => ({
focus() {
refObj.current.focus();
}
}))
return (
<div><input ref={refObj}></input></div>
)
}
let ForwardChild = React.forwardRef(Child);
function Parent() {
function getFocus() {
// 如果不用useImperativeHandle,此处的refObj.current指向的就是真实的input输入框
// 可以在此处进行任意的操作,refObj.current.value = 'xxx',不应该改值
// 使用以后,此处的current指向的就是useImperativeHandle第二个参数的返回值
refObj.current.focus();
}
return (
<div>
<ForwardChild ref={refObj}/>
<button onClick={() => refObj.current.focus()}>获得焦点</button>
</div>
) }
8:useLayoutEffect
- 会在所有的DOM 变更之后同步调用effect
- 可以用它来读取DOM布局并同步触发重新渲染
- useLayoutEffect在Layout的时候执行;useEffect在渲染之后也就是Display之后执行
1、HTML => HTML Parse => DOM Tree
2、Style Sheets => CSS Parse => Style Rules
3、DOM Tree + Style Rules => Render Tree <===> Layout(执行useLayoutEffect) => Painting => Display 之后(执行useEffect)
function LayoutEffect() {
let [color, setColor] = useState('red');
useLayoutEffect(() => {
alert(color);
});
useEffect(() => {
console.log('当前的颜色useEffect', color);
});
return (
<>
<div id="myDiv" style={{ backgroundColor: color }}>颜色</div>
<button onClick={() => setColor('red')}>红</button>
<button onClick={() => setColor('yellow')}>黄</button>
<button onClick={() => setColor('blue')}>蓝</button>
</>
)
}
// 点击按钮:先alert =>(alert确定后) => 先变 backgroundColor => 最后打印当前颜色
9:自定义Hook
- hook是一种复用状态逻辑的方式,它不复用state本身。
- 每次调用都有完全独立的state,Counter1和Counter2中的随机数number每次都不同
function useCounter() {
let [number, setNumber] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setNumber(Math.random());
}, 1000);
return () => {
clearInterval(timer);
}
});
return number;
}
function Counter1() {
let number = useCounter();
return ( <div> {number} </div> )
}
function Counter2() {
let number = useCounter();
return ( <div> {number} </div> )
}
10:hooks实现中间件的用法
- 怎么用hooks发出请求,实现中间件的用法
1、模拟一个redux-logger,在每次状态变更后打印新的状态值
function reducer(state, action) { switch(action.type){
case: 'INCREAMENT' return ...
}}
function useLogger(_reducer, initState){
const [state, dispatch] = useReducer(_reducer, initState);
function loggerDispatch (payload) {
console.log('老状态', state);
dispatch(payload);
}
useEffect(() => console.log('新状态', state), [state]);
return [ state, loggerDispatch]
}
function App() {
let [state, dispatch] = useLogger(reducer, {number: 1});
return (
<div>
<button onClick={() => dispatch({ type: INCREMENT })}>+</button>
<button onClick={() => dispatch({ type: DECREMENT })}>-</button>
</div>
)
}
2、实现thunk,thunk的dispacth,派发的是一个函数
function useThunk(reducer, initialState) {
let [state, dispatch] = useReducer(reducer, initialState);
function thunkDispatch(payload) {
if (typeof payload === 'function') {
payload(thunkDispatch, () => state)
} else {
dispatch(payload);
}
}
return [state, thunkDispatch];
}
function App() {
let [state, dispatch] = useThunk(reducer, {number: 1});
return (
<button onClick={() => dispatch(function(dispatch, getState){
dispatch({ type:INCREMENT })
})}>+</button>
)
}
3、usePromise, dispatch一个new Promise
function usePromise(reducer, initialState) {
let [state, dispatch] = useReducer(reducer, initialState);
function promiseDispatch(action) {
if (typeof action.then === 'function') {
// action.then(res => promiseDispatch(res))
action.then(promiseDispatch);
} else {
dispatch(action);
}
}
return [state, promiseDispatch];
}
function App() {
let [state, dispatch] = usePromise(reducer, initialState);
return (
<div>
<p>{state.number}</p>
<button onClick={() => dispatch(new Promise(function (resolve) {
setTimeout(function () { // 一秒之后 + 1
resolve({ type: INCREMENT });
}, 1000);
}))}>+</button>
</div>
)
}