Open blankPen opened 2 years ago
由于近期团队技术需要调研如何使用 React DSL 实现类似 SvelteJs 的去除vdom+diff的前端框架,所以才有了以下文章的产生。
如果你还不知道什么是 SvelteJs ,那说明你已经out了,赶紧爬起来学习吧。
传送门:
所以本篇文章我将给大家介绍一下 SvelteJs 的实现原理?才怪~
本次我要介绍的是另一个前端框架———SolidJs(前端框架已经这么多了么???)
关于 SolidJs 的介绍大家可以参考掘金大佬的文章传送门,我这里就不过多描述了。
简单来说,SolidJs 是借鉴了 SvelteJs 的理念,使用React DSL开发的新框架。(是不是和我前面提到的调研方向非常匹配?大家的思路相当一致嘛)
下面我会针对 SolidJs 对他进行详细的拆解。
因为只是总结,介绍不会特别全面,如果看不懂可能需要先了解一下源码、看看编译前后产物的差距,再结合文章一起食用。
在正式开始之前需要介绍一件事情,无论是 SvelteJs 还是 SolidJs,他们都有一个最核心的特性——将声明式代码编译成命令式代码。这也是我主要要介绍的内容。
什么是声明式代码?
// jsx,html等都是声明式代码,通过声明代码内容让程序自己去解析展示 <div>hello world</div>
什么是命令式代码?
// dom api, jquery等这些都是命令式代码,通过调用指令去执行逻辑 const el = document.createElement('div'); el.innerText = 'hello world'; document.body.appendChild(el);
那 SolidJs 做了什么呢?左边是源码,右边是编译后的代码。Demo链接
我们先大致扫一眼,接下来会仔细介绍。
参考链接:
从模块划分看,主要由两个部分构成:
而说到JS中的编译转换那必然就不可避免的会使用到 Babel,在 SolidJs 中 babel-plugin-jsx-dom-expressions 就是干这个事的。
babel-plugin-jsx-dom-expressions
观察源码我们可以发现,主要配置项如下:
{ exclude: 'node_modules/**', babelHelpers: "bundled", plugins: [ [require("babel-plugin-jsx-dom-expressions"), { moduleName: 'dom', // 模块名可以自定义 delegateEvents: false, // 是否使用委托事件,我们应该不需要委托事件 // contextToCustomElements: true, // wrapConditionals: true }] ] }
整体的编译流程,如下图所示:
babel-plugin-jsx-dom-expressions 入口源码如下,主要是针对 JSXElement,JSXFragment 进行了转换,其他JS逻辑基本没有处理。
import SyntaxJSX from "@babel/plugin-syntax-jsx"; import { transformJSX } from "./shared/transform"; import postprocess from "./shared/postprocess"; import preprocess from "./shared/preprocess"; export default () => { return { name: "JSX DOM Expressions", inherits: SyntaxJSX, visitor: { JSXElement: transformJSX, JSXFragment: transformJSX, Program: { enter: preprocess, exit: postprocess } } }; };
而这其中最重要的就是 transformElement(path, info),他是整个编译过程拆解的核心。 他主要的作用就是通过AST将 JSXElement 转换成一个 Result 对象,结构如下:
transformElement(path, info)
{ template: '<button type="button">before<text></text></button>', // 用来创建节点的模板语句 decl: // 变量定义宣言 [ { type: 'VariableDeclarator', id: [Object], init: [Object] }, { type: 'VariableDeclarator', id: [Object], init: [Object] } ], exprs: // DOM 命令式创建的表达式,包含insert,addEventListener等 [ { type: 'ExpressionStatement', expression: [Object] }, { type: 'ExpressionStatement', expression: [Object] }, { type: 'ExpressionStatement', expression: [Object] } ], dynamics: // 涉及到到动态计算相关的属性语句 [ { elem: [Object], key: 'style:width', value: [Node], isSVG: false, isCE: false }, { elem: [Object], key: 'style:height', value: [Node], isSVG: false, isCE: false } ], postExprs: [], isSVG: false, tagName: 'mview', // 标签名称 id: { type: 'Identifier', name: '_el$2' }, // 这个JSXElement对应在JS中的的变量名 hasHydratableEvent: false }
他将 JSXElement 解析成了一个对象,最终会根据这个对象来生成最终输出的 output代码;这么说可能有点抽象,我们结合实际产物来对比。
源代码如下:
class App { state = { value: 1 } render() { return ( <button type="button" style={{ width: Math.random() * 100, height: Math.random() * 100 }} onClick={Math.random() > 0.5 ? this.increment : null} > before <text>{this.state.value}</text> {[1, 2, 3].map(k => <Button key={k} >自定义组件</Button>)} </button> ); } }
编译后产物如下:
import { template, delegateEvents, addEventListener, insert, createComponent, effect } from 'solid-js/web'; const _tmpl$ = template(`<button type="button">before<text></text></button>`, 4); /* source: main.tsx */ class App { state = { value: 1 }; render() { const _self$ = this; return (() => { const _el$ = _tmpl$.cloneNode(true), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling; addEventListener(_el$, "click", Math.random() > 0.5 ? _self$.increment : null, true); insert(_el$3, () => _self$.state.value); insert(_el$, () => [1, 2, 3].map(k => createComponent(Button, { key: k, children: "\u81EA\u5B9A\u4E49\u7EC4\u4EF6" })), null); effect(_p$ => { const _v$ = Math.random() * 100, _v$2 = Math.random() * 100; _v$ !== _p$._v$ && _el$.style.setProperty("width", _p$._v$ = _v$); _v$2 !== _p$._v$2 && _el$.style.setProperty("height", _p$._v$2 = _v$2); return _p$; }, { _v$: undefined, _v$2: undefined }); return _el$; })(); } } delegateEvents(["click"]);
根据transformElement(path, info)的产物 Result 对象结构拆解来看
Result.template 对应编译后代码中的 _temp$,主要用于创建节点的 Element 实例
// { "template": "<button type=\"button\">before<text></text></button>", } const _tmpl$ = template(`<button type="button">before<text></text></button>`, 4);
Result.decl 对应编译后代码中的 _el$ 等节点变量声明
/* { "decl": [{ "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "_el$2" }, "init": { "type": "MemberExpression", } }, { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "_el$3" }, "init": { "type": "MemberExpression", } }], } */ return (() => { const _el$ = _tmpl$.cloneNode(true), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling;4); // ... }
Result.exprs 对应编译后代码中的 insert,addEventListener 等 DOM 创建绑定相关的命令式创建的表达式;
insert,addEventListener
/* { "exprs": [{ "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "_$addEventListener" }, "arguments": [ { "type": "Identifier", "name": "_el$2" }, { "type": "StringLiteral", "value": "click" }, { "type": "ConditionalExpression" } ] } }, { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "_$insert" }, "arguments": [ { "type": "Identifier", "name": "_el$4" }, { "type": "ArrowFunctionExpression", "params": [], "body": {}, "async": false } ] } }, { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "_$insert" }, "arguments": [ { "type": "Identifier", "name": "_el$2" }, { "type": "ArrowFunctionExpression", "params": [], "body": {}, "async": false }, { "type": "NullLiteral" } ] } }], } */ addEventListener(_el$, "click", Math.random() > 0.5 ? _self$.increment : null, true); insert(_el$3, () => _self$.state.value); insert(_el$, () => [1, 2, 3].map(k => createComponent(Button, { key: k, children: "\u81EA\u5B9A\u4E49\u7EC4\u4EF6" })), null);
Result.dynamics 对应编译后代码中的 涉及到到动态计算相关的属性语句
/* { "dynamics": [{ "elem": { "type": "Identifier", "name": "_el$2" }, "key": "style:width", "value": { "type": "BinaryExpression" /* */ }, "isSVG": false, "isCE": false }, { "elem": { "type": "Identifier", "name": "_el$2" }, "key": "style:height", "value": { "type": "BinaryExpression" /* */ }, "isSVG": false, "isCE": false }], } */ effect(_p$ => { const _v$ = Math.random() * 100, _v$2 = Math.random() * 100; _v$ !== _p$._v$ && _el$.style.setProperty("width", _p$._v$ = _v$); _v$2 !== _p$._v$2 && _el$.style.setProperty("height", _p$._v$2 = _v$2); return _p$; }, { _v$: undefined, _v$2: undefined });
Result.tagName=button,标识标签的名称
button
Result.id={ "type": "Identifier", "name": "_el$2" }, 用来当前转换的JSX这个节点最终生成的变量名
{ "type": "Identifier", "name": "_el$2" }
最重要的就以上这几个了,其余的就是和HTML特性或者SSR相关的逻辑。
DOM-Expressions 主要是提供了一些标准API提供给 编译器 jsx-to-dom-expressions 使用 API主要的基础能力依赖于 DOM API
从上面编译后的代码可以看到,从solidjs/web中导入了很多方法
solidjs/web
import { template, delegateEvents, addEventListener, insert, createComponent, effect } from 'solid-js/web';
比如插入元素的insert,根据字符串创建Element的template,而这些全都来自 dom-expressions这个库,而他底层封装就是DOM API。他所有提供的接口如下:
insert
template
dom-expressions
export function render(code, element, init) { } // 根据模板字符串生成Element export function template(html, check, isSVG) { } // ================== 属性相关相关 // 设置属性 export function setAttribute(node, name, value) { } export function setAttributeNS(node, namespace, name, value) { } // 获取类名列表 export function classList(node, value, prev = {}) { } // 设置样式 export function style(node, value, prev = {}) { } // ================== 事件相关 // 委托事件收集 export function delegateEvents(eventNames, document = window.document) { } // 清除委托事件收集 export function clearDelegatedEvents(document = window.document) { } // 注册事件 export function addEventListener(node, name, handler, delegate) { } // ================== Utils相关 // Utils,合并对象 export function mergeProps(...sources) { } // 定义动态属性 export function dynamicProperty(props, key) { } // 将props的所有项赋值到node中 export function assign(node, props, isSVG, skipChildren, prevProps = {}) { } // ================== dom修改 // TODO export function spread(node, accessor, isSVG, skipChildren) { } // 插入node节点到指定位置,如果有需要计算的属性也会开启effect反馈收集 export function insert(parent, accessor, marker, initial) { if (marker !== undefined && !initial) initial = []; if (typeof accessor !== "function") return insertExpression(parent, accessor, initial, marker); effect(current => insertExpression(parent, accessor(), current, marker), initial); } // ================== SSR相关 export function hydrate(code, element) { } export function gatherHydratable(element) { } export function getNextElement(template) { } export function getNextMatch(el, nodeName) { } export function getNextMarker(start) { } export function runHydrationEvents() { } export function getHydrationKey() { } export function Assets() { } export function NoHydration(props) { }
到这基本就是SolidJs转换的核心链路了,主要方式就是通过 AST 将 JSXElement 进行拆解,主要分解成以下4个部分:
effect
其实到目前为止,我的调研基本已经可以得出结论了——可行。
babel-plugin-jsx-to-dom-expressions
kbone
以上文章主要都是介绍的和编译时相关的内容,至于运行时的逻辑我这里就直接略过了;因为最初目标只是使用JSX实现类似 SvelteJS 的框架,而最核心的就是JSX的转换成命令式代码,而数据响应式驱动已经有成千的案例文章供我们参考了。
整篇文档是忙里偷闲挤出来的,写的很草率;只是很久没有特地花时间去解析别人代码了就记录一下,整体的代码难度不是很大,大家花一天时间足以,不过需要提前预备babel等知识储备,感兴趣的可以自行研究研究,还是挺有意思的。
最后在这个内卷圈子还是学点东西提升自我更有价值!祝大家早日晋升。
theme: channing-cyan
前言
由于近期团队技术需要调研如何使用 React DSL 实现类似 SvelteJs 的去除vdom+diff的前端框架,所以才有了以下文章的产生。
传送门:
所以本篇文章我将给大家介绍一下 SvelteJs 的实现原理?才怪~
本次我要介绍的是另一个前端框架———SolidJs(前端框架已经这么多了么???)
关于 SolidJs 的介绍大家可以参考掘金大佬的文章传送门,我这里就不过多描述了。
简单来说,SolidJs 是借鉴了 SvelteJs 的理念,使用React DSL开发的新框架。(是不是和我前面提到的调研方向非常匹配?大家的思路相当一致嘛)
下面我会针对 SolidJs 对他进行详细的拆解。
正片
在正式开始之前需要介绍一件事情,无论是 SvelteJs 还是 SolidJs,他们都有一个最核心的特性——将声明式代码编译成命令式代码。这也是我主要要介绍的内容。
什么是声明式代码?
什么是命令式代码?
那 SolidJs 做了什么呢?左边是源码,右边是编译后的代码。Demo链接
我们先大致扫一眼,接下来会仔细介绍。
模块拆解
参考链接:
从模块划分看,主要由两个部分构成:
而说到JS中的编译转换那必然就不可避免的会使用到 Babel,在 SolidJs 中
babel-plugin-jsx-dom-expressions
就是干这个事的。观察源码我们可以发现,主要配置项如下:
编译详解
整体的编译流程,如下图所示:
babel-plugin-jsx-dom-expressions
入口源码如下,主要是针对 JSXElement,JSXFragment 进行了转换,其他JS逻辑基本没有处理。而这其中最重要的就是
transformElement(path, info)
,他是整个编译过程拆解的核心。 他主要的作用就是通过AST将 JSXElement 转换成一个 Result 对象,结构如下:他将 JSXElement 解析成了一个对象,最终会根据这个对象来生成最终输出的 output代码;这么说可能有点抽象,我们结合实际产物来对比。
源代码如下:
编译后产物如下:
根据
transformElement(path, info)
的产物 Result 对象结构拆解来看Result.template 对应编译后代码中的 _temp$,主要用于创建节点的 Element 实例
Result.decl 对应编译后代码中的 _el$ 等节点变量声明
Result.exprs 对应编译后代码中的
insert,addEventListener
等 DOM 创建绑定相关的命令式创建的表达式;Result.dynamics 对应编译后代码中的 涉及到到动态计算相关的属性语句
Result.tagName=
button
,标识标签的名称Result.id=
{ "type": "Identifier", "name": "_el$2" }
, 用来当前转换的JSX这个节点最终生成的变量名最重要的就以上这几个了,其余的就是和HTML特性或者SSR相关的逻辑。
DOM-Expressions
从上面编译后的代码可以看到,从
solidjs/web
中导入了很多方法比如插入元素的
insert
,根据字符串创建Element的template
,而这些全都来自dom-expressions
这个库,而他底层封装就是DOM API。他所有提供的接口如下:总结
到这基本就是SolidJs转换的核心链路了,主要方式就是通过 AST 将 JSXElement 进行拆解,主要分解成以下4个部分:
effect
进行绑定。 再结合dom-expressions
提供的API对Element进行创建绑定操作,实现页面渲染。写在最后
其实到目前为止,我的调研基本已经可以得出结论了——可行。
dom-expressions
拆解的非常干净,我只需要按照我环境实现一个类似的API,再改造下babel-plugin-jsx-to-dom-expressions
进行转换;kbone
一样)就可以移植到小程序、客户端等其他容器场景;以上文章主要都是介绍的和编译时相关的内容,至于运行时的逻辑我这里就直接略过了;因为最初目标只是使用JSX实现类似 SvelteJS 的框架,而最核心的就是JSX的转换成命令式代码,而数据响应式驱动已经有成千的案例文章供我们参考了。
整篇文档是忙里偷闲挤出来的,写的很草率;只是很久没有特地花时间去解析别人代码了就记录一下,整体的代码难度不是很大,大家花一天时间足以,不过需要提前预备babel等知识储备,感兴趣的可以自行研究研究,还是挺有意思的。
最后在这个内卷圈子还是学点东西提升自我更有价值!祝大家早日晋升。