【react】Redux的设计思想与工作原理

Redux 的设计理念

Redux 的设计采用了 Facebook 提出的 Flux 数据处理理念

在 Flux 中通过建立一个公共集中数据仓库 Store 进行管理,整体分成四个部分即: View (视图层)、Action (动作)、Dispatcher (派发器)、Store (数据层)

如下图所示,当我们想要修改仓库的数据时,需要从 View 中触发 Action,由 Dispatcher 派发到 Store 修改数据,从而驱动视图更新

这种设计的好处在于其数据流向是单一的,数据的修改一定是会经过 Action、Dispatcher 等动作才能实现,方便预测、维护状态的流向。

当我们了解了 Flux 的设计理念后,便可以照葫芦画瓢了。

如下图所示,在 Redux 中同样需要维护一个公共数据仓库 Store, 而数据流向只能通过 View 触发 Action、 Reducer更新派发, Store 改变从而驱动视图更新

工作原理

当我们了解了 Redux 的设计理念后,趁热打铁炫一波 Redux 的工作原理,我们知道使用 Redux 进行状态管理的第一步就是需要先创建数据仓库 Store, 也就会需要调用 createStore 方法。那我们就先拿 createStore 开炫。

createStore

从 Redux 源码中我们不难看出,createStore 接收 reducer初始化state中间件三个参数,当执行 createStore 时会记录当前的 state 状态,并返回 store 对象,包含 dispatch、subscribe、getState 等属性。

其中

  • dispatch: 用来触发 Action
  • subscribe: 当 store 值的改变将触发 subscribe 的回调
  • getState: 用来获取当前的 state 状态。

getState 比较简单,直接返回当前的 state 状态,接下来我们将着重了解 dispatch 与 subscribe 的实现。

function createStore(reducer, preloadedState, enhancer) {let currentReducer = reducer // 记录当前的 reducerlet currentState = preloadedState // 记录当前的 statelet isDispatching = false // 是否正在进行 dispatchfunction getState() {return currentState // 通过 getState 获取当前的 state}// 触发 actionfunction dispatch(action: A) {}function subscribe(listener: () => void) {}// 初始化 statedispatch({ type: ActionTypes.INIT } as A)// 返回一个 sttoreconst store = {dispatch: dispatch as Dispatch<A>,subscribe,getState}return store
}

dispatch

在 Redux 中, 修改数据的唯一方式就是通过 dispatch,而 dispatch 接受一个 action 对象作为参数,执行 dispatch 方法,将生成新的 state,并触发监听事件。

function dispatch(action) {// 如果已经在触发中,则不允许再次出发 dispatch (禁止套娃)// 例如:在 reducer 中触发 dispatchif (isDispatching) {throw new Error('Reducers may not dispatch actions.')}try {// 上锁isDispatching = true// 调用 reducer,获取新的 statecurrentState = currentReducer(currentState, action)} finally {isDispatching = false}// 触发订阅事件const listeners = (currentListeners = nextListeners)listeners.forEach(listener => {listener()})return action}

subscribe

在 Redux 中, 可以通过 subscribe 方法来订阅 store 的变化, 一旦 store 发生了变化, 就会执行订阅的回调函数

可以看到 subscribe 方法接收一个回调函数作为参数, 执行 subscribe 方法将会返回一个 unsubscribe 函数, 用于取消订阅


function subscribe(listener: () => void) {if (isDispatching) {throw new Error()}let isSubscribed = true // 防止调用多次 unsubscribeensureCanMutateNextListeners() // 确保 nextListeners 是 currentListeners 的快照,而不是同一个引用const listenerId = listenerIdCounter++nextListeners.set(listenerId, listener) //nextListeners 添加订阅事件// 取消订阅事件return function unsubscribe() {if (!isSubscribed) {return}if (isDispatching) {throw new Error()}isSubscribed = falseensureCanMutateNextListeners(); // 如果某个订阅事件执行了 unsubscribe, nextListeners 创建了新的内存地址,而原先的listeners 依然保持不变 (dispatch 方法中的312 行)nextListeners.delete(listenerId)currentListeners = null}}

ensureCanMutateNextListeners 与 currentListeners 的作用

承接上文,在 subscribe 中不管是注册监听还是取消监听都会调用 ensureCanMutateNextListeners 的方法,那么这个方法是做什么的呢?

从函数的逻辑上不难得出答案:

ensureCanMutateNextListeners 确保 nextListeners 是 currentListeners 的快照,而不是同一个引用

function ensureCanMutateNextListeners() {if (nextListeners === currentListeners) { // currentListeners 用来确保循环的稳定性nextListeners = new Map()currentListeners.forEach((listener, key) => {nextListeners.set(key, listener)})}
}

在 dispatch 或者 subscribe 函数中,都是通过 nextListeners 触发监听,那为何还需要使用 currentListeners?

这里就不卖关子了,这里的 currentListeners 用于确保在 dispatch 中 listener 的数量不会发生变化, 确保当前循环的稳定性。

请看下面的例子👇

const a = store.subscribe(() => {/* a */
});
const b = store.subscribe(() => a());
const c = store.subscribe(() => {/*/ c */
});
store.dispatch(action);

上面的代码在 Redux 中是被允许的, 通过 subscribe 注册监听函数 a、b、c,此时 nextListeners 指向 [a, b, c]

当执行 dispatch 时, listener、currentListeners、nextListeners 将指向地址 [a, b, c];

// dispatch 触发监听事件的逻辑
// 触发订阅事件 
const listeners = (currentListeners = nextListeners)
listeners.forEach(listener => { listener() })

当执行到 b 监听函数时,将解绑 a 函数的监听事件,如果直接修改 nextListeners, 在循环中操作数组是非常危险的事情, 因此借助 ensureCanMutateNextListeners、currentListeners 为 nextListeners 开辟了新的内存地址,对 nextListeners 的操作将不影响 listener。

实现一个 mini react-redux

上文我们说到,一个组件如果想从 store 存取公用状态,需要进行四步操作:

  1. import引入store
  2. getState获取状态
  3. dispatch修改状态
  4. subscribe订阅更新

代码相对冗余,我们想要合并一些重复的操作,而 react-redux 就提供了一种合并操作的方案:react-redux提供 Providerconnect 两个API, Provider 将 store 放进 this.context 里,省去了 import 这一步, connect将 getState、dispatch 合并进了this.props,并自动订阅更新,简化了另外三步,下面我们来看一下如何实现这两个API:

Provider

Provider 组件比较简单,接收 store 并放进全局的 context 对象,使 store 可用于任何需要访问 Redux store 的嵌套组件

import React, { createContext } from 'react';
let StoreContext;
const Provider = (props) => {StoreContext = createContext(props.store);return <StoreContext.Provider value={props.store}>{ props.children }</StoreContext.Provider>
}

connect

下面我们来思考一下如何实现 connect ,我们先回顾一下connect的使用方法

connect(mapStateToProps, mapDispatchToProps)(App)

connect 接收 mapStateToProps、mapDispatchToProps 两个函数,然后返回一个高阶函数, 最终将 mapStateToProps、mapDispatchToProps 函数的返回值通过 props 形式传递给 App 组件

我们直接放出connect的实现代码,并不复杂:

import React, { createContext, useContext, useEffect } from 'react';
export function connect(mapStateToProps, mapDispatchToProps) {return function (Component) {const connectComponent: React.FC = (props) => {const store = useContext(StoreContext);const [, updateState] = useState();const forceUpdate = useCallback(() => updateState({}), []);const handleStoreChange = () => {// 强制刷新forceUpdate();}useEffect(() => {store.subscribe(handleStoreChange)}, [])return (<Component// 传入该组件的props,需要由connect这个高阶组件原样传回原组件  { ...(props) }// 根据 mapStateToProps 把 state 挂到 this.props 上 { ...(mapStateToProps(store.getState())) }// 根据mapDispatchToProps把dispatch(action)挂到this.props上{ ...(mapDispatchToProps(store.dispatch)) }/>)}return connectComponent;}
}

可以看出 connect 通过 useContext 实现和 store 的链接,将 state 作为第一个参数传给 mapStateToProps、将 dispatch 作为第一个参数传递给 mapDispatchToProps,最终将结果通过 props 形式传递给子组件。

其实 connect 这种设计,是装饰器模式的实现,所谓装饰器模式,简单地说就是对类的一个包装,动态地拓展类的功能。这里的 connect 以及 React 中的高阶组件(HoC)都是这一模式的实现。

对类的装饰常用于拓展类的功能,对类中函数的装饰常用于 AOP 切面

@decorator
class A {}// 等同于class A {}
A = decorator(A) || A;

装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。 如果一定要装饰函数,可以使用高阶函数

mini react-redux

通过上文,我们了解了 Provider 与 connect 的实现,我们可以写个 mini react-redux 来测试一下

1 创建如下目录结构

image.png

2 实现 createStore 函数 创建一个 createStore.ts 文件,createStore 最终将返回 store 对象,包含 getState、dispatch、subscribe

export const createStore = (reducer: Function) => {let currentState: undefined = undefined;const obervers: Array<Function> = [];function getState() {return currentState;}function dispatch(action: { type: string}) {currentState = reducer(currentState, action);obervers.forEach(fn => fn());}function subscribe(fn: Function) {obervers.push(fn);}dispatch({ type: '@@REDUX/INIT' }); // 初始化 statereturn {getState,dispatch,subscribe}
}

3 实现 reducer

createStore 函数接收一个 reducer 方法,reducer 常用来分发 action, 并返回新的 state

// reducer.ts
const initialState = {count: 0
}export function reducer(state = initialState, action: { type: string}) {switch (action.type) {case 'add': return {...state,count: state.count + 1}case 'reduce':return {...state,count: state.count - 1}default:return initialState;}
}

4 实现 Provider 与 connect

/* eslint-disable react-hooks/rules-of-hooks */
//@ts-nocheck 
import React, { createContext, useContext, useEffect } from 'react';let StoreContext;
const Provider = (props) => {StoreContext = createContext(props.store);return <StoreContext.Provider value={props.store}>{ props.children }</StoreContext.Provider>
}
export default Provider;
export function connect(mapStateToProps, mapDispatchToProps) {return function (Component) {const connectComponent: React.FC = (props) => {      const store = useContext(StoreContext);       const [, updateState] = React.useState();      const forceUpdate = React.useCallback(() => updateState({}), []);const handleStoreChange = () => {// 强制刷新forceUpdate();}useEffect(() => {store.subscribe(handleStoreChange)}, [])return (<Component// 传入该组件的props,需要由connect这个高阶组件原样传回原组件   { ...(props) }// 根据 mapStateToProps 把 state 挂到 this.props 上       { ...(mapStateToProps(store.getState())) }// 根据mapDispatchToProps把dispatch(action)挂到this.props上              { ...(mapDispatchToProps(store.dispatch)) }/>)}return connectComponent;}
}

5 修改 main.tsx

// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import  Provider from './react-redux/index.tsx';
import { createStore } from './react-redux/createStore.ts';
import { reducer } from './react-redux/reducer.ts';ReactDOM.createRoot(document.getElementById('root')!).render(<React.StrictMode><Provider store={createStore(reducer)}><App /></Provider></React.StrictMode>,
)

6 修改 App.tsx

// App.tsx
import { useState } from 'react';
import { connect } from './react-redux';const addAction = {type: 'add'
}const mapStateToProps = (state: { count: number }) => {return {count: state.count}
}const mapDispatchToProps = (dispatch: any) => {return {addCount: () => {dispatch(addAction)}}
}interface Props {count: number;addCount: () => void;
}function App(props: Props): JSX.Element {const { count, addCount } = props;return (<div className="App">        { count }        <button onClick={ () => addCount() }>增加</button>      </div>);
}export default connect(mapStateToProps, mapDispatchToProps)(App);

运行项目,点击增加按钮,如能正确计数,我们整个redux、react-redux的流程就走通了。

中间件

在大部分场景下, 我们需要自定义 dispatch 的行为, 在 Redux 中, 我们可以使用 中间件来拓展 dispatch 的功能

类似于 Express 或者 Koa, 在这些框架中,我们可以使用中间件来拓展 请求 和 响应 之间的功能

而 Redux 中间件的作用是在 action 发出之后, 到达 reducer 之前, 执行一系列的任务

image.png

在 Redux 中我们可以通过 applyMiddleware 生成一个强化器 enhancer 作为 createStore 的第二个参数传递。

import { createStore, applyMiddleware } from 'redux'  
import rootReducer from './reducer'  
import { print1, print2, print3 } from './exampleAddons/middleware'  const middlewareEnhancer = applyMiddleware(print1, print2, print3)  // Pass enhancer as the second arg, since there's no preloadedState  
const store = createStore(rootReducer, middlewareEnhancer)  export default store

正如它们的名称所示,每个中间件在调度操作时都会打印一个数字

import store from './store'  store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })  
// log: '1'  
// log: '2'  
// log: '3'

在这个例子中,当触发 dispatch 的内部执行顺序如下:

  • The print1 middleware (which we see as store.dispatch)
  • The print2 middleware
  • The print3 middleware
  • The original store.dispatch
  • The root reducer inside store

实现一个中间件

从上文得知, 我们了解了如何使用中间件, 接下来我们将实现一个中间件。

在 Redux 中,中间件其实是由三个嵌套函数组成

function exampleMiddleware(storeAPI) {  return function wrapDispatch(next) {  return function handleAction(action) {  // Do anything here: pass the action onwards with next(action),  // or restart the pipeline with storeAPI.dispatch(action)  // Can also use storeAPI.getState() here  return next(action)  }  }  
}

最外层函数 exampleMiddleware 将会被 applyMiddleware 调用,并传入 storeAPI 对象( 形如 {dispatch, getState} ),

const loggerMiddleware = storeAPI => next => action => {  console.log('dispatching', action)  let result = next(action) //调用下一个中间件或最终的dispatchconsole.log('next state', storeAPI.getState())  return result  
}

在这个中间件中:

  1. storeAPI 是传递给中间件的 store 对象,包含 dispatch 和 getState 方法。
  2. next 是指向下一个中间件的函数,如果没有更多中间件,则指向原始的 dispatch 方法。
  3. action 是被分发的 action 对象。

写完 logger 中间件后,我们尝试在 Redux 中使用,如下

import { createStore, applyMiddleware } from "redux";const initialState = {count: 0
}function reducer(state = initialState, action: { type: string}) {switch (action.type) {case 'add': return {...state,count: state.count + 1}case 'reduce':return {...state,count: state.count - 1}default:return initialState;}
}
const logger1 = storeAPI => next => action => {  console.log('logger1 开始');const result = next(action)  console.log('logger1 结束');return result  
}const logger2 = storeAPI => next => action => {  console.log('logger2 开始');const result = next(action)  console.log('logger2 结束');return result  
}const logger3 = storeAPI => next => action => {  console.log('logger3 开始');const result = next(action)  console.log('logger3 结束');return result  
}
const middlewares = applyMiddleware(logger1, logger2, logger3);
const store = createStore(reducer, middlewares);
store.dispatch({ type: 'add' });

最终将打印

从打印的记过来看,如果之前有接触过 Express 或者 Koa 的同学,应该可以很快发现,这个是一个洋葱模型

示例2:

在创建store时,使用applyMiddleware方法将中间件应用到store上

import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers'; // 假设您有一个根 reducer
import loggerMiddleware from './loggerMiddleware'; 
// 导入您实现的 logger 中间件const store = createStore(rootReducer,applyMiddleware(loggerMiddleware)
);

applyMiddleware 的实现原理

从上可知,Redux 提供了一个 applyMiddleware 方法用于将中间件拓展到 dispatch 上

具体是如何拓展的呢?

从源码我们不难看出,最终是通过 compose 也就是利用 reduce 方法,将下一个的中间件函数作为参数,在上一个中间件的函数体内执行。

注意这里传入 compose 内的每一个函数都是一个双层嵌套函数。

applyMiddleware 是一个高阶函数,它接收一个或多个中间件作为参数,并返回一个函数。这个函数接收 createStore 的参数(reducer、初始 state 和可选的 store 增强器),并返回一个新的 createStore 函数。新的 createStore 函数创建的 store 有一个增强的 dispatch 方法,该方法能够依次通过所有中间件。

// applyMiddleware 源码
export default function applyMiddleware(...middlewares
) {// 返回一个接收 createStore为入参的函数return createStore => (reducer, preloadedState) => {// 创建 storeconst store = createStore(reducer, preloadedState)let dispatch: Dispatch = () => {throw new Error('Dispatching while constructing your middleware is not allowed. ' +'Other middleware would not be applied to this dispatch.')}/*** middleware 形如:* ({dispatch, getState}) => next => action => { ... return next(action) }*/const middlewareAPI: MiddlewareAPI = {getState: store.getState,dispatch: (action, ...args) => dispatch(action, ...args)}const chain = middlewares.map(middleware => middleware(middlewareAPI))dispatch = compose(...chain)(store.dispatch)return {...store,dispatch}}
}
  1. compose 函数用于将多个中间件函数组合成一个。它使用 reduce 方法将中间件函数串联起来,形成一个洋葱模型。
  2. applyMiddleware 函数接收中间件数组,并返回一个函数。这个函数接收 createStore 函数和它的参数(reducer、初始 state 和可选的增强器),并返回一个新的 createStore 函数。
  3. 新的 createStore 函数创建的 store 有一个增强的 dispatch 方法,该方法通过所有中间件。
### 中间件在大部分场景下,我们需要自定义 `dispatch` 的行为。
在 Redux 中,我们可以使用中间件来拓展 `dispatch` 的功能。中间件的作用是在 action 发出之后,到达 reducer 之前,执行一系列的任务。
类似于 Express 或 Koa,在这些框架中,我们可以使用中间件来拓展请求和响应之间的功能。#### 实现一个 Logger 中间件```typescript
const loggerMiddleware = storeAPI => next => action => {console.log('Dispatching:', action);let result = next(action); // 调用下一个中间件或最终的 dispatchconsole.log('Next State:', storeAPI.getState());return result;
};
function compose(...funcs) {if (funcs.length === 0) {// infer the argument type so it is usable in inference down the linereturn (arg:) => arg}if (funcs.length === 1) {return funcs[0]}return funcs.reduce((a, b) =>(...args) =>a(b(...args)))
}

模拟洋葱模型

洋葱模型形象地描述了Redux中间件的工作流程。

想象一下,每个中间件都像洋葱的一层皮,action就像一颗子弹,从外向内穿过每一层中间件,然后再由内向外穿出。

在每层中间件中,都可以对action进行处理或修改,然后再传递给下一层。

工作流程

1、action的传递

当一个action被dispatch时,它会首先进入最外层的中间件。

在这个中间件中,可以对action进行处理,然后调用next(action)将action传递给下一层中间件。2、逐层处理

action会依次穿过每一层中间件,每层中间件都可以对action进行处理。

处理完毕后,再调用next(action)将action传递给下一层。

3、到达reducer

当action穿过所有中间件后,最终会到达reducer。

Reducer根据action的类型和当前的状态来计算出一个新的状态。

4、逐层返回

在action被reducer处理后,结果会由内向外逐层返回给每一层中间件。

在每层中间件中,可以对返回的结果进行进一步的处理或修改。

5、最终返回

当action从最内层中间件返回时,整个中间件链的处理流程结束。

最终的结果会被返回给dispatch函数,完成整个action的处理流程。

// 定义三个中间件
const middleware1 = store => next => action => {console.log('中间件1之前');const result = next(action); // 调用下一个中间件或reducerconsole.log('中间件1之后');return result;
};const middleware2 = store => next => action => {console.log('中间件2之前');const result = next(action); // 调用下一个中间件或reducerconsole.log('中间件2之后');return result;
};const middleware3 = store => next => action => {console.log('中间件3之前');const result = next(action); // 调用reducer(因为没有下一个中间件了)console.log('中间件3之后');return result;
};// 应用中间件到Redux store
const store = createStore(reducer,applyMiddleware(middleware1, middleware2, middleware3)
);// 派发一个action
store.dispatch({ type: 'SOME_ACTION' });

输出结果

中间件1之前
中间件2之前
中间件3之前
// reducer的处理结果(假设有日志输出)
中间件3之后
中间件2之后
中间件1之后

拓展

Redux洋葱模型和Koa洋葱模型的区别

Redux洋葱模型

1、概念

Redux洋葱模型描述了中间件如何围绕action和reducer进行工作。

每个中间件都像洋葱的一层皮,action从外向内穿过每一层中间件,然后再由内向外穿出。

2、执行流程

当一个action被dispatch时,它会首先进入最外层的中间件。

中间件可以对action进行处理或修改,然后调用next(action)将action传递给下一层中间件。

最终,action会到达reducer进行处理,然后结果会由内向外逐层返回给每一层中间件。

3、应用场景

Redux中间件常用于异步操作、日志记录、错误报告等功能。

例如,redux-thunk、redux-saga等中间件允许在Redux中进行异步操作。

Koa洋葱模型

1、概念

Koa洋葱模型描述了中间件如何围绕HTTP请求和响应进行工作。

每个中间件都像洋葱的一层皮,请求从外向内穿过每一层中间件,响应则由内向外穿出。

2、执行流程

当一个HTTP请求到达时,它会首先进入最外层的中间件。

中间件可以对请求进行处理或修改,然后调用next()将控制权传递给下一个中间件。

一旦所有的中间件都通过next()调用完成入栈阶段的执行,控制流开始回溯,即从最内部的中间件开始向外部返回响应。

3、应用场景

Koa中间件常用于日志记录、错误处理、身份验证、数据解析等HTTP请求和响应相关的功能。

Koa的中间件机制允许开发者在请求和响应之间添加额外的逻辑,从而实现对HTTP请求和响应的灵活处理。

区别总结

1、应用场景不同

Redux洋葱模型主要应用于Redux状态管理库中的中间件处理,特别是与异步操作和日志记录等相关的功能。

Koa洋葱模型则主要应用于Koa Web框架中的中间件处理,特别是与HTTP请求和响应相关的功能。

2、中间件写法不同

Redux中间件通常是一个函数,它接收store作为参数,并返回一个函数,这个函数再接收next和action作为参数。

Koa中间件则是一个异步函数(在Koa 2.x中使用async/await),它接收ctx(上下文对象)和next作为参数。

3、执行流程细节不同

在Redux洋葱模型中,action在穿过所有中间件后到达reducer进行处理,然后结果再逐层返回给中间件。

在Koa洋葱模型中,请求在穿过所有中间件后控制权开始回溯,中间件可以在这个过程中处理响应或进行任何清理操作。

码字不易,大佬们点点赞

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/65883.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PCB层叠结构设计

PCB层叠结构设计 层叠结构设计不合理完整性相关案例&#xff1a;在构成回流路径时&#xff0c;由于反焊盘的存在&#xff0c;使高速信号回流路径增长&#xff0c;造成信号回流路径阻抗不连续&#xff0c;对信号质量造成影响。 PCB层叠结构实物&#xff1a;由Core 和 Prepreg&a…

【Cesium】七、设置Cesium 加载时的初始视角

文章目录 一、前言二、实现方法2.1 获取点位、视角2.2 设置 三、App.vue 一、前言 在前面的文章 【Cesium】三、实现开场动画效果 中有提到过 虽然也能回到初始点位但是有一个明显的动画过程。下面方法加载时就是在初始点位 没有动画效果&#xff0c;根据需求选择。 本文参考…

Edge安装问题,安装后出现:Could not find Edge installation

解决&#xff1a;需要再安装&#xff08;MicrosoftEdgeWebView2RuntimeInstallerX64&#xff09;。 网址&#xff1a;https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/?formMA13LH#download 如果已经安装了edge&#xff0c;那就再下载中间这个独立程序安装就…

日期时间选择(设置禁用状态)

目录 1.element文档需要 2.禁用所有过去的时间 3.设置指定日期的禁用时间 <template><div class"block"><span class"demonstration">起始日期时刻为 12:00:00</span><el-date-pickerv-model"value1"type"dat…

【《python爬虫入门教程11--重剑无峰168》】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 【《python爬虫入门教程11--selenium的安装与使用》】 前言selenium就是一个可以实现python自动化的模块 一、Chrome的版本查找&#xff1f;-- 如果用edge也是类似的1.chrome…

系统架构风险、敏感点和权衡点的理解

系统架构是软件开发过程中的关键环节&#xff0c;它决定了系统的可扩展性、稳定性、安全性和其他关键质量属性。然而&#xff0c;架构设计并非易事&#xff0c;其中涉及的风险、敏感点和权衡点需要仔细考虑和处理。本文将详细探讨系统架构风险、敏感点和权衡点的概念&#xff0…

leetcode热题100(79. 单词搜索)dfs回溯 c++

链接&#xff1a;79. 单词搜索 - 力扣&#xff08;LeetCode&#xff09; 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的…

用PicGo向Github图床上传图片,然后通过markdown语言显示图片

目录 下载PicGo软件图床GitHub设置在Markdown中使用图片 下载PicGo软件 先进入Pic官网&#xff0c;然后点击下图中的免费下载 然后点击下载下图中PicGo-Setup-2.4.0-beta.9.exe这个可执行软件 图床GitHub设置 点击PicGo中的图床设置&#xff0c;再点击其中的Github&#xff…

bilibili 哔哩哔哩小游戏SDK接入

小游戏的文档 简介 bilibili小游戏bilibili小游戏具有便捷、轻量、免安装的特点。游戏包由云端托管&#xff0c;在哔哩哔哩APP内投放和运行&#xff0c;体验流畅&#xff0c;安全可靠。https://miniapp.bilibili.com/small-game-doc/guide/intro/ 没想过接入这个sdk比ios还难…

Spring Cloud Alibaba2022之Sentinel总结

Spring Cloud Alibaba2022之Sentinel学习 Sentinel介绍 Sentinel是一个面向云原生微服务的流量控制、熔断降级组件。 Sentinel 分为两个部分&#xff1a; 核心库&#xff1a;&#xff08;Java 客户端&#xff09;不依赖任何框架/库&#xff0c;能够运行于所有 Java运行时环 …

HarmonyOS:删除多层ForEach循环渲染的复杂数据而导致的一系列问题

目录 1.页面效果及需求 2.遇到问题时的初始代码及问题 代码 问题 3.状态变化不能深层监听&#xff1f; 解答 4.使用了ObjectLink装饰器后为什么数据仍然无法被监听&#xff1f; Demo 结论 代码修改 5.子组件中定义一个箭头函数&#xff0c;在父组件中通过this.传入方…

leecode188.买卖股票的最佳时机IV

这道题目我在买卖股票III就已经得出规律了&#xff0c;具体可看买卖股票的最佳时机||| class Solution { public:int maxProfit(int k, vector<int>& prices) {int nprices.size();vector<vector<int>> dp(n,vector<int>(2*k1,0));for(int j1;j&l…

如何通过深度学习提升大分辨率图像预测准确率?

随着科技的不断进步&#xff0c;图像处理在各个领域的应用日益广泛&#xff0c;特别是在医疗影像、卫星遥感、自动驾驶、安防监控等领域中&#xff0c;大分辨率图像的使用已经成为了一项不可或缺的技术。然而&#xff0c;大分辨率图像带来了巨大的计算和存储压力&#xff0c;同…

【Spring Boot】SpringBoot自动装配-Import

目录 一、前言二、 定义三、使用说明 3.1 创建项目 3.1.1 导入依赖3.1.2 创建User类 3.2 测试导入Bean 3.2.1 修改启动类 3.3 测试导入配置类 3.3.1 创建UserConfig类3.3.2 修改启动类 3.4 测试导入ImportSelector 3.4.1 创建UseImportSelector类3.4.2 修改启动类3.4.3 启动测试…

操作系统课后题总复习

目录 一、第一章 1.1填空题 1.2单项选择题 1.3多项选择题 1.4判断题 1.5名词解释 1.6简答题 二、第二章 2.1填空题 2.2单项选择题 2.3 多项选择题 2.4判断题 2.5名词解释 2.6简答题 三、第三章 3.1填空题 3.2单项选择题 3.3多项选择题 3.4判断题 3.5名词解…

Debian-linux运维-ssh配置(兼容Jenkins插件的ssh连接公钥类型)

系统版本&#xff1a;Debian 12.5、11.1 1 生成密钥对 可以用云服务商控制台生成的密钥对&#xff0c;也可以自己在客户端或者服务器上生成&#xff0c; 已经有密钥对就可以跳过这步 用户默认密钥文件路径为 ~/.ssh/id_rsa&#xff0c;可以在交互中指定路径&#xff0c;也可…

基于服务器部署的综合视频安防系统的智慧快消开源了。

智慧快消视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;从而大大减少企业级应用约95%的开发成本。国产化人工智能“…

【网络安全实验室】SQL注入实战详情

如果额头终将刻上皱纹&#xff0c;你只能做到&#xff0c;不让皱纹刻在你的心上 1.最简单的SQL注入 查看源代码&#xff0c;登录名为admin 最简单的SQL注入&#xff0c;登录名写入一个常规的注入语句&#xff1a; 密码随便填&#xff0c;验证码填正确的&#xff0c;点击登录…

_使用CLion的Vcpkg安装SDL2,添加至CMakelists时报错,编译报错

语言&#xff1a;C20 编译器&#xff1a;gcc 14.2 摘要&#xff1a;初次使用Vcpkg添加SDL2&#xff0c;出现CMakelists找不到错误、编译缺失main错误、运行失败错误。 CMakelists缺失错误&#xff1a; 使用CLion的Vcpkg安装SDL2时&#xff0c;按照指示把对应代码添加至CMakel…

可解释性:走向透明与可信的人工智能

随着深度学习和机器学习技术的迅速发展&#xff0c;越来越多的行业和领域开始应用这些技术。然而&#xff0c;这些技术的“黑盒”特性也带来了不容忽视的挑战&#x1f3b2;。在许多任务中&#xff0c;尽管这些模型表现出色&#xff0c;取得了相当高的精度&#xff0c;但其决策过…