Hooks函数
在传统的函数组件中,没有状态,无法更新视图等一些列问题,因此我们引入了类组件,在类组件中引入了状态,生命周期函数等实现静态组件无法完成的事情,处理视图更新。
而接下来引入的Hooks函数,本质是函数组件,只不过又结合了类组件的特征,让本来是静态的函数组件能够动态化。
useState函数
useState使用与处理机制
Hooks函数都挂载到react身上,需要引入使用
import { useState } from 'react'
基本使用格式如下: let res = useState(0)
,其中调用useState()
方法并传入一个初始状态值
。该方法的返回值是一个数组:[状态值,修改状态的方法],数组的第一个元素就是创建的状态值,第二个元素是修改状态值的方法同时能更新视图,传入什么值,状态值最终就会被修改为对应的值。
如下就是一段点击按钮实现数字累加效果的代码
export const Demo1 = () => {
let [num, setNum] = useState(0)
const addNum = () => {
setNum(num + 1)
}
return <div className="box">
<span className="num">{num}</span>
<Button type="primary" onClick={addNum}>按钮</Button>
</div>
}
在类组件中,每次执行的时候都会将组件标签执行,创建组件实例,然后根据类组件的机制,执行render方法渲染,在每次更新的时候不会去创建新的组件实例,而是走类组件内部的渲染流程,再次执行render方法更新视图。
而在函数组件中,如果需要更新视图,那么就需要重新调用该函数组件,然后会创建一个全新的上下文,内部的所有代码也会重新执行。而hooks也是如此。
如下是hooks函数的渲染流程。第一次渲染的时候创建一个全新的上下文即作用域,然后相应的操作,在更新的时候会执行当前作用域的handle方法,而在该方法的作用域中无所需的数据,于是会往上层作用域查找执行setNum方法,该方法执行修改状态,视图更新。于是整个函数又会重新执行一次,创建全新的作用域和私有属性,但是更新的时候useState方法使用的初始值是上次setNum设置的初始值。
如下是一道例题:执行完毕后的num值为0。这是因为在当前函数组件中创建了一个作用域,执行函数的时候,开启定时器。定时器位于当前作用域中开启的。所以无论如何都只会寻找当前作用域中的num,因此为0。
let [num, setNum] = useState(0)
const addNum = () => {
setNum(100)
setTimeout(() => {
console.log(num);
}, 1000);
}
底层大致机制代码实现
let state
function useState(Initialvalue) {
if (typeof state === 'undefined') state = Initialvalue
const setState = (value) => {
state = value
}
return [state, setState]
}
setState(10)
useState需要注意的地方
当使用useState
方法会返回一个更新状态的方法,每次调用该方法的时候都会将状态整体更新,无法实现在类组件中使用setState方法实现部分状态更新。 比如原本是对象,后期调用修改为数值也是可以的。
如下一段代码,因为当前函数组件中有多个状态需要更新,因此将状态保存在一个对象中存储。但是这样子就会出现一个更新时候的问题。如图
export const Vote = (props) => {
let [state, setState] = useState({
supNum: 10,
oppNum: 5
})
const handle = (type) => {
if (type === '+') {
setState({
supNum: state.supNum + 1
})
} else {
setState({
oppNum: state.oppNum + 1
})
}
}
return <div className="box">
<div className="title">
<h1>{props.title}</h1>
<h4>{state.oppNum + state.supNum}</h4>
</div>
<div className="main">
<h4>支持人数:{state.supNum}</h4>
<h4>反对人数:{state.oppNum}</h4>
</div>
<div className="footer">
<Button type='primary' onClick={handle.bind(null, '+')}>支持</Button>
<Button type='dashed' onClick={handle.bind(null, '-')}>反对</Button>
</div>
</div>
}
这是因为,每次点击更新的时候调用更新状态的函数,但是在函数中误以为可以像类组件中使用setState
方法一样实现部分更新,这是错误的。在这里,更新状态为什么值,下一次的state就是什么值。于是更新的时候丢失了oppNum的值,再次进行累加操作就会出现问题。
解决方法如下,每次将拥有的值展开,后续将需要更新的值替换即可。
setState({
...state,
supNum: state.supNum + 1
})
但是如上这种管理操作多个状态的方法不推荐使用。官方推荐一个状态对应一个useState()
管理。修改后的代码如下,操作的时候操作对应的代码即可。
let [supNum, setSupNum] = useState(10)
let [oppNum, setOppnum] = useState(5)
useState是同步还是异步
编写如下测试代码,注意: 我们需要在该函数组件的作用域中输出语句查看。
export const Demo1 = () => {
console.log('render');
let [num, setNum] = useState(0)
let [num2, setNum2] = useState(100)
let [num3, setNum3] = useState(1000)
const addNum = () => {
setNum(num + 1)
setNum2(num2 + 1)
setNum3(num3 + 1)
}
return。。。。
}
经过测试:useState方法也是异步的,原理类似类组件中的setState方法,都是基于批处理完成的。细节也同setState一样。 如果想执行调用一次setNum就更新一次视图可以使用flushSync方法
实现
useState的更新和优化机制
在使用useState
更新视图的时候,如果发现两次更新的内容一模一样的时候,就不会重新去执行函数组件。本身提供了优化机制,减少了重复浪费的渲染次数。类组件的setState
无论状态前后是否改变,都进行更新操作
let [num, setNum] = useState(0)
const addNum = () => {
setNum(0)
}
如下这段代码,经过循环后,页面只会渲染一次,且视图中显示的num值为1。在这里是基于更新队列,采样批处理优化。且num每次都是取当前上下文中的值,即0,所以在更新队列中的每一个操作均为:setNum(0+1)
console.log('render');
let [num, setNum] = useState(0)
const addNum = () => {
for (let i = 0; i < 10; i++) {
setNum(num + 1)
}
}
当把代码改成如下情况,则页面一共渲染2次,且页面最终显示的值为1.(这题可以不看)
console.log('render');
let [num, setNum] = useState(0)
const addNum = () => {
for (let i = 0; i < 10; i++) {
flushSync(() => {
setNum(num + 1)
})
}
}
在useState
返回的更新状态的方法中,也可以如类组件的setState方法中直击传入值更新状态操作,或者传入一个函数更新状态。代码如下:render只会输出一次
console.log('render');
let [num, setNum] = useState(0)
const addNum = () => {
for (let i = 0; i < 10; i++) {
setNum((prev) => { //上一次状态的值
console.log(prev);
return prev + 1
})
}
}
useState(callback)用法
在之前的使用过程中,都是直接将需要初始化的数值放在useState
中,如果初始化的值是需要经过一个函数计算得出的,且该函数在初始化计算处结果后就不需要再使用了,就会造成一定的性能浪费。如下代码,total初始化的值只在第一次的时候有用,后期再次视图渲染调用组件函数执行的时候该方法执行的新total不会被使用了。如何解决这个问题,让这个初始化的函数只会执行一次
let total = 0
for (let i = 0; i <= 10; i++) {
console.log('执行了');
total += i
}
let [num, setNum] = useState(total)
const addNum = () => {
flushSync(() => {
setNum(num + 1)
})
flushSync(() => {
setNum(num + 2)
})
setNum(num + 3)
}
修改后的代码如下,这样子除了初始化的时候会执行一次该函数,之后更新的时候不会再次执行了。这样子大大提高了性能
let [num, setNum] = useState(() => {
let total = 0
for (let i = 0; i <= 10; i++) {
console.log('执行了');
total += i
}
return total
})
底层机制大致如下
let state
function useState(initialValue) {
if (typeof state === 'undefined') {
if (typeof initialValue === 'function') {
state = initialValue()
} else {
state = initialValue
}
}
const setState = value => {
if (typeof value === 'function') {
state = value(state)
} else {
state = value
}
}
return [state, setState]
}
useEffect函数
在Hooks组件中如何像类组件一样使用生命周期函数是通过useEffect
方法实现。
以下是基本代码
let [num, setNum] = useState(0)
const addNum = () => {
setNum(num + 1)
}
return <div className="box">
<span className="num">{num}</span>
<Button type="primary" onClick={addNum}>按钮</Button>
</div>
useEffect(callback)
当在useEffect(callback)
函数中只传入一个回调函数的时候,那么该回调会在组件初次挂载成功的时候执行一次,这类似类组件的componentDidMount
钩子,且每次点击按钮更新视图的时候会触发一次,这类似类组件的componentDidUpdate
钩子
useEffect(() => {
console.log('执行了');
})
useEffect(callback,[])
当传入第二个参数为空数组的时候,该函数只会在初次挂载完成的时候执行一次,后续更新不会再执行。
useEffect(() => {
console.log('@1', num);
})
useEffect(() => {
console.log('@2', num);
}, [])
如果在数组中传入依赖的值,则该函数回调会在每次依赖改变的时候执行一次,如果依赖的值没有变化则不会执行。 这类似vue的计算属性。
如下代码并没有通过setX
方法修改状态的值。所以每次所依赖的值x并无变化,因此只会初始化的时候执行一次,后续更新不会再触发。
let [x, setX] = useState(100)
useEffect(() => {
console.log(`@3 num:${num} x:${x}`);
}, [x])
可以在数组配置中传入依赖的多个值,这样子只要有一个值发生改变,回调函数都会被执行
useEffect(() => {
console.log(`@3 num:${num} x:${x}`);
}, [num, x])
useEffect( ()=> callback)
在useEffect
的回调函数中返回一个回调函数,那么外部回调函数的触发时机和规则和之前的一样,而内部返回的回调函数初次挂载的时候不会执行,而是视图第二次更新的时候,会立即执行这个返回的回调函数。因此如果在返回的函数中获取num的值,则是更新前状态的值。
useEffect(() => {
console.log('@4 外');
return () => {
console.log('@4 里',num);
}
})
useEffect细节
副作用函数可以返回一个清理函数,这个清理函数会在组件卸载或者下一次执行副作用之前调用,以避免内存泄漏或者无效操作,当在useEffect中不设置任何依赖项([ ]),那么副作用函数只会在组件挂载时执行一次,并且在组件卸载时执行清理函数。
在使用useEffect
的时候,要求该方法不能被任何分支循环判断包裹,否则报错,其次传入的回调函数callback不能被async等修饰,要求必须是一个函数,
例如使用分支包裹住useEffect
函数会立即报错
if (num >= 5) {
useEffect(() => {
console.log('@1', num);
})
}
修改成如下代码就没有问题
useEffect(() => {
if (num >= 5) {
console.log('@1', num);
}
})
在开发中,我们经常需要向服务器发生请求获取数据,那么就可能写出如下代码,对useEffect的callback进行async修饰
。这就会引出一个问题,在async/await语法中,每一个被包裹的函数执行体都会默认返回一个promise,而callback要求return返回值只能返回一个普通函数,所以这里就会出现问题
let api = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([1, 2, 3])
}, 1000)
})
}
useEffect(async () => {
let res = await api()
})
修改代码如下就没有问题
useEffect(() => {
api().then(res => {
console.log(res);
})
})
或者修改如下格式,都是正确的的,只要不影响useEffect的callback函数格式即可
useEffect(() => {
let fun = async () => {
let res = await api()
console.log(res);
}
fun()
})
useLayoutEffect和useEffect区别
如下这段基本代码,根据num的值渲染背景色为红还是绿色。初次渲染的时候,打印一次render,然后执行useEffect方法的时候根据内部机制插入链表等待,随后执行背景色判断操作,num初始为0,所以背景色是红,最后执行链表中的操作,因为useEffect方法挂载完成的时候会执行一次,所以会进入内部判断,这个时候num在当前作用域中为0.所以会执行setNum操作更新状态值渲染视图。又会打印一次render,执行useEffect插入链表,执行jsx视图渲染,这个时候num为10,背景色为绿,最后执行useEffect的时候因为依赖的状态改变了从0变10,所以会进入操作,但是这个时候num为10,所以不执行视图更新操作。后期点击按钮的时候操作过程基本一致
console.log("render");
const [num, setNum] = useState(0)
useEffect(() => {
if (num === 0) {
setNum(10)
}
}, [num])
return <div className="box" style={{ backgroundColor: num === 0 ? 'red' : 'green' }}>
<span className="num">{num}</span>
<Button type="primary" onClick={() => {
setNum(0)
}}>按钮</Button>
</div>
但是根据输出和效果图会发现一个问题,render每次执行之间的时间间隔又所以不会很快的连续输出导致屏幕中会出现红跟绿的切换过程。
当将useEffect
修改为useLayoutEffect
的时候,打印如下,基本过程是一样的,但是执行的速度变快了,看不到界面颜色之间切换
useLayoutEffect(() => {
if (num === 0) {
setNum(10)
}
}, [num])
分析为什么useEffect
会慢于useLayoutEffect
?
在经过render方法创建真实DOM的时候,即使已经全部创建了,最终也需要等待浏览器绘制和渲染用户才会看见最终内容。
useLayoutEffect
方法会阻塞浏览器渲染真实DOM到页面中,当阻塞的时候,会同步执行链表中的callback函数(num=0,设置完成后为10),这个时候有会创建真实DOM,进入阻塞,执行callback操作,但是内部函数无需执行再次渲染操作(10不为0)。于是退出并执行浏览器渲染。可以理解将最新生成的DOM进行处理,所以最终直接把绿色的显示给用户看见useEffect
方法不会阻塞浏览器渲染真实DOM到页面中,会异步执行链表中的callback方法(0的时候可能已经渲染成功,同时设置为10),这个时候页面可能已经呈现内容了,但是会执行一次更新操作。再次创建真实DOM异步渲染,继续执行回调方法(不执行操作)。因此可能看见两种颜色在切换。- 在两个方法中,都可以执行获取真实DOM操作
useRef函数
如何在hooks组件中获取DOM元素有如下几种方法
- 方法一:使用函数式给某一个元素绑定DOM元素,ref会自动获取为当前绑定的DOM元素,在同时将该元素保存到变量box保存,最后会在
useEffect
的回到函数中确保获取到DOM元素
<div className="box" ref={ref => box = ref}>。。。</div>
let box
useEffect(() => {
console.log(box);
},[])
- 方法二:使用
React.createRef()
方法创建一个ref对象使用
<div className="box" ref={box}>
......
let box = React.createRef()
useEffect(() => {
console.log(box);
},[])
- 方法三:通过提供了
useRef
函数,,该方法本质和React.createRef()
一致,都是创建ref对象,只不过使用方式不同
<div className="box" ref={box}>..</div>
....
let box = useRef(null) //指定current的初始值
useEffect(() => {
console.log(box);
},[])
在hooks函数组件中,既然useRef
和React.createRef
都是创建ref对象,那么他们的区别是什么?
useRef初次执行的时候会创建ref对象,后期如果hooks组件再次调用,则该方法不会重复执行,不会再次创建ref对象。每次都是使用同一个ref对象。
在hooks组件中React.createRef()方法则不一样,初次创建ref对象,如果组件更新,则每次都会重复创建新的ref对象。浪费性能。(但是在类组件中不一样,类组件中只会在实例化组件的时候执行一次创建,后期更新不会重复创建)。
总结:在类组件中使用React.createRef(),而在hook组件中使用useRef()
<span className="num" ref={box}>{num}</span>
<span className="num" ref={box2}>hhh</span>
let prev1;
let prev2;
const Demo4 = () => {
const [num, setNum] = useState(0)
let box = useRef(null) //指定current的初始值
let box2 = React.createRef()
if (!prev1) {
prev1 = box;
prev2 = box2
} else {
console.log(prev1 === box);
console.log(prev2 === box2);
}
。。。
}
当使用useRef()
获取类组件或函数组件的时候,效果和类组件中的操作一致。
类组件获取当前组件实例
class Child extends React.Component {
state = {
x: 10
}
render() {
return <div className='child'>{this.state.x}</div>
}
}
const Demo4 = () => {
let box = useRef(null);
useEffect(() => {
console.log(box.current);
})
return <div className="box" >
<Child ref={box}></Child>
</div>
}
函数组件不能直接使用,依旧需要使用React.forwardRef()
const Child = function () {
let title = '你好'
return <div className="child">{title}</div>
}
const Demo4 = () => {
let box = useRef(null);
useEffect(() => {
console.log(box.current);
})
return <div className="box" >
<Child ref={box}></Child>
</div>
}
使用React.forwardRef()
依旧是在父组件中可以获取一个子函数组件内部的元素使用。
const Child = React.forwardRef(function (props, ref) { //ref = {current:null}
let title = '你好'
return <div ref={ref} className="child">{title}</div>
})
但是如果我给这个函数组件赋予状态,使其变为hooks组件,基本使用方式还是和函数组件一致的,但是如果想获取当前hooks组件的状态和方法就需要使用useImperativeHandle方法
完成
const Child = React.forwardRef(function (props, ref) { //ref = {current:nul}
let [title, setTitle] = useState('你好')
const createTit = () => {
setTitle('哈哈')
}
return <div ref={ref} className="child">{title}</div>
})
useImperativeHandle()方法
修改代码如下,该方法的第一个参数为传入的ref对象,第二参数为函数,该函数需要返回一个对象,对象返回什么,那么ref对象最终就是什么内容。会将{current:null}
中的current
的值替换替换,且即使给元素绑定ref属性也无效,无法获取DOM元素
const SonSon = React.forwardRef((props, ref) => { //这里的ref => {current:null}
let [n, setN] = useState(10);
useImperativeHandle(ref, () => { //该 ref 是你从 forwardRef 渲染函数 中获得的第二个参数。
return {
n,
setN,
};
});
return (
<>
<h2>SonSon组件{n}</h2>
</>
);
});
const Son = () => {
let ref = useRef(null);
useEffect(() => {
console.log(ref);
});
return (
<>
<h1>Son组件</h1>
<hr />
<SonSon ref={ref}></SonSon>
</>
);
};
useMemo函数
如下这段基本代码,如果是页面需要展示的数据,如sup,opp,res,那么希望每次更新的时候都会重新执行一次渲染视图。但是如果我们点击的是x按钮,但是该数值在页面中不需要进行任何展示处理。同样的我们的视图也会重新渲染,又会将total,sup等重新计算,这是浪费性能的。我们希望的是只有sup和opp改变的时候才会执行计算比例。
let [sup, setSup] = useState(15)
let [opp, setOpp] = useState(5)
let [x, setX] = useState(0)
let total = sup + opp
let res
if (total !== 0 && sup !== 0) {
res = (sup / total * 100).toFixed(2) + '%'
}
return <div className="box" >
<h5>支持:{sup}</h5>
<h5>反对:{opp}</h5>
<h5>比例:{res}</h5>
<Button type='primary' onClick={() => setSup(sup + 1)}>支持</Button>
<Button type='dashed' onClick={() => setOpp(opp + 1)}>反对</Button>
<Button type='link' onClick={() => setX(x + 1)}>其他</Button>
</div >
第一种方法使用useEffect
函数完成,同时设置依赖条件,监听两个值变化的时候才会执行操作。但是这么做存在一个问题,就是每次更新的时候都会重新渲染两次视图(点击按钮的时候更新一次视图,然后在useEffect
中又更新一次,存在浪费问题)
console.log('render');
let [sup, setSup] = useState(15)
let [opp, setOpp] = useState(5)
let [x, setX] = useState(0)
let [res, setRes] = useState(0)
useEffect(() => {
let total = sup + opp
if (total !== 0 && sup !== 0) {
setRes((sup / total * 100).toFixed(2))
}
}, [sup, opp])
第二种方法借助useMemo
函数完成,该函数的格式基本和useEffect
一致,函数会在初次挂载成功的时候执行一次回调同时依赖的对象更新的时候执行一次,会执行视图渲染操作。但是该函数具有缓存效果,如果依赖值没有更新,则不会去执行回调,会继续使用缓存中的值。即之前作用域中的值。在该函数中必须存在返回值。(类似vue的计算属性)
let res = useMemo(() => {
console.log('执行了');
let total = sup + opp
if (total !== 0 && sup !== 0) {
return (sup / total * 100).toFixed(2)
} else {
return 0
}
}, [sup, opp])
useCallback函数
函数组件在每次数据更新的时候,会重新调用,创建新的作用域。但是在大多数函数中,逻辑代码基本是相似的,因此不需要被重复创建。如下代码
let fun
const Demo7 = () => {
let [x, setX] = useState(0)
const handle = () => {
console.log('hello');
}
useEffect(() => {
if (!fun) {
fun = handle
} else {
console.log(fun === handle);
}
}, [x])
return <div className="box" >
<h5>支持:{x}</h5>
<Button type='link' onClick={() => setX(x + 1)}>其他</Button>
</div >
}
引入useCallback
函数使用。该函数也可以传入依赖项如下,代表初次挂载的时候会创建一次,后续更新的时候不再创建新的堆内存地址。
const handle = useCallback(() => {
console.log('hello');
}, [])
但是也不是每一个函数都使用useCallback
处理后会更好,使用不恰当可能会造成问题。
大部分使用场景如下:父组件嵌套子组件,父组件将内部方法传递给子组件使用,子组件使用useCallback
处理会更好。
- 如果希望父传子的属性是一个普通的函数,基本逻辑代码不会改变,而子组件视图基本不会改变的情况下,子组件不用更新,则需要满足条件。
- 首先是父组件中每次传递的函数必须是同一个地址
- 其次子组件每次对于父组件传递来的方法进行判断是否改变,如果没有变化就不进行更新操作。
//父组件
const Demo7 = () => {
let [x, setX] = useState(0)
const handle = useCallback(() => {
console.log('hello');
}, [])
return <div className="box" >
<Child handle={handle}></Child>
<Child2 handle={handle}></Child2>
<h5>支持:{x}</h5>
<Button type='link' onClick={() => setX(x + 1)}>其他</Button>
</div >
}
当子组件是类组件的时候,可以直接使用PureComponent
类实现传递的新老属性比较,从而达到更新判断操作。当传递来的函数是同一个地址的时候,经过PureComponent
底层默认的shouldComponentUpdate
钩子的操作,最终会阻止视图更新。
class Child extends React.PureComponent {
render() {
console.log("child1 render");
return <div>child1</div>
}
}
当子组件是函数组件的时候,传递接收的函数为同一个地址,但是如何判断两次函数是同一个的操作该如何实现,如何像类组件中使用PureComponent
中一样快速判断。React中提供了memo()
函数,memo
允许你的组件在 props 没有改变的情况下跳过重新渲染。
const Child2 = React.memo((props) => {
console.log("child2 render");
return <div>child1</div>
})
自定义hooks
封装一个自定义函数的普通命名都是以use
起头命名。在这里我们尝试封装一个hooks函数可也实现部分状态更新,因为在之前我们已经测试过了useState
无法支持部分状态更新,因此我们需要自定义实现。
//函数组件主体代码
let [state, setPartialState] = usePartial({
supNum: 10,
oppNum: 5
})
// 按钮点击的公共函数
function handle(type) {
if (type === '+') {
setPartialState({
supNum: state.supNum + 1
})
return
}
setPartialState({
oppNum: state.oppNum + 1
})
}
const usePartial = (initialValue) => {
// 基于react提供的函数封装
let [state, setState] = useState(initialValue)
// 实现部分状态更新操作
const setPartialState = (partialValue) => {
//设计更新视图操作,需要借助react
setState({
...state,
...partialValue
})
}
return [state, setPartialState]
}
大部分自定义hooks函数都是将复用的部分提出,带自定义hooks函数内部使用react提供的hooks完成某些操作。