react规定我们必须把hooks写在函数的最外层,不能写在if else 或内部函数等条件语句当中,来确保hooks的调用顺序在每次渲染中都是相同的。
useState
第一次渲染会读取设置的初始值,之后渲染时则会忽略设置的初始值;useState改变状态依然是异步的,修改后无法实时获取最新的改变之后的值。
- 传入 useState 参数后返回一个带有默认状态和改变状态函数(如下setInfo)的数组;
const [info, setInfo] = useState({
name: 'dyx',
age: 24,
});
- setState是非覆盖式更新状态,只会更新传入的值,而useState是覆盖式更新状态
const [info, setInfo] = useState({
name: 'dyx',
age: 24,
});
const changeInfo = addAge => {
setInfo({
...info,
age: info.age + addAge,
});
}
- 使用useState更新值时获取更新前的值
const [num, setNum] = useState(2);
setNum((oldNum) => {
return oldNum + 2;
})
- 修改引用类型数据的变化,必须重新返回一个新的解构后的值
const [info, setInfo] = useState({
name: 'dyx',
age: 24,
});
// 页面会更新
const changeInfo = addAge => {
setInfo((oldInfo) => {
return {
...oldInfo,
age: oldInfo.age + addAge,
}
});
}
const changeInfoCopy = addAge => {
setInfo((oldInfo) => {
oldInfo.age += addAge;
// return oldInfo; 这样不会触发页面更新
return {...oldInfo}; // 这样会更新
});
}
// 简单类型数据可以触发更新
const [num, setNum] = useState(2);
const changeNum = addNum => {
setNum((oldNum) => {
oldNum += addNum;
return oldNum;
});
}
useEffect
- useEffect第二个参数中数组没有传值时代表不监听任何参数变化,只有在组件初始化时才会触发,用来代替 componentDidMount
useEffect(() => {
console.log('componentDidMount');
}, [])
- 如果第二个参数中的值改变则触发第一个参数中的方法,组件初始化也会触发此方法,引用类型数据也可以监听到
const [info, setInfo] = useState({ name: 'dyx', age: 24 });
useEffect(() => {
console.log(info); // 改变后的值
}, [info]);
- 第二个参数可以监听多个参数,有其一改变就会触发
const [info, setInfo] = useState({ name: 'dyx', age: 24 });
const [num, setNum] = useState(2);
// 只要num和info有一个变化就会触发
useEffect(() => {
console.log(info);
console.log(num);
}, [info, num]);
- 没有第二个参数时类似于componentDidMount 和 componentDidUpdate, 每一次渲染都会执行次函数
useEffect(() => {
console.log('render');
})
- didMount注册事件,componentWillUnmount销毁事件
const func = () => {
console.log('func');
}
useEffect(() => {
document.addEventListener('click', func);
return () => {
document.removeEventListener('click', func);
};
}, []);
// componentWillUnmount功能
useEffect(() => {
return () => {
console.log('componentWillUnmount');
}
}, [])
- 监听props的改变,可以监听整个props或者监听某个或多个props值
// 监听props
useEffect(() => {
console.log(props);
}, [props]);
useLayoutEffect
useLayoutEffect和useEffect使用形式相同。useEffect执行顺序: 组件更新挂载完成 -> 浏览器 dom 绘制完成 -> 执行 useEffect 回调。useLayoutEffect 执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器dom绘制完成
useRef
createRef每次渲染都会返回一个新的引用,而useRef每次都会返回相同的引用。
- 创建useRef时候,会创建一个原始对象,只要函数组件不被销毁,原始对象就会一直存在,可以利用这个特性,来通过useRef保存一些数据。ref在所有的render中保持着唯一引用,因此所有对ref的赋值或取值,拿到的都是一个最终状态,不会在每个render间存在隔离。
// ref的值需要手动更新, 当无法在某一场景获取到最新的props时可以定义ref为最新的props值
const queryRef = useRef(props);
useEffect(() => {
queryRef.current = props;
}, [props])
- 将ref对象传入元素,ref对象的.current属性将是当前的DOM节点。
/**
* ref绑定元素的使用
*/
import React, { useRef } from 'react';
const DemoOne = () => {
const refEle = useRef();
const clickToFocus = () => {
refEle && refEle.current && refEle.current.focus();
}
return (
<div>
<input ref={refEle} type="text" />
<button onClick={clickToFocus}>click</button>
</div>
);
}
export default DemoOne;
useImperativeHandleref
useImperativeHandle 可以配合 forwardRef 自定义暴露给父组件的实例值。子组件如果是class类组件,我们可以通过ref获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过ref的,那么此时useImperativeHandle和 forwardRef配合就能达到效果。
/**
* 父组件调用子组件方法的 父组件
*/
import React from 'react';
import Child from './child';
const Demo = () => {
let childRef = null
const getChild = () => {
console.log(childRef); // 子组件useImperativeHandle返回的值
childRef && childRef.childFunc(3);
}
return (
<div>
<Child ref={ref => childRef = ref} />
<button onClick={getChild}>触发子组件方法</button>
</div>
);
}
export default Demo;
/**
* 父组件调用子组件方法的 子组件
*/
import React, { useState, useImperativeHandle, forwardRef } from 'react';
// props子组件中需要接受ref
const Child = (props, ref) => {
const [val, setVal] = useState(1);
const changeVal = val => {
setVal(val);
}
// useImperativeHandle方法的的第一个参数是目标元素的ref引用
useImperativeHandle(ref, () => ({
// childFunc 就是暴露给父组件的方法
childFunc: (newVal) => {
changeVal(val + newVal);
}
}));
return (
<div>{val}</div>
);
}
export default forwardRef(Child);
useMemo
- 只有依赖项改变才会调用某一个方法时可以使用useMemo,避免在每次渲染时都进行高开销的计算。返回的是函数运行的结果。
// 父组件
import React, { useState } from 'react';
import DemoOne from './demoOne';
const UseMemoDemo = (props) => {
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(1);
return (
<div>
<DemoOne {...props} count={count} count1={count1} />
<button onClick={() => setCount(count + 1)}>count</button>
<button onClick={() => setCount1(count1 + 1)}>count1</button>
</div>
);
}
export default UseMemoDemo;
// 子组件
import React, { useState, useMemo } from 'react';
const DemoOne = (props) => {
const [num, setNum] = useState(100);
const { count, count1 } = props;
const operationProps = (props) => {
console.log('propsChange')
return {
newcount: props.count + 10,
newcount1: props.count1 + 10,
}
}
// 如果不使用useMemo在组件内部状态改变时也会重新调用operationProps方法,使用之后只有在props改变时才会重新调用operationProps方法
const { newcount, newcount1 } = useMemo(() => operationProps(props), [props]);
return (
<div>
<div>
props count: {count} count1: {count1}
</div>
<div>
new count: {newcount} count1: {newcount1}
</div>
<div>
num {num}
<button onClick={() => setNum(num + 1)}>changeNum</button>
</div>
</div>
);
}
export default DemoOne;
- 没有依赖项时在第一次渲染时调用方法,可以用于debounce方法的声明
import _ from 'lodash';
const onChange = () => {
console.log('onChange');
}
const debounceOnChange = useMemo(() => {
return _.debounce(onChange, 600);
}, []) // 不能依赖onChange函数
useCallback
返回的是函数。父组件重新渲染时,声明的函数也会重新定义,如果此函数传递给子组件,那么子组件会重新render,使用useCallback对函数进行包裹,可以避免子组件不必要的重新渲染,子组件使用memo包裹。
// 父组件
import React, { useState, useCallback } from 'react';
import DemoOne from './demoOne';
const UseMemoDemo = () => {
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(1);
const clickCount= () => {
setCount(count + 1);
};
const clickCount1 = useCallback(() => {
setCount1(count1 + 1);
}, [count1]);
return (
<div>
<div>count: {count}, count1: {count1}</div>
<DemoOne clickCount={clickCount}>普通函数形式</DemoOne>
<DemoOne clickCount={clickCount1}>useCallback函数形式</DemoOne>
</div>
);
}
export default UseMemoDemo;
/**
* 避免父组件重新渲染,导致传递的方法重新定义导致子组件重新渲染
*/
import React from 'react';
const DemoOne = (props) => {
console.log(props.children)
return (
<div>
<div>
{props.children}
</div>
<button onClick={props.clickCount}>调用父组件方法</button>
</div>
);
}
export default React.memo(DemoOne);
useContext
// colorContext.js
import { createContext } from 'react';
const ColorContext = createContext();
export default ColorContext;
// useContextDemo.js
import React from 'react';
import DemoOne from './demoOne';
import ColorContext from './colorContext'
const UseContextDemo = () => {
return (
// 此处的value才是子孙组件读取到的值
<ColorContext.Provider value="red">
<DemoOne />
</ColorContext.Provider>
);
}
export default UseContextDemo;
// demoOne.js
import React from 'react';
import DemoTwo from './demoTwo';
const DemoOne = () => {
return (
<DemoTwo />
);
}
export default DemoOne;
// demoTwo.js
import React, { useContext } from 'react';
import ColorContext from './colorContext'
const DemoTwo = () => {
const color = useContext(ColorContext);
return (
<div style={{ color }}>
{color}
</div>
);
}
export default DemoTwo;
useReducer
useReducer 接受的第一个参数是一个函数,我们可以认为它就是一个 reducer , reducer 的参数就是常规 reducer 里面的 state 和 action ,返回改变后的 state , useReducer 第二个参数为 state 的初始值 返回一个数组,数组的第一项就是更新之后 state 的值 ,第二个参数是派发更新的 dispatch 函数。
import React, { useReducer } from 'react';
const UseReducerDemo = () => {
/* count为更新后的state值, dispatchCount 为当前的派发函数 */
const [count, dispatchCount] = useReducer((state, action) => {
const { payload, type } = action
/* return的值为新的state */
switch(type) {
case 'add':
return state + 1
case 'sub':
return state - 1
case 'reset':
return payload
}
return state
}, 0)
return (
<div>
<div>{ count }</div>
<button onClick={() => dispatchCount({ type:'add' })}>add</button>
<button onClick={() => dispatchCount({ type:'sub' })}>sub</button>
<button onClick={() => dispatchCount({ type:'reset', payload: 123 })}>reset</button>
</div>
)
}
export default UseReducerDemo
Capture Value
- 每次render的内容都会形成一个快照并保留下来,当rerRender的时候,就形成了n个render状态,而每个render状态都拥有自己固定不变的props和state。
- useState 中定义的变量在每一次render的时候都是一个固定的常量,只是不同的render时机,对应的常量的值可能不同。
- 每次 Render 都有自己的事件处理
如下示例中如果将console的值由内部的状态改为父组件传过来的props,如果在3秒内父组件的props发生变化,console的值还是原来的值,不是最新的值。但是class形式组件是最新的值,虽然props是不可变的,但是this在class形式组件中是可变的,因此this.props形式的调用每次都访问最新的props。而Function形式组件不存在 this.props的语法,因此props在一次render中时固定不变的。
import React, { useState } from 'react';
const CaptureValueDemo = () => {
const [num, setNum] = useState(0);
const log = () => {
setTimeout(() => {
// 每一次console的值都是执行setNum之前的值
// 在log函数执行的那个render中,num的值还是执行setNum之前的值
// 执行setNum()之后会交由一个全新的render中,新的render中不会执行log函数。
// 而三秒后执行的内容是由setNum前的那个render执行的,所以console的num自然也是setNum前的那个render中的num值
// num、log都拥有 Capture Value 特性
console.log(num);
}, 3000);
};
return (
<div>
{num}
<button
onClick={() => {
setNum(num + 1);
log();
}}
>
click
</button>
</div>
);
};
export default CaptureValueDemo;
- useRef没有Capture Value特性。ref在所有的render中保持着唯一引用,因此所有对ref的赋值或取值,拿到的都是一个最终状态,不会在每个render间存在隔离。
- useEffect 也一样具有 Capture Value 的特性
// Capture Value特性
const [num, setNum] = useState(0);
useEffect(() => {
// 由于useEffect的Capture Value的特性,拿到的num值永远是初始化的0。
// setInterval永远在count为0基础上加一,setInterval在一直循环但是后续的setNum没有什么作用
const intervalId = setInterval(() => {
setNum(num + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return (
<div>{num}</div>
);
// 监听状态
const [num, setNum] = useState(0);
useEffect(() => {
// useEffect监听num的变化可以拿到了最新的num。
// 但是计时器不准了,因为每次count变化时都会销毁并重新计时。计时器频繁的挂载和销毁 定时器带来了一定性能负担。
const intervalId = setInterval(() => {
setNum(num + 1);
}, 1000);
return () => clearInterval(intervalId);
}, [num]);
return (
<div>{num}</div>
);
// 不依赖外部变量(依赖useState的回调形式进行更新操作)
const [num, setNum] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setNum(oldNum => oldNum + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return (
<div>{num}</div>
);
记录
- function函数中定义的变量或者方法 每一次render都会重新定义,如果是引用类型数据,引用类型数据每一次都是不同的,即使数据依赖的值没有变化,因为应用数据指向的地址每一次都会变化,所以在相关hook中以此数据作为依赖每一次都会变化。
- 定时器的flag定义在函数组件内部,每一次重新render定义的flag都会重新定义,但是在useEffect的return中获取到的还是第一次赋值给flag的值,在这里销毁没有问题。