1. hooks的定义
“hooks”直译是“钩子”,它并不仅是React
,甚至不仅是前端界的专业术语,而是整个行业所熟知的用语。通常指:
系统运行到某一时期时,会触发被注册到该十几的回调函数
比较常见的钩子有: windows
系统的钩子能够监听到系统的各种事件,浏览器提供的onload
或addEventListener
能注册在浏览器各种时机被调用的方法。
在reaact@16.x
之前,当我们谈论hooks
时,我们谈论的可能是“组件的生命周期”
现在以react
为例, hooks
是:
一系列以
“use”
作为开头的方法,他们提供了让你可以完全避免class式的写法
,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。
2. 为什么我们需要hooks
2.1 Mixins 的弊端
mixin
虽然解决了状态复用的能力, 但是弊端比较多
当mixins 嵌套的比较多,mixins 嵌套的东西比较多时, 难以追溯的方法与属性
- mixins 之间覆盖,同名。导致复用功能失败。
2.2 hooks 的优势
相比于 mixins
,它们简直太棒了!
-
方法和属性好追溯吗?这可太好了,谁产生的,哪儿来的一目了然。
-
会有重名、覆盖问题吗?完全没有!内部的变量在闭包内,返回的变量支持定义别名。
-
多次使用,没开N度?你看上面的代码块内不就“梅开三度” 了吗?
-
相比较于class 组件, 没有this指向的烦恼。
2.3 react 组件复用的手法
react16.x
之前渲染属性(Render Props)和高阶组件(Higher-Order Components)。
react 16.x
之后 hooks
渲染属性指的是使用一个值为函数的prop来传递需要动态渲染的nodes或组件。如下面的代码可以看到我们的DataProvider
组件包含了所有跟状态相关的代码,而Cat
组件则可以是一个单纯的展示型组件,这样一来DataProvider
就可以单独复用了。
import Cat from 'components/cat'
class DataProvider extends React.Component {
constructor(props) {
super(props);
this.state = { target: 'Zac' };
}
render() {
return (
<div>
{this.props.render(this.state)}
</div>
)
}
}
<DataProvider render={data => (
<Cat target={data.target} />
)}/>
高阶组件这个概念就更好理解了,说白了就是一个函数接受一个组件作为参数,经过一系列加工后,最后返回一个新的组件。看下面的代码示例,withUser
函数就是一个高阶组件,它返回了一个新的组件,这个组件具有了它提供的获取用户信息的功能。
const withUser = WrappedComponent => {
const user = sessionStorage.getItem("user");
return props => <WrappedComponent user={user} {...props} />;
};
const UserPage = props => (
<div class="user-container">
<p>My name is {props.user}!</p>
</div>
);
export default withUser(UserPage);
3.基本的hooks使用
1. useState()
let showFruit = true;
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
if(showFruit) {
const [fruit, setFruit] = useState('banana');
showFruit = false;
}
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
复制代码
这样一来,
//第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
//第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
// useState('banana');
useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错
react是怎么保证多个useState的相互独立的?
react是根据useState出现的顺序来定的。 鉴于此,react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。
在同一个事件中并不会因为调用了两次setCount
而让count
增加两次,试想如果在同一个事件中每次调用setCount
都生效,那么每调用一次setCount
组件就会重新渲染一次,这无疑使非常影响性能的;实际上如果修改的state
是同一个,最后一个setCount
函数中的新state
会覆盖前面的
2. useEffect
通过使用useEffect 解绑一些副作用(比如组件创建一个定时器,组件销毁是清空)
useEffect(() => {
let ter = setInterval(() => {}, 3000)
return () => {
clearInterval(ter)
}
})
3. useEffect && useLayoutEffect
- 使用
这两个hook
用法一致,第一个参数是回调函数,第二个参数是数组,数组的内容是依赖项deps
,依赖项改变后执行回调函数;注意组件每次渲染会默认执行一次,如果不传第二个参数只要该组件有state
改变就会触发回调函数,如果传一个空数组,只会在初始化执行一次。另外,如果用return
返回了一个函数,组件每次重新渲染的时候都会先执行该函数再调用回调函数。
- 区别
表面上看,这两个hook
的区别是执行时机不同,useEffect
的回调函数会在页面渲染后执行;useLayoutEffect
会在页面渲染前执行。实际上是React
对这两个hook
的处理不同,useEffect
是异步调用,而useLayoutEffect
是同步调用。 那什么时候用useEffect
,什么时候用useLayoutEffect
呢? 我的理解是视情况而定 如果回调函数会修改state
导致组件重新渲染,可以useLayoutEffect
,因为这时候用useEffect
可能会造成页面闪烁; 如果回调函数中去请求数据或者js执行时间过长,建议使用useEffect
;因为这时候用useLayoutEffect
堵塞浏览器渲染。
4. useMemo && useCallback
这两个hook
可用于性能优化,减少组件的重复渲染;现在就来看看这两个神奇的hook
怎么用。
- uesMemo
function MemoDemo() {
let [count, setCount] = useState(0);
let [render,setRender] = useState(false)
const handleAdd = () => {
setCount(count + 1);
};
const Childone = () => {
console.log("子组件一被重新渲染");
return <p>子组件一</p>;
};
const Childtwo = (props) => {
return (
<div>
<p>子组件二</p>
<p>count的值为:{props.count}</p>
</div>
);
};
const handleRender = ()=>{
setRender(true)
}
return (
<div style={{display:"flex",justifyContent:'center',alignItems:'center',height:'100vh',flexDirection:'column'}}>
{
useMemo(() => {
return <Childone />
}, [render])
}
<Childtwo count={count} />
<button onClick={handleAdd}>增加</button>
<br/>
<button onClick={handleRender} >子组件一渲染</button>
</div>
);
}
复制代码
Childone
组件只有render
改变才会重新渲染
这里顺带讲下,React.memo
,用React.memo
包裹的组件每次渲染时会和props
会和旧的props
进行浅比较,如果没有变化则组件不渲染;示例如下
const Childone = React.memo((props) => {
console.log("子组件一被重新渲染",props);
return <p>子组件一{props.num}</p>;
})
function MemoDemo() {
let [count, setCount] = useState(0);
let [render,setRender] = useState(false)
let [num,setNum] = useState(2)
const handleAdd = () => {
setCount(count + 1);
};
const Childtwo = (props) => {
return (
<div>
<p>子组件二</p>
<p>count的值为:{props.count}</p>
</div>
);
};
const handleRender = ()=>{
setRender(true)
}
return (
<div style={{display:"flex",justifyContent:'center',alignItems:'center',height:'100vh',flexDirection:'column'}}>
{/* {
useMemo(() => {
return <Childone />
}, [render])
} */}
<Childone num={num}/>
<Childtwo count={count} />
<button onClick={handleAdd}>增加</button>
<br/>
<button onClick={handleRender} >子组件一渲染</button>
</div>
);
}
复制代码
这个例子是把上个例子中的Childone
拆出来套上React.memo
的结果,点击增加后组件不会该组件不会重复渲染,因为num
没有变化
- useCallback
还是上面那个例子,我们把handleRender
用useCallback
包裹,也就是说这里num
不变化每次都会传同一个函数,若是这里不用useCallback
包裹,每次都会生成新的handleRender
,导致React.memo
函数中的props
浅比较后发现生成了新的函数,触发渲染
const Childone = React.memo((props) => {
console.log("子组件一被重新渲染",props);
return <p>子组件一{props.num}</p>;
})
export default function MemoDemo() {
let [count, setCount] = useState(0);
let [render,setRender] = useState(false)
let [num,setNum] = useState(2)
const handleAdd = () => {
setCount(count + 1);
};
const Childtwo = (props) => {
return (
<div>
<p>子组件二</p>
<p>count的值为:{props.count}</p>
</div>
);
};
const handleRender = useCallback(()=>{
setRender(true)
},[num])
return (
<div style={{display:"flex",justifyContent:'center',alignItems:'center',height:'100vh',flexDirection:'column'}}>
{/* {
useMemo(() => {
return <Childone />
}, [render])
} */}
<Childone num={num} onClick={handleRender}/>
<Childtwo count={count} />
<button onClick={handleAdd}>增加</button>
<br/>
<button onClick={handleRender} >子组件一渲染</button>
</div>
);
}
复制代码
5. useRef
1. useRef 的基本使用
- 返回一个可变的 ref 对象,该对象只有个 current 属性,初始值为传入的参数( initialValue )。
- 返回的 ref 对象在组件的整个生命周期内保持不变
- 当更新 current 值时并不会 re-render ,这是与 useState 不同的地方
- 更新 useRef 是 side effect (副作用),所以一般写在 useEffect 或 event handler 里
- useRef 类似于类组件的 this
这个hook
通常用来获取组件实例,还可以用来缓存数据❗ 获取实例就不过多解释了,需要注意的是只有类组件才有实例; 重点看下useRef
如何缓存数据的:
function UseRef() {
let [data, setData] = useState(0)
let initData = {
name: 'lisa',
age: '20'
}
let refData = useRef(initData) //refData声明后组件再次渲染不会再重新赋初始值
console.log(refData.current);
refData.current = { //修改refData后页面不会重新渲染
name: 'liyang ',
age: '18'
}
const reRender = () => {
setData(data + 1)
}
return (
<div>
<button onClick={reRender}>点击重新渲染组件</button>
</div>
)
}
复制代码
组件重新渲染后,变量会被重新赋值,可以用useRef
缓存数据,这个数据改变后是不会触发组件重新渲染的,如果用useState
保存数据,数据改变后会导致组件重新渲染,所以我们想悄悄保存数据,useRef
是不二选择👊
2. 为什么要使用useRef
跨渲染取值
import React, { useState } from "react";
const LikeButton: React.FC = () => {
const [like, setLike] = useState(0)
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like}`)
//形成闭包,所以弹出来的是当时触发函数时的like值
}, 3000)
}
return (
<>
<button onClick={() => setLike(like + 1)}>{like}赞</button>
<button onClick={handleAlertClick}>Alert</button>
</>
)
}
export default LikeButton
现象: 在like为6的时候, 点击 alert , 再继续增加like到10, 弹出的值为 6, 而非 10. (形成了不同渲染结果不同步的情况)
使用useRef
import React, { useRef } from "react";
const LikeButton: React.FC = () => {
// 定义一个实例变量
let like = useRef(0);
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like.current}`);
}, 3000);
}
return (
<>
<button
onClick={() => {
like.current = like.current + 1;
}}
>
{like.current}赞
</button>
<button onClick={handleAlertClick}>Alert</button>
</>
);
};
export default LikeButton;
现象: 在like为6的时候, 点击 alert , 再继续增加like到10, 弹出的值为 10.
总结 采用useRef, 作为组件实例的变量, 保证获取到的数据肯定是最新的
3. createRef() 与 useRef()的不同
createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用
4. useImperativeHandle
使用useImperativeHandle用于定义暴露给父组件的ref方法
import React, {
MutableRefObject,
useState,
useImperativeHandle,
useRef,
forwardRef,
useCallback
} from 'react'
interface IProps {
label: string
}
let ChildInput = forwardRef((props: IProps, ref: any) => {
const { label } = props
const [value, setValue] = useState('')
// 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
// 参数1: 父组件传递的ref属性
// 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
useImperativeHandle(ref, () => ({
getValue
}))
const handleChange = (e: any) => {
const value = e.target.value
setValue(value)
}
const getValue = useCallback(() => {
return value
}, [value])
return (
<div>
<span>{label}:</span>
<input type="text" value={value} onChange={handleChange} />
</div>
)
})
const ParentCom: React.FC = (props: any) => {
const childRef: MutableRefObject<any> = useRef({})
const handleFocus = () => {
const node = childRef.current
alert(node.getValue())
}
return (
<div>
<ChildInput label={'名称'} ref={childRef} />
<button onClick={handleFocus}>focus</button>
</div>
)
}
export default ParentCom
4. 自定义hook
自定义hook
,也就是hook
的封装,至于为什么要封装hook
呢?react
官网给出了答案
使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。 通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
先来看下这个例子:
export default function CustomHook() {
let refone = useRef(null)
let X, Y, isMove = false,left,top
//基于鼠标事件实现拖拽
useEffect(() => {
const dom = refone.current
dom.onmousedown = function (e) {
isMove = true
X = e.clientX - dom.offsetLeft;
Y = e.clientY - dom.offsetTop;
}
dom.onmousemove = function (e) {
if (isMove) {
left = e.clientX - X
top = e.clientY - Y
dom.style.top = top + "px"
dom.style.left = left + "px"
}
}
dom.onmouseup = function (e) {
isMove = false
}
}, [])
return (
<div style={{ display: "flex", justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div ref={refone} style={{ width: '70px', height: '70px', backgroundColor: '#2C6CF9',position:'absolute' }}></div>
</div>
)
}
复制代码
我们利用鼠标事件简单的实现了一个拖拽方格的效果,那如果在其他页面也需要这个效果呢?😏于是,我们可以考虑把这段相同的逻辑封装起来,就像我们提取公共组件一般。来看下面这个例子:
import {useEffect, useRef } from "react";
function useDrop() {
let refone = useRef(null)
let X, Y, isMove = false,left,top
//基于鼠标事件实现拖拽
useEffect(() => {
const dom = refone.current
dom.onmousedown = function (e) {
isMove = true
X = e.clientX - dom.offsetLeft;
Y = e.clientY - dom.offsetTop;
}
dom.onmousemove = function (e) {
if (isMove) {
left = e.clientX - X
top = e.clientY - Y
dom.style.top = top + "px"
dom.style.left = left + "px"
}
}
dom.onmouseup = function (e) {
isMove = false
}
}, [])
return refone
}
export default function CustomHook() {
let refone = useDrop()
let reftwo = useDrop()
return (
<div style={{ display: "flex", justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div ref={refone} style={{ width: '70px', height: '70px', backgroundColor: '#2C6CF9',position:'absolute' }}></div>
<div ref={reftwo} style={{ width: '70px', height: '70px', backgroundColor: 'red',position:'absolute' }}></div>
</div>
)
}
复制代码
这里为来减少代码量只展示了在同一个页面使用封装过的hook
,事实上封装这段hook
我们只改了几行代码,却实现了逻辑的重用是不是很神奇!😆需要注意的是hook
的封装函数必须要以use
开头,因为使用hook
本身是有规则的,比如不能在条件语句中使用hook
,不能在函数外使用hook
;如果不适用use
开头封装hook
,则react
无法自动检查该函数内使用的hook
是否符合规则。