最近陆陆续续有人开始关注DLight了,github小星星也在一颗颗的涨起来了。所以我感觉还是有必要在中文社区写点文章来介绍一下DLight。之所以我前面没那么想宣传DLight,是因为在国外社区里和别人讨论,除了个别很感兴趣的人,剩下都是一群只写过react,然后看到不是functional component,不是jsx,就开喷的人。所以感觉它只是个符合小众taste的东西,自己爽就完了,跟别人解释设计原理难道还要让人家把web ios android几十个框架都用一遍然后有自己的思考再来讨论?
但看最近来讨论DLight的人越来越多了,大家还是聪明又友善的,所以我觉得还是有必要写点东西来介绍一下。打算写个小系列,从语法到feature到render表现,大致的过一下。当然,只讲现象,不讲原理。这篇文章呢就先从语法以及对比开始。然后我nicely please一眼class不行的人请划走,too tired to explain。
什么是DLight.js
我一直称DLight.js是一个DX-first的渲染库。因为写react真写累了,看vue文档也真看累了,然后我到现在都不知道solid传prop该传getter还是value,也不知道svelte4的两套reactivity到底为什么能这么抽象(哦,但是svelte5变成solid了)。所以就想写个框架,只要你会写js,然后看一遍文档,就能构建出性能好的、易开发的app。对于DLight,性能好那确实,DLight和solid和svelte5速度差不多,内存占用要比他们小30%左右(去看current benchmark)。易开发这点见仁见智吧,把这篇文章看完看看你觉得好不好用。
Fake FP vs. Real OOP
很多人觉得FP写的爽,但你js的functional component能叫FP吗!对于个signal-based的MVVM框架,你function只跑一次啊,你function只跑一次啊,跑完了你component数据存哪啊?跑到不能自动回收的closure里了吧。那为什么不主动建一个view-model来hold这个数据呢。。
咱都MVVM了,就别假FP了,试试真OOP构建ui呢。
语法对比
我也就不说什么废话了,直接show you the code。由于90%的人都用react,这边就以react为对比(这边对比会用useCallback,useMemo,memo来包,因为我们是要写real world的web出来用的,不能让原本就差的性能更差了,别说我cherry-pick捏)。
React:
function Counter() {
const [count, setCount] = useState(10);
const doubleCount = useMemo(() => count * 2, [count]);
const increment = useCallback(() => {
setCount(count + 1)
}, [count]);
return (
<>
<div>{doubleCount}</div>
<button onClick={increment}>Increment</button>
</>
)
}
DLight:
@View
class Counter {
count = 10;
doubleCount = this.count * 2;
increment() {
this.count++;
}
View() {
div(this.doubleCount)
button("Increment")
.onClick(this.increment)
}
}
所以我们看看DLight的这个Counter干了啥:
- @View声明了一个DLight component (DLight里面decorators都是只用了语法手动解析的假decorator,所以不存在decorator的性能和experimental的问题),我觉得很符合直觉。
- 声明了一坨property和method。诶?好奇怪哦,怎么我普通class怎么定义函数变量在DLight component里就怎么定义咧?咋没有各种奇奇怪怪的函数捏?答案是编译的magic让他符合直觉了。
- View()里面只声明了UI的结构,ui和逻辑分离了。这里面是DLight的dsl,看一遍文档就会了(别说不如jsx,说就是你taste不行)。
你想看的
没有显式声明state,setter或者.value;没有memo,store,副作用等
因为没有,所以就不写了。
深度更新和数组更新
@View
class Counter {
obj = {
data: {
nested: {
count: 10,
arr: [1, 2, 3]
}
}
}
View() {
div(this.obj.data.nested.count) // 点了就更新
button("Increment")
.onClick(() => {
this.obj.data.nested.count++;
})
// -------------------
div(this.obj.data.nested.arr.join(",")) // 点了就更新
button("Add")
.onClick(() => {
this.obj.data.nested.arr.push(0);
})
}
}
这里没用proxy,也没有什么奇奇怪怪的store来包裹。你就定义值,赋值,用值。但是能支持所有的object更新,array,set,map的函数赋值调用,而且更新粒度是到最后一个member的property的。
DLight DSL的一些
真的写条件渲染写烦了:
// React
{light === "red" && <span>STOP</span>}
{light === "orange" && <span>SLOW DOWN</span>}
{light === "green" && <span>GO</span>}
// Svelte
{#if light === "red"}
<span>STOP</span>
{:else if light === "orange"}
<span>SLOW DOWN</span>
{:else if light === "green"}
<span>GO</span>
{/if}
// Vue
<span v-if="light === 'red'">STOP</span>
<span v-else-if="light === 'orange'">SLOW DOWN</span>
<span v-else-if="light === 'green'">GO</span>
// Solid
<Switch>
<Match when={light() === "red"}>
<span>STOP</span>
</Match>
<Match when={light() === "orange"}>
<span>SLOW DOWN</span>
</Match>
<Match when={light() === "green"}>
<span>GO</span>
</Match>
</Switch>
// Angular
@switch (light) {
@case ("red") {
<span>STOP</span>
}
@case ("orange") {
<span>SLOW DOWN</span>
}
@case ("green") {
<span>GO</span>
}
}
能不能不要引入自己的语法?像DLight这样:
// DLight
if (light === "red") {
span("STOP")
} else if (light === "orange") {
span("SLOW DOWN")
} else if (light === "green") {
span("GO")
}
// or
switch (light) {
case "red":
span("STOP")
break;
case "orange":
span("SLOW DOWN")
break;
case "green":
span("GO")
break;
}
循环也是啊。。。
// React
{colors.map((color) => (
<li key={color}>{color}</li>
))}
// Svelte
{#each colors as color (color)}
<li>{color}</li>
{/each}
// Vue
<li
v-for="color in colors"
:key="color"
>
{{ color }}
</li>
// Solid
<For each={colors}>
{(color) => <li>{color}</li>}
</For>
// Angular
@for (let color of colors) {
<li>{{ color }}</li>
}
DLight:
for (let color of colors) {
li(color)
}
前面说你觉得jsx比较好你taste就不行可能是开玩笑的,但你看看这俩最常用的语法,家人们咱就是说平心而论一下是吧。
你已经会了这些,再加上:
- 组件传prop
@View
class Child {
@Content name
@Prop age;
View() {
div(this.name)
div(this.age)
}
}
@View
class Parent {
View() {
Child("小明")
.age(100)
}
}
- 一些上面没有提到的DSL:
// html
div("hello")
.style({color: "red"})
.class("red")
.onClick(() => {
console.log("它点我了!")
})
// nested
div(); {
div("1")
div("2")
table().style({border: "1px solid black"})
{
tr(); {
td("3")
td("4")
}
}
}
// text
"ok"
`cool`
'都是text'
- 监听变化
@View
class Counter {
count = 10
@Watch
watchCount() {
console.log(this.count, "变啦","你不需要手动写监听")
}
View() {
div(this.count)
}
}
- 生命周期
@View
class Counter {
willMount() {
console.log("马上有了")
}
didMount() {
console.log("已经有了")
}
willUnmount() {
console.log("马上没了")
}
didUnmount() {
console.log("已经没了")
}
View() {
div(this.count)
}
}
你说,这不是也要记吗????:D
- style
- 这是js文件,你想用啥用啥。你用emotion也行,用tailwind也行,你用stylex也行,你手写css module我都不拦着。
好了,你现在已经会了90%的DLight了,可以写95%的网页了。还有些很爽的feature比如environment subview啥的就不在这里说了,感兴趣的可以去官网看看。
你说hooks,我说model
你说hooks好用,那确实,算是functional component糟糕的心智模型(react不糟糕,其他signal的糟糕)下为数不多的优点了。
但是你看看这个,比如我们要写一个简易的useFetch拉数据的hook:
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
使用:
function App() {
const { data, loading, error } = useFetch("https://api.example.com/data");
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return <div>{data}</div>;
}
确实香,但你useEffect里面都不能用async,除非再包一层。
但在DLight的心智模型下面,这东西就是一个抽象的公共的model,你可以这么写:
@Model
class FetchModel {
@Prop url
data = null;
loading = true;
error = null;
@Watch
async fetch(url) {
this.loading = true;
try {
const res = await fetch(url);
const data = await res.json();
this.data = data;
} catch (error) {
this.error = error;
}
this.loading = false;
}
}
使用:
@View
class App {
fetchModel = use(FetchModel, {url: "https://api.example.com/data"})
View() {
if (this.fetchModel.loading) {
div("Loading...")
} else if (this.fetchModel.error) {
div(`Error: , ${this.fetchModel.error.message}`)
} else {
div(this.fetchModel.data)
}
}
}
在这里,写@Model和写@View的逻辑是一模一样的,一切declared property都有可能是state,你不需要显式定义,它只要被你用了,它就变成响应的了。
好玩的项目
有个discord老哥用DLight俩小时写了个小游戏:codesandbox.io/p/devbox/dl…
他刚听说DLight一天,看了遍文档就写了,不能说DLight好用了,确实是他牛逼。
总结
欢迎大家进wx群或者discord一起来讨论,整出一个解放画图的时代,好去做点技术力强的事情。
DLight的主页:dlight.dev/
Github:github.com/dlight-js/d…
Discord Link:discord.gg/SV6cMaQHHk
wx群:
或者加我(ID: iandxssxx)拉你进群