React Hooks
在个人上一个项目中获得了充分的使用,对于这个项目来讲,咱们跳过传统的类组件直接过渡到函数组件,确实是一个不小的挑战。在项目开发过程当中也发现项目中的其余小伙伴(包括我本身)有时候会存在使用不当的状况,所以对官方的几个钩子函数作一个较为全面的总结。vue
为何会出现函数式组件,由于传统的类组件确实有很多缺点:react
this
指向有点绕mixin
,很容易带来数据来源指向不清楚的问题咱们知道,在过去,函数式组件被称做“傻瓜组件”,由于它并不具备自身的状态,一般被用来作一些渲染视图的工做,即UI = render(props)
。这是一个纯粹的输入输出模型,无任何反作用。可是React Hooks
的出现,让函数式组件拥有自身的状态成为了可能。web
函数式组件在运行过程当中会被调用不少次,假如咱们将状态保存在函数体里面,毫无疑问是不可行的。由于函数是一种“用完即销毁”的东西。api
这正是是Hooks
所作的事情:将一个函数组件的状态保存在函数外面。准确来讲,是这个函数组件对应的Hooks
链表。当函数式组件须要用到该状态的时候,经过Hooks
这一钩子将状态从函数体外部“钩进来”。数组
函数式组件的生命周期能够分为如下三部分:async
初次渲染(first-render
) ---> 重渲染 (re-render
) ---> 销毁(destroy
)编辑器
当咱们第一次使用函数式组件的时候,会触发初次渲染(first-render
);若其 props 改变,就会调用该 render 函数,触发重渲染(re-render
)。函数
每一次的渲染,都是独立的。这正是函数式组件的美妙之处。性能
那么react如何决定要不要调用 render
函数来更新 UI 视图呢?这取决于data
有没有更新。从整个组件树来看,data
指的是整个组件的state
;从具体到某个功能组件来看,data
也能够被认为是props
和自身state
的结合体。fetch
render
的执行取决于 data
变化,而 data
中的 state
数据是保存在链表中的。
链表的特性是啥?就是每一个元素都有一个
next
指针指向下一个元素,一环扣一环关联起来。因此为何 hooks 不能用在条件判断/循环/嵌套中,由于这些都不能保证每次渲染时读取 hooks 链表的顺序是彻底一致的。尤为对于状态读取来讲,读取顺序和初次渲染链表记录的顺序不一致,会直接致使一些useState
钩子读取到错误的状态值。
const [count, setCount] = useState(0);
首先,useState
会生成一个状态和修改状态的函数。这个状态会保存在函数式组件外面,每次重渲染时,这一次渲染都会去外面把这个状态钩回来,读取成常量并写进该次渲染中。
经过调用修改状态的函数,会触发重渲染。到这里咱们总结:props
的改变和 setState
的调用,都会触发 re-render
。
因为每次渲染都是独立的,因此每次渲染都会读到一个独立的状态值,这个状态值,就是经过钩子钩到的 state
并读取到的常量。
这就是所谓的capture value
特性,每次的渲染都是独立的,每次渲染的状态其实都只是常量罢了。
让咱们看深刻一下本质,看看 useState
和 re-render
到底如何关联起来:
useState
依次执行,生成hooks链表,里面记录了每一个 state
的初始值和对应的 setter
函数useState
或相应 setter
访问setSetter
,将会直接改变这个hooks链表useState
时,由于初次执行已经挂载过一个 hooks 链表了,这个时候就会直接读取链表的相应值这也就是为何叫useState
,而不是createState
。
useRef主要有两个做用:
咱们先来看看前者怎么用吧:
const inputRef = useRef(null); const handleClick = () => { inputRef.current?.focus(); } return ( <input ref={inputRef} /> <button onClick={handleClick}>点击</button> )
这样就能够方便地访问DOM节点。
前面咱们提到,useState
能够方便地保存状态值,可是因为函数式组件的capture value
特性,使得咱们并不能以一种比较方便的形式获取到更改后的状态值。
const [num, setNum] = useState(0);
const increaseNum = () => {
setNum(prev => prev + 1);
console.log(num); // 打印的仍然是旧值,由于num在这一帧被常量化了
}
而useRef
将会建立一个ref
对象,并把这个ref
对象保存在函数式组件外部,这样的好处在于:
capture value
以外存储,不用担忧得到过期变量的问题;咱们试验以下:
const numRef = useRef(0);
const increaseNum = () => {
numRef.current += 1;
console.log(numRef.current); // 能获取最新值
}
可是要注意⚠️:因为引用没变,上述操做并不会引发函数式组件的重渲染。 这是一个很容易引发错误的地方!
useEffect
的模型十分之简洁,以下:
useEffect(effectFn, deps);
useEffect 能够模拟旧时代的三个生命周期:componentDidMount
、shouldComponentUpdate
、componentWillUnmount
,至关于三个生命周期合并为一个 api。
所谓shouldComponentUpdate
,其实就是去除deps
依赖数组,如此一来这个反作用的 effectFn
会在首次渲染以后和每次重渲染以后执行,至关于模拟了 shouldComponentUpdate
这一辈子命周期,以下:
useEffect(() => {
// xxx
});
而所谓componentDidMount
,则是传入一个空数组做为依赖,由于当有 deps
数组时,里面 effectFn
是否执行取决于 deps
数组内的数据是否变化,空数组內无数据,因此对比天然也就无变化,使用以下:
useEffect(() => {
// xxx
}, []);
而componentWillUnmount
,则是在effectFn
中返回一个清除函数,以下:
useEffect(() => {
// 执行反作用
// ...
return () => {
// 清除上面的反作用
// ...
};
}, []);
此外咱们应该始终遵循一个原则:那就是不要对 deps 依赖撒谎。不然会引起一系列 bug。固然编辑器的 linter 也不会容许咱们这样作,这一点很是关键。
effectFn
就是当依赖变化时执行的反作用函数,这里的反作用,并非一个贬义词,而是一个中性词。
函数内部与外部发生的任何交互都算反作用,好比打印个日志、开启一个定时器,发一个请求,读取全局变量等等等等。
好,如今这个 effectFn
能够返回一个清理函数cleanUp
,用于清除这个反作用。典型的清理函数,如:clearInterval
、clearTimeout
,如:
useEffect(() => {
const timer = setTimeout(() => console.log("over"), 1000);
return () => clearTimout(timer);
});
useEffect
实际上是每次渲染完成后都会执行,可是 effectFn
是否执行,就要看依赖有没有变化了。执行 useEffect
的时候,会拿此次渲染的依赖跟上次渲染的对应依赖对比,若是没变化,就不执行 effectFn
,若是有变化,才执行 effectFn
。
若是连依赖都没有,那 react 就认为每次都有变化,每次运行 useEffect
必运行 effectFn
。
useEffect
有典型的三大特色:
effectFn
以前,要把前一次运行 effectFn
遗留的cleanUp
函数执行掉(若是有的话)effectFn
遗留的 cleanUp
函数执行掉。deps 数组里面的各个依赖与上次的依赖是否相同,须要经过Object.is
来比较,好比:
Object.is(22, 22); // true
Object.is([], []); // false
这样就会有一个隐患,当 deps
数组里面的子元素为引用类型的时候,每次对比都会是false
,从而执行effectFn
。由于 Object.is
对比引用类型的时候,比较的是两个指针是否指向堆内存中的同一个地址。
useEffect
的执行机制,是在初次渲染时,执行到 useEffect
就将内部的 effectFn
放到两个地方:一个是 hooks
链表中,另一个则是EffectList
队列中。在渲染完成后,会依次执行 EffectList
里面的 effectFn
集合。
因此,说白了,要不要 re-render
,彻底取决于链表里面的东西有没有变化。
不一样于 vue 里面有async mounted
,在 useEffect
里面的 effectFn
,应该始终坚持一个原则:要么不返回,要么返回一个 cleanUp 清除函数。像下面这样写是不行的:
// 错误的用法❌
useEffect(async () => {
const response = await fetch("...");
// ...
});
另外咱们很容易发现:咱们并不须要把 useState
返回的第二个 Setter
函数做为useEffect
的依赖。实际上,React 内部已经对 Setter
函数作了 Memoization
处理,所以每次渲染拿到的 Setter
函数都是彻底同样的,不须要把这个Setter
函数放到deps
数组里面。