React知识回顾(初级篇)
目前就我自己而言用的比较趁手的框架是react,为了回避面试多余的问题,我直接把vue在简历上去掉了(因为加上去似乎也没加多少分,该已读不回还是已读不回)。不过即便是我现在自认为玩react玩的还行,应付面试题还是不大够。所以需要回顾一下比较基础的部分,不管开发用不用的到,因为面试可能要用到。
基本认知
是什么:一个 JavaScript 库、UI 层面的解决方案、一次编写,到处运行(跨平台)
特性:组件系统、函数式、虚拟 DOM、单向数据绑定、JSX 语法
组件系统
提示
React 的组件有两类:类组件和函数组件。这里就不讲怎么去写一个类组件或者函数组件了,毕竟不是新手向文章。这部分主要把React组件相关的知识放到一起。
LifeCycle
React 中,仅有类组件存在所谓的生命周期。
React 的生命周期可分为三个阶段:创建、更新和卸载。
创建:
constructor、getDerivedStateFromProps、render、componentDidMount更新:
getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdate、componentDidUpdate卸载:
componentWillUnmount
关于上面提及的生命周期方法,值得注意的有以下几个点:
constructor中的super:在
constructor中使用super后,constructor中才能访问到this对象,如:constructor内部调用super():class ComponentA extends React.Component { constructor(props) { super(); console.log(props); // {} console.log(this.props); // undefined } }constructor内部调用super(props):class ComponentA extends React.Component { constructor(props) { super(props); console.log(props); // {} console.log(this.props); // {} } }因为组件作为子类,没有自己的
this对象,是需要继承父类的this的。虽然React在后面会帮我们完成这一过程(如在render中使用this.props),但在constructor阶段该过程并没有被执行,所以在调用super之前,都是拿不到this对象的。关于
getDerivedStateFromProps这个方法的执行时机是组件的创建和更新阶段。在调用
render方法之前,该方法会被调用。传入的参数有两个:第一个参数为nextProps,即变化后的props;第二个参数为preState,即改变前的state。一般我们会使用
getDerivedStateFromProps来让组件在props变化时更新state。关于
shouldComponentUpdate组件在每次触发更新前,都会调用
shouldComponentUpdate。这个方法有两个参数,第一个参数为nextProps,第二个参数为nextState。方法的返回值为布尔类型,标识是否继续渲染。我们可以根据这个方法的两个参数的变化情况来对渲染进行控制,从而达到减少渲染,优化性能的目的。注意
在这个方法中不要使用
setState对组件状态进行修改,会造成死循环。同样的,不要在render中进行setState操作。关于
getSnapshotBeforeUpdate这个方法并不是很常用到,它在
render之后,dom改变之前执行,其返回值作为componentDidUpdate的第三个参数被接受,可以在组件更新前将组件的一些 UI 状态如滚动位置传给componentDidUpdate,从而实现组件 UI 状态的恢复。
函数组件中没有LifeCycle的概念,不过我们可以根据React提供的一些hooks来模拟一部分的生命周期。
首先看一下useEffect的使用:
useEffect(() => {
// 依赖数组中依赖项变化时执行的逻辑
return () => {
// 组件销毁时调用的逻辑
};
}, dep);当dep为空时,useEffect仅会被执行一次,因此可以利用空依赖数组来模拟componentDidMount。相应的,componentWillUnmount也可以通过这种方式模拟,对应的逻辑写在useEffect第一个参数函数的返回值函数中。
如果我们在函数组件中想要监听props变化,也可以使用useEffect。我们只需将props注入依赖数组即可,这样可以在props变化时对组件内部维护的数据状态进行更新。
当useEffect中只传第一个参数时,在每次的函数组件执行的时候都会调用一次,可以利用这个特性来模拟componentDidUpdate。
State 和 Props
这两类数据的根本区别就在于,一个是组件自身维护的数据,一个是来自于组件外部的数据。这两部分数据都可能被render所使用并渲染出视图。
在类组件中,state通过setState改变;在函数组件中则使用useState进行组件内部状态的创建和维护。props数据由父组件传入子组件,这点不管是类组件还是函数组件都是一样的。
关于类组件的setState,有一些值得注意的点:
setState有两个参数,第一个可以是对象,也可以是函数,用于状态的更新;第二个是回调,即更新完状态后的回调。setState根据第一个参数的类型不同,相应的逻辑也有区别。第一个参数为函数类型,如:
this.setState((state, props) => { return { name: "XiaoMing" }; });函数的参数为当前的
state和props,返回值对象会和旧的state合并后进行state的更新。第一个参数为对象类型,如:
this.setState({ name: "XiaoMing", });传入的对象将会和旧的
state合并后进行state的更新。
setState的批量更新机制:例:类组件中的一个方法有如下的更新代码:
//假设 this.state.number 为 1 this.setState( { number: this.state.number + 1, }, () => { console.log(this.state.number); } ); console.log(this.state.number); this.setState( { number: this.state.number + 1, }, () => { console.log(this.state.number); } ); console.log(this.state.number); this.setState( { number: this.state.number + 1, }, () => { console.log(this.state.number); } ); console.log(this.state.number); // 结果: 0 0 0 1 1 1可以看到,虽然调用的多次
setState,最后state中的number值还是为 1。这是因为React中存在State批量更新的规则,几次对state的更新被合并为 1 次。注意,批量更新的规则在异步的代码如
Promise、setTimeout中不管用,如果要在这些异步代码中使用批量更新,则需手动调用ReactDOM提供的unstable_batchedUpdates方法。另外,在这个机制下可以使用
flushSync提升更新的优先级,当一个对state的更新在flushSync内部调用,这个更新的优先级是比较高的。如:this.setState({ a: "xiaohong" }); ReactDOM.flushSync(() => { this.setState({ a: "xiaoming" }); }); this.setState({ a: "xiaohuang" });执行上面代码,
state会有两次更新,第一次是触发了flushSync,立马执行更新,之前的setState会被合并,a变为xiaoming;第二次是其他两行代码出发的更新,根据批量更新的规则,a变为xiaohuang。
关于state和props,还有一个经常被提及的概念:受控组件和非受控组件。其实很好理解,其区别在于控制组件中渲染数据的主体是哪个。如果组件渲染数据取决于其自身维护的状态,那就是非受控组件;如果组件渲染依赖于外部数据如props里的数据,即为受控组件。理解完这些后,啥时候该用,啥时候不该用心里也有数了。
组件通信
一般在有组件系统的框架中,我们都会探讨关于组件间通信的问题。
React的组件通信方式大致有下面几种:
props+callbackref状态管理(
redux、mobx)eventbuscontext
props + callback
props + callback比较常用于父子组件通信,父组件通过props向子组件传值,子组件中也可以通过调用父组件传入的callback(实际上也是props的数据)来反馈数据给父组件。如:
function Father() {
return <Son callback={(e) => {
console.log(e)
}/>
}
function Son({ callback }) {
return <button onClick={ callback }></button>
}ref
ref即reference的缩写,任何需要被引用的数据都可以保存在ref中,包括组件实例。
创建组件实例ref的形式有三种:
传入字符串:使用时通过
this.refs.xxx的格式获取对应的元素(已不推荐使用)传入对象:传入
ref的对象是使用React.createRef()方式创建出来,使用时获取到创建的对象中存在current属性即为对应的元素,如:class MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } render() { return <div ref={this.myRef} />; } }传入函数,该函数会在
DOM被挂载时进行回调,这个函数会传入一个元素对象,可以自己保存,使用时,直接拿到之前保存的元素对象即可。如:class MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } render() { return <div ref={(element) => (this.myref = element)} />; } }使用
useRef():函数组件使用ref的方式,如:function App(props) { const myref = useRef(); return ( <> <div ref={myref}></div> </> ); }
对于函数组件而言,使用ref时,要配合forwardRef和useImperativeHandle一起用。因为这种用法还是比较常见的,所以写了下面这个小 demo:
const { useState, useRef, forwardRef, useImperativeHandle } = React;
const Son = forwardRef((props, ref) => {
const [message, setMessage] = useState("");
useImperativeHandle(ref, () => {
return {
setMessage,
};
});
return (
<div class="son">
<h1>子组件</h1>
<div>子组件内部状态message:{message}</div>
</div>
);
});
export default () => {
const sonRef = useRef(null);
const changeInput = (e) => {
sonRef.current.setMessage(e.target.value);
};
return (
<div class="father">
<h1>父组件</h1>
<span>父组件input: </span>
<input class="input" onChange={changeInput} />
<Son ref={sonRef} />
</div>
);
};.father,
.input {
background: white;
color: black;
}
.father {
padding: 20px;
}
.son {
margin-top: 20px;
border: 1px solid black;
padding: 10px;
}状态管理
可以使用状态管理工具来进行组件之间的传值,后面再细说。
context
React的Context是一种在组件树中共享数据的方法,可以避免通过props层层传递数据的繁琐操作。通过Context,我们可以将某个组件下需要共享的数据传递给所有子组件,无论它们在组件树中的哪个位置,而不需要一层层地传递属性。
注意
使用Consumer的组件必须是Provider的后代组件,否则无法获取共享的数据。如果在组件树中没有找到对应的Provider,Consumer会使用defaultValue。
下面是一个函数组件使用context的demo:
const { useState, useRef, useContext, createContext } = React;
const Context = createContext();
const Son = (props) => {
const value = useContext(Context);
return (
<div class="son">
<h1>子组件{props.index || 0}</h1>
<div>{value}</div>
</div>
);
};
export default () => {
const [message, setMessage] = useState("");
return (
<Context.Provider value={message}>
<div class="father">
<h1>父组件</h1>
<span>输入数据:</span>
<input class="input" onChange={(e) => setMessage(e.target.value)} />
<Son index="1" />
<Son index="2" />
</div>
</Context.Provider>
);
};.father {
padding: 10px;
background: white;
color: black;
}
.input {
background: white;
color: black;
}
.son {
margin-top: 10px;
padding: 10px;
border: 1px solid black;
}当然,类组件也可以用。不过因为我不喜欢写类组件,所以就没写demo,只贴代码,代码是从别人那里偷的:
const ThemeContext = React.createContext(null);
const ThemeProvider = ThemeContext.Provider; //提供者
const ThemeConsumer = ThemeContext.Consumer; // 订阅消费者
// 类组件 - contextType 方式
class ConsumerDemo extends React.Component {
render() {
const { color, background } = this.context;
return <div style={{ color, background }}>消费者</div>;
}
}
ConsumerDemo.contextType = ThemeContext;
const Son = () => <ConsumerDemo />;
export default function ProviderDemo() {
const [contextValue, setContextValue] = React.useState({
color: "#ccc",
background: "pink",
});
return (
<div>
<ThemeProvider value={contextValue}>
<Son />
</ThemeProvider>
</div>
);
}高阶组件 HOC
HOC(Higher-Order Component)是指一个函数,接受一个组件作为参数并返回一个新的组件。HOC可以在不修改现有组件代码的情况下添加新的功能,例如可以将逻辑代码和状态管理代码从组件中抽离出来,提高代码复用性。
简单来说,HOC就是一个包装器,传入一个组件,返回一个增强版组件,用于增强拓展组件的功能而无需对传入组件的代码进行侵入式的修改。
HOC有如下优势:
可以对现有组件进行包装,使其具有更多的功能,而不需要修改该组件的代码。
可以将相同的逻辑应用于多个组件,从而实现代码的复用。
可以将多个 HOC 组合在一起,形成更复杂的功能。
HOC有如下劣势:
HOC 可能会导致代码的复杂性增加,因为它们需要嵌套在组件中。
可能会出现命名冲突问题,因为 HOC 可能会添加新的属性或方法到组件中。
私以为React逻辑复用的灵魂,一个是HOC,另一个是React Hook。所以下面我们来聊聊React Hook。
React Hook
在没有React Hook之前,函数组件能够做的只是接受Props、渲染UI,以及触发父组件传过来的事件。React Hook让函数组件也能做类组件的事情,有自己的状态,可以处理一些副作用,能获取 ref ,也能做数据缓存。
从React团队的开发方向来看,虽然其在文档上表明并没有移除class组件的意思,不过也看得出官方更推荐使用函数组件。可见:动机 | Hook | React 中文文档
使用React Hook要留意下面几个点:
以
use开头的函数均会被React视作一个Hook(官方提供的linter有这个规则)仅能在函数组件或者其他
Hook内部被使用不要在循环,条件或嵌套函数中调用
Hook, 确保总是在你的React函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。
React Hook有如下优点:
可以将状态逻辑和 UI 逻辑分离,使代码更加清晰易懂。
可以将相同的逻辑应用于多个组件,从而实现代码的复用。
可以使用自定义 hooks,将复杂的逻辑抽象出来,使代码更加模块化。
官方提供了不少hooks,下面摘取几个比较常用的做讲解:
useState
useState可以让函数组件具有状态(state)的能力。它接收一个初始值作为参数,返回一个包含state和更新state的函数的数组。每次调用更新函数时,React会重新渲染组件,并重新计算组件的UI。
一个简单的调用示例如下:
const [state, setState] = useState(initialState);其中,state表示当前状态的值,initialState表示初始值。setState是一个更新state的函数,它可以接收一个新的值,并触发组件的重新渲染。
useState的特点包括:
可以在函数组件中添加状态,避免了使用类组件的繁琐;
可以多次使用
useState,每个state之间是独立的;每次修改
state都会重新渲染组件,但React会尽可能地优化性能,避免不必要的渲染;更新
state可以使用函数式更新,避免了异步更新带来的问题。
useReducer
useReducer用于管理组件中的状态。它的作用类似于useState,可以用来更新组件中的状态。但是,相较于useState,useReducer更适合处理复杂的状态逻辑,例如多个状态之间的关系、状态的计算逻辑等。
useReducer接受两个参数:reducer函数和初始状态。reducer函数接受两个参数,一个是当前状态,一个是操作指令,根据操作指令对当前状态进行更新,并返回新的状态。通过调用dispatch函数,组件可以将操作指令传递给reducer函数,从而更新状态。
下面是一个简单的demo:
const { useReducer } = React;
const initialState = {
count: 0,
};
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "decrement":
return { ...state, count: state.count - 1 };
default:
throw new Error("Invalid action type");
}
}
export default () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h1>Count: {state.count}</h1>
<div style={{ display: "flex", columnGap: 10 }}>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
</div>
);
};我们可以用useState实现一个useReducer:
import { useState } from "react";
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}useEffect
useEffect用于在组件渲染完成后执行一些副作用操作,例如异步数据请求、DOM 操作等。
useEffect接受两个参数,第一个参数是一个回调函数,用于定义需要执行的副作用操作,可以在这个函数的返回值指定一个函数,指定的函数会在组件卸载阶段执行;第二个参数是一个数组,用于指定副作用操作依赖的数据,当这些数据发生变化时,useEffect才会重新执行。
关于使用useEffect模拟生命周期的话题,前面提过了所以不再重复提了。
注意
在React中有另一个hook经常会拿来和useEffect做比较,那就是useLayoutEffect。
这两个hook函数的执行时机不一样,useEffect被异步调度,当页面渲染完成后再去执行,不会阻塞页面渲染。 useLayoutEffect是在commit阶段新的DOM准备完成,但还未渲染到屏幕之前,同步执行。
当useEffect里面的操作需要处理DOM,并且改变页面的样式,建议使用useLayoutEffect,否则可能会出现出现闪屏问题。
useMemo 和 useCallback
这两个Hook都是用做缓存的,放在一起说。
useMemo用于缓存计算结果,避免重复计算。useMemo 接收两个参数,第一个参数是计算函数,第二个参数是依赖项数组。只有当依赖项数组中的值发生变化时,计算函数才会重新执行。
useCallback用于缓存函数,避免重复创建。useCallback 接收两个参数,第一个参数是回调函数,第二个参数是依赖项数组。只有当依赖项数组中的值发生变化时,回调函数才会重新创建。
可以吧useCallback看作一种语法糖,因为其作用使用useMemo也能实现,不过就是麻烦点。下面是基于useMemo实现的useCallback:
import { useMemo } from "react";
function useCallback(callback, dependencies) {
const memoizedCallback = useMemo(() => callback, dependencies);
return memoizedCallback;
}useRef
useRef是 React 中的一个 Hook 函数,它可以创建一个可变的引用对象。useRef返回一个对象,该对象的current属性包含一个变量,可以存储任何值。与useState不同的是,useRef不会引起组件重新渲染。
useRef的主要作用是保存组件中的状态,以供后续的组件使用,也可以用来获取DOM元素和组件实例的引用,后者在实际开发中用的比较多。
下面是一个简单的示例:
const { useRef } = React;
export default () => {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<div style={{ display: "flex", columnGap: 10 }}>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>Focus Input</button>
</div>
);
};useContext
在前面组件通信的内容中我们了解了React的Context API,useContext的作用就是允许我们在函数组件中消费Context。
useContext接收一个Context对象并返回该Context的当前值。如果我们使用了useContext,则当Context的值更改时,组件将会重新渲染。
关于useContext的用法,可以看组件通信章节中的示例。
闭包陷阱
所谓闭包陷阱,比较常发生的场景是在使用性能优化相关的hook函数如:useMemo、useCallback。在内部包裹的函数于外部的state形成闭包,而依赖数组却没有包含该值,导致调用缓存的函数时,函数中获取到的 state值不是最新的。如果依赖数组包含该值,而这个值经常变动,又会失去使用useMemo和useCallback进行性能优化的意义。
解决这个问题可以从两个方面入手:
非必要不使用
useMemo和useCallback进行性能优化利用
useRef的特性,把函数放在ref里,如:const App = () => { const [name,setName] = useState(""); const fnRef = useRef(); fnRef.current = function() { console.log(name) } const onClick = useCallback(() => { fnRef.current(); },[]) return ( <button onClick={onClick}>Click</button> ) }
路由工具(React Router)
React的路由工具就是React Router。
React Router 是一个用于 React 应用程序中的路由库,可以帮助用户实现单页应用程序(SPA),同时也可以方便地管理应用程序的 URL 和视图之间的映射关系。
作为一个工具,我们确实不需要对它有完整的了解,要用的时候查文档就行。不过面试的话还是会问,比如React Router的几种模式以及区别、如何实现hash和browser模式等。这边仅针对一些比较常用的点做讲解。
常用组件和 hooks
下面是React Router比较常用的组件:
<BrowserRouter>:用于包裹整个应用程序,并处理路由匹配和导航。<Route>:用于定义路由匹配规则和渲染相应的组件。<Link>:用于在应用程序中创建导航链接,使用户能够浏览到不同的页面。<Switch>:用于包裹多个<Route>组件,只匹配第一个匹配的路由规则。<Redirect>:用于重定向用户到指定的URL。<Params>:用于从URL中提取参数,并将参数传递给路由组件。<Outlet>:用于渲染当前匹配的路由组件。<Navigate>:用于在路由组件之间导航,而无需使用<Link>组件
React Router v6的常用hooks主要有以下几个:
useRoutes():用于定义路由规则和路由组件。如:import { useRoutes } from "react-router-dom"; const routes = [ { path: "/", element: <Home /> }, { path: "/about", element: <About /> }, { path: "/users", element: <Users />, children: [ { path: "/", element: <UsersList /> }, { path: ":id", element: <UserDetails /> }, ], }, ]; const App = () => { const routing = useRoutes(routes); return <div>{routing}</div>; };useNavigate():用于进行编程式导航。如:import { useNavigate } from "react-router-dom"; const Home = () => { const navigate = useNavigate(); const handleClick = () => { navigate("/about"); }; return ( <div> <h1>Home</h1> <button onClick={handleClick}>Go to About</button> </div> ); };useParams():用于获取路由参数。如:import { useParams } from "react-router-dom"; const UserDetails = () => { const { id } = useParams(); return ( <div> <h1>User Details</h1> <p>ID: {id}</p> </div> ); };useLocation():用于获取当前路由的位置信息。如:import { useLocation } from "react-router-dom"; const About = () => { const location = useLocation(); return ( <div> <h1>About</h1> <p>Current location: {location.pathname}</p> </div> ); };useMatch():用于获取当前路由的匹配信息。如:import { useMatch } from "react-router-dom"; const UsersList = () => { const match = useMatch("/users"); return ( <div> <h1>Users List</h1> {match && <p>Matched path: {match.path}</p>} </div> ); };
路由模式
React Router包括下面几种路由模式:
HashRouter:基于URL中的hash(#)来实现路由,不会向服务器发送请求,适合单页面应用(SPA)。在 URL 中的hash发生变化时,会触发浏览器的hashchange事件,React Router会根据新的hash值来匹配对应的路由。BrowserRouter:基于HTML5的history API实现路由,可以更自然地呈现URL,不需要#。但是需要后端支持,否则在刷新页面时会出现404错误。MemoryRouter:将历史记录存储在内存中,适合测试或其他需要在内存中进行的场景。StaticRouter:用于服务器端渲染,接收一个location和context对象作为props,根据这些参数来决定渲染哪个路由。
编程式导航
react-router-dom v6版本移除了useHistory和withRouter,增加了useNavigate。所以相应的编程式导航和之前版本有所不同。下面的内容均以v6版本的使用为主。
在v6版本中,如果想在类组件中使用编程导航,必须将该组件包裹在Router组件中。这样以后,可以通过props获取history对象,然后使用push或replace方法来实现编程导航。例如:
import { BrowserRouter as Router } from "react-router-dom";
ReactDOM.render(
<Router>
<MyComponent />
</Router>,
document.getElementById("root")
);对应组件MyComponent的使用示例:
import React, { Component } from "react";
class MyComponent extends Component {
handleClick = () => {
this.props.history.push("/path");
};
render() {
return <button onClick={this.handleClick}>跳转到/path</button>;
}
}
export default MyComponent;在以上代码中,MyComponent类组件通过props获取了history对象,并在handleClick函数中使用push方法实现了编程导航。在render函数中,按钮的点击事件会触发 handleClick 函数,从而跳转到指定的路径。
对于函数组件而言,在v6要使用useNavigate替代老版本的useHistory。
在项目中为了使用更方便,可以对useNavigate进行封装:
import { useNavigate } from "react-router-dom";
/**
* 封装 useNavigate
* @returns
*/
export const useRouter = () => {
const navigation = useNavigate();
return {
push: (path: string, option?: { state?: any }) => navigation(path, option),
replace: (path: string, option?: { state?: any }) =>
navigation(path, { ...option, replace: true }),
go: (delta: number) => navigation(delta),
};
};懒加载
React可以利用lazy和Suspense实现路由懒加载,这也是React Router常用的优化手段:
import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);
}
export default App;状态管理
React本身并没有提供完整的状态管理方案,而只是提供了一些基本的状态管理方法,如state和props等。在实际开发中,我们可能会遇到包括但不限于以下场景:
应用状态复杂,需要对状态进行细粒度的控制和管理。
应用状态需要共享给多个组件或者页面使用。
应用需要进行异步操作,如网络请求或定时器等。
虽然没有状态管理工具也能实现这些场景,但是代码的层面就惨不忍睹了,可维护性也会差很多。所以,Redux、Mobx和Zustand等状态管理的工具就应运而生了。
注
状态管理虽然确实好,不过个人还是有些抵触,因为之前在上家公司给一老哥整出心理阴影了,在vue3项目里瞎几把用pinia,最后搞成屎山了,还得我去擦屁股😠😠😠
Flux 架构
在动机 | Redux 中文官网中,提到了Flux架构,实际上,Redux是Flux架构思想的一种具体实现(虽然Redux并不严格遵循Flux)。因此,在学习Redux之前,有必要了解一下flux架构,这能帮助我们理解Redux中数据的流向。
Flux架构是一种基于单向数据流的设计模式,它由四个主要的组件构成:视图(View)、动作(Action)、调度器(Dispatcher)和数据存储器(Store)。
下面的图来自阮一峰老师的博客:

从上图可以看到,数据是单向流动的。其执行流程描述如下:
store监听dispatcher的事件变化,即当dispatcher事件变化会对store数据进行更新view监听store的数据变化,即store数据改变会驱动视图更新action是视图层发出的消息,仅有dispatcher能够处理分发action
而Flux架构的单向数据流设计,使状态的变化变得可预测。即:store中数据变化仅可能是dispatcher处理分发action后进行的。这种设计简化了代码的逻辑,也能帮助开发者更快定位数据出错的原因。
关于Flux架构,阮一峰老师的文章Flux 架构入门教程 - 阮一峰的网络日志 (ruanyifeng.com)写的很好。
Redux
Redux 是一个使用叫作 "actions" 的事件去管理和更新应用状态的模式和工具库。 它以集中式 Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。
在Flux架构中,我们有Action、Dispatcher、Store和View四个部分。在Redux中也有类似的概念:
Action:Redux中的Action是一个包含type和payload属性的普通对象,用来描述对状态的修改操作。Reducer: 在Redux中,Reducer是一个纯函数,用来根据Action来修改应用程序状态的状态树。Store:Redux中的Store是一个保存应用程序状态的容器。它可以订阅状态修改事件,并在状态变化时通知View进行更新。View: 在Redux中,View是指 React 组件。React组件通过connect函数连接到Redux的Store中,通过props注入的方式获得应用程序的状态和修改状态的方法。
下面是一个简单的使用示例,它涵盖了Redux的基本用法:
import { createStore } from "redux";
// 定义初始状态
const initialState = {
count: 0,
};
// 定义Reducer
function counterReducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
// 创建Store
const store = createStore(counterReducer);
// 订阅状态变化事件
store.subscribe(() => {
console.log(store.getState());
});
// 派发Action
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "DECREMENT" });我们可以将dispatch理解为action的触发器。当你调用dispatch函数,并传入一个action对象时,这个action对象会被派发到reducer中进行处理。reducer会根据action的类型和payload属性来修改store中的状态。之后,所有订阅store的组件(使用connect包裹的组件)都会收到store的更新通知,从而重新渲染组件以反映最新的状态。
实际上,我们一般会在页面或组件里面使用到Redux Store的数据,这时候就要使用到connect函数。它是将React组件和Redux store连接起来的重要工具,可以将store中的数据注入到React组件中,使得组件可以访问store中的数据,并且可以监听store的变化,实现视图的自动更新。基本用法如下:
connect(mapStateToProps, mapDispatchToProps)(Component);其中,mapStateToProps和mapDispatchToProps是两个参数函数,分别用于将store中的state和dispatch函数映射到组件的props属性中。Component是要被连接的React组件。
mapStateToProps函数的作用是将store中的state数据映射到组件的props属性中。它接受一个state参数,表示Redux store中的state数据。在函数内部,我们可以根据需要从state中取出需要的数据,并将其以props的形式传递给组件。
在connect函数执行后,它会返回一个新的高阶组件,这个高阶组件会将包装后的原组件返回,并将store中的数据以props的形式传递给原组件。当store中的数据发生变化时,高阶组件会自动更新原组件,从而实现数据自动更新的效果。
下面是一个示例代码,演示了如何使用 connect 函数将 store 中的数据注入到 React 组件中:
import React from "react";
import { connect } from "react-redux";
class MyComponent extends React.Component {
render() {
const { name, age } = this.props;
return (
<div>
<p>My name is {name}.</p>
<p>I am {age} years old.</p>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
name: state.name,
age: state.age,
};
};
export default connect(mapStateToProps)(MyComponent);在这个示例中,我们定义了一个MyComponent组件,并使用connect函数将其与Redux store连接起来。我们通过mapStateToProps函数将store中的name和age数据映射到组件的props属性中。在组件内部,我们可以通过this.props.name和this.props.age访问这些数据。当store中的数据发生变化时,组件会自动更新。
在Redux中还有中间件(Middleware)的概念,它是一个用于增强Redux的功能的机制。在dispatch函数发送action到reducer之前,便是中间件的执行时机。中间件可以用于很多场景,例如:
异步操作:通过中间件可以实现异步操作,例如异步请求数据、定时器等操作。
日志记录:通过中间件可以记录
action、state等信息,方便调试和排查问题。错误处理:通过中间件可以捕获异常,避免应用程序崩溃。
权限控制:通过中间件可以实现权限控制,例如判断用户是否登录等操作。
下面是一个简单的打印日志的中间件示例代码:
const loggerMiddleware = (store) => (next) => (action) => {
console.log("dispatching", action);
const result = next(action);
console.log("next state", store.getState());
return result;
};使用中间件时,在createStore通过applyMiddleware进行注册:
const store = createStore(reducer, applyMiddleware(loggerMiddleware));