fre3——静悄悄地,给 fre 收个尾
halo 大家好久不贱,俺是 132,今天给大家带来一篇关于 fre3 的文章
fre 是一个前端框架,也是近几年我身上的一个标签,它诞生于 2018 我大二的时候,彼时 react16 的时间切片如火如荼,我为了研究它背后的原理,写了 fre,也是 1kb 的 React 实现
俺对 fre 是有感情的,可以说我的整个职业历程都和 fre 脱不开干系,比如 c 站是 fre 写的,小程序引擎是基于 fre 的,homo 也是基于 fre 的
但,今非昔比,前端框架已经到头了,我不得不重新思考,fre 该以怎样的形态活下去了
成也 Vdom 败也 Vdom
近几年大家对 vdom 的批评多余褒扬,主要是因为 vdom 虽然可以最小操作 dom,但实际上它是一种纯粹的开销,一般的框架,如 Fre2,Vue3,inferno,甚至使用 O(ND) 的 diff 算法,来换最短编辑距离
这其实是针对浏览器的一种特性优化
如果脱离浏览器,脱离 dom,或者说 dom 操作不那么昂贵,这种使用较高复杂度的算法,换来的就是负提升了
所有一些新兴的框架,比如 svelte,solidjs,追求细粒度的响应性,使用编译的空间换时间
那么问题来了,如果让我选,我会选什么呢?
fre3 也是一个编译器
是的没错了,fre3 的整体思路和 solidjs 差不多,因为前端框架已经走向末路,我不得不给 fre 安排一个“最终形态”
由于我过往的经验,不管是小程序,还是 homo,其实我更多的是将 fre 用于“跨端”,而不是纯 web
尤其是 homo,它几乎开启了嵌入式跨端的可能性,homo 是一个嵌入式跨端框架,众所周知,嵌入式需要的是最小内存占用,最小空间占用,最低的算法复杂度,不坏的性能
而 vdom 框架在内存和空间方面,是绝对无法做到极致的
于是 fre3 我下定决心走编译路线
整体设计
input
import f from 'fre'
function App(){
const count = f.signal(0)
return <button onclick={()=>count(count()+1)}>{count()}</button>
}
document.body.appendChild(<App/>)
output
function App() {
const count = f.signal(0)
return (() => {
let f0, f1;
f1 = f.ce('button');
f.ac(f0, f1);
f.ael(f1, 'onclick', () => count(count() + 1));
f.effect(() => f.stc(f1, count())); // 是闭包!
return f1;
})()
}
document.body.appendChild(App())
以上,细心的同学会发现一些奥秘,俺挑着说一说
- signal 是闭包,而不是 Proxy
大家喜欢 signal,是因为这玩意的细粒度更新,但是大家知道吗,signal 有两种本质不同的实现方式
一种是利用 Proxy,在 runtime 做到细粒度更新,比如 vue,preact-signal,qwik,都是这种
这样做有个终极无敌致命的缺陷:Proxy 的解构问题
另外一种是基于编译时+闭包实现的,这种实现方式有点像 hooks,通过闭包缓存一些信息,没什么缺点,只是编译器的语义成本而已
(p.s. 也没有顺序问题哦)
fre 当然会选择第二种方式,实际上 vue 也可以选择第二种方式,因为它也可以编译嘛,但很明显,人家想要同时集成 Proxy 和 vdom 的缺点,谁也拦不住
- 根节点级别保持语义
什么意思呢,就是这样:
function App() {
const count = f.signal(0)
return (() => { // 这里是 return 了一个 dom 哦
let f0, f1;
...
return f1;
})()
}
document.body.appendChild(App())
也就是说,根 jsx 是保持语义的,以前 return 了 vdom,现在 return 真实 dom
那么问题来了,既然只能做到根节点的语义,实际上 jsx 内部的语义是完全扭曲了的,这也是这类框架的通病,包括 solidjs
{list().map(a=><li>{a}</li>)}
会编译成
list().map(a=>{
f.effect(()=> f1.stc(a))
})
细心的童鞋可以发现,这里的 map 语义是没有被保留的,换成 forEach 也无所谓,因为根本不需要返回值……
编译器设计
说完了设计细节,说一下编译器细节
fre3 的编译器我准备用 rust 手写,而不是用 babel,这是很多原因导致的
首先 fre 未来更重要的是适配 homo 而不是 web,所以基本上不打算依赖 node 环境,你让一群写 c 的人装 node 实在是过于奢侈了
而 rust 可以交叉编译,也可以和 c 互调用,所以我姑且用 rust 写一下
然后就是一些小细节
- jsx 比 html 多了 expression
{...} // 也就是这玩意
只要解决掉这玩意儿,其余其实和 HTML parser 没啥区别了就
- CST 而不是 AST
fre 这次使用 CST,通俗描述每一个字符的位置,这更加有利于做 lint 啥的
其它
fre 虽然转向编译器了,但我仍然希望只求极简的代码里,也符合我一贯的风格
与此同时,这个编译器我希望可以同时用于 fre-miniapp(解析 wxml),asta(SSR)
总而言之,即便没有 fre,我也迟早需要一个手写完成度良好的 jsx 编译器
前端框架已经进入尾声,事到如今我也不指望 fre 能火什么的了,也不想和其他框架作者 battle,我就静悄悄地给 fre 收个尾就好了
最后放上 fre 的 github 地址,欢迎大家来讨论
转载自:https://juejin.cn/post/7229343968702021669