React原理

本文主要讲手写React中重要的几个部分,有助于建立对React源码的认知。

1. CreateElement

相信大家一定对jsx不陌生

<div title="box"><p>jsx</p><span>hhh</span>
</div>

React中的jsx其实就是一个语法糖,上述jsx经过babel翻译后是

React.createElement('div', {title: 'box'}, React.createElement('p', {}, 'jsx'),React.createElement('span', {}, 'hhh')
)React.createElement: (type, props, ...children) => vDom

也就是说我们在写jsx实际上就是在写一个又一个嵌套的React.createElement。只是这样写太难维护了,所以使用了jsx。

React.createElement是干什么的?产生vDom的。
vDom(Virtual DOM),虚拟Dom节点,也就是自定义的一种数据结构,用来对应页面上真实的Dom节点。我们通过操纵vDom来操作真实的节点。
为什么使用vDom?

  1. vDom比真实Dom轻量太多,真实Dom挂载的属性太多,很多根本用不上
  2. 可进一步支持跨平台,如RN

vDom结构如下

vDom: {type,props: {...props,children}
}

我们自己写的createElement如下

// 将页面节点分为两类,text和非text
function createElement(type, props, ...children) {return {typp,props: {...props,children: children.map(child => typeof child === 'object' ? child: createTextNode(child))}}
}// 单独定义text vDom
function createTextNode(text) {return {type: 'TEXT',props: {nodeValue: text,children: []}}
}

一切都很清楚了。我们写了一堆jsx以为描述了页面上真实dom的排布,实际上,babel将jsx翻译为了一堆的React.createElement,也就是说,最后我们写的jsx变成了一个vDom树
就拿最开始的例子

<div title="box"><p>jsx</p><span>hhh</span>
</div>====={type: 'div',props: {title: 'box',children: [{type: 'p',props: {children: [{type: 'TEXT', props: {nodeValue: 'jsx', children: []}}]}},{type: 'span',props: {children: [{type: 'TEXT', props: {nodeValue: 'jsx', children: []}}]}}]}
}

最后我们得到了上面这个数据结构,它就是我们所描述的页面,下面,就是将这个数据结构渲染成真实dom

2. fiber

根据上面的vDom树,直接渲染出真实页面很简单(递归createElement,appendChild),但是存在一个问题,每次render都会重绘整个页面,而这个过程是同步的,很耗时,会阻塞高优先级的任务,比如用户输入,动画之类。
React的解决办法是:

将长时间的同步任务拆分成多个小任务,从而让浏览器能够抽身去响应其他事件,等他空了再回来继续计算

这个是思路,实现可以使用requestIdleCallbackfiber

requestIdleCallback是一个浏览器实验性API,实现让浏览器空闲的时候来计算(React团队自己实现了这个API)
fiber是一种数据结构,可进行中断和回溯
具体来说,实现如下

nextUnitOfWork和workInProgressRoot是两个全局变量
nextUnitOfWork表示下一个访问的fiber节点
workInProgressRoot也叫wipRoot表示本次渲染的fiber树根节点
performUnitOfWork: (fiber) => fiber,传入要访问(工作)的fiber节点,返回下一个待处理的fiber节点
commitRoot: 提交所有vDom修改,一次性渲染到页面上function workLoop(deadline) {while (nextUnitOfWork && deadline.timeRemaining() > 1) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);}if (!nextUnitOfWork && workInProgressRoot) {commitRoot();}requestIdleCallback(workLoop);
}requestIdleCallback(workLoop);这段代码的意思是:
如果浏览器空闲,且存在待处理fiber,
就会处理该fiber并返回下一个待处理fiber
如果不存在待处理fiber了,而且本次要执行渲染,
就会将修改提交到页面上。
这个工作由浏览器调度,一直持续着。

那fiber到底长什么样呢?首先,说了fiber就是一种数据结构,不要害怕它
fiber我认为就是对vDom的一个扩展。按面向对象来说,可以认为fiber extends vDom

fiber: {// 和vDom相同的属性type,props,//----dom, // 对应的真实dom节点child, // 子指针,指向第一个儿子sibling, // 兄弟指针,指向后一个相邻兄弟return, // 父指针,每个儿子都有alternate, // 老的fiber节点,用于diffeffectTag, // 标记,用于向页面提交更改,REPLACEMENT | UPDATE | DELETIONhooks // 该fiber上挂载的hook 
}

后面三个属性可以先不看,相信你已经知道fiber长什么样了,就是一棵多了几个指向的树
下面分别来看一下提到的几个函数,performUnitOfWork,commitRoot

3. performUnitOfWork

按照先儿子后兄弟的顺序,深度遍历fiber树,每次遍历一个

function performUnitOfWork(fiber) {// reconcile(第一次是构建,后面是更新)下一层fiber树const isFunctionComponent = fiber.type instanceof Function;if (isFunctionComponent) {updateFunctionComponent(fiber);} else {updateHostComponent(fiber);}// 找到fiber树的下一个节点,也即下一个工作单元,按照深度优先遍历child,后sibling的顺序if (fiber.child) {return fiber.child;}let nextFiber = fiber;while (nextFiber) {// 如果有sibling,那么下一个工作单元就是该sibling,直接返回if (nextFiber.sibling) {return nextFiber.sibling;}// 没有sibling,回到父节点,再去找父节点的siblingnextFiber = nextFiber.return;}// end, default return undefined, fiber tree stop working
}

代码中,虽然有两个函数没有提,但也能看懂,performUnitOfWork函数就是对当前fiber做了一定的处理,然后找到下一个fiber并返回
我们来看一下,对fiber做了什么处理

function updateFunctionComponent(fiber) {// 支持useState,初始化变量wipFiber = fiber;hookIndex = 0;wipFiber.hooks = [];        // hooks用来存储具体的state序列// 函数组件的type是函数,执行可获得vDomconst children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}function updateHostComponent(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber);}const elements = fiber.props && fiber.props.children;reconcileChildren(fiber, elements);
}

这里需要解释一下,对于函数式组件,babel解析jsx时,也会生成一个vDom(对这个函数,函数式组件是一个函数),这个vDom的type呢就是这个函数,我们知道函数式组件执行的返回值就是jsx写的页面,所以fiber.type(fiber.props)就得到了真正的内容。
reconcileChildren是干嘛的?如果是第一次渲染,就会构建fiber树(只会构建一层,fiber和children之间的关系),后续渲染,就会比对fiber树,实现diff算法。

这里就拿第一次渲染来解释一下。
客户端传入了一个函数式组件,得到了一个vDom树。首先我们为container生成一个vdom/fiber,它的dom设置为container,props.children设置为[vDom树的根节点],将其设置为下一个工作单元(nextUnitOfWork)和workInProgressRoot(正在处理的树的根),浏览器空闲的时候就会自动调用performUnitOfWork。

第一次调用时,传入的fiber是container对应的fiber,进入updateHostComponent,该fiber有dom(即为container),就不挂载了,直接进行reconcileChildren,构建下一层fiber树,重新进入performUnitOfWork,得到下一个处理fiber,即为函数组件对应的fiber

第二次调用时,传入的fiber的type是一个函数,于是进入updateFunctionComponent,执行type函数,得到包裹的vDom,传入reconcileChildren函数中,构建了一层fiber树(包括建立了child,sibling,return指针的关系,以及effectTag的标记,都是REPLACEMENT,这个后续再说)。

然后回到performUnitOfWork中,执行后续代码,根据建立好的一层fiber树找到下一个处理fiber,并返回,此时nextUnitOfWork变为了该fiber。

该fiber就是jsx的根节点,下一次浏览器空闲调用performUnitOfWork时,就先进入updateHostComponent。

updateHostComponent中,先为这个有效vDom挂载真实dom节点(根据type,使用document.createElement,添加除children以外的props,注意对事件特殊处理),再继续构建下一层fiber树。

知到performUnitOfWork返回的下一个处理节点为undefined,处理结束,在workLoop中会进入commitRoot函数,也就是将vDom/fiber到页面上。

3. commitRoot

遍历fiber树,提交修改。修改存在于fiber的effectTag属性上,之前有提到过。
effectTag属性有三个值:REPLACEMENT | UPDATE | DELETION
REPALCEMENT表示添加节点,UPDATE表示更新节点(意思是原dom节点不变,修改上面的props),DLETETION表示删除节点。
第一次渲染时,所有fiber节点的effectTag都为REPALCEMENT

// 统一提交vdom/fiber上的修改,渲染为真实dom到页面上
function commitRoot() {// deletions是一个全局数组,每次渲染,将要删除的fiber push进去deletions.forEach(commitRootImpl);commitRootImpl(workInProgressRoot.child);// currentRoot也是一个全局变量,上一次渲染的fiber树的树根currentRoot = workInProgressRoot;// 将wipRoot置为null,表示本次渲染结束workInProgressRoot = null;
}// 递归遍历fiber树,将修改作用于真实dom
function commitRootImpl(fiber) {if (!fiber) {return;}// 找到该fiber的有dom的父节点(即跳过函数fiber那一层)let parentFiber = fiber.return;while (!parentFiber.dom) {parentFiber = parentFiber.return;}const parentDom = parentFiber.dom;if (fiber.effectTag === 'REPLACEMENT' && fiber.dom) {parentDom.appendChild(fiber.dom);} else if (fiber.effectTag === 'DELETION') {commitDeletion(fiber);} else if (fiber.effectTag === 'UPDATE' && fiber.dom) {updateDom(fiber.dom, fiber.alternate.props, fiber.props);}commitRootImpl(fiber.child);commitRootImpl(fiber.sibling);
}function commitDeletion(fiber, domParent) {if (fiber.dom) {// dom存在,是普通节点domParent.removeChild(fiber.dom);} else {// dom不存在,是函数组件,向下递归查找真实DOMcommitDeletion(fiber.child, domParent);}
}

至此,第一次渲染的流程已经很清晰了,我们来仔细看一下reconcileChildren函数的实现

4. reConcileChildren

也就是所谓的diff算法
每次构建/比对一层的fiber树

function reconcileChildren(wipFiber, elements) {let prevSibling = nulllet index = 0// 找到上一次渲染时与elements对应的fiber// 相当于拿到第一个elements的alternatelet oldFiber = wipFiber.alternate && wipFiber.alternate.child// elements没有遍历完,或oldFiber存在(原因见下),就继续循环// 因为如果发生了删除,旧fiber树的节点就没有遍历完,没有打上DELETION标签,也就不会从页面上删除掉while (index < elements.length || oldFiber) {const element = elements[index]let newFiber = null// 判断oldFiber和element的类型是否相同const sameType = oldFiber && element && oldFiber.type === element.type// 类型相同,执行update相关操作// 也就是更新fiber的props,其它属性沿用oldFiber的if (sameType) {// updatenewFiber = {type: oldFiber.type,// 更新propsprops: element.props,return: wipFiber,dom: oldFiber.dom,alternate: oldFiber,effectTag: 'UPDATE'}}// 类型不同,但是element存在,执行placement相关操作// 生成newFiberif (element && !sameType) {// addnewFiber = {type: element.type,props: element.props,return: wipFiber,effectTag: 'REPLACEMENT'}}// 类型不同,但是oldFiber存在,执行deletion相关操作// 给oldFiber打上DELETION标签,放入待删除的数组if (oldFiber && !sameType) {// deleteoldFiber.effectTag = 'DELETION'deletions.push(oldFiber)}// 如果index===0,那么newFiber就是wipFiber的childif (index === 0) {wipFiber.child = newFiber} else {// 不是0,当前fiber就是上一次fiber的siblingprevSibling.sibling = newFiber}// 如果oldFiber存在,就让oldFiber指向它的sibling// 也就是element和oldFiber一起迭代,实现对应if (oldFiber) {oldFiber = oldFiber.sibling}// 保存上一次生成的fiberprevSibling = newFiber// 迭代index++}
}

下面我们考虑一下更新,先完成一个useState Hook吧。

5. hook

还记得fiber上定义的hooks属性吗?

// 申明两个全局变量,用来处理useState
// wipFiber是当前的函数组件fiber节点
// hookIndex是当前函数组件内部useState状态计数
let wipFiber = null;
let hookIndex = null;
function useState(initial) {// 获得该函数组件中的该hook对应的旧hookconst oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]// 初始化当前hookconst hook = {// 旧hook存在的话就延续旧hook的值,否则就是第一次渲染,接收传入的initial初始化值state: oldHook?.state || initial,// actions,动作队列// 为什么要用队列?// 因为一次性可能触发多次setState,比如handleClick里调用5次setState,这时queue里就有5个action// 并不是说调用一次setState就马上更新页面,这种情况是在handleClick结束后,再去重新渲染// 个人理解是:handleClick还没有执行完,浏览器没有空闲时间去执行页面的渲染queue: []}const actions = oldHook?.queue || []// 调用actionactions.forEach(action => {// action是函数if (typeof action === 'function') {hook.state = action(hook.state)} else {// action是值hook.state = action}})const setState = action => {// 动作队列中压入actionhook.queue.push(action)// 重新渲染页面,看似是重新遍历整个fiber树,但经过diff算法,只有被修改的部分会作用于真实dom上wipRoot = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot}deletions = []nextUnitOfWork = wipRoot}// 把生成的hook压入hooks中wipFiber.hooks.push(hook)// 待进入该组件的下一个hook,更新hookIndexhookIndex++return [hook.state, setState]
}

6. demo

让我们来捋一下整个react执行的过程
这里写一个小demo

export default function App(props) {const [count, setCount] = useState(0);return (<div title={props.title}><div>{count}</div><button onClick={() => setCount(prev => prev + 1)}>+1</button>	</div>)
}React.render(<App title="demo"/>, document.getElementById('root'));
  1. <App title="demo"/>被babel翻译为
React.createElement(App, {title: 'demo'})
App()得到div为根的vDom树
App这个vDom和内部的vDom树并没有连接起来,此时vDom结构是这样的:
vDom1:
{type: App,props: {title: 'demo'}
}
vDom2:
{type: 'div',props: {title: props.title,children: [{type: 'div',props: {children: [{type: 'TEXT', props: {nodeValue: count, children: []}}]}},{type: 'button',props: {onClick: () => setCount(prev => prev + 1),children: [{type: 'TEXT', props: {nodeValue: '+1', children: []}}]}}]}
}
  1. 开始渲染
    将两棵vDom树渲染成一棵fiber树
    2.1 将container和App渲染在一起
初始为container设置一个fiber,dom为container,children为App,
并且设置该fiber为第一个工作单元
经过第一次performUnitOfWork后,fiber树如下
{dom: container,props: {children: [App]}child: {type: App,props: {title: 'demo'}return: *container,}
}

2.2 处理App

下一个工作单元是App,会经过updateFunctionComponent,
处理后,将App与内部的组件连接到一起,
并且会更新wipFiber和清空hooks和hookIndex,
直到遇到下一个嵌套的函数组件之前,wipFiber都指向这个函数组件对应的fiber。
调用fiber.type()会执行App函数,同时会执行useState hook,
此时该fiber的hooks属性会推入一个hook,并且hookIndex=1
此时,fiber树如下
{dom: container,props: {children: [App]},child: {type: App,props: {title: 'demo'}return: *container,effectTag: 'REPLACEMENT',hooks: [{state: 0, queue: []}],	// hookchild: {type: 'div',props: {title: 'demo',children: [...]}return: *App,effectTag: 'REPLACEMENT'}}
}

2.3 最终fiber树

{dom: container,props: {children: [App]},child: {type: App,props: {title: 'demo'}return: *container,effectTag: 'REPLACEMENT',hooks: [{state: 0, queue: []}],	// hookchild: {type: 'div',props: {title: 'demo',children: [...]},dom,return: *App,effectTag: 'REPLACEMENT',child: {type: 'div',props: {...},dom,return: *div,effectTag: 'REPLACEMENT',child: {type: 'TEXT',props: {nodeValue: 0, ...},dom,return: *div,effectTag: 'REPALCEMENT'},sibling: {type: 'button',props: {onClick...},dom,return: *div,effectTag: 'REPLACEMENT',child: {type: 'TEXT',props: {nodeValue: '+1', ...},dom,return: *button,effectTag: 'REPLACEMENT'}}}}}
}

2.4 commitRoot
此时fiber树已经渲染好了,nextUnitOfWork也等于undefined了,执行commitRoot提交修改到页面上
commitRoot从container开始遍历fiber树开始渲染,根据fiber节点的effectTag对真实dom进行操作,这里都是REPLACEMENT,所以把所有fiber节点都相应地添加进页面里。
至此,第一次渲染完毕。

  1. 更新
    点击+1 button,调用setCount函数
    执行了hook.queue.push(prev => prev + 1)并重新设置了wipRoot和nextUnitOfWork
const setState = action => {hook.queue.push(action)workInProgressRoot = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot}deletetions = [];nextUnitOfWork = workInProgressRoot
}

currentRoot其实就是上一次渲染的fiber树的根节点,也就是container。
于是又从container节点开始重新来一遍,fiber树已经构建好了,所以这次遍历fiber树reconcile其实就是去diff,打标签
当nextUnitOfWork是App时,进行updateFunctionComponent,设置wipFiber,hookIndex置0,调用fiber.type(fiber.props),其中又会调用一次useState方法,这次在useState方法中,就存在了oldFiber

function useState(initial) {// 获得该函数组件中的该hook对应的旧hookconst oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]const hook = {state: oldHook?.state || initial,queue: []}const actions = oldHook?.queue || []// 调用actionactions.forEach(action => {// action是函数if (typeof action === 'function') {hook.state = action(hook.state)} else {// action是值hook.state = action}})const setState = action => {...}// 把生成的hook压入hooks中wipFiber.hooks.push(hook)// 待进入该组件的下一个hook,更新hookIndexhookIndex++return [hook.state, setState]
}

所以state还是oldFiber中存的值,此时

hook = {state: 0,queue: [(prev) => prev + 1]
}
actions = [(prev) => prev + 1]
遍历actions,hook.state = action(hook.state) ---> hook.state = 1
然后返回值count也是1,此时App内部组件count对应的TEXT节点就改变了

将fiber遍历完后,新的fiber树为

{dom: container,props: {children: [App]},child: {type: App,props: {title: 'demo'}return: *container,effectTag: 'UPDATE',hooks: [{state: 1, queue: []}],	// hookchild: {type: 'div',props: {title: 'demo',children: [...]},dom,return: *App,effectTag: 'UPDATE',child: {type: 'div',props: {...},dom,return: *div,effectTag: 'UPDATE',child: {type: 'TEXT',// -------------------// notify hereprops: {nodeValue: 1, ...},dom,return: *div,effectTag: 'UPDATE'},sibling: {type: 'button',props: {onClick...},dom,return: *div,effectTag: 'UPDATE',child: {type: 'TEXT',props: {nodeValue: '+1', ...},dom,return: *button,effectTag: 'UPDATE'}}}}}
}

可以看到effectTag全都变为了UPDATE,commitRoot中,会将所有的节点原始dom保持不变,而update上面的属性(主要是nodeValue update from 0 to 1)
至此,页面更新结束

7. 结语

React主要涉及这么几个方面:

  1. React.createElement创建vDom
  2. 浏览器闲时调度
  3. fiber
  4. diff
  5. hooks

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

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

相关文章

微信小程序的页面交互2

一、自定义属性 &#xff08;1&#xff09;定义&#xff1a; 微信小程序中的自定义属性实际上是由data-前缀加上一个自定义属性名组成。 &#xff08;2&#xff09;如何获取自定义属性的值&#xff1f; 用到target或currentTarget对象的dataset属性可以获取数据 &#xff…

基于双向长短期神经网络的碳排放量预测,基于bilstm的碳排放量预测

目录 背影 摘要 LSTM的基本定义 LSTM实现的步骤 BILSTM神经网络 基于双向长短期神经网络的碳排放量预测,基于bilstm的碳排放量预测 完整代码: Bilstm双向神经网络碳排放量预测.zip资源-CSDN文库 https://download.csdn.net/download/abc991835105/89087117 效果图 结果分析 展…

c# wpf LiveCharts 简单试验2

1.概要 1.1 说明 1.2 要点 1.2.1 添加命名控件 xmlns:lvc"clr-namespace:LiveCharts.Wpf;assemblyLiveCharts.Wpf" 1.2.2 图片控件 <lvc:CartesianChart Name"chart" LegendLocation"Right"/> 1.3 代码文件引用 using LiveCharts…

LeetCode刷题之31.下一个排列

文章目录 1. 题目2.分析3.解答3.1 先排序&#xff0c;后交换3.2 先交换&#xff0c;后排序 1. 题目 整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。 例如&#xff0c;arr [1,2,3] &#xff0c;以下这些都可以视作 arr 的排列&#xff1a;[1,2,3]、[1,3,2]、[3…

贪心算法|45.跳跃游戏II

力扣题目链接 class Solution { public:int jump(vector<int>& nums) {if (nums.size() 1) return 0;int curDistance 0; // 当前覆盖最远距离下标int ans 0; // 记录走的最大步数int nextDistance 0; // 下一步覆盖最远距离下标for (int i 0;…

go | gin 重定向路由重定向

web 重定向 重定向有一点要注意&#xff0c;重定向是在客户端那边执行的&#xff0c;一次服务器只能响应一次请求。但是要注意路由重定向 路由重定向是在服务器内部完成重定向资源请求 package mainimport ("github.com/gin-gonic/gin""fmt" )/* func main…

网络安全之命令注入

漏洞原理&#xff1a; 应用系统设计需要给用户提供指定的远程命令操作的接口&#xff0c;比如&#xff1a;路由器&#xff0c;防火墙&#xff0c;入侵检测等设备的web管理界面。一般会给用户提供一个ping操作的web界面 用户从web界面输入目标IP&#xff0c;提交后台会对改IP地…

【ARM 嵌入式 C 常用数据结构系列 25.1 -- linux 双向链表 list_head 使用详细介绍】

请阅读【嵌入式开发学习必备专栏 】 文章目录 内核双向链表双向链表的数据结构初始化双向链表在双向链表中添加元素遍历双向链表链表使用示例注意事项 内核双向链表 在Linux内核中&#xff0c;双向链表是一种广泛使用的数据结构&#xff0c;允许从任意节点高效地进行前向或后向…

树莓派5使用体验

原文地址&#xff1a;树莓派5使用体验 - Pleasure的博客 下面是正文内容&#xff1a; 前言 好久没有关于教程方面的博文了&#xff0c;由于最近打算入门嵌入式系统&#xff0c;所以就去购入了树莓派5开发板 树莓派5是2023年10月23日正式发售的&#xff0c;过去的时间不算太远吧…

期权的各种套利分类

标的方向、波动率 期权的交易策略主要可以分为两大类&#xff0c;一类是交易标的方向&#xff0c;另一类是交易波动率 目前市场上基本把所有不考虑标的方向、纯交易期权波动率的策略都统称为“波动率套利”策略。&#xff08;做“波动率套利”的交易员们不愿意(或没有能力)预…

Python向带有SSL/TSL认证服务器发送网络请求小实践(附并发http请求实现asyncio+aiohttp)

1. 写在前面 最近工作中遇到这样的一个场景&#xff1a;给客户发送文件的时候&#xff0c;为保证整个过程中&#xff0c;文件不会被篡改&#xff0c;需要在发送文件之间&#xff0c; 对发送的文件进行签名&#xff0c; 而整个签名系统是另外一个团队做的&#xff0c; 提供了一…

基于Whisper的实时语音识别(1): 流式显示视频帧和音频帧

Whistream &#xff08;微流&#xff09;是基于openai-whisper 大语音模型下的流式语音识别工具 本期主要介绍实时显示工具Whishow&#xff0c;可以实时逐帧显示视频流&#xff08;RTSP/RTMP&#xff09;和离线文件&#xff08;mp4,avi等&#xff09; 下载地址&#xff1a;ht…

京东云16核64G云服务器租用优惠价格500元1个月、5168元一年,35M带宽

京东云16核64G云服务器租用优惠价格500元1个月、5168元一年&#xff0c;35M带宽&#xff0c;配置为&#xff1a;16C64G-450G SSD系统盘-35M带宽-8000G月流量 华北-北京&#xff0c;京东云活动页面 yunfuwuqiba.com/go/jd 活动链接打开如下图&#xff1a; 京东云16核64G云服务器…

基于YOLOv8的铁路工人安全作业检测系统

&#x1f4a1;&#x1f4a1;&#x1f4a1;本文摘要&#xff1a;基于YOLOv8的铁路工人安全作业检测系统&#xff0c;属于小目标检测范畴&#xff0c;并阐述了整个数据制作和训练可视化过程&#xff0c; 博主简介 AI小怪兽&#xff0c;YOLO骨灰级玩家&#xff0c;1&#xff0…

图像过曝、低照度下Gamma矫正

由于项目场景的需要&#xff0c;Gamma变换在进行使用过程中可以对于图像的对比度进行调节&#xff0c;对过曝和低照度场景下对图像轮廓进行调节。按照论文里给的理论&#xff0c;加了一行代码实现灰度图像的自适应变换&#xff0c;进行一下记录。 #include <opencv2/opencv…

鸿运(通天星CMSV6车载)主动安全监控云平台inspect_file/upload存在任意文件上传漏洞

声明&#xff1a; 本文仅用于技术交流&#xff0c;请勿用于非法用途 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任。 简介 鸿运(通天星CMSV6车载)主动安全监控云平台实现对计…

机器学习概念:监督学习、无监督学习、回归、聚类

监督学习&#xff08;Supervised Learning&#xff09;&#xff1a; 在监督学习中&#xff0c;训练数据包含了输入特征&#xff0c;和相应的标签&#xff08;目标值&#xff09;。监督学习的目标是学习一个从输入到输出的映射&#xff0c;使得模型能够根据输入预测相应的输出。…

双连通分量算法

1. 连通图概念 连通图&#xff1a;无向图任意两点之间存在通路。 强连通&#xff1a;有向图&#xff08;前提&#xff09;中&#xff0c;任意两点都有至少一条通路&#xff0c;则此图为强连通图。 弱连通图&#xff1a;将有向图的有向边换成无向边得到的图是连通图&#xff0c…

初识二叉树和二叉树的基本操作

目录 一、树 1.什么是树 2. 与树相关的概念 二、二叉树 1.什么是二叉树 2.二叉树特点 3.满二叉树与完全二叉树 4.二叉树性质 相关题目&#xff1a; 5.二叉树的存储 6.二叉树的遍历和基本操作 二叉树的遍历 二叉树的基本操作 一、树 1.什么是树 子树是不相交的;…

计算机网络——37认证

认证 目标&#xff1a;Bob需要Alice证明他的身份 Protocol ap1.0&#xff1a;Alice说"A am Alice" 可能出现的问题&#xff1a; 在网络上Bob看不到Alice&#xff0c;因此Trudy可以简单的声称他是Alice 认证&#xff1a;重新尝试 Protocol ap2.0&#xff1a;Alice…