类组件 与 函数组件 的区别
- 1. 类组件
- 2. 函数组件
- Hook
- useState
- useEffect
- useCallback
- useMemo
- useContext
- useRef
- 3. 函数组件与类组件的区别
- 3.1 表面差异
- 3.2 最大不同
- 原因
1. 类组件
在React中,类组件就是基于ES6语法,通过继承 React.component
得到的组件。
class Demo extends React.Component {
// 初始化类组件的 state
state = {
text: "111"
};
// 编写生命周期方法 didMount
componentDidMount() {
// 省略业务逻辑
}
// 编写自定义的实例方法
changeText = (newText) => {
// 更新 state
this.setState({
text: newText
});
};
// 编写生命周期方法 render
render() {
return (
<div className="demoClass">
<p>{this.state.text}</p>
<button onClick={() => this.changeText(222)}>点我修改</button>
</div>
);
}
}
2. 函数组件
在函数组件也称为无状态组件,顾名思义就是以函数形态存在的 React 组件。
在
hooks
出现之前,react 中的函数组件通常只考虑负责UI的渲染,没有自身的状态,没有业务逻辑代码,是一个纯函数。
下面这个函数组件就是一个纯函数,它的输出只由参数props决定,不受其他任何因素影响。
function DemoFunction(props) {
const { text } = props
return (
<div className="demoFunction">
<p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
</div>
);
}
但是这种函数组件一旦我们需要给组件加状态,那就只能将组件重写为类组件,因为函数组件没有实例,没有生命周期。
所以我们说在 hook
之前的 函数组件和类组件最大的区别就是 状态的有无。
Hook
React Hooks
是从 React 16.8 版本推出的新特性,目的是解决 React 的状态共享以及组件生命周期管理混乱的问题。 React Hooks 的出现标志着,React 不会再存在无状态组件的情况,React 将只有类组件和函数组件的概念。
hooks
为函数组件提供了状态,也支持在函数组件中进行数据获取、订阅事件解绑事件等等。
下面先介绍几个最基本的hook作为基础知识。
useState
const [count,setCount] = useState(0);
通过 useState 为组件提供状态。useState 的参数是 state 的初始值,他只有在组件第一次渲染的时候会生效,他的返回值是一个数组,第一个是 state,第二个是设置state的函数。
useEffect
//1. 不设置依赖项数组,那么在组件挂载时和每次渲染时,都会执行一次副作用
useEffect(() => {
console.log("useEffect副作用执行");
//用setTimeout模拟ajax请求
setTimeout(() => {
setList(result);
}, 3000);
});
//2. 设置依赖项为空数组,那么只有在组件挂载时执行副作用
useEffect(() => {
console.log("useEffect副作用执行");
setTimeout(() => {
setList(result);
}, 3000);
},[]);
//3. 如果设置依赖项且不为空,那么在组件挂载时和数组中的依赖项发生变化时,副作用就会重新执行。
useEffect(() => {
console.log("useEffect副作用执行");
setTimeout(() => {
setList(result);
}, 3000);
}, [list]);
useEffect用于处理组件的副作用。
- 第一个参数是一个回调函数,在里面进行业务逻辑代码的书写(
副作用
操作)- 通常在副作用中进行ajax请求,事件的绑定与解绑,设置定时器与清除等等。
- 第二个参数是依赖项数组,指定副作用函数的触发条件。
- 如果不设置第二个参数,那么当该组件挂载和组件每渲染一次,副作用就会执行一次
- 如果数组中的依赖设置为空,那么只有在组件挂载时,该副作用才会执行
- 如果数组中存在依赖项,当组件挂载时和依赖项数组中的依赖发生变化,那么该副作用就会重新执行
useCallback
用于缓存函数,以避免无效的函数重新创建。 第一个参数为要缓存的函数,第二个参数为依赖项数组,如果依赖发生了变化,那么就会生成一个新的函数;否则当组件重新渲染时,不会重新定义这个函数,而是会取缓存。
useMemo
用于缓存计算结果,以避免无效的计算重复执行。 第一个参数为要缓存的函数(注意实际被缓存的是函数被执行过后的值),第二个参数为依赖项数组,如果依赖发生了变化,那么就会重新执行这个函数,得到新的返回值;否则当组件重新渲染时,不会重新执行这个函数,而是直接取被缓存的该函数的返回值。
useCallback
,useMemo
都是在函数式组件中用作优化性能的。默认情况下,当一个组件重新渲染时, React 将递归渲染它的所有子组件。组件中的函数就会重新被创建。因为在 JavaScript 中,
function () {}
或者() => {}
总是会生成不同的函数。
useCallback
- 避免在内部编写额外的嵌套函数
- 避免组件的重新渲染
比如说在父组件中接收的props有{ A,B,C },函数handleSubmit()依赖于A和B,将这个函数作为子组件的props),如果C发生变化,会导致重新创建一个函数handleSumit(),也就导致子组件的props是不同,从而引起子组件重新渲染,但其实这是不必要,这时就可以采用useCallback,将函数handleSubmit传递给useCallback,并设置依赖项([A,B]),保证只有依赖项(A,B)发生变化时,才会重新生成handleSubmit函数,子组件才会重新渲染,从而避免了组件内部重新创建函数,避免引起不必要的渲染。
useMemo
- 避免无效的重新计算
- 跳过组件的重新渲染
同理,比如说父组件中接收的props有{A,B,C},有一个对象count是依赖于A和B计算得出的,并将这个对象count作为子组件的props,正常情况当C发生变化,会导致重新创建对象count,导致传递给子组件的props不同,从而引起子组件的重新渲染,但这是不必要的重复渲染,这时就可以采用useMemo,将定义对象count的计算函数用useMemo包裹,并设置依赖项[A,B],这样就可以保证只有在依赖项发生变化时,才会重新执行计算函数生成新的count对象,子组件才会重新渲染,从而避免了组件内部重新进行无效计算,避免引起不必要的渲染。
useContext
用于跨组件通信,以便共享数据和功能。它接收一个上下文对象(通过 React.createContext 创建)并返回当前上下文的值。这样,函数组件就可以消费上下文中的数据,而不必通过一层层的属性传递。
import { createContext, useContext } from 'react';
const ThemeContext = createContext(null);
export default function MyApp() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
)
}
function Form() {
return (
<Panel title="Welcome">
<Button>Sign up</Button>
<Button>Log in</Button>
</Panel>
);
}
function Panel({ title, children }) {
const theme = useContext(ThemeContext);
const className = 'panel-' + theme;
return (
<section className={className}>
<h1>{title}</h1>
{children}
</section>
)
}
function Button({ children }) {
const theme = useContext(ThemeContext);
const className = 'button-' + theme;
return (
<button className={className}>
{children}
</button>
);
}
useContext() 总是在调用它的组件 上面 寻找最近的 provider。它向上搜索, 不考虑 调用 useContext() 的组件中的 provider。
useRef
const ref = useRef(initialValue)
-
用于在函数组件之间保存可变的引用,类似于类组件中的 ref。改变 ref 不会触发重新渲染。
-
使用 ref 操作 DOM
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <MyInput ref={inputRef} />;// 无法获取自定义组件的 ref <button onClick={handleClick}> 聚焦输入框 </button> </> ); }
-
无法获取自定义组件的 ref
默认情况下,自定义组件不会暴露它们内部 DOM 节点的 ref。
像这样将自定义子组件包装在forwardRef
里,父级组件就可以得到它的 ref。import { forwardRef } from 'react'; const MyInput = forwardRef(({ value, onChange }, ref) => { return ( <input value={value} onChange={onChange} ref={ref} /> ); }); export default MyInput;
3. 函数组件与类组件的区别
3.1 表面差异
- 类组件有生命周期,函数组件没有
- 类组件需要继承 Class,函数组件不需要
- 类组件可以获取实例化的 this,并且基于 this 做各种操作,函数组件不行
- 类组件内部可以定义并维护 state, 函数组件为无状态组件(可以通过hooks实现)
函数组件相比较类组件,优点是更轻量与灵活,便于逻辑的拆分复用。
3.2 最大不同
函数式组件捕获了渲染时所使用的值
, 这是两类组件最大的不同。
import React from "react";
import ReactDOM from "react-dom";
import ProfilePageFunction from './ProfilePageFunction';
import ProfilePageClass from './ProfilePageClass';
class App extends React.Component {
state = {
user: 'Dan',
};
render() {
return (
<>
<label>
<b>Choose profile to view: </b>
<select
value={this.state.user}
onChange={e => this.setState({ user: e.target.value })}
>
<option value="Dan">Dan</option>
<option value="Sophie">Sophie</option>
<option value="Sunil">Sunil</option>
</select>
</label>
<h1>Welcome to {this.state.user}’s profile!</h1>
<p>
//函数组件
<ProfilePageFunction user={this.state.user} />
<b> (function)</b>
</p>
<p>
//类组件
<ProfilePageClass user={this.state.user} />
<b> (class)</b>
</p>
<p>
Can you spot the difference in the behavior?
</p>
</>
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
类组件 —— ProfilePageClass.js
import React from 'react';
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
export default ProfilePage;
函数组件 —— ProfilePageFunction.js
import React from 'react';
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
export default ProfilePage;
仔细对比,以上代码的不同
在函数组件的测试情况下,下拉框中选中 Dan,点击关注按钮,迅速将下拉框切换到Sophie,3秒之后,alert弹窗内容仍然是 Followed Dan
在类组件的测试情况下,重复相同的动作,3秒之后,alert弹窗将会显示 Followed Sophie
此案例中,当父组件的
state
改变时,子组件进行重新渲染,子组件的props
改变;
原因
-
类组件:
props改变(Dan->Sophie),类组件也改变了值,永远保持一致。原因:类组件捕获最新的值(永远保持一致)当实例的props属性发生修改时,class组件直接使用this(组件的实例),所以可以直接获取组件最新的props。
-
函数组件:
函数式组件捕获了渲染时所用的值
当改变props时(Dan->Sophie),你会发现函数组件会渲染之前的值Dan这个名字。原因:函数式组件捕获了渲染所使用的值。在函数组件中,之前的props参数,已经因为javaScript
闭包
的特性,保存在内存之中,无法从外部进行修改(维护多个人的状态)。 所以在定时器执行callback时,打印的还是旧值。
下面是在javaScript闭包中的测试
<body>
<button onclick="openClick()">开始</button>
<button onclick="handleClick()">改变a的值</button>
</body>
<script>
function parent(arg){
let a=arg;
return function children(){
alert('a的值是:'+ a);
}
}
function openClick(){
setTimeout(()=>{
parent(111)();
},3000);
}
function handleClick(){
parent(222);
}
</script>
当点击【开始】按钮时,会使用定时器在三秒后调用闭包函数,页面弹出a的值111,如果在这三秒内点击了【改变a的值】按钮,触发事件修改闭包的外层函数的作用域下的a的值,但到3秒时页面弹出a的值仍然是111。