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整体的主要处理流程,具体如下图:

Solid流程

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

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

相关文章

Vue的`provide`和`inject`特性:上下文传递与数据共享

Vue的provide和inject特性&#xff1a;上下文传递与数据共享 Vue.js 是一款流行的前端 JavaScript 框架&#xff0c;它提供了丰富的功能来构建可维护和可扩展的用户界面。其中&#xff0c;provide 和 inject 特性是 Vue 中的一项强大功能&#xff0c;它们允许你在父组件提供数…

客户成功体系如何构建?请看这7步

文章目录 1. 当下客户成功的痛点2. 客户成功体系构建七步法2.1 第一步&#xff1a;定义客户成功章程2.2 第二步&#xff1a;客户成功组织设置与组织绩效管理设置2.3 第三步&#xff1a;关键岗位设置2.4 第四步&#xff1a;客户成功文化转型2.5 第五步&#xff1a;客户成功人才招…

HEC-RAS 1D/2D水动力与水环境模拟从小白到精通

专题一 水动力模型基础 1.水动力模型的本质 2.水动力模型的基本方程与适用范围 3.模型建模要点 4.注意事项与建模经验 专题二 恒定流模型(1D/2D) 1.恒定流及其适用范围 2.水面线分析及其数据要求 3.曼宁公式与恒定流&#xff0c;后处理 4.HEC-RA的水工建筑物&#xff…

虚拟机桥接模式下没有无线网卡选项

我以为是雷电模拟器占用了网卡的缘故&#xff0c;但想起之前可能修改了无线网卡的某些内容&#xff0c;于是到网络属性里面查看。 如下所示&#xff0c;原来是之前我不小心把这个红箭头指向的项目取消勾选了。

基于微信小程序的车位预定系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言用户的主要功能有&#xff1a;管理员的主要功能有&#xff1a;具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W…

SpringAOP入门案例

package com.elf.spring.aop.aspectj; /*** author 45* version 1.0*/ public interface UsbInterface {public void work(); }package com.elf.spring.aop.aspectj; import org.springframework.stereotype.Component; /*** author 45* version 1.0*/ Component //把Phone对象…

通过http发送post请求的三种Content-Type分析

通过okhttp向服务端发起post网络请求&#xff0c;可以通过Content-Type设置发送请求数据的格式。 常用到的三种&#xff1a; 1&#xff09;application/x-www-form-urlencoded; charsetutf-8 2&#xff09;application/json; charsetutf-8 3&#xff09;multipart/form-dat…

【论文阅读 09】融合门控自注意力机制的生成对抗网络视频异常检测

2021年 中国图象图形学报 摘 要 背景&#xff1a; 视频异常行为检测是智能监控技术的研究重点&#xff0c;广泛应用于社会安防领域。当前的挑战之一是如何提高异常检测的准确性&#xff0c;这需要有效地建模视频数据的空间维度和时间维度信息。生成对抗网络&#xff08;GANs&…

后端大厂面试-16道面试题

1 java集合类有哪些&#xff1f; List是有序的Collection&#xff0c;使用此接口能够精确的控制每个元素的插入位置&#xff0c;用户能根据索引访问List中元素。常用的实现List的类有LinkedList&#xff0c;ArrayList&#xff0c;Vector&#xff0c;Stack。 ArrayList是容量…

2023 “华为杯” 中国研究生数学建模竞赛(D题)深度剖析|数学建模完整代码+建模过程全解全析

问题一&#xff1a;区域碳排放量以及经济、人口、能源消费量的现状分析 思路&#xff1a; 定义碳排放量 Prediction 模型: CO2 P * (GDP/P) * (E/GDP) * (CO2/E) 其中: CO2:碳排放量 P:人口数量 GDP/P:人均GDP E/GDP:单位GDP能耗 CO2/E:单位能耗碳排放量 2.收集并统计相关…

实现爬虫加速的可实现办法

网络爬虫在数据采集和信息监测中发挥着重要作用。然而&#xff0c;由于网络环境复杂和大量数据需求&#xff0c;爬虫速度可能面临挑战。本文将为您分享一些实现爬虫加速的可行方法&#xff0c;帮助您让爬虫快如闪电&#xff01;让我们一起探索吧&#xff01; 一、多线程并发请…

将 Ordinals 与比特币智能合约集成:第 2 部分

在上一篇文章中&#xff0c;我们展示了一种将 Ordinal 与智能合约集成的方法&#xff0c;即将Ordinal和合约放在同一个 UTXO 中。 今天&#xff0c;我们介绍了一种集成它们的替代方案&#xff0c;即它们位于单独的 UTXO 中。 作为展示&#xff0c;我们开发了一个智能合约&…

指针笔试题讲解-----让指针简单易懂(2)

目录 回顾上篇重点 &#xff1a; 一.笔试题 ( 1 ) 二.笔试题 ( 2 ) 科普进制知识点 (1) 二进制 (2) 八进制 (3)十六进制 三.笔试题&#xff08; 3 &#xff09; 四.笔试题&#xff08; 4 &#xff09; 五.笔试题&#xff08; 5 &#xff09; 六.笔试题&#xff08; …

在比特币上使用可检索性证明支付存储费用

我们为用户开发了一种为云存储付费的新方法。 与亚马逊的 S3 等传统云存储相比&#xff0c;用户不必信任服务器。 我们使用比特币智能合约来确保支付取决于服务器的可检索性证明 (PoR)&#xff0c;该证明只能在数据仍然可用且需要时可以检索的情况下生成。 可检索性证明 (PoR)…

Java基础(一)——Hello World,8种数据类型,键盘录入

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

FPGA — Vivado下ILA(逻辑分析仪)详细使用方法

使用软件&#xff1a; Vivado 开发板&#xff1a; EGO1采用Xilinx Artix-7系列XC7A35T-1CSG324C FPGA 使用程序&#xff1a;按键案例 ILA详细使用方法 一、ILA简介二、ILA的使用方法方法1 — 使用IP核创建ILA调试环境创建ILA IP核 方法二 — 使用 Debug 标记创建 ILA对需观察信…

中国核动力研究设计院使用 DolphinDB 替换 MySQL 实时监控仪表

随着仪表测点的大幅增多和采样频率的增加&#xff0c;中国核动力研究设计院仪控团队原本基于 MySQL 搭建的旧系统已经无法满足大量数据并发写入、实时查询和聚合计算的需求。他们在研究 DB-Engines 时序数据库榜单时了解到国内排名第一的 DolphinDB。经过测试&#xff0c;发现其…

【C++面向对象侯捷】8.栈,堆和内存管理

文章目录 栈&#xff0c;堆stack object的生命周期static local object的生命周期global object的生命周期heap objects 的生命期new&#xff1a;先分配memory&#xff0c;再调用构造函数delete: 先调用析构函数&#xff0c;再释放 memory动态分配所得的内存块&#xff0c;in V…

Vue系列(三)之 基础语法下篇【事件处理,表单综合案例,组件通信】

一. 事件处理 在 Vue.js 中&#xff0c;v-on 指令被用于监听 DOM 事件&#xff0c;并在事件触发时执行相应的方法&#xff0c;这些方法就是事件处理器。v-on 指令有简写形式 &#xff0c;例如 click"handleClick" 会监听点击事件并执行 handleClick 方法。 事件处理…

CentOS 7 安装Libevent

CentOS 7 安装Libevent 1.下载安装包 新版本是libevent-2.1.12-stable.tar.gz。&#xff08;如果你的系统已经安装了libevent&#xff0c;可以不用安装&#xff09; 官网&#xff1a;http://www.monkey.org/~provos/libevent/ 2.创建目录 # mkdir libevent-stable 3.解压 …