参考文章:
juejin.cn/post/684490…
juejin.cn/post/684490…
juejin.cn/post/684490…
1. Hooks解决的问题
(1)class组件的不足
- 状态逻辑难复用:
在组件之间复用状态逻辑很难,可能要用到 render props (渲染属性)或者 HOC(高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余趋向复杂难以维护:
- 在生命周期函数中混杂不相干的逻辑:
在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其他的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug )
类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件
- this 指向问题:
父组件给子组件传递函数时,必须绑定 this
(2)hooks的优势
- 能优化类组件的三大问题
- 能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks )
- 能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
- 副作用的关注点分离:副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。而useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。
2. Hooks使用前提
- 只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用
- 只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用
3. useState
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useState 会返回一个数组:一个 state,一个用于更新 state 的函数
useState 唯一的参数就是初始 state,在初始化渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同,
注意事项:
- React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。因为react是根据每个useState定义时的顺序来确定你在更新State值时更新的是哪个state
- 你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并,而是直接替换
- initialState 参数只会在组件的初始化渲染中起作用,后续渲染时会被忽略
- Hook 内部使用 Object.is 来比较新/旧 state 是否相等,与 class 组件中的 setState 方法不同,如果你修改状态的时候,传的状态值没有变化,则不重新渲染
- useState不能直接存储函数或函数组件,他会调用该函数并且将函数的返回值作为最终state值进行存储或更新。如果必须这么做可以作为一个数组的元素或对象的某个属性进行存储
- useState没有设置类似class组件中的setState的回调函数来拿到最新state然后进行后续操作;可通过useEffect并设置相应依赖来实现。因为useEffect就是在渲染完成后调用的
useState在异步操作中的状态不同步问题:
函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面。当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包)。比如下面的一个例子:
import React, { useState } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
const onAlertButtonClick = () => {
setTimeout(() => {
alert("Value: " + counter);
}, 3000);
};
return (
<div>
<p>You clicked {counter} times.</p>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
<button onClick={onAlertButtonClick}>
Show me the value in 3 seconds
</button>
</div>
);
};
export default Counter;
当你点击Show me the value in 3 seconds的后,紧接着点击Click me使得counter的值从0变成1。三秒后,定时器触发,但alert出来的是0(旧值),但我们希望的结果是当前的状态1。
这个问题在class component不会出现,因为class component的属性和方法都存放在一个instance上,调用方式是:this.state.xxx和this.method()。因为每次都是从一个不变的instance上进行取值,所以不存在引用是旧的问题。
除了setTimout这种异步,还有类似事件监听函数(比如滚动监听的回调函数)中访问State也会是旧的
这个问题目前的普遍解决方案是使用useRef
(见下方)
4. useEffect
什么是React中的副作用操作?
副作用操作可以分两类:需要清除的(例如事件绑定/解绑)和不需要清除的。
原先在函数组件内(这里指在 React 渲染阶段)改变 dom 、发送 ajax 请求以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
一个需求实现:需要实时让document.title显示你最新的点击次数(coutnt)
class组件实现:
class Counter extends React.Component{
state = {number:0};
add = ()=>{
this.setState({number:this.state.number+1});
};
componentDidMount(){
this.changeTitle();
}
componentDidUpdate(){
this.changeTitle();
}
changeTitle = ()=>{
document.title = `你已经点击了${this.state.number}次`;
};
render(){
return (
<>
<p>{this.state.number}</p>
<button onClick={this.add}>+</button>
</>
)
}
}
因为需要实时让document.title显示你最新的点击次数(coutnt),所以就必须在componentDidMount 或 componentDidUpdate中编写重复的代码来重新设置document.title
hooks实现:
import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
const [number,setNumber] = useState(0);
// useEffect里面的这个函数会在第一次渲染之后和更新完成后执行
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
document.title = `你点击了${number}次`;
});
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
</>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'));
useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API
与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。
4.1 清除副作用
function Counter(){
let [number,setNumber] = useState(0);
let [text,setText] = useState('');
// 相当于componentDidMount 和 componentDidUpdate
useEffect(()=>{
console.log('开启一个新的定时器')
let $timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
// useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
// useEffect 在执行副作用函数之前,会先调用上一次返回的函数
// 如果要清除副作用,要么返回一个清除副作用的函数
return ()=>{
console.log('destroy effect');
clearInterval($timer);
}
});
// },[]);//要么在这里传入一个空的依赖项数组,这样就不会去重复执行
return (
<>
<input value={text} onChange={(event)=>setText(event.target.value)}/>
<p>{number}</p>
<button>+</button>
</>
)
}
useEffect 接收一个函数,该函数会在组件渲染到屏幕之后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容
默认情况下,useEffect在第一次渲染之后和每次更新之后都会执行。而useEffect 接收的函数参数所返回的清除副作用的函数则会在组件更新和卸载前执行,然后更新后执行useEffect 接收的函数。然后等待下一次组件更新或卸载,执行清除副作用的函数......如此循环往复
4.2 跳过 effect 进行性能优化
默认情况下,useEffect在每次更新之后都会执行
有时候,我们只想useEffect只在组件挂载时执行,然后卸载时执行清除副作用函数,不想更新时也执行useEffect(比如原生事件的绑定/解绑)或者只想让指定state发生更新时才执行useEffect(比如某些state更新后拿到最新state进行后续操作)
此时,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:
- 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
- 如果指定state发生更新时才执行useEffect,可以传递一个包含指定state元素的的数组作为第二个参数
function Counter(){
let [number,setNumber] = useState(0);
let [text,setText] = useState('');
// 相当于componentDidMount 和 componentDidUpdate
useEffect(()=>{
console.log('useEffect');
let $timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
},[text]);// 数组表示 effect 依赖的变量,只有当这个变量发生改变之后才会重新执行 efffect 函数
return (
<>
<input value={text} onChange={(event)=>setText(event.target.value)}/>
<p>{number}</p>
<button>+</button>
</>
)
}
4.3 使用多个useEffect
useEffect可以声明多个,React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
我们可以根据具体副作用操作的性质分类将不同种类的操作放到多个useEffect中
// Hooks 版
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
//dom操作相关的effect
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
//订阅/取消订阅的相关effect
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
4.4 useEffect 不能接收 async 作为回调函数
在useEffect中发起异步请求是很常见的场景,由于异步请求通常都是封装好的异步方法,所以新手很容易写成下面这样:
function App() {
const [data, setData] = useState({ hits: [] });
// 注意 async 的位置
// 这种写法,虽然可以运行,但是会发出警告
// 每个带有 async 修饰的函数都返回一个隐含的 promise
// 但是 useEffect 函数有要求:要么返回清除副作用函数,要么就不返回任何内容
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
更优雅的写法:
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
// 更优雅的方式
//将异步请求封成一个独立async函数然后在useEffect中调用
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
5. useReducer
useReducer 和 redux 中 reducer 很像。useState 内部就是靠 useReducer 来实现的
useReducer接收两个参数:reducer函数(含preState和action两个参数)和初始化的state。
useReducer返回值为一个数组,包含最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。
// 官方 useReducer Demo
// 第一个参数:应用的初始化
const initialState = {count: 0};
// 第二个参数:state的reducer处理函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
// 返回值:最新的state和dispatch函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
// useReducer会根据dispatch的action,返回最终的state,并触发rerender
Count: {state.count}
// dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
选择useReducer还是useState:
- 如果你的页面state很简单,可以直接使用useState
- 如果你的页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer
- 对于复杂的state操作逻辑(比如某项操作同时需要操作或更新多个state值),嵌套的state的对象,推荐使用useReducer
- 如果你的页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext
6. useContext
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext
或者 <MyContext.Consumer>
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
使用useContext和useReducer模拟实现简易Redux:
Provider组件:
import React, { useReducer } from 'react'
import './App.css'
import ComponentA from './components/ComponentA'
import ComponentB from './components/ComponentB'
import ComponentC from './components/ComponentC'
const initialState = 0
const reducer = (state, action) => {
switch (action) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
case 'reset':
return initialState
default:
return state
}
}
export const CountContext = React.createContext()
function App() {
const [count, dispatch] = useReducer(reducer, initialState)
return (
<CountContext.Provider
value={{ countState: count, countDispatch: dispatch }}
>
<div className="App">
{count}
<ComponentA />
<ComponentB />
<ComponentC />
</div>
</CountContext.Provider>
)
}
export default App
Component A:
//组件A
function ComponentA() {
const countContext = useContext(CountContext)
return (
<div>
Component A {countContext.countState}
<button onClick={() => countContext.countDispatch('increment')}>Increment</button>
<button onClick={() => countContext.countDispatch('decrement')}>Decrement</button>
<button onClick={() => countContext.countDispatch('reset')}>Reset</button>
</div>
)
}
Component B:
function ComponentB() {
const countContext = useContext(CountContext)
return (
<div>
Component B {countContext.countState}
<button onClick={() => countContext.countDispatch('increment')}>Increment</button>
<button onClick={() => countContext.countDispatch('decrement')}>Decrement</button>
<button onClick={() => countContext.countDispatch('reset')}>Reset</button>
</div>
)
}
7. useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变
useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个。
但使用 React.createRef ,每次重新渲染组件都会重新创建 ref
示例:用useRef存储dom节点
function Child() {
const inputRef = useRef();
console.log('input===inputRef', input === inputRef);
input = inputRef;
function getFocus() {
inputRef.current.focus();
}
return (
<>
<input type="text" ref={inputRef} />
<button onClick={getFocus}>获得焦点</button>
</>
)
}
用useRef解决useState异步更新不同步的问题:
针对上面useState谈到的异步更新不同步的问题,用useRef返回的immutable RefObject(把值保存在current属性上)来保存state,你可以把useRef存储的值看成class组件实例中通过this存储的属性
。然后取值方式从counter变成了: counterRef.current。如下:
import React, { useState, useRef, useEffect } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
const counterRef = useRef(counter);
const onAlertButtonClick = () => {
setTimeout(() => {
alert("Value: " + counterRef.current);
}, 3000);
};
useEffect(() => {
counterRef.current = counter;
});
return (
<div>
<p>You clicked {counter} times.</p>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
<button onClick={onAlertButtonClick}>
Show me the value in 3 seconds
</button>
</div>
);
};
export default Counter;
React.forwardRef
在useRef出来之前,由于函数组件是没有实例的,所以函数组件无法使用ref属性来获取dom引用,而对应的解决方法就是React.forwardRef:
TextInput函数组件:
const TextInput = forwardRef((props,ref) => {
//设置input标签node节点作为TextInput组件的ref引用
return <input ref={ref}></input>
})
function TextInputWithFocusButton() {
// 关键代码
const inputEl = useRef(null);
const onButtonClick = () => {
// 关键代码,`current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
// 用useRef存储TextInput设置的ref引用
<TextInput ref={inputEl}></TextInput>
<button onClick={onButtonClick}>Focus the input</button>
</>
);
上面例子说明forwardRef和useRef配合 可以在父组件中操作子组件的 ref 对象
8. useCallback
useCallback缓存一个函数,这个函数如果是由父组件作为props传递给子组件,或者自定义hooks里面的函数【通常自定义hooks里面的函数不会依赖于引用它的组件里面的数据】,这时候我们可以考虑缓存这个函数,好处:
- 不用每次重新声明新的函数,避免释放内存、分配内存的计算资源浪费
- 子组件不会因为这个函数的变动重新渲染。【和React.memo搭配使用】
function Example(){
const [count, setCount]= useState(1);
const getNum = useCallback(() => {
return (555 * 666666 )+count
//只有count值改变时,才会重新计算
},[count])
const Child = React.memo(({getNum}) =>{
return <h4>总和{getNum()}</h4>
})
return <div>
<Child getNum={getNum}/>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
}
上面例子,将一个函数交给useCallBack处理并且作为props传递给memo包裹的子组件并子组件调用该方法,定义只有当coutn变化时才会触发子组件重新渲染
因为通过useCallBack包裹后的函数通过props传递给子组件的永远是该函数的引用
9. useMemo
useMemo 主要用于渲染过程优化,两个参数依次是计算函数(通常是组件函数)和依赖状态列表,当依赖的状态发生改变时,才会触发计算函数的执行。如果没有指定依赖,则每一次渲染过程都会执行该计算函数。
useMemo的返回值就是计算函数的返回值
function Example(){
const [count, setCount]= useState(1);
const getNum = useMemo(() => {
return (555 * 666666 )+count
//只有count值改变时,才会重新计算
},[count])
return <div>
<h4>总和:{getNum}</h4>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
}
export default Example;
上面例子只有当count变化时才会触发getNum函数的重新计算和渲染;如果不使用useMemo则任何一个state发生变化都会导致组件重新渲染进而导致getNum重新计算,耗费性能
注意:
useMemo和useCallback的区别 useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据
共同作用:
- 仅仅 依赖数据 发生变化,才会重新计算结果,也就是起到缓存的作用。
两者区别:
- useMemo 计算结果是计算函数返回来的值,主要用于 缓存计算结果的值,应用场景如: 需要计算的状态
- useCallback计算结果是计算函数,主要用于 缓存函数,应用场景如:需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
10. 自定义Hook
自定义 Hook 必须以 use 开头吗?
(1)useDidMount
import { useEffect } from 'react';
const useDidMount = fn => useEffect(() => fn && fn(), []);
export default useDidMount;
(2)useWillUnmount
useEffect 时已经提及过,其允许返回一个 清除副作用的 函数,当依赖项为[]时,其相当于componentWillUnMount
import { useEffect } from 'react';
const useWillUnmount = fn => useEffect(() => () => fn && fn(), []);
export default useWillUnmount;
(3)实现类似class组件可支持回调的setState方法
class组件更新状态时,setState可以通过第二个参数拿到更新完毕后的回调函数。很遗憾,虽然hooks函数的useState第二个参数回调支持类似class组件的setState的第一个参数的用法(通过传入一个函数并将函数的返回值作为新的state进行更新),但不支持第二个参数回调,但是很多业务场景中我们又希望hooks组件能支持更新后的回调这一方法。
借助useRef和useEffect配合useState来实现这一功能:
const useXState = (initState) => {
const [state, setState] = useState(initState)
//表示有state值更新了
let isUpdate = useRef()
const setXState = (state, cb) => {
//这里setState是使用了函数参数的方式更新useState的值,而不是直接更新成指定的参数值
setState(prev => {
isUpdate.current = cb
return typeof state === 'function' ? state(prev) : state
})
}
useEffect(() => {
if(isUpdate.current) {
//存在更新state,则执行回调
isUpdate.current()
}
})
return [state, setXState]
}
export default useXState
说明:
利用useRef的特性来作为标识区分是挂载还是更新,当执行setXstate时,会传入和setState一模一样的参数,并且将回调赋值给useRef的current属性,这样在更新完成时,我们手动调用current即可实现更新后的回调这一功能
11. Hooks vs Render Props vs HOC
没有 Hooks 之前,高阶组件和 Render Props 本质上都是将复用逻辑提升到父组件中。而 Hooks 出现之后,我们将复用逻辑提取到组件顶层,而不是强行提升到父组件中。这样就能够避免 HOC 和 Render Props 带来的「嵌套地狱」。但是,像 Context 的 和 这样有父子层级关系(树状结构关系)的,还是只能使用 Render Props 或者 HOC。
对于 Hooks、Render Props 和高阶组件来说,它们都有各自的使用场景:
Hooks:
Render Props:
高阶组件:
不过,能使用 Hooks 的场景还是应该优先使用 Hooks,其次才是 Render Props 和 HOC。当然,Hooks、Render Props 和 HOC 不是对立的关系。我们既可以用 Hook 来写 Render Props 和 HOC,也可以在 HOC 中使用 Render Props 和 Hooks。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!