(1)class组件的不足
在组件之间复用状态逻辑很难,例如同一个生命周期可能会掺杂多种不相关的业务逻辑或者同一种业务逻辑会分配到不同的生命周期了。虽然可以通过 render props (渲染属性)或者 HOC(高阶组件)方案来优化此种问题,但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余趋向复杂难以维护:
在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其他的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug )
类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件
父组件给子组件传递函数时,必须绑定 this
(2)hooks的优势
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) 值相同,
注意事项:
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
(见下方)
什么是React中的副作用操作?
指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。
副作用操作可以分两类:需要清除的(例如事件绑定/解绑)和不需要清除的。
原先在函数组件内(这里指在 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 相同。
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 接收的函数。然后等待下一次组件更新或卸载,执行清除副作用的函数…如此循环往复
默认情况下,useEffect在每次更新之后都会执行
有时候,我们只想useEffect只在组件挂载时执行,然后卸载时执行清除副作用函数,不想更新时也执行useEffect(比如原生事件的绑定/解绑)或者只想让指定state发生更新时才执行useEffect(比如某些state更新后拿到最新state进行后续操作)
此时,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:
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>
</>
)
}
注意:无论你是否指定了useEffct的第二个参数,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);
};
});
// ...
}
在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>
);
}
与useEffect的区别
使用场景:
大部分情况下使用useEffect即可。针对小部分特殊情况如短时间内触发了多次状态更新导致渲染多次以致屏幕闪烁的情况,使用useLayoutEffect会在浏览器渲染之前同步更新
DOM 数据,哪怕是多次的操作,也会在渲染前一次性处理完,再交给浏览器绘制。这样不会导致闪屏现象发生。
useReducer 和 redux 中 reducer 很像。useState 内部就是靠 useReducer 来实现的
useReducer接收两个参数:reducer函数(含preState和action两个参数)和初始化的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:
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的
当组件上层最近的
useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext
或者
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>
)
}
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变
useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个。
也就是说,useRef的更新不会引起当前组件或子组件的更新渲染
但使用 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 对象
useCallback缓存一个函数,这个函数如果是由父组件作为props传递给子组件,或者自定义hooks里面的函数【通常自定义hooks里面的函数不会依赖于引用它的组件里面的数据】,这时候我们可以考虑缓存这个函数,好处:
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传递给子组件的永远是该函数的引用
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的返回值是作为props传给子组件的,当该值的类型是原始值时(string,number等),不需要使用useMemo。因为只要该值不变,react本身是不会重新渲染函数子组件的。如果是引用类型值,则可以使用useMemo,因为即使它的内容不变,但是引用会变。所以用useMemo来缓存它的引用
useMemo和useCallback的区别
useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据
共同作用:
两者区别:
自定义 Hook 必须以 use 开头吗?
必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
(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即可实现更新后的回调这一功能
没有 Hooks 之前,高阶组件和 Render Props 本质上都是将复用逻辑提升到父组件中。而 Hooks 出现之后,我们将复用逻辑提取到组件顶层,而不是强行提升到父组件中。这样就能够避免 HOC 和 Render Props 带来的「嵌套地狱」。但是,像 Context 的 和 这样有父子层级关系(树状结构关系)的,还是只能使用 Render Props 或者 HOC。
对于 Hooks、Render Props 和高阶组件来说,它们都有各自的使用场景:
Hooks:
替代 Class 的大部分用例,除了 getSnapshotBeforeUpdate 和 componentDidCatch 还不支持。
可提取复用逻辑。除了有明确父子关系的,其他场景都可以使用 Hooks。
Render Props:
在组件渲染上拥有更高的自由度,可以根据父组件提供的数据进行动态渲染。适合有明确父子关系的场景。
高阶组件:
适合用来做注入,并且生成一个新的可复用组件。适合用来写插件。
不过,能使用 Hooks 的场景还是应该优先使用 Hooks,其次才是 Render Props 和 HOC。当然,Hooks、Render Props 和 HOC 不是对立的关系。我们既可以用 Hook 来写 Render Props 和 HOC,也可以在 HOC 中使用 Render Props 和 Hooks。
参考文章:
https://juejin.cn/post/6844903985338400782
https://juejin.cn/post/6844903958968795149
https://juejin.cn/post/6844904074433789959