文章目录
- React Hook函数全解
- React渲染更新原理
- React Hook函数的运行调用
- React实现MVVM
- 技术细节( useEffect 清理监听事件和 定时器)
- diff算法
- react 父子组件生命周期应用及渲染
React Hook函数全解
1. useState
- 参数
- 初始值:可以是任意类型(数字、字符串、对象、数组等),用于设定状态的初始状态。例如
const [count, setCount] = useState(0)
,这里0
就是count
状态的初始值。
- 初始值:可以是任意类型(数字、字符串、对象、数组等),用于设定状态的初始状态。例如
- 应用实例
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
- 应用场景
- 用于在函数组件中管理简单的状态,如表单输入值、开关状态、计数器等。
2. useEffect
- 参数
- 第一个参数:一个函数,在组件挂载后、更新后(如果依赖项改变)和卸载前执行。这个函数用于执行副作用操作,如数据获取、订阅事件、手动修改DOM等。
- 第二个参数:依赖项数组,用于指定副作用函数在哪些状态或属性变化时重新执行。空数组表示只在组件挂载和卸载时执行一次;如果省略,副作用函数会在每次组件渲染后都执行。
- 应用实例
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
- 应用场景
- 数据获取:在组件挂载时从API获取数据并更新状态,如获取用户列表展示在页面上。
- 事件监听:添加和清除事件监听器,比如在组件挂载时添加
window.scroll
事件监听器,在组件卸载时清除它,防止内存泄漏。
3. useContext
- 参数
- 接收一个
Context
对象,这个对象是通过React.createContext
创建的。例如const value = useContext(MyContext)
,其中MyContext
是已创建的上下文对象。
- 接收一个
- 应用实例
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <button onClick={() => setTheme(theme === 'light'? 'dark' : 'light')}>Toggle Theme</button>;
}
- 应用场景
- 跨组件共享数据,避免多层级组件通过
props
层层传递数据,如主题、用户认证信息等。
- 跨组件共享数据,避免多层级组件通过
4. useReducer
- 参数
- 第一个参数:一个
reducer
函数,它接收当前状态和一个action
作为参数,并返回新的状态。例如(state, action) => { switch (action.type) {... } return newState; }
。 - 第二个参数:初始状态,和
useState
的初始值类似。 - 第三个参数(可选):一个初始化函数,用于惰性初始化状态。
- 第一个参数:一个
- 应用实例
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
- 应用场景
- 当状态逻辑比较复杂,如状态更新依赖于前一个状态,或者有多个子状态需要一起更新时,使用
useReducer
可以使状态管理更清晰。
- 当状态逻辑比较复杂,如状态更新依赖于前一个状态,或者有多个子状态需要一起更新时,使用
5. useCallback
- 参数
- 第一个参数:一个内联函数,这个函数会被缓存。
- 第二个参数:依赖项数组,用于指定什么时候重新创建这个缓存的函数。
- 应用实例
import React, { useState, useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const memoizedCallback = useCallback(() => {
// 一些复杂的计算或操作
console.log(count);
}, [count]);
return (
<div>
<Child callback={memoizedCallback} />
<button onClick={() => setCount(count + 1)}>Update Count</button>
</div>
);
}
function Child({ callback }) {
// 使用传入的回调函数
return <button onClick={callback}>Execute Callback</button>;
}
- 应用场景
- 优化子组件的重新渲染,当传递给子组件的回调函数依赖的状态没有改变时,避免子组件因为函数引用变化而重新渲染。
6. useMemo
- 参数
- 第一个参数:一个函数,用于计算一个值。
- 第二个参数:依赖项数组,用于指定什么时候重新计算这个值。
- 应用实例
import React, { useState, useMemo } from 'react';
function ExpensiveComponent({ a, b }) {
const result = useMemo(() => {
// 复杂的计算
console.log('Calculating...');
return a * b;
}, [a, b]);
return <div>Result: {result}</div>;
}
function App() {
const [a, setA] = useState(2);
const [b, setB] = useState(3);
return (
<div>
<ExpensiveComponent a={a} b={b} />
<button onClick={() => setA(a + 1)}>Update A</button>
<button onClick={() => setB(b + 1)}>Update B</button>
</div>
);
}
- 应用场景
- 用于缓存计算结果,避免在每次组件渲染时都进行昂贵的计算,只有当依赖项改变时才重新计算。
7. useRef
- 参数
- 初始值:可以是任何类型的值,用于初始化
ref
对象的current
属性。
- 初始值:可以是任何类型的值,用于初始化
- 应用实例
import React, { useRef, useEffect } from 'react';
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
- 应用场景
- 访问DOM元素或保存一个可变的值,在组件的整个生命周期内保持不变,如存储定时器ID、表单元素引用等。
8. useImperativeHandle
- 参数
- 第一个参数:一个
ref
对象,通常是通过useRef
创建的。 - 第二个参数:一个函数,用于定义暴露给父组件的实例值。
- 第三个参数(可选):依赖项数组,用于指定什么时候重新定义暴露的值。
- 第一个参数:一个
- 应用实例
import React, { forwardRef, useImperativeHandle, useState } from 'react';
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
increment: () => setCount(count + 1)
}));
return <div>{count}</div>;
});
function Parent() {
const childRef = useRef();
const handleClick = () => {
childRef.current.increment();
};
return (
<>
<Child ref={childRef} />
<button onClick={handleClick}>Increment Child</button>
</>
);
}
- 应用场景
- 用于在使用
forwardRef
的情况下,自定义暴露给父组件的子组件实例属性和方法。
- 用于在使用
9. useLayoutEffect
- 参数和使用方式类似useEffect
- 区别在于
useLayoutEffect
会在所有DOM变更之后同步调用,在浏览器进行绘制之前。而useEffect
是在浏览器绘制之后异步调用。
- 区别在于
- 应用场景
- 当需要在DOM更新后立即读取布局信息并进行同步操作时使用,如获取更新后的DOM元素的位置、尺寸等信息。
10. useDebugValue
- 参数
- 第一个参数:要在React开发者工具中显示的值。
- 第二个参数(可选):一个格式化函数,用于格式化显示的值。
- 应用场景
- 用于在React开发者工具中为自定义Hook提供调试信息,方便开发者查看Hook的内部状态。
React渲染更新原理
1. 虚拟DOM(Virtual DOM)
- React会根据组件的状态和属性构建一个虚拟DOM树。虚拟DOM是一个轻量级的JavaScript对象,它描述了真实DOM应该是什么样子。例如,一个简单的
React
组件:
function MyComponent() {
return <div><p>Hello</p></div>;
}
对应的虚拟DOM可能是类似这样的结构:
{
type: 'div',
props: {
children: [
{
type: 'p',
props: {
children: 'Hello'
}
}
]
}
}
- 当组件的状态或属性发生变化时,React会重新构建一个新的虚拟DOM树。
2. 协调(Reconciliation)
- React使用
diff
算法来比较新旧虚拟DOM树。这个算法有一些优化策略,例如:- 不同类型的元素:如果元素类型改变(如从
<div>
变为<span>
),React会认为整个子树都改变了,会销毁旧的DOM节点并创建新的DOM节点。 - 同类型的元素:React会比较它们的属性和子元素。对于属性,只会更新变化的属性;对于子元素,会采用高效的比较算法来最小化DOM操作。
- 不同类型的元素:如果元素类型改变(如从
- 例如,有以下组件更新:
function MyComponent() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
当count
状态改变时,React会构建新的虚拟DOM,比较新旧虚拟DOM,发现<p>
标签的内容发生了变化,只会更新<p>
标签的文本内容,而不会重新创建整个div
和按钮。
3. 批量更新和DOM操作
- React会根据
diff
的结果,计算出最小的DOM操作集合,然后批量更新真实DOM。这样可以减少浏览器重排(reflow)和重绘(repaint)的次数,提高性能。例如,在一次更新中如果有多个组件状态变化导致DOM更新,React会将这些更新合并,一次性应用到真实DOM上。
React Hook函数的运行调用
- Hook函数必须在函数组件的顶层调用。这是为了保证Hook的调用顺序在每次组件渲染时都是一致的。例如,下面是错误的调用方式:
function MyComponent() {
if (someCondition) {
const [count, setCount] = useState(0);
}
//...
}
- 每次组件重新渲染时,Hook函数会按照它们在组件中定义的顺序重新执行。React内部通过一个链表结构来管理Hook。在函数组件内部,有一个隐藏的状态指针,每次调用Hook函数时,这个指针会移动到下一个Hook节点。当组件重新渲染时,这个指针又会从头开始,按照相同的顺序调用Hook,从而保证状态的正确更新和副作用的正确执行。
React实现MVVM
- Model(数据层)
- 在React中,
state
和props
可以看作是模型的一部分。state
用于存储组件内部的状态,props
用于接收外部传入的数据。例如,在一个用户信息展示组件中,state
可能存储用户的编辑状态(如是否处于编辑模式),props
可能接收用户的基本信息(如姓名、年龄等)。
- 在React中,
- View(视图层)
- React组件的
JSX
部分可以看作是视图。它根据state
和props
来渲染出用户界面。例如,一个简单的列表组件:
- React组件的
function ListComponent({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
这里items
是props
,JSX
代码根据items
渲染出一个无序列表视图。
- ViewModel(数据 - 视图绑定层)
- 在React中,
useState
、useEffect
等Hook函数可以看作是数据 - 视图绑定的工具。useState
用于将state
和视图绑定,当state
改变时,视图会重新渲染。useEffect
可以用于在state
或props
变化时执行副作用操作,间接影响视图。例如:
- 在React中,
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
这里useState
绑定了count
状态和视图中的显示内容,useEffect
在count
变化时更新文档标题,实现了数据 - 视图的双向绑定。
技术细节( useEffect 清理监听事件和 定时器)
-
清理监听事件
- 在
useEffect
中返回一个清理函数来处理事件监听器的清理。当组件卸载或者useEffect
的依赖项发生变化重新执行时,这个清理函数会被调用。 - 例如,当组件挂载时添加一个
window
的resize
事件监听器,在组件卸载或者依赖项变化时需要移除这个监听器,以避免内存泄漏。 - 以下是一个示例代码:
import React, { useState, useEffect } from 'react'; function ResizeComponent() { const [windowWidth, setWindowWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => { setWindowWidth(window.innerWidth); }; window.addEventListener('resize', handleResize); // 返回清理函数,用于移除事件监听器 return () => { window.removeEventListener('resize', handleResize); }; }, []); return ( <div> <p>当前窗口宽度: {windowWidth}px</p> </div> ); } export default ResizeComponent;
- 在这个示例中,
useEffect
的第一个参数是一个函数,在这个函数内部添加了window
的resize
事件监听器handleResize
。这个监听器会在window
大小改变时更新windowWidth
状态,从而更新组件的显示内容。useEffect
的第二个参数是一个空数组[]
,这意味着这个effect
只会在组件挂载和卸载时执行一次。返回的清理函数return () => { window.removeEventListener('resize', handleResize); }
用于在组件卸载或者依赖项改变重新执行useEffect
时移除resize
事件监听器。
- 在
-
清理定时器
- 同样是在
useEffect
中返回一个清理函数来处理定时器的清除。 - 例如,设置一个定时器每隔一秒更新一次计数器的值,当组件卸载或者依赖项变化重新执行
useEffect
时,需要清除这个定时器,以防止定时器继续运行导致内存泄漏或者其他错误。 - 以下是一个示例代码:
import React, { useState, useEffect } from 'react'; function TimerComponent() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount((prevCount) => prevCount + 1); }, 1000); // 返回清理函数,用于清除定时器 return () => { clearInterval(timer); }; }, []); return ( <div> <p>计数器: {count}</p> </div> ); } export default TimerComponent;
- 在这个示例中,
useEffect
内部通过setInterval
创建了一个定时器,每隔一秒会更新count
状态。返回的清理函数return () => { clearInterval(timer); }
用于在组件卸载或者依赖项改变重新执行useEffect
时清除这个定时器。
- 同样是在
diff算法
-
Diff算法的目的
- React的Diff算法主要用于比较新旧虚拟DOM树,以确定最小的DOM操作集合来更新真实DOM。由于直接操作真实DOM的性能开销较大,Diff算法的高效性有助于减少不必要的DOM操作,如重排(reflow)和重绘(repaint),从而提高应用的性能。
-
分层比较策略
- React将虚拟DOM树按照层级进行比较。比较从树的根节点开始,递归地对每个层级的节点进行比较。
- 例如,有一个简单的组件树结构如下:
<div> <p>Hello</p> <span>World</span> </div>
- React会先比较最外层的
div
节点,然后再比较div
节点下的p
和span
子节点。
-
不同类型元素的比较
- 当发现新旧虚拟DOM树中节点的类型不同时,React会认为该节点及其子节点完全不同。
- 例如,旧的节点是
<div>
,新的节点是<span>
,React会销毁旧的div
节点及其所有子节点,然后创建新的span
节点及其子节点。 - 假设原来的组件是这样的:
function OldComponent() { return <div><p>Old Content</p></div>; }
- 现在更新为:
function NewComponent() { return <span><p>New Content</p></span>; }
- React会直接替换整个DOM结构,因为根节点的类型从
div
变成了span
。
-
同类型元素的比较
- 对于相同类型的元素,React会比较它们的属性。只有属性发生变化的部分才会更新到真实DOM上。
- 例如,有一个
<input>
元素,旧的属性是{type: "text", value: "old value"}
,新的属性是{type: "text", value: "new value"}
,React会只更新value
属性,而不会重新创建整个input
元素。 - 假设组件中有一个输入框:
function InputComponent() { const [value, setValue] = useState('Initial'); useEffect(() => { setTimeout(() => { setValue('Updated'); }, 1000); }, []); return <input type="text" value={value} />; }
-
在这里,当
value
状态更新时,React会比较新旧input
元素(因为类型相同),然后只更新value
属性。 -
同时,对于同类型元素的子节点,React会采用高效的比较策略。如果子节点是列表形式,React会使用
key
属性来帮助识别每个子节点。 -
例如,有一个列表组件:
function ListComponent() { const [items, setItems] = useState(['a', 'b', 'c']); useEffect(() => { setTimeout(() => { setItems(['b', 'c', 'd']); }, 1000); }, []); return ( <ul> {items.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ); }
- 在这个例子中,当
items
数组更新时,React会根据key
(这里使用了索引index
,但在实际应用中最好使用唯一标识符)来比较每个li
子节点。如果key
相同,就比较节点内容;如果key
不同,就认为是新的节点或者旧节点被删除了。不过,使用索引作为key
可能会导致性能问题和错误的更新行为,在可能的情况下,最好使用数据的唯一标识符作为key
。
-
Diff算法的优化措施
- React的Diff算法基于一些假设和优化策略来提高性能。例如,它假设在同一层级上,节点的顺序和类型不会频繁地、大幅度地变化。这种假设使得React可以采用简单而高效的比较策略。
- 同时,React会尽量复用已有的DOM节点。当节点可以复用(类型相同且大部分属性相同)时,就不会重新创建DOM节点,而是更新节点的属性和内容。这有助于减少DOM操作的数量,从而提高应用的整体性能。
react 父子组件生命周期应用及渲染
-
挂载阶段(Mounting)
- 父组件挂载
- 当一个React应用启动时,首先会挂载根组件。在挂载过程中,父组件的
constructor
(如果是类组件)会被调用,用于初始化组件的状态和绑定事件处理函数等。 - 例如,一个简单的父组件
ParentComponent
:
import React, { Component } from 'react'; import ChildComponent from './ChildComponent'; class ParentComponent extends Component { constructor(props) { super(props); this.state = { parentData: 'Initial Parent Data' }; } componentDidMount() { console.log('Parent Component Mounted'); } render() { return ( <div> <ChildComponent parentData={this.state.parentData} /> </div> ); } } export default ParentComponent;
- 在
constructor
中初始化了parentData
状态,然后在componentDidMount
生命周期方法中打印出Parent Component Mounted
,这个方法会在组件挂载到DOM后被调用,常用于进行数据获取、订阅事件等操作。
- 当一个React应用启动时,首先会挂载根组件。在挂载过程中,父组件的
- 子组件挂载
- 当父组件的
render
方法返回包含子组件的JSX
时,子组件开始挂载。子组件同样会经历constructor
(如果是类组件)初始化过程。 - 例如,
ChildComponent
:
import React, { Component } from 'react'; class ChildComponent extends Component { constructor(props) { super(props); console.log('Child Constructor'); } componentDidMount() { console.log('Child Component Mounted'); } render() { return ( <div> <p>{this.props.parentData}</p> </div> ); } } export default ChildComponent;
- 在
constructor
中会打印Child Constructor
,用于初始化子组件相关的操作。componentDidMount
方法会在子组件挂载到DOM后被调用,这里会打印Child Component Mounted
。
- 当父组件的
- 挂载顺序总结
- 挂载顺序是先父组件的
constructor
,然后是父组件的render
,在父组件render
过程中如果有子组件,会先调用子组件的constructor
,最后是子组件的componentDidMount
,再是父组件的componentDidMount
。在上述例子中,控制台输出顺序是:Child Constructor
->Child Component Mounted
->Parent Component Mounted
。
- 挂载顺序是先父组件的
- 父组件挂载
-
更新阶段(Updating)
- 父组件更新
- 当父组件的状态或属性发生变化时,父组件会重新渲染。在更新过程中,
shouldComponentUpdate
(如果定义了这个方法)会首先被调用,用于决定组件是否需要更新。 - 例如,在
ParentComponent
中添加一个更新状态的方法:
import React, { Component } from 'react'; import ChildComponent from './ChildComponent'; class ParentComponent extends Component { constructor(props) { super(props); this.state = { parentData: 'Initial Parent Data' }; } componentDidMount() { console.log('Parent Component Mounted'); } updateParentData = () => { this.setState({ parentData: 'Updated Parent Data' }); }; render() { return ( <div> <button onClick={this.updateParentData}>Update Parent Data</button> <ChildComponent parentData={this.state.parentData} /> </div> ); } } export default ParentComponent;
- 当点击
Update Parent Data
按钮时,parentData
状态更新,shouldComponentUpdate
(如果有)会被调用。如果这个方法返回true
(默认行为),组件会继续更新。
- 当父组件的状态或属性发生变化时,父组件会重新渲染。在更新过程中,
- 子组件更新
- 因为父组件的状态或属性变化导致子组件的
props
变化,子组件也会更新。子组件会先调用componentWillReceiveProps
(如果是类组件且定义了这个方法)来接收新的props
,然后根据新的props
进行更新。 - 对于
ChildComponent
,如果定义componentWillReceiveProps
方法可以这样:
import React, { Component } from 'react'; class ChildComponent extends ClassComponent { constructor(props) { super(props); console.log('Child Constructor'); } componentWillReceiveProps(nextProps) { console.log('Child Will Receive Props'); console.log('Old Props:', this.props); console.log('New Props:', nextProps); } componentDidUpdate(prevProps, prevState) { console.log('Child Component Updated'); console.log('Previous Props:', prevProps); console.log('Current Props:', this.props); } render() { return ( <div> <p>{this.props.parentData}</p> </div> ); } } export default ChildComponent;
- 当父组件更新导致子组件
props
变化时,componentWillReceiveProps
会被调用,用于处理新的props
。然后componentDidUpdate
会在子组件更新完成后被调用,用于执行更新后的操作,如检查props
或状态的变化情况。
- 因为父组件的状态或属性变化导致子组件的
- 更新顺序总结
- 当父组件状态或属性更新导致子组件
props
更新时,更新顺序一般是父组件的shouldComponentUpdate
(如果有) -> 父组件的render
-> 子组件的componentWillReceiveProps
(如果有) -> 子组件的render
-> 子组件的componentDidUpdate
-> 父组件的componentDidUpdate
。
- 当父组件状态或属性更新导致子组件
- 父组件更新
-
卸载阶段(Unmounting)
-
子组件卸载
- 当父组件的
render
方法不再返回某个子组件时,子组件会被卸载。此时,子组件的componentWillUnmount
方法会被调用,用于清理在组件挂载和使用过程中产生的副作用,如清除定时器、取消事件订阅等。 - 例如,在
ParentComponent
中添加一个条件渲染,使得ChildComponent
可以被卸载:
import React, { Component } from 'react'; import ChildComponent from './ChildComponent'; class ParentComponent extends Component { constructor(props) { super(props); this.state = { showChild: true }; } componentDidMount() { console.log('Parent Component Mounted'); } toggleChild = () => { this.setState((prevState) => ({ showChild:!prevState.showChild })); }; render() { return ( <div> <button onClick={this.toggleChild}>Toggle Child</button> {this.state.showChild && <ChildComponent parentData={this.state.parentData} />} </div> ); } } export default ParentComponent;
- 在
ChildComponent
中添加componentWillUnmount
方法:
import React, { Component } from 'react'; class ChildComponent extends Component { constructor(props) { super(props); console.log('Child Constructor'); } componentWillUnmount() { console.log('Child Component Unmounted'); } render() { return ( <div> <p>{this.props.parentData}</p> </div> ); } } export default ChildComponent;
- 当点击
Toggle Child
按钮,showChild
状态改变导致ChildComponent
被卸载时,会打印Child Component Unmounted
。
- 当父组件的
-
父组件卸载
- 当整个应用或者包含父组件的上级组件被卸载时,父组件的
componentWillUnmount
方法也会被调用,用于清理父组件相关的副作用。
- 当整个应用或者包含父组件的上级组件被卸载时,父组件的
-
卸载顺序总结
- 先卸载子组件,调用子组件的
componentWillUnmount
,然后如果父组件也被卸载,再调用父组件的componentWillUnmount
。
- 先卸载子组件,调用子组件的
-