React Hooks源码解析-剖析useState的执行过程

本文主要是介绍React Hooks源码解析-剖析useState的执行过程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文基于 React 16.8.6 进行讲解

使用的示例代码:

import React, { useState } from 'react'
import './App.css'export default function App() {const [count, setCount] = useState(0);const [name, setName] = useState('Star');// 调用三次setCount便于查看更新队列的情况const countPlusThree = () => {setCount(count+1);setCount(count+2);setCount(count+3);}return (<div className='App'><p>{name} Has Clicked <strong>{count}</strong> Times</p><button onClick={countPlusThree}>Click *3</button></div>)
}

代码非常简单,点击button使count+3,count的值会显示在屏幕上。

一. 前置知识

 

1. 函数组件和类组件

本节参考:How Are Function Components Different from Classes?

本节主要概念:
**

  • 函数组件和类组件的区别

  • React如何区分这两种组件

我们来看一个简单的Greeting组件,它支持定义成类和函数两种性质。在使用它时,不用关心他是如何定义的。

// 是类还是函数 —— 无所谓
<Greeting />  // <p>Hello</p>

如果 Greeting 是一个函数,React 需要调用它。

// Greeting.js
function Greeting() {return <p>Hello</p>;
}// React 内部
const result = Greeting(props); // <p>Hello</p>

但如果 Greeting 是一个类,React 需要先将其实例化,再调用刚才生成实例的 render 方法:

// Greeting.js
class Greeting extends React.Component {render() {return <p>Hello</p>;}
}// React 内部
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>

React通过以下方式来判断组件的类型:

// React 内部
class Component {}
Component.prototype.isReactComponent = {};// 检查方式
class Greeting extends React.Component {}
console.log(Greeting.prototype.isReactComponent); // {}

 

2. React Fiber

本节参考:A cartoon intro to fiber

本节主要概念(了解即可):

  • React现在的渲染都是由Fiber来调度

  • Fiber调度过程中的两个阶段(以Render为界)

Fiber(可译为丝)比线程还细的控制粒度,是React 16中的新特性,旨在对渲染过程做更精细的调整。

产生原因:

  1. Fiber之前的reconciler(被称为Stack reconciler)自顶向下的递归mount/update,无法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,影响体验

  2. 渲染过程中没有优先级可言

React Fiber的方式:

把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。

React Fiber把更新过程碎片化,执行过程如下面的图所示,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

维护每一个分片的数据结构,就是Fiber。

有了分片之后,更新过程的调用栈如下图所示,中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。让线程处理别的事情

Fiber的调度过程分为以下两个阶段:

render/reconciliation阶段 — 里面的所有生命周期函数都可能被执行多次,所以尽量保证状态不变

  • componentWillMount

  • componentWillReceiveProps

  • shouldComponentUpdate

  • componentWillUpdate

Commit阶段 — 不能被打断,只会执行一次

  • componentDidMount

  • componentDidUpdate

  • compoenntWillunmount

Fiber的增量更新需要更多的上下文信息,之前的vDOM tree显然难以满足,所以扩展出了fiber tree(即Fiber上下文的vDOM tree),更新过程就是根据输入数据以及现有的fiber tree构造出新的fiber tree(workInProgress tree)

与Fiber有关的所有代码位于packages/react-reconciler中,一个Fiber节点的详细定义如下:

function FiberNode(tag: WorkTag,pendingProps: mixed,key: null | string,mode: TypeOfMode,
) {// Instancethis.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null;// Fiberthis.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; this.pendingProps = pendingProps;this.memoizedProps = null; this.updateQueue = null;// 重点this.memoizedState = null;this.contextDependencies = null; this.mode = mode;// Effects/** 细节略 **/
}

我们只关注一下this.memoizedState

这个key用来存储在上次渲染过程中最终获得的节点的state,每次render之前,React会计算出当前组件最新的state然后赋值给组件,再执行render— 类组件和使用useState的函数组件均适用。

记住上面这句话,后面还会经常提到memoizedState

有关Fiber每个key的具体含义可以参见源码的注释

 

3. React渲染器与setState

本节参考:How Does setState Know What to Do?

本节主要概念:

  • React渲染器是什么

  • setState为什么能够触发更新

由于React体系的复杂性以及目标平台的多样性。react包只暴露一些定义组件的API。绝大多数React的实现都存在于 渲染器(renderers)中。

react-domreact-dom/server、 react-native、 react-test-renderer、 react-art都是常见的渲染器

这就是为什么不管目标平台是什么,react包都是可用的。从react包中导出的一切,比如React.ComponentReact.createElement、 React.Children 和 Hooks都是独立于目标平台的。无论运行React DOM,还是 React DOM Server,或是 React Native,组件都可以使用同样的方式导入和使用。

所以当我们想使用新特性时,react 和 react-dom都需要被更新。

例如,当React 16.3添加了Context API,React.createContext()API会被React包暴露出来。
但是React.createContext() 其实并没有实现 context。因为在React DOM 和 React DOM Server 中同样一个 API 应当有不同的实现。所以createContext()只返回了一些普通对象:
所以,如果你将react升级到了16.3+,但是不更新react-dom,那么你就使用了一个尚不知道Provider 和 Consumer类型的渲染器。这就是为什么老版本的react-dom会报错说这些类型是无效的。

这就是setState 尽管定义在React包中,调用时却能够更新DOM的原因。它读取由React DOM设置的this.updater,让React DOM安排并处理更新。

Component.setState = function(partialState, callback) {// setState所做的一切就是委托渲染器创建这个组件的实例this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

各个渲染器中的updater触发不同平台的更新渲染

// React DOM 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;// React DOM Server 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;// React Native 内部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;

至于updater的具体实现,就不是这里重点要讨论的内容了,下面让我们正式进入本文的主题:React Hooks

 

二. 了解useState

 

1. useState的引入和触发更新

本节主要概念:

  • useState是如何被引入以及调用的

  • useState为什么能触发组件更新

所有的Hooks在React.js中被引入,挂载在React对象中

// React.js
import {useCallback,useContext,useEffect,useImperativeHandle,useDebugValue,useLayoutEffect,useMemo,useReducer,useRef,useState,
} from './ReactHooks';

我们进入ReactHooks.js来看看,发现useState的实现竟然异常简单,只有短短两行

// ReactHooks.js
export function useState<S>(initialState: (() => S) | S) {const dispatcher = resolveDispatcher();return dispatcher.useState(initialState);
}

看来重点都在这个dispatcher上,dispatcher通过resolveDispatcher()来获取,这个函数同样也很简单,只是将ReactCurrentDispatcher.current的值赋给了dispatcher

// ReactHooks.js
function resolveDispatcher() {const dispatcher = ReactCurrentDispatcher.current;return dispatcher;
}

所以useState(xxx) 等价于 ReactCurrentDispatcher.current.useState(xxx)

看到这里,我们回顾一下第一章第三小节所讲的React渲染器与setState,是不是发现有点似曾相识。

与updater是setState能够触发更新的核心类似,ReactCurrentDispatcher.current.useStateuseState能够触发更新的关键原因,这个方法的实现并不在react包内。下面我们就来分析一个具体更新的例子。

 

2. 示例分析

以全文开头给出的代码为例。

我们从Fiber调度的开始:ReactFiberBeginwork来谈起

之前已经说过,React有能力区分不同的组件,所以它会给不同的组件类型打上不同的tag, 详见shared/ReactWorkTags.js所以在beginWork的函数中,就可以根据workInProgess(就是个Fiber节点)上的tag值来走不同的方法来加载或者更新组件。

// ReactFiberBeginWork.js
function beginWork(current: Fiber | null,workInProgress: Fiber,renderExpirationTime: ExpirationTime,
): Fiber | null {/** 省略与本文无关的部分 **/// 根据不同的组件类型走不同的方法switch (workInProgress.tag) {// 不确定组件case IndeterminateComponent: {const elementType = workInProgress.elementType;// 加载初始组件return mountIndeterminateComponent(current,workInProgress,elementType,renderExpirationTime,);}// 函数组件case FunctionComponent: {const Component = workInProgress.type;const unresolvedProps = workInProgress.pendingProps;const resolvedProps =workInProgress.elementType === Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);// 更新函数组件return updateFunctionComponent(current,workInProgress,Component,resolvedProps,renderExpirationTime,);}// 类组件case ClassComponent {/** 细节略 **/}}

下面我们来找出useState发挥作用的地方。

 

2.1 第一次加载

mount过程执行mountIndeterminateComponent时,会执行到renderWithHooks这个函数

function mountIndeterminateComponent(_current,workInProgress,Component,renderExpirationTime,
) {/** 省略准备阶段代码 **/ // value就是渲染出来的APP组件let value;value = renderWithHooks(null,workInProgress,Component,props,context,renderExpirationTime,);/** 省略无关代码 **/ }workInProgress.tag = FunctionComponent;reconcileChildren(null, workInProgress, value, renderExpirationTime);return workInProgress.child;
}

执行前:nextChildren = value

执行后:value= 组件的虚拟DOM表示

至于这个value是如何被渲染成真实的DOM节点,我们并不关心,state值我们已经通过renderWithHooks取到并渲染

 

2.2 更新

点击一下按钮:此时count从0变为3

更新过程执行的是updateFunctionComponent函数,同样会执行到renderWithHooks这个函数,我们来看一下这个函数执行前后发生的变化:

执行前:nextChildren = undefined

执行后:nextChildren=更新后的组件的虚拟DOM表示

同样的,至于这个nextChildren是如何被渲染成真实的DOM节点,我们并不关心,最新的state值我们已经通过renderWithHooks取到并渲染

所以,renderWithHooks函数就是处理各种hooks逻辑的核心部分

 

三. 核心步骤分析

ReactFiberHooks.js包含着各种关于Hooks逻辑的处理,本章中的代码均来自该文件。

 

1. Hook对象

在之前的章节有介绍过,Fiber中的memorizedStated用来存储state

在类组件中state是一整个对象,可以和memoizedState一一对应。但是在Hooks中,React并不知道我们调用了几次useState所以React通过将一个Hook对象挂载在memorizedStated上来保存函数组件的state

Hook对象的结构如下:

// ReactFiberHooks.js
export type Hook = {memoizedState: any, baseState: any,    baseUpdate: Update<any, any> | null,  queue: UpdateQueue<any, any> | null,  next: Hook | null, 
};

重点关注memoizedStatenext

  • memoizedState是用来记录当前useState应该返回的结果的

  • query:缓存队列,存储多次更新行为

  • next:指向下一次useState对应的Hook对象。

结合示例代码来看:

import React, { useState } from 'react'
import './App.css'export default function App() {const [count, setCount] = useState(0);const [name, setName] = useState('Star');// 调用三次setCount便于查看更新队列的情况const countPlusThree = () => {setCount(count+1);setCount(count+2);setCount(count+3);}return (<div className='App'><p>{name} Has Clicked <strong>{count}</strong> Times</p><button onClick={countPlusThree}>Click *3</button></div>)
}

第一次点击按钮触发更新时,memoizedState的结构如下

只是符合之前对Hook对象结构的分析,只是queue中的结构貌似有点奇怪,我们将在第三章第2节中进行分析。

 

2. renderWithHooks

renderWithHooks的运行过程如下:

// ReactFiberHooks.js
export function renderWithHooks(current: Fiber | null,workInProgress: Fiber,Component: any,props: any,refOrContext: any,nextRenderExpirationTime: ExpirationTime,
): any {renderExpirationTime = nextRenderExpirationTime;currentlyRenderingFiber = workInProgress;// 如果current的值为空,说明还没有hook对象被挂载// 而根据hook对象结构可知,current.memoizedState指向下一个currentnextCurrentHook = current !== null ? current.memoizedState : null;// 用nextCurrentHook的值来区分mount和update,设置不同的dispatcherReactCurrentDispatcher.current =nextCurrentHook === null// 初始化时? HooksDispatcherOnMount// 更新时: HooksDispatcherOnUpdate;// 此时已经有了新的dispatcher,在调用Component时就可以拿到新的对象let children = Component(props, refOrContext);// 重置ReactCurrentDispatcher.current = ContextOnlyDispatcher;const renderedWork: Fiber = (currentlyRenderingFiber: any);// 更新memoizedState和updateQueuerenderedWork.memoizedState = firstWorkInProgressHook;renderedWork.updateQueue = (componentUpdateQueue: any);/** 省略与本文无关的部分代码,便于理解 **/
}

 

2.1 初始化时

核心:创建一个新的hook,初始化state, 并绑定触发器

初始化阶段ReactCurrentDispatcher.current 会指向HooksDispatcherOnMount 对象

// ReactFiberHooks.jsconst HooksDispatcherOnMount: Dispatcher = {
/** 省略其它Hooks **/useState: mountState,
};// 所以调用useState(0)返回的就是HooksDispatcherOnMount.useState(0),也就是mountState(0)
function mountState<S>(initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {// 访问Hook链表的下一个节点,获取到新的Hook对象const hook = mountWorkInProgressHook();
//如果入参是function则会调用,但是不提供参数if (typeof initialState === 'function') {initialState = initialState();}
// 进行state的初始化工作hook.memoizedState = hook.baseState = initialState;
// 进行queue的初始化工作const queue = (hook.queue = {last: null,dispatch: null,eagerReducer: basicStateReducer, // useState使用基础reducereagerState: (initialState: any),});// 返回触发器const dispatch: Dispatch<BasicStateAction<S>,> = (queue.dispatch = (dispatchAction.bind(null,//绑定当前fiber结点和queue((currentlyRenderingFiber: any): Fiber),queue,));// 返回初始state和触发器return [hook.memoizedState, dispatch];
}// 对于useState触发的update action来说(假设useState里面都传的变量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {return typeof action === 'function' ? action(state) : action;
}

重点讲一下返回的这个更新函数 dispatchAction

function dispatchAction<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,action: A,
) {/** 省略Fiber调度相关代码 **/// 创建新的新的update, action就是我们setCount里面的值(count+1, count+2, count+3…)const update: Update<S, A> = {expirationTime,action,eagerReducer: null,eagerState: null,next: null,};// 重点:构建query// queue.last是最近的一次更新,然后last.next开始是每一次的actionconst last = queue.last;if (last === null) {// 只有一个update, 自己指自己-形成环update.next = update;} else {const first = last.next;if (first !== null) {update.next = first;}last.next = update;}queue.last = update;/** 省略特殊情况相关代码 **/// 创建一个更新任务scheduleWork(fiber, expirationTime);}

dispatchAction中维护了一份query的数据结构。

query是一个有环链表,规则:

  • query.last指向最近一次更新

  • last.next指向第一次更新

  • 后面就依次类推,最终倒数第二次更新指向last,形成一个环。

所以每次插入新update时,就需要将原来的first指向query.last.next。再将update指向query.next,最后将query.last指向update.

下面结合示例代码来画图说明一下:

前面给出了第一次点击按钮更新时,memorizedState中的query值

其构建过程如下图所示:

即保证query.last始终为最新的action, 而query.last.next始终为action: 1

 

2.2 更新时

核心:获取该Hook对象中的 queue,内部存有本次更新的一系列数据,进行更新

更新阶段 ReactCurrentDispatcher.current 会指向HooksDispatcherOnUpdate对象

// ReactFiberHooks.js// 所以调用useState(0)返回的就是HooksDispatcherOnUpdate.useState(0),也就是updateReducer(basicStateReducer, 0)const HooksDispatcherOnUpdate: Dispatcher = {/** 省略其它Hooks **/useState: updateState,
}function updateState(initialState) {return updateReducer(basicStateReducer, initialState);
}// 可以看到updateReducer的过程与传的initalState已经无关了,所以初始值只在第一次被使用// 为了方便阅读,删去了一些无关代码
// 查看完整代码:https://github.com/facebook/react/blob/487f4bf2ee7c86176637544c5473328f96ca0ba2/packages/react-reconciler/src/ReactFiberHooks.js#L606
function updateReducer(reducer, initialArg, init) {
// 获取初始化时的 hookconst hook = updateWorkInProgressHook();const queue = hook.queue;// 开始渲染更新if (numberOfReRenders > 0) {const dispatch = queue.dispatch;if (renderPhaseUpdates !== null) {// 获取Hook对象上的 queue,内部存有本次更新的一系列数据const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);if (firstRenderPhaseUpdate !== undefined) {renderPhaseUpdates.delete(queue);let newState = hook.memoizedState;let update = firstRenderPhaseUpdate;// 获取更新后的statedo {const action = update.action;// 此时的reducer是basicStateReducer,直接返回action的值newState = reducer(newState, action);update = update.next;} while (update !== null);// 对 更新hook.memoized hook.memoizedState = newState;// 返回新的 state,及更新 hook 的 dispatch 方法return [newState, dispatch];}}}// 对于useState触发的update action来说(假设useState里面都传的变量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {return typeof action === 'function' ? action(state) : action;
}

 

2.3 总结

单个hooks的更新行为全都挂在Hooks.queue下,所以能够管理好queue的核心就在于

  • 初始化queue - mountState

  • 维护queue - dispatchAction

  • 更新queue - updateReducer

结合示例代码:

  • 当我们第一次调用[count, setCount] = useState(0)时,创建一个queue

  • 每一次调用setCount(x),就dispach一个内容为x的action(action的表现为:将count设为x),action存储在queue中,以前面讲述的有环链表规则来维护

  • 这些action最终在updateReducer中被调用,更新到memorizedState上,使我们能够获取到最新的state值。

 

四. 总结

 

 

 

1. 对官方文档中Rules of Hooks的理解

官方文档对于使用hooks有以下两点要求:

 

2.1 为什么不能在循环/条件语句中执行

以useState为例:

和类组件存储state不同,React并不知道我们调用了几次useState,对hooks的存储是按顺序的(参见Hook结构),一个hook对象的next指向下一个hooks。所以当我们建立示例代码中的对应关系后,Hook的结构如下:

// hook1: const [count, setCount] = useState(0) — 拿到state1
{memorizedState: 0next : {// hook2: const [name, setName] = useState('Star') - 拿到state2memorizedState: 'Star'next : {null}}
}// hook1 => Fiber.memoizedState
// state1 === hook1.memoizedState
// hook1.next => hook2
// state2 === hook2.memoizedState

所以如果把hook1放到一个if语句中,当这个没有执行时,hook2拿到的state其实是上一次hook1执行后的state(而不是上一次hook2执行后的)。这样显然会发生错误。

 

2.2 为什么只能在函数组件中使用hooks

只有函数组件的更新才会触发renderWithHooks函数,处理Hooks相关逻辑。

还是以setState为例,类组件和函数组件重新渲染的逻辑不同 :

类组件:用setState触发updater,重新执行组件中的render方法

函数组件:用useState返回的setter函数来dispatch一个update action,触发更新(dispatchAction最后的scheduleWork),用updateReducer处理更新逻辑,返回最新的state值(与Redux比较像)

 

2. useState整体运作流程总结

说了这么多,最后再简要总结下useState的执行流程~

初始化:构建dispatcher函数和初始值

更新时:

  1. 调用dispatcher函数,按序插入update(其实就是一个action)

  2. 收集update,调度一次React的更新

  3. 在更新的过程中将ReactCurrentDispatcher.current指向负责更新的Dispatcher

  4. 执行到函数组件App()时,useState会被重新执行,在resolve dispatcher的阶段拿到了负责更新的dispatcher。

  5. useState会拿到Hook对象,Hook.query中存储了更新队列,依次进行更新后,即可拿到最新的state

  6. 函数组件App()执行后返回的nextChild中的count值已经是最新的了。FiberNode中的memorizedState也被设置为最新的state

  7. Fiber渲染出真实DOM。更新结束。

原文https://mp.weixin.qq.com/s/Err3W38ZMAX9Bm__SAI1jg

这篇关于React Hooks源码解析-剖析useState的执行过程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/508984

相关文章

详解Vue如何使用xlsx库导出Excel文件

《详解Vue如何使用xlsx库导出Excel文件》第三方库xlsx提供了强大的功能来处理Excel文件,它可以简化导出Excel文件这个过程,本文将为大家详细介绍一下它的具体使用,需要的小伙伴可以了解... 目录1. 安装依赖2. 创建vue组件3. 解释代码在Vue.js项目中导出Excel文件,使用第三

Linux中shell解析脚本的通配符、元字符、转义符说明

《Linux中shell解析脚本的通配符、元字符、转义符说明》:本文主要介绍shell通配符、元字符、转义符以及shell解析脚本的过程,通配符用于路径扩展,元字符用于多命令分割,转义符用于将特殊... 目录一、linux shell通配符(wildcard)二、shell元字符(特殊字符 Meta)三、s

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

SpringBoot 整合 Grizzly的过程

《SpringBoot整合Grizzly的过程》Grizzly是一个高性能的、异步的、非阻塞的HTTP服务器框架,它可以与SpringBoot一起提供比传统的Tomcat或Jet... 目录为什么选择 Grizzly?Spring Boot + Grizzly 整合的优势添加依赖自定义 Grizzly 作为

mysql-8.0.30压缩包版安装和配置MySQL环境过程

《mysql-8.0.30压缩包版安装和配置MySQL环境过程》该文章介绍了如何在Windows系统中下载、安装和配置MySQL数据库,包括下载地址、解压文件、创建和配置my.ini文件、设置环境变量... 目录压缩包安装配置下载配置环境变量下载和初始化总结压缩包安装配置下载下载地址:https://d

vue解决子组件样式覆盖问题scoped deep

《vue解决子组件样式覆盖问题scopeddeep》文章主要介绍了在Vue项目中处理全局样式和局部样式的方法,包括使用scoped属性和深度选择器(/deep/)来覆盖子组件的样式,作者建议所有组件... 目录前言scoped分析deep分析使用总结所有组件必须加scoped父组件覆盖子组件使用deep前言

VUE动态绑定class类的三种常用方式及适用场景详解

《VUE动态绑定class类的三种常用方式及适用场景详解》文章介绍了在实际开发中动态绑定class的三种常见情况及其解决方案,包括根据不同的返回值渲染不同的class样式、给模块添加基础样式以及根据设... 目录前言1.动态选择class样式(对象添加:情景一)2.动态添加一个class样式(字符串添加:情

使用Python实现批量访问URL并解析XML响应功能

《使用Python实现批量访问URL并解析XML响应功能》在现代Web开发和数据抓取中,批量访问URL并解析响应内容是一个常见的需求,本文将详细介绍如何使用Python实现批量访问URL并解析XML响... 目录引言1. 背景与需求2. 工具方法实现2.1 单URL访问与解析代码实现代码说明2.2 示例调用

springboot整合gateway的详细过程

《springboot整合gateway的详细过程》本文介绍了如何配置和使用SpringCloudGateway构建一个API网关,通过实例代码介绍了springboot整合gateway的过程,需要... 目录1. 添加依赖2. 配置网关路由3. 启用Eureka客户端(可选)4. 创建主应用类5. 自定

SSID究竟是什么? WiFi网络名称及工作方式解析

《SSID究竟是什么?WiFi网络名称及工作方式解析》SID可以看作是无线网络的名称,类似于有线网络中的网络名称或者路由器的名称,在无线网络中,设备通过SSID来识别和连接到特定的无线网络... 当提到 Wi-Fi 网络时,就避不开「SSID」这个术语。简单来说,SSID 就是 Wi-Fi 网络的名称。比如