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…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...

Zustand 状态管理库:极简而强大的解决方案
Zustand 是一个轻量级、快速和可扩展的状态管理库,特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...

【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

ios苹果系统,js 滑动屏幕、锚定无效
现象:window.addEventListener监听touch无效,划不动屏幕,但是代码逻辑都有执行到。 scrollIntoView也无效。 原因:这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作,从而会影响…...
NPOI Excel用OLE对象的形式插入文件附件以及插入图片
static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...

nnUNet V2修改网络——暴力替换网络为UNet++
更换前,要用nnUNet V2跑通所用数据集,证明nnUNet V2、数据集、运行环境等没有问题 阅读nnU-Net V2 的 U-Net结构,初步了解要修改的网络,知己知彼,修改起来才能游刃有余。 U-Net存在两个局限,一是网络的最佳深度因应用场景而异,这取决于任务的难度和可用于训练的标注数…...
前端高频面试题2:浏览器/计算机网络
本专栏相关链接 前端高频面试题1:HTML/CSS 前端高频面试题2:浏览器/计算机网络 前端高频面试题3:JavaScript 1.什么是强缓存、协商缓存? 强缓存: 当浏览器请求资源时,首先检查本地缓存是否命中。如果命…...