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())

以上,细心的同学会发现一些奥秘,俺挑着说一说

  1. signal 是闭包,而不是 Proxy

大家喜欢 signal,是因为这玩意的细粒度更新,但是大家知道吗,signal 有两种本质不同的实现方式

一种是利用 Proxy,在 runtime 做到细粒度更新,比如 vue,preact-signal,qwik,都是这种

这样做有个终极无敌致命的缺陷:Proxy 的解构问题

另外一种是基于编译时+闭包实现的,这种实现方式有点像 hooks,通过闭包缓存一些信息,没什么缺点,只是编译器的语义成本而已

(p.s. 也没有顺序问题哦)

fre 当然会选择第二种方式,实际上 vue 也可以选择第二种方式,因为它也可以编译嘛,但很明显,人家想要同时集成 Proxy 和 vdom 的缺点,谁也拦不住

  1. 根节点级别保持语义

什么意思呢,就是这样:


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 写一下

然后就是一些小细节

  1. jsx 比 html 多了 expression
{...} // 也就是这玩意

只要解决掉这玩意儿,其余其实和 HTML parser 没啥区别了就

  1. CST 而不是 AST

fre 这次使用 CST,通俗描述每一个字符的位置,这更加有利于做 lint 啥的

其它

fre 虽然转向编译器了,但我仍然希望只求极简的代码里,也符合我一贯的风格

与此同时,这个编译器我希望可以同时用于 fre-miniapp(解析 wxml),asta(SSR)

总而言之,即便没有 fre,我也迟早需要一个手写完成度良好的 jsx 编译器

前端框架已经进入尾声,事到如今我也不指望 fre 能火什么的了,也不想和其他框架作者 battle,我就静悄悄地给 fre 收个尾就好了

最后放上 fre 的 github 地址,欢迎大家来讨论

github.com/frejs/fre

原文链接:https://juejin.cn/post/7229343968702021669 作者:132

(0)
上一篇 2023年5月5日 上午10:31
下一篇 2023年5月5日 上午10:41

相关推荐

发表回复

登录后才能评论