I have developed different applications and built different libraries with different UI frameworks for four years since I joined the frontend area.
加入前端领域四年来,我开发了不同的应用程序,并使用不同的用户界面框架构建了不同的库。
My first experience is a PHP + Vue.js 2 PWA. (At that time, people are suggesting using event bus + jQuery into Vue2), After that, I used redux and mobx (and I liked the mobx idea at the beginning of the React experience). Then nowadays, people are using Zustand, jotai, xstate, and SWR more. Even most of the frontend UI libraries/frameworks would like to say how much DOM renders performance than others, in 5k items or even higher, that I have never tried in my life. But I would like to see how they deal with the state.
我的第一次体验是 PHP + Vue.js 2 PWA。(当时,人们建议在 Vue2 中使用事件总线 + jQuery),之后,我使用了 redux 和 mobx(在 React 体验之初,我很喜欢 mobx 的想法)。如今,人们开始更多地使用 Zustand、jotai、xstate 和 SWR。甚至大多数前端 UI 库/框架都想说 DOM 渲染性能比其他库/框架高出多少,达到 5k 项甚至更高,但我这辈子都没试过。但我想看看他们是如何处理状态的。
Why does the state matter? I think most of the complexity of a frontend application(even in native apps like iOS or Android) comes from the complexity of its state.
状态为何重要?我认为前端应用程序(即使是 iOS 或 Android 等原生应用程序)的大部分复杂性都来自于其状态的复杂性。
State comes from initial data; every application has initial data. The client does some operations and then sends it back to the server for backup. Even though there is no real server, we can treat local storage as a 0-delay server. We abstract our app into a simple code:
状态来自初始数据;每个应用程序都有初始数据。客户端执行一些操作,然后将其发送回服务器进行备份。即使没有真正的服务器,我们也可以将本地存储视为 0 延迟服务器。我们将应用程序抽象为简单的代码:
const data = fetchInitialData() const app = createApp(data) app.mount(dom)
But hold on; there is a trap; how do we know what initial data is?
但是,等等,这里面有个陷阱;我们怎么知道什么是初始数据?
The answer is no, we don't know; some would say let's throw all the data to the user (like UDP)
答案是 "不",我们不知道;有人会说,让我们把所有数据都扔给用户吧(比如 UDP)。
Another kind of people are writing like this:
另一种人是这样写的:
function createUI() { const { data } = useFetch('/another_data') return createUIWithData(data) } function createApp() { const { data } = useFetch('/initial_data') return createAppWithData(data) } const app = createApp() app.mount(dom)
But there is another approach that puts a state cache into fetch; then, the server will know what exactly you need.
但还有另一种方法,即把状态缓存放入 fetch 中;这样,服务器就会知道你到底需要什么。
const data = fetchInitialData( getPreviousStateCache() ) const app = createApp(data) app.mount(dom)
Then we think up a great idea: let's call a prefetch into all the sub-components and let the compiler collect all the tags.
于是我们想到了一个好主意:让我们向所有子组件调用预取,让编译器收集所有标记。
// A.ts prefetch('component_A_initial_data') function ComponentA() { /* ... */ } // B.ts prefetch('component_B_initial_data') function ComponentB() { /* ... */ }
So, this is what SWR or tanstack/query are doing right now. It is a tag system on top of the applications. There is a trade-off: when your application grows, and data must have the relationship(posts need user, a user needs userId, userId needs login...), you must remember what tags are being collected and not name duplicates. This approach is also painful to many developers.
这就是 SWR 或 tanstack/query 目前正在做的事情。它是应用程序之上的标签系统。这需要权衡利弊:当您的应用程序不断增长,数据必须具有关联性(帖子需要用户,用户需要 userId,userId 需要登录......)时,您必须记住正在收集的标签,并且不能重复命名。这种方法对许多开发人员来说也很痛苦。
Then, people have another great idea: reactivity.
然后,人们又有了一个好主意:反应性。
//This is jotai, actually, but similar to vue3 reactivity. const idAtom = atom(fetchUserIdFromServer) const userAtom = atom(get => { const id = get(idAtom) // ... }) const postAtom = atom(get => { const user = get(userAtom) // ... })
Then, we never need to consider whether fetching multiple times in the downside components is possible because our computer memory already has a dependency graph. We need to check the graph and call all of the leaves.
这样,我们就不需要考虑是否可以在下部组件中多次取用,因为我们的计算机内存中已经有了一个依赖图。我们需要检查该图,并调用所有叶子。
But there is no free lunch. You can only write atoms like a tree, no cyclic graph. What's more, what if I want to change the id and don't change the user? You might think there won't be such a situation. Yesterday, a guy said his form component has over 100 fields, and some fields need to be refreshed when some other fields change, but in some cases, there's no refreshing. What do you think we should do?
但天下没有免费的午餐。你只能像写树一样写原子,而不能写循环图。更何况,如果我想改变 id 但不改变用户怎么办?你可能认为不会出现这种情况。昨天,一个人说他的表单组件有 100 多个字段,当其他字段发生变化时,有些字段需要刷新,但有些情况下却不需要刷新。你觉得该怎么办?
Yes, we have the state machine. Like in an FPS game, you cannot fire or move and stop simultaneously when you change the magazine. But the sad thing is that not all of the states in the frontend app can be turned into a state machine.
是的,我们有状态机。就像在 FPS 游戏中,更换弹夹时不能同时开火或移动和停止。但可悲的是,并非前端应用程序中的所有状态都能转化为状态机。
Actually, there is no silver bullet. People should reconsider what they need for their code. But there is not; some people might finish the prototype as fast as possible; others might treat one of the solutions above like Jesus reborn or another red sun rising in a web application.
事实上,没有什么灵丹妙药。人们应该重新考虑自己的代码需要什么。但事实并非如此;有些人可能会以最快的速度完成原型;有些人可能会把上述解决方案中的一种当作耶稣再世或网络应用中升起的另一轮红日。
People are faking an impossible triangle, like in the React Server Component.
人们正在伪造一个不可能的三角形,比如 React 服务器组件。
https://youtu.be/TQQPAU21ZUw?si=HA3Bm5Sc2ezTYftl&t=35
Good (UX) & Cheap (Maintenance) & Fast(Performance)
好(用户体验)、便宜(维护)、快(性能)
In reality, I think it will cause
实际上,我认为这会导致
Bad (DX) & Expensive (Server Cost) & Slow (Server Performance)
差(DX)、贵(服务器成本)、慢(服务器性能)
Even I think RSC is cool, and I'm developing it.
甚至连我自己都觉得 RSC很酷,而且我正在开发它。
But, re-think about the getPreviousStateCache function. If all data components are inside a server, we don't need prefetch because everything is collected on the server. Is this really like the idea of Inversion of Control (IoC)?
但是,请重新考虑一下 getPreviousStateCache 功能。如果所有数据组件都在服务器内,我们就不需要 prefetch 了,因为所有数据都在服务器上收集。这真的是控制反转(IoC)的概念吗?
Anyway, we solve the initial data problems. Let's render them: we have a button to increase the counter. Then counter plus one and cause UI to re-render. This UI is a simple model; we can tell there is a subscriber(counter state) and a publisher(button click event). And we can promise that every UI can be treated as a sub-pub model. Like interval is a publisher from your computer timer, or your typing is a keyboard interrupt from one callback in your macOS.
总之,我们解决了初始数据问题。让我们来渲染一下:我们有一个增加计数器的按钮。然后计数器加一,导致用户界面重新渲染。这个用户界面是一个简单的模型;我们可以知道有一个订阅者(计数器状态)和一个发布者(按钮点击事件)。我们可以保证,每个用户界面都可以被视为子发布者模型。比如,时间间隔就是电脑计时器的发布者,或者你的打字就是 MacOS 中一个回调的键盘中断。
But the real question is: how do we wrap the sub-pub better?
但真正的问题是:如何更好地包装子刊?
jQuery or vanilla JS gives you the raw power to control everything. But nobody like to use it now because people are too lazy. To summarize, people made up four solutions:
jQuery 或 vanilla JS 为您提供了控制一切的原始能力。但现在没人喜欢用它,因为人们太懒了。总而言之,人们提出了四种解决方案:
- Another version of sub-pub: people say, "I just like it."
useState+useEffectwill solve everything. Or even a complex version of a sub-pub, like a state machine.
另一个版本的 sub-pub:人们说,"我就是喜欢"。useState+useEffect就能解决一切问题。甚至是复杂版本的子ub,比如状态机。 - Proxy: people will use it when people call it.
代理:当人们呼叫它时,人们就会使用它。 - Dependency detection: people will use it when importing it.
依赖检测:人们在导入时会使用它。 - Stream flow: such people will say: There is no state. The only thing we have is the stream, like water from top to bottom.
溪流:这类人会说:没有国家。我们唯一有的就是溪流,就像从上到下的水。
2 and 3 are slightly different. Proxy uses ES6 Proxy feature, but Dependency detection uses object reference itself.
2 和 3 略有不同。代理使用 ES6 代理功能,但依赖检测使用对象引用本身。
I bet all the libraries are on this list, and no one is perfect.
我敢打赌,所有图书馆都在这份名单上,没有一个是完美的。
There is no perfect, like no free lunch or silver bullet. The reason is simple.
世界上没有十全十美的东西,就像没有免费的午餐或银弹一样。原因很简单。
Interesting take!有趣的观点
Nothing to preview