SolidJs节点级响应性
前言
随着组件化、响应式、虚拟DOM等技术思想引领着前端开发的潮流,相关的技术框架大行其道,就以目前主流的Vue、React框架来说,它们都基于组件化、响应式、虚拟DOM等技术思想的实现,但是具有不同开发使用方式以及实现原理,这里就不再赘述了相关内容,这里关注的焦点在于虚拟DOM。
无论是Vue还是React都应用虚拟DOM,通过虚拟DOM从而来减少频繁的DOM操作,优化页面性能。随着虚拟DOM应用到实际生产中后,无论是Vue还是React都少不了增加虚拟DOM对象以及相应Diff过程,特别是Diff过程恰恰是影响速度的重要点。人们渐渐思考虚拟DOM真的就比直接操作DOM要快吗?渐渐出现了放弃虚拟DOM的现代化技术框架,例如Solid、Svelte等。
Svelte和Solid都是放弃虚拟DOM但是应用组件化、响应式等技术的现代化框架,Svelte则比较偏向于Vue形式风格,Solid框架则偏向于React形式风格,但是底层响应式实现完全不同,这篇文章就聊聊Solid框架以及背后相关思想。
Solid基本说明
实际上最初Solid吸引我的点就是所谓号称比React还react的言论,作为多年的React框架使用者,我了解背后的实现思想以及实际开发中的痛点,而Solid框架有很多让我想要探索的点:
- 放弃虚拟DOM下的快速更新效率,没有虚拟DOM 或广泛的差异对比
- 保持React Hooks风格下的没有所谓的Hook规则
- 细粒度的响应性控制,函数组件只执行一次以及节点级的UI视图更新策略
Solid最吸引我的点就是细粒度的响应性,相比于React以及Vue更新策略,Solid可以做到节点级的视图更新:
- React更新策略基本描述:父组件的状态变更会导致当前子树下所有组件重新运行,需要对无需更新的子组件进行memo操作
- Vue更新策略的基本描述:每一个组件都对应一个Watcher对象,组件响应性状态与视图Watcher对象关联,组件的状态变更只会影响与之关联的对应视图Watcher对象,即只更新与状态建立联系的组件
- Solid更新策略的基本描述:组件的状态变更只会影响使用该状态的节点UI视图的更新
对于Solid的使用可以查看其官网,Solid的基本使用案例如下:
import { render } from 'solid-js/web';
import { createSignal } from 'solid-js';function App() {const [loggedIn, setLoggedIn] = createSignal(false);const toggle = () => setLoggedIn(!loggedIn())return (<><button onClick={toggle}>Log out</button><button onClick={toggle}>Log in</button></>);
}render(() => <App />, document.getElementById('app'))
render处理
组件经过Solid相关工具编译处理得到的最终代码逻辑如下:
// render(() => <Counter />, document.getElementById("app")!);
render(() => _$createComponent(Counter, {}), document.getElementById("app"));
上面实例初始化阶段的处理逻辑,render函数的逻辑如下:
function render(code, element, init, options = {}) {let disposer;createRoot(dispose => {disposer = dispose;element === document ? code() : insert(element, code(), element.firstChild ? null : undefined, init);}, options.owner);return () => {disposer();element.textContent = "";};
}
从上面逻辑可以如下调用链:
render -> createRoot -> runUpdates -> 顺序执行createRoot传入的回调函数、completeUpdates
createRoot回调逻辑主要逻辑如下:
element === document ? code() : insert(element, code(), element.firstChild ? null : undefined, init);
主要就是两点逻辑:
- 执行code函数:code就是render传入的第一个参数,这里就会被执行,通常来说就是函数组件,即函数组件会被执行
- 执行insert函数:实现组件内容挂载到页面上
insert函数
insert函数具体处理逻辑如下:
function insert(parent, accessor, marker, initial) {if (marker !== undefined && !initial) initial = [];if (typeof accessor !== "function") return insertExpression(parent, accessor, initial, marker);createRenderEffect(current => {insertExpression(parent, accessor(), current, marker);}, initial);
}
insert函数的accessor参数有两种情况:
- 非函数类型:意味着是DOM挂载点,就会调用insertExpression,即将节点插入到页面DOM中,从而显示视图
- 函数类型:意味着是响应性状态,需要调用createRenderEffect函数
Solid响应性原理
类比于React的useState、useEffect,Solid提供createSignal、createEffect这两个API:
- createSignal提供响应性状态定义
- createEffect提供监听响应性状态变更后需要运行的副作用处理
createSignal是响应性状态定义来源,这里以下面实例来看看其背后的响应性原理逻辑:
import { render } from "solid-js/web";
import { createSignal } from "solid-js";function Counter() {const [getCount, setCount] = createSignal(1);const increment = () => setCount(getCount() + 1);// 只会执行一次console.log('hello');return (<><button type="button" onClick={increment}>点击</button><div>{getCount()}</div></>);
}render(() => <Counter />, document.getElementById("app")!);
很简单的点击按钮增加计数的逻辑,当点击按钮后计数加1,而对应的div节点就会更新但是button节点不会更新。
实际上上面实例中Counter组件经过处理会变成下面形式:
unction Counter() {const [getCount, setCount] = createSignal(1);const increment = () => setCount(getCount() + 1);return [(() => {const _el$ = _tmpl$();_el$.$$click = increment;return _el$;})(), (() => {const _el$2 = _tmpl$2();_$insert(_el$2, getCount);return _el$2;})()];
}
Counter组件的视图部分被处理成一个个自执行函数了,即button节点和div节点分别对应一个自执行函数,如果嵌套多级子节点呢?难道每一个节点都对应一个自执行函数吗?实际上并不是如此逻辑,从实际调试得到的两点逻辑如下:
- 只有组件的一级子节点才会一一对应一个自执行函数,次级子节点不会
- 只要节点包含响应性状态内容则会调用_$insert函数来处理
实际上render过程会触发根组件,即Counter函数组件执行,而函数组件内部中对于Signal状态的节点的处理需要调用_$insert,实际上该函数就是insert函数。此时insert函数的accessor参数就是响应性状态的getter函数,整个调用链如下:
- 函数组件内部调用insert函数 -> createRenderEffect -> 顺序执行createComputation 、 updateComputation
- updateComputation -> runComputation -> 执行createRenderEffect传递的回调函数
createRenderEffect回调函数的逻辑如下:
createRenderEffect(current => {insertExpression(parent, accessor(), current, marker);}, initial);
其逻辑分为两点:
- accessor函数执行:对应着Signal状态下上文getter函数,即响应性状态的getter函数会在runComputation后被调用处理
- insertExpression:会将Signal状态值生成的DOM插入到父节点中
在accessor函数执行之前,需要了解createSignal创建响应性状态的具体逻辑,该API主要处理逻辑代码如下:
function createSignal(value, options) {...const s = {value,observers: null,observerSlots: null,comparator: options.equals || undefined};const setter = value => {...return writeSignal(s, value);};return [readSignal.bind(s), setter];
}
createSignal的主要逻辑很简单,就是返回响应的getter、setter函数并没有其他复杂逻辑执行:
- 定义包含初始值的s对象,表示上下文对象,后面的getter函数的this对象就是该上下文对象,setter函数也需要使用该上下文对象
- 上下文对象中的observers本意是观察者,必然是发布订阅模式实现的节点级更新逻辑,后面会与相关对应进行关联从而实现的,和Vue底层逻辑相似,但是不像Vue使用Proxy来实现,Solid仅仅就是单纯的函数
getter函数执行
当accessor函数执行时本质上就是执行Signal状态的getter函数,此时就会执行readSignal函数,下面是简化的逻辑:
function readSignal() {...if (Listener) {const sSlot = this.observers ? this.observers.length : 0;if (!Listener.sources) {Listener.sources = [this];Listener.sourceSlots = [sSlot];} else {Listener.sources.push(this);Listener.sourceSlots.push(sSlot);}if (!this.observers) {this.observers = [Listener];this.observerSlots = [Listener.sources.length - 1];} else {this.observers.push(Listener);this.observerSlots.push(Listener.sources.length - 1);}}...return this.value;
}
主要逻辑就是:当Listener全局变量存在的情况下,就会将Listener存入上下文对象的observers属性中,那么Listener什么时候有值呢?源码中全局查找Listenerd的赋值操作,就有一个地方,即updateComputation函数的处理,但是该函数的调用来源很多。在初始化阶段updateComputation函数的调用链上面就已经描述清楚了,其来源于createRenderEffect。
updateComputation中Listener的相关处理逻辑如下:
function updateComputation(node) {if (!node.fn) return;...Listener = Owner = node;runComputation(node, Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value, time);...
}
这里的node参数就是createComputation函数的对象,即Computation对象,该对象的属性有:
const c = {fn,state: state,updatedAt: null,owned: null,sources: null,sourceSlots: null,cleanups: null,value: init,owner: Owner,context: Owner ? Owner.context : null,pure};
所以Listener本质上就是Computation对象,其中该对象的fn属性存放的就是触发响应性状态getter函数的回调函数。
所以当初始化调用链触发getter函数执行时,Listener就已经存在,之后的处理逻辑就是:
- 对应Signal状态的上下文对象中保存Listener,即Signal状态上下文中保存Listener到Observes属性中
- Listener指向的Computation对象的sources属性会保存对应Signal状态的上下文
此时Computation对象与Signal状态上下文对象就互相关联起来了。后续初始化的处理逻辑就是将生成的节点通过DOM插入方法添加到节点中,没有虚拟DOM Diff的过程,这里就不继续说明了。
故而函数组件在初始化阶段内部的处理逻辑就非常清晰,具体如下:
- insert -> createRenderEffect -> 顺序执行createComputation、updateComputation
- updateComputation -> runComputation -> 执行createRenderEffect传递的回调函数
- createRenderEffect传递的回调函数内部逻辑会顺序执行Signal状态getter函数、insertExpression,insertExpression会将Signal状态节点插入到父节点中
更新阶段
现在点击了按钮,此时上面实例中:首先是先调用getter函数,然后再调用setter函数。此时Signal状态的getter再次被执行,但是Listener会在每次赋值操作后被重置为之前的状态,即初始化阶段updateComputation最后处理时被重置为null,所以getter函数此时执行仅仅返回当前值而已。
setter函数执行
执行了setter函数,此时就会执行writeSignal函数,去看看该函数做了什么处理,下面是简化的逻辑(移除了Transition相关的逻辑):
function writeSignal(node, value, isComp) {let current = node.value;if (!node.comparator || !node.comparator(current, value)) {...node.value = value;if (node.observers && node.observers.length) {runUpdates(() => {for (let i = 0; i < node.observers.length; i += 1) {const o = node.observers[i];...if (TransitionRunning ? !o.tState : !o.state) {if (o.pure) Updates.push(o);else Effects.push(o);if (o.observers) markDownstream(o);}if (!TransitionRunning) o.state = STALE;else o.tState = STALE;}if (Updates.length > 10e5) {Updates = [];if (false) ;throw new Error();}}, false);}}return value;
}
在初始化处理阶段,Signal状态对应的上下文对象中observers已经保存了Listener对应的Computation对象。在更新阶段时setter函数被调用,此时observers是有值的。故而其调用栈逻辑如下:
- writeSignal -> runUpdates -> 顺序执行传入的回调函数、completeUpdates
- 这里的回调函数的逻辑实际上就是将Computation对象更加对应的条件条件到Updates、Effects队列中
- completeUpdates -> runQueue(对Updates、Effects 进行处理) -> 循环遍历队列依次处理,即依次调用runTop -> 一般情况下就会调用updateComputation函数
需要注意的是更新阶段updateComputation的处理有一个重要的逻辑,即cleanNode函数调用:
function updateComputation(node) {...cleanNode(node);...Listener = Owner = node;...
}function cleanNode() {if (node.sources) {while (node.sources.length) {const source = node.sources.pop(),index = node.sourceSlots.pop(),obs = source.observers;if (obs && obs.length) {const n = obs.pop(), s = source.observerSlots.pop();if (index < obs.length) {n.sourceSlots[s] = index;obs[index] = n;source.observerSlots[index] = s;}}}}
}
Computation对象与Signal状态上下文对象建立的关联在cleanNode对象中被解绑了,即当某个Signal状态更新后,Solid会将对应Signal状态的旧Computation解绑,这就是更新阶段cleanNode主要处理逻辑。
updateComputation之后的处理就是设置新的Listener对象,之后的处理逻辑就更初始化时相同,即:
updateComputation -> runComputation -> 执行createRenderEffect传递的回调函数
从而实现最新的Computation对象与Signal状态上下文关联,之后更新到对应的DOM节点,这个过程就实现了节点级别的视图更新。
在初始化阶段因为createRenderEffect传递的回调函数的参数中就有当前响应性状态所在位置的父节点,通过闭包特性保证了每次更新都是同一节点位置,从而实现Solid的节点级别的视图更新逻辑。
副作用处理
createEffect API是Solid用来处理副作用的方法,该方法类比React的useEffect,不同于useEffect的是不需要指明依赖项,它会自动收集依赖项。
通过下面实例来了解createEffect的执行过程:
import { render } from "solid-js/web";
import { createSignal } from "solid-js";function Counter() {const [getCount, setCount] = createSignal(1);const increment = () => setCount(getCount() + 1);createEffect(() => {console.log(getCount());});return (<><button type="button" onClick={increment}>点击</button><div>{getCount()}</div></>);
}render(() => <Counter />, document.getElementById("app")!);
初始化阶段
createEffect方法的逻辑具体如下:
function createEffect(fn, value, options) {runEffects = runUserEffects;const c = createComputation(fn, value, false, STALE),s = SuspenseContext && useContext(SuspenseContext);if (s) c.suspense = s;if (!options || !options.render) c.user = true;Effects ? Effects.push(c) : updateComputation(c);
}
在之前分析createSignal就梳理了初始化过程的关键处理过程,知道相关函数的作用:
- createComputation:就是创建新的Computation对象
- updateComputation:就是解绑对应Signal状态上下文对象与旧的Computation对象之间关联,之后将当前新的Computation对象重新与Signal状态上下文对象绑定
在初始化阶段createEffect逻辑主要就是两点:
- 创建一个新的Computation对象,即每一个createEffect都会对应一个Computation对象
- 判断Effects队列是否存在,Effects为数组就会将Computation对象保存到Effects队列中
当render函数执行时其内部会调用runUpdates函数,之后会执行completeUpdates函数,该函数内部逻辑就是处理Effects、Updates队列中内容。根据render函数的处理过程可知:
createEffect内部保存在Effects队列中副作用逻辑,会在组件挂载到节点之后被执行,初始化阶段createEffect副作用函数会立即执行,此时并没有于Signal状态上下文建立关联
当副作用逻辑执行时,如果内部存在Signal状态对象就会执行其getter函数,从而将当前Computation对象与Signal状态上下文建立关联,从而实现依赖的收集。
更新阶段
当createEffect对应的Computation与Signal状态对象建立关联后,所谓的更新阶段就是Signal状态更新时的处理逻辑流程了,实际上就是处理createEffect对应的Computation对象而已。这里需要注意的是更新阶段createEffect的处理有以下几点说明:
- createEffect对应的Computation对象的处理总是在视图节点的Computation对象之后的
- Solid中createEffect整个的处理过程是同步而非异步,不同于React useEffect的异步处理
总结
实际上从Solid的风格以及底层原理实现,你可以看到其他框架的影子:
- Solid底层实现和Vue底层响应性实现非常相似,都是基于发布订阅模式实现两个对象的绑定从而构建响应的基石,只不过在Vue中Dep对象与Watcher对象,Solid中是Signal状态上下文对象和Computation对象
- 除了采用React Hooks,Solid只支持函数组件这一种形式,还支持并发渲染即时间分片,底层实现方式与React相似,这里不再赘述了
这里梳理下Solid整体的主要处理流程,具体如下图:
相关文章:

SolidJs节点级响应性
前言 随着组件化、响应式、虚拟DOM等技术思想引领着前端开发的潮流,相关的技术框架大行其道,就以目前主流的Vue、React框架来说,它们都基于组件化、响应式、虚拟DOM等技术思想的实现,但是具有不同开发使用方式以及实现原理&#…...

数据采集技术在MES管理系统中的应用及效果
在现代制造业中,MES生产管理系统已成为生产过程中不可或缺的一部分。MES管理系统能够有效地将生产计划、生产执行、质量管理等各个生产环节有机地衔接起来,从而实现生产过程的全面优化。本文将以某车间为例,探讨结合MES系统的数据采集技术的应…...

php函数usort使用方法
在 PHP 中,usort() 函数用于对数组进行排序,它允许你使用自定义的比较函数来确定元素的顺序。以下是 usort() 函数的使用方法: usort(array &$array, callable $cmp_function): bool参数说明: $array:要排序的数…...
35.浅谈贪心算法
概述 相信大家或多或少都对贪心算法有所耳闻,今天我们从一个应用场景展开 假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号? 广播台覆盖地区k1北京、上海、天津…...
QT时间日期定时器类(1.QDate类)【QT基础入门 Demo篇】
使用时候需要包含头文件 创建一个 QDate 实例 设置 QDate 的日期 获取 QDate 的日期 获取当前是周几 判断 QDate 的有效性 格式化 QDate 的显示字符串 计算 QDate 的差值 QDate显示格式 年月日转换时间戳时间戳转换年月日 QDate相关…...

记一次实战案例
1、目标:inurl:news.php?id URL:https://www.lghk.com/news.php?id5 网站标题:趋时珠宝首饰有限公司 手工基础判断: And用法 and 11: 这个条件始终是为真的, 也就是说, 存在SQL注入的话, 这个and 11的返回结果必定是和正常页…...

Serv-U FTP服务器结合cpolar内网穿透实现共享文件并且外网可远程访问——“cpolar内网穿透”
文章目录 1. 前言2. 本地FTP搭建2.1 Serv-U下载和安装2.2 Serv-U共享网页测试2.3 Cpolar下载和安装 3. 本地FTP发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1. 前言 科技日益发展的今天,移动电子设备似乎成了我们生活的主角,智能…...

EasyWindow - Android 悬浮窗框架
官网 https://github.com/getActivity/EasyWindow 项目介绍 本框架意在解决一些极端需求,如果是普通的 Toast 封装推荐使用 Toaster 集成步骤 如果你的项目 Gradle 配置是在 7.0 以下,需要在 build.gradle 文件中加入 allprojects {repositories {/…...

tp5连接多个数据库
一、如果你的主数据库配置文件都在config.php里 直接在config.php中中定义db2: 控制器中打印一下: <?php namespace app\index\controller; use think\Controller; use think\Db; use think\Request; class Index extends Controller {public fun…...

SAP PO运维(一):系统概览异常处理
打开SAP PIPO Netweaver Administration界面,系统概览下显示异常: 参考SAP note: 2577844 - AS Java Monitoring and Logging parametrization best practice service/protectedwebmethods = SDEFAULT -GetVersionInfo -GetAccessPointList -ListLogFiles -ReadLogFile -Para…...

安全厂商安恒信息加入龙蜥社区,完成 与 Anolis OS 兼容适配
近日,杭州安恒信息技术股份有限公司(以下简称“安恒信息”)签署了 CLA(Contributor License Agreement,贡献者许可协议),正式加入龙蜥社区(OpenAnolis),并成为…...

maven找不到jar包
配置settings.xml文件之后出现报错找不到jar包 先改maven设置: 然后在重新清理构建项目: 可以通过执行以下命令清理本地 Maven 仓库 mvn dependency:purge-local-repository...

MySQL的数据目录
文章目录 MySQL的数据目录1. MYSQL目录结构2. 数据库与文件系统的关系2.1 查看默认数据库2.2 数据库在文件系统中的表示2.1.1 MyISAM存储引擎模式2.1.2 InnoDB存储引擎模式 2.3 视图在文件系统中的表示2.4 小结 MySQL的数据目录 1. MYSQL目录结构 查询主要目录结构:…...

详解MySQL索引+面试题
前言: 📕作者简介:热爱编程的小七,致力于C、Java、Python等多编程语言,热爱编程和长板的运动少年! 📘相关专栏Java基础语法,JavaEE初阶,数据库,数据结构和算法系列等,大家有兴趣的可以看一看。 😇😇😇有兴趣的话关注博主一起学习,一起进步吧! 一、索引概述…...
设计模式:桥接器模式(C++实现)
桥接器模式(Bridge Pattern)是一种结构设计模式,它将抽象部分与实现部分分离,使它们可以独立地变化。桥接器模式通常用于需要在多个维度上扩展和变化的情况下,将抽象和实现解耦。 以下是一个简单的C桥接器模式的示例&a…...

公网远程访问GeoServe Web管理界面【内网穿透】
文章目录 前言1.安装GeoServer2. windows 安装 cpolar3. 创建公网访问地址4. 公网访问Geo Servcer服务5. 固定公网HTTP地址 前言 GeoServer是OGC Web服务器规范的J2EE实现,利用GeoServer可以方便地发布地图数据,允许用户对要素数据进行更新、删除、插入…...

AIMS医院手术麻醉信息系统全套源码,自主版权,开箱即用
手术麻醉临床信息系统有着完善的临床业务功能,能够涵盖整个围术期的工作,能够采集、汇总、存储、处理、展现所有的临床诊疗资料。通过该系统的实施,能够规范麻醉科的工作流程,实现麻醉手术过程的信息数字化,自动生成麻…...

中秋特辑——3D动态礼盒贺卡(可监听鼠标移动)
前言 「作者主页」:雪碧有白泡泡 「个人网站」:雪碧的个人网站 「推荐专栏」: ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄,vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄ÿ…...

Json文件序列化读取
Json文件 [{"name":"清华大学","location":"北京","grade":"1"},{"name":"北京大学","location":"北京","grade":"2"} ] 安装包 代码 Program.c…...
ClickHouse(15)ClickHouse合并树MergeTree家族表引擎之GraphiteMergeTree详细解析
GraphiteMergeTree该引擎用来对Graphite数据(图数据)进行瘦身及汇总。对于想使用ClickHouse来存储Graphite数据的开发者来说可能有用。 如果不需要对Graphite数据做汇总,那么可以使用任意的ClickHouse表引擎;但若需要,那就采用GraphiteMerge…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...
Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务
通过akshare库,获取股票数据,并生成TabPFN这个模型 可以识别、处理的格式,写一个完整的预处理示例,并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务,进行预测并输…...

cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/55aefaea8a9f477e86d065227851fe3d.pn…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...