React.memo() 和 useMemo() 都是 React 中用于优化性能的钩子函数,但它们的用途和区别是不同的。
React.memo() 是一个高阶组件 (Higher-Order Component),用于优化函数组件的渲染。它类似于 React.PureComponent,可以通过比较前后两次的 props 是否相等来决定是否重新渲染组件。当父组件的状态变化时,如果传递给 React.memo() 的组件的 props 没有发生变化,那么它将会跳过重新渲染,从而提高性能。使用方法如下:
const MyComponent = React.memo((props) => {
// 组件的渲染逻辑
});
// 使用 MyComponent
useMemo() 是一个用于记忆化计算结果的钩子函数。它接收一个计算函数和依赖项数组,它会在依赖项发生变化时重新计算,并且会将计算结果缓存起来。当组件重新渲染时,如果依赖项不变,useMemo() 将直接返回上一次缓存的结果,而不会重新执行计算函数。这可以用于优化一些昂贵的计算或者需要缓存结果的场景。使用方法如下:
const memoizedValue = useMemo(() => {
// 计算逻辑,返回一个值
}, [dependency1, dependency2]);
区别总结如下:
React.memo() 是用于优化函数组件的渲染,通过比较前后两次的 props 是否相等来决定是否重新渲染组件。
useMemo() 是用于记忆化计算结果的钩子函数,通过缓存计算结果来避免重复计算。
React.memo() 用于组件函数本身的性能优化,而 useMemo() 用于某个计算结果的优化。
React.memo() 是一个高阶组件,需要将组件作为参数传递进去,而 useMemo() 是一个钩子函数,可以在函数组件内部使用。
React.memo() 的依赖项是组件的 props,而 useMemo() 的依赖项是自定义的依赖数组。
中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的
在上篇文章中,了解到了Redux整个工作流程,当action发出之后,reducer立即算出state,整个过程是一个同步的操作
那么如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件
Redux中,中间件就是放在就是在dispatch过程,在分发action进行拦截处理,如下图:
有很多优秀的redux中间件,如:
redux-thunk:用于异步操作
redux-logger:用于日志记录
上述的中间件都需要通过applyMiddlewares进行注册,作用是将所有的中间件组成一个数组,依次执行
然后作为第二个参数传入到createStore中
const store = createStore(
reducer,
applyMiddleware(thunk, logger)
);
redux-thunk
redux-thunk是官网推荐的异步处理中间件
默认情况下的dispatch(action),action需要是一个JavaScript的对象
redux-thunk中间件会判断你当前传进来的数据类型,如果是一个函数,将会给函数传入参数值(dispatch,getState)
dispatch函数用于我们之后再次派发action
getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态
所以dispatch可以写成下述函数的形式:
const getHomeMultidataAction = () => {
return (dispatch) => {
axios.get("http://xxx.xx.xx.xx/test").then(res => {
const data = res.data.data;
dispatch(changeBannersAction(data.banner.list));
dispatch(changeRecommendsAction(data.recommend.list));
})
}
}
redux-logger
如果想要实现一个日志功能,则可以使用现成的redux-logger
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(logger)
);
这样我们就能简单通过中间件函数实现日志记录的信息
Redux中间件的实现原理是利用了Redux的dispatch机制和函数的柯里化特性。
当一个action被dispatch时,中间件可以拦截到这个action,并在传递给reducer之前进行一些处理,然后再将处理后的action传递给下一个中间件或者最终的reducer。
中间件的实现可以通过对dispatch函数的重写来实现,或者利用装饰器模式来封装原始的dispatch函数。当中间件链中没有下一个中间件时,可以直接调用原始的dispatch函数将action传递给reducer。
在React中,事件的执行顺序可以分为三个阶段:捕获阶段、目标阶段和冒泡阶段。这和原生事件的执行顺序是相同的。
捕获阶段:事件从最外层的元素开始向内层元素进行捕获,直到达到事件的目标元素。在这个阶段,事件的捕获处理程序会被依次触发。
目标阶段:事件到达目标元素后,在目标元素上触发事件处理程序。
冒泡阶段:事件从目标元素开始向外层元素进行冒泡,直到到达最外层的元素。在这个阶段,事件的冒泡处理程序会被依次触发。
在React中,事件的执行顺序是通过合成事件机制来模拟的,React会在DOM树上监听事件,然后根据捕获和冒泡阶段触发对应的事件处理程序。React合成事件的触发是在冒泡阶段完成的,而不是在原生事件捕获或冒泡阶段。
需要注意的是,在React中,通过使用e.stopPropagation()方法可以阻止事件继续传播,而e.preventDefault()方法可以阻止事件的默认行为。
React合成事件是React框架提供的一种事件系统,用于处理用户交互和DOM事件。它是对原生浏览器事件的封装和模拟,提供了一致的跨浏览器和跨平台的事件处理机制。
React合成事件的特点和好处包括:
跨浏览器兼容性:React合成事件对不同浏览器的事件兼容性进行了处理,提供了一致的事件接口,使得开发者不需要关心不同浏览器的差异。
性能优化:React合成事件使用了事件委托的方式,将事件监听器添加到父级元素上,减少了事件监听器的数量,提高了性能。
合成事件对象:React合成事件提供了一个合成事件对象,该对象封装了原生事件的相关信息,如事件类型、目标元素、键盘状态等,开发者可以方便地获取和操作这些信息。
事件池和事件重用:React合成事件使用了事件池技术,即在事件处理完毕后,将合成事件对象重置并放回事件池中,以便下次事件使用,减少了对象的创建和垃圾回收的开销。
使用React合成事件,可以方便地在React组件中处理各种用户交互和DOM事件,如点击、滚动、键盘输入等。
function handleClick() {
console.log('Button clicked!');
}
function MyComponent() {
return <button onClick={handleClick}>Click me</button>;
}
function handleChange(event) {
console.log('Input value:', event.target.value);
}
function MyComponent() {
return <input type="text" onChange={handleChange} />;
}
function handleKeyDown(event) {
if (event.key === 'Enter') {
console.log('Enter key pressed!');
}
}
function MyComponent() {
return <input type="text" onKeyDown={handleKeyDown} />;
}
function handleMouseEnter() {
console.log('Mouse entered!');
}
function handleMouseLeave() {
console.log('Mouse left!');
}
function MyComponent() {
return (
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
Move the mouse over me
</div>
);
}
以上示例展示了几个常见的React合成事件,通过在组件上添加对应的事件处理函数,可以实现对用户交互和DOM事件的处理和响应。需要注意,合成事件对象在事件处理函数中作为第一个参数传递,并包含了事件的相关信息。
在 React 16.8 之前,函数组件也称为无状态组件,因为函数组件也不能访问 react 生命周期,也没有自己的状态。react 自 16.8 开始,引入了 Hooks 概念,使得函数组件中也可以拥有自己的状态,并且可以模拟对应的生命周期。
我们应该在什么时候使用 Hooks 呢?
官方并不建议我们把原有的 class 组件,大规模重构成 Hooks,而是有一个渐进过程:
首先,原有的函数组件如果需要自己的状态或者需要访问生命周期函数,那么用 Hooks 是再好不过了;
另外就是,我们可以先在一些逻辑较简单的组件上尝试 Hooks ,在使用起来相对较熟悉,且组内人员比较能接受的前提下,再扩大 Hooks 的使用范围。
那么相对于传统class, Hooks 有哪些优势?
State Hook 使得组件内的状态的设置和更新相对独立,这样便于对这些状态单独测试并复用。
Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分,这样使得各个逻辑相对独立和清晰。
class 生命周期在 Hooks 中的实现
Hooks 组件更接近于实现状态同步,而不是响应生命周期事件。但是,由于我们先熟悉的 class 的生命周期,在写代码时,难免会受此影响,那么 Hooks 中如何模拟 class 的中的生命周期呢:
class 组件 | Hooks 组件 |
---|---|
getDerivedStateFromProps | useState |
shouldComponentUpdate | React.memo |
render | 函数本身 |
componentDidMount | useEffect 第二个参数为[] |
getSnapshotBeforeUpdate | useEffect和useRef模拟 |
componentDidUpdate | useEffect 配合useRef |
componentWillUnmount | useEffect 里面返回的函数 |
componentDidCatch | 无 |
getDerivedStateFromError | 无 |
useEffect函数内部的回调函数在组件挂载后立即执行,相当于componentDidMount。在这个回调函数中,可以进行一些初始化的操作,例如发起异步请求或订阅事件。
第二个参数是一个依赖数组,当数组中的值发生变化时,会触发useEffect内部的回调函数。类似于componentDidUpdate,这里可以根据需要监听特定状态的变化。
返回一个清理函数,用于在组件卸载时执行清理操作,相当于componentWillUnmount。
需要注意的是,useEffect的回调函数中不能直接使用async/await语法,因此在上述例子中使用了一个自定义的异步方法来模拟数据获取的过程。
通过使用不同参数的useEffect和自定义的函数,结合useState等其他Hooks,可以实现类组件中的各种生命周期方法的功能。
getDerivedStateFromProps是React类组件中的生命周期方法之一。它的作用是在组件接收到新的props之后,根据新的props和之前的state计算并返回一个新的state。
getDerivedStateFromProps方法的用法如下:
class MyComponent extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
// 根据新的props和之前的state计算新的state
// 返回一个对象,表示新的state
return {
derivedState: nextProps.value * 2
};
}
constructor(props) {
super(props);
this.state = {
derivedState: null
};
}
render() {
return <div>{this.state.derivedState}</div>;
}
}
在上述示例中,getDerivedStateFromProps方法接收两个参数:nextProps和prevState。nextProps表示新的props,prevState表示之前的state。
在这个方法内部,可以根据新的props和之前的state计算并返回一个新的state对象。这个新的state会被合并到组件的state中,从而更新组件的UI。
需要注意的是,getDerivedStateFromProps方法必须是一个静态方法,即使用static关键字定义,不可以访问this关键字,也不可以调用setState方法。它的返回值将作为组件的新state,可以在render方法中使用。
getDerivedStateFromProps方法在React 16.3版本中引入,用于取代旧的componentWillReceiveProps生命周期方法。但需要注意的是,使用getDerivedStateFromProps方法可能会导致过度渲染,因此应谨慎使用。
要使用useEffect手动对比props,并实现类似getDerivedStateFromProps的功能,可以结合useState和useEffect来实现。以下是一个示例:
import React, { useState, useEffect } from 'react';
function MyComponent(props) {
const [derivedState, setDerivedState] = useState(null);
useEffect(() => {
if (props.value !== derivedState) {
setDerivedState(props.value);
}
}, [props.value]);
return <div>{derivedState}</div>;
}
在上述示例中,通过useState定义了一个状态变量derivedState和对应的更新函数setDerivedState。然后,使用useEffect监听props.value的变化。
当props.value发生变化时,useEffect内部的回调函数会执行,判断props.value是否等于derivedState,如果不相等,则调用setDerivedState更新derivedState的值。
这样,当组件接收到新的props时,会手动对比props的值并更新状态derivedState,实现了类似getDerivedStateFromProps的功能。在组件的return语句中,可以使用derivedState的值更新UI。
需要注意的是,在使用useEffect进行props的对比时,要确保将需要对比的props作为useEffect依赖数组的一部分,以便在props变化时触发回调函数。
在React中,getSnapshotBeforeUpdate是一个生命周期方法,用于在组件更新之前获取当前的DOM状态。它的用法如下:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
snapshot: null
};
this.inputRef = React.createRef();
}
componentDidMount() {
this.inputRef.current.focus();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevState.value !== this.state.value) {
return this.inputRef.current.scrollHeight;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
// 使用获取到的snapshot进行DOM操作,如滚动到底部等
this.inputRef.current.scrollTop = snapshot;
}
}
handleChange = (event) => {
this.setState({
value: event.target.value
});
};
render() {
return (
<div>
<textarea
ref={this.inputRef}
value={this.state.value}
onChange={this.handleChange}
/>
</div>
);
}
}
在上述示例中,getSnapshotBeforeUpdate方法接收三个参数:prevProps,prevState和snapshot。它可以在组件更新之前获取到先前的props和state,并返回一个值作为快照(snapshot)。
在getSnapshotBeforeUpdate方法中,可以根据先前的state和当前的state进行比较,并返回一个快照值。在componentDidUpdate方法中,可以使用这个快照进行DOM操作,例如滚动到指定位置。
需要注意的是,getSnapshotBeforeUpdate方法的返回值将作为第三个参数传递给componentDidUpdate方法。在componentDidUpdate方法中,可以根据这个快照值进行相应的操作。
getSnapshotBeforeUpdate是在React 16.3版本中引入的,用于取代旧的componentWillUpdate生命周期方法。它提供了一种在组件更新之前获取DOM状态的方式,可以用于在更新后保持滚动位置、动画等效果.
在React函数组件中,可以使用useEffect和useRef来模拟getSnapshotBeforeUpdate方法。以下是一个示例:
import React, { useState, useEffect, useRef } from 'react';
function MyComponent() {
const [value, setValue] = useState('');
const inputRef = useRef(null);
const prevValueRef = useRef(null);
useEffect(() => {
if (prevValueRef.current !== null && prevValueRef.current !== value) {
const snapshot = inputRef.current.scrollHeight;
// 使用获取到的snapshot进行DOM操作,如滚动到底部等
inputRef.current.scrollTop = snapshot;
}
prevValueRef.current = value;
}, [value]);
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<div>
<textarea
ref={inputRef}
value={value}
onChange={handleChange}
/>
</div>
);
}
在上述示例中,我们使用useRef创建了两个引用:inputRef和prevValueRef。inputRef用于获取textarea的DOM节点,prevValueRef用于存储先前的value值。
然后,我们使用useEffect监听value的变化。在useEffect的回调函数中,我们检查prevValueRef.current是否不为null且不等于当前的value。如果满足条件,说明组件正在更新,我们可以获取到先前的DOM状态并进行相应的DOM操作。
在更新DOM后,我们将当前的value存储到prevValueRef.current,以便在下一次更新时进行比较。
这样,我们就能够在函数组件中模拟getSnapshotBeforeUpdate的功能,通过useEffect和useRef来获取先前的DOM状态并进行相应的操作。
注意:
当使用useEffect时,如果依赖数组不为空,那么在组件首次渲染时,useEffect的回调函数不会被执行。(上面这个带依赖项的useEffect在组件首次渲染时候不会执行)。
当组件首次渲染时,React会运行一次完整的渲染周期,包括渲染组件和执行副作用。在这个过程中,React会跳过useEffect的回调函数,因为依赖项的值还没有发生变化,所以没有必要执行该回调函数。
只有在组件的依赖项发生变化时,useEffect的回调函数才会被执行。React会比较依赖项的新旧值,如果发现有任何一个依赖项的值发生了变化,就会运行回调函数。
因此,当依赖数组不为空时,useEffect的回调函数只会在组件首次渲染后的依赖项变化时被执行,而不会在组件首次渲染时执行。如果希望在组件首次渲染时执行,可以将依赖数组设置为空数组[]。这样,useEffect的回调函数将在组件首次渲染后立即执行一次。
服务端渲染(Server-Side Rendering ,简称SSR),指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程
其解决的问题主要有两个:
在react中,实现SSR主要有两种形式:
npm install react express react-dom
// 引入依赖库
const express = require("express");
const React = require("react");
const ReactDOMServer = require("react-dom/server");
// 创建Express实例
const app = express();
// 定义React组件
function HelloWorld() {
return <h1>Hello World!</h1>;
}
// 定义路由和请求处理程序
app.get("/", (req, res) => {
// 将React组件渲染为HTML字符串
const html = ReactDOMServer.renderToString(<HelloWorld />);
// 将HTML发送到客户端
res.send(`
React SSR Example
${html}
`);
});
// 启动服务器
app.listen(3000, () => {
console.log("Server is listening on port 3000");
});
import React from "react";
import ReactDOM from "react-dom";
import HelloWorld from "./HelloWorld";
// 在客户端挂载React组件
ReactDOM.hydrate(<HelloWorld />, document.getElementById("root"));
import React from "react";
function HelloWorld() {
return <h1>Hello World!</h1>;
}
export default HelloWorld;
node server.js
在上面的示例中,我们使用Express创建了一个简单的服务器。当客户端请求根路径时,服务端会将React组件HelloWorld渲染为HTML字符串,并将其发送到客户端。在客户端,我们使用ReactDOM.hydrate方法将HTML字符串转换为可交互的React组件。