過(guò)去很長(zhǎng)一段時(shí)間,前端框架們都在往響應(yīng)式的方向發(fā)展。大家都在基于 signal 實(shí)現(xiàn)自己的底層。這種趨勢(shì)看上去非常火熱,給人一種前端框架不往這個(gè)方向發(fā)展就落后了一樣。
同時(shí)又由于 React hooks 的深遠(yuǎn)影響,函數(shù)式 + 響應(yīng)式成為了不少前端心中最理想的前端框架模樣。Solid 成為了這種模式里最具代表性的框架。
但是,盡管如此,我依然對(duì)他保持一種不太愿意接納的態(tài)度,并不是說(shuō)我對(duì) solid 不熟悉,或者抗拒接受新的知識(shí),其根本原因,還是在語(yǔ)法設(shè)計(jì)上的問(wèn)題。
基于響應(yīng)式能實(shí)現(xiàn)細(xì)粒度更新,拋去虛擬 DOM 的 diff 成本,性能能夠得到很大的提升。這種設(shè)想其實(shí)非常美好,但是,在語(yǔ)法設(shè)計(jì)上會(huì)面臨巨大的挑戰(zhàn)。
我們來(lái)觀察并分析一下 solid.js 在語(yǔ)法設(shè)計(jì)上存在的問(wèn)題。
function Counter() { const [count, setCount] = createSignal(0) return <div onClick={() => setCount(count() + 1)}> Count: {count()} </div>}
在這個(gè)案例中,我們可以使用 createSignal 創(chuàng)建一個(gè)響應(yīng)式數(shù)據(jù)。這里的問(wèn)題就在于,返回的響應(yīng)式數(shù)據(jù) count 他不是一個(gè)數(shù)據(jù),而是一個(gè)獲取數(shù)據(jù)的 getter 方法。
因?yàn)榈讓踊?Proxy 來(lái)實(shí)現(xiàn),我們需要監(jiān)聽(tīng)到數(shù)據(jù)的變化,那么就需要借助 Proxy 中的 getter 方法來(lái)實(shí)現(xiàn),因此反饋到語(yǔ)法上,count 就只能是一個(gè)函數(shù)。
當(dāng)我們想要將其渲染到 JSX 中時(shí),在 solid 中就將其設(shè)計(jì)成 {count()}。這里設(shè)計(jì)成 count() 是沿用了 React 對(duì)于 JSX 的理解,想要傳入一個(gè)值給 JSX。
當(dāng)我們?cè)邳c(diǎn)擊事件中使用該響應(yīng)式數(shù)據(jù)時(shí)。
setCount(count() + 1);
如果你要精準(zhǔn)理解 count(),那么理解成本就有點(diǎn)高了,這里的 count() 執(zhí)行,表達(dá)了兩層含義。
初始化時(shí),count() 表示會(huì)隱式的收集依賴。在跟蹤范圍內(nèi),調(diào)用 getter 會(huì)導(dǎo)致調(diào)用 getter 的函數(shù)依賴于對(duì)應(yīng)的 signal。當(dāng) signal 更新時(shí),這些依賴都會(huì)被重新執(zhí)行。
更新時(shí)是依賴重新執(zhí)行,不只是 count() 重新執(zhí)行。許多人理解成 count 重新執(zhí)行,那么在語(yǔ)義上會(huì)有更進(jìn)一步的沖突。
例如:
const double_count = () => count() * 2// 或者在 jsx 中<div>count: {count()}</div>
更新時(shí),當(dāng)我們通過(guò)點(diǎn)擊等行為觸發(fā)更新,此時(shí)當(dāng)我們使用 count(),則只是簡(jiǎn)單的計(jì)算出 count 當(dāng)前的值。
setCount(count() + 1);
這里其實(shí)就是語(yǔ)法設(shè)計(jì)上的沖突問(wèn)題。同樣的函數(shù)執(zhí)行,由于編譯手段的強(qiáng)勢(shì)侵入,在不同的場(chǎng)景里表達(dá)了不同的含義。
其實(shí) solid.js 的開(kāi)發(fā)團(tuán)隊(duì)也希望 count 就像是直觀表達(dá)的那樣,他不是一個(gè) getter,而就是直接是一個(gè)值,因此就有類似于如下的語(yǔ)法設(shè)計(jì)
// 這個(gè)時(shí)候就變得正常了setCount((count) => count + 1);
但是很顯然,如果直接完全像 React 那樣符合直覺(jué)的語(yǔ)法設(shè)計(jì),響應(yīng)式的能力就得不到保證了。因此這是擁抱響應(yīng)式不得不做出的犧牲。
Solid 的這個(gè)語(yǔ)法割裂,在組件傳參的語(yǔ)法設(shè)計(jì)中,表現(xiàn)得尤為明顯。
例如你看下面這段代碼,令人意外的是,props.msg 是可以具備響應(yīng)性的,當(dāng)我還不熟悉 Solid 的時(shí)候直接大吃一驚。
function Message(props: Props) { return <div> <h1> hello, this message is: {props.msg} </h1> <Child /> </div>}
這是擁抱響應(yīng)式的無(wú)奈之舉。因?yàn)樵诮M件傳參的時(shí)候,其實(shí)可能存在兩種類型,一種類型是普通數(shù)據(jù),例如:
<Message msg='hello world' />
而另外一種,就是響應(yīng)性數(shù)據(jù),例如:
<Message msg={msg()} />
如果我希望一個(gè)字段,他可以傳普通類型、也可以傳響應(yīng)性類型,那么問(wèn)題就來(lái)了,子元素內(nèi)部如何判斷父組件到底會(huì)傳什么類型過(guò)來(lái)呢?
solid 的解決方案就是,只允許在父組件傳參時(shí),這樣寫(xiě) {msg()}。下面這種寫(xiě)法就會(huì)報(bào)錯(cuò)。
<Message msg={msg} />
這樣做的好處就是 solid 可以利用編譯手段去識(shí)別 msg() 然后深入子組件內(nèi)部做依賴收集,從而讓子組件內(nèi)部不需要做額外的判斷。但是付出的代價(jià)就是語(yǔ)法割裂更嚴(yán)重了。
除此之外,正因?yàn)橛泻诳萍嫉膹?qiáng)勢(shì)侵入,因此 solid 中的 JSX 與 React 中的 JSX 表現(xiàn)并完全不一致,也不能按照常規(guī)的表達(dá)式去理解。
語(yǔ)法的割裂是我不愿意擁抱 Solid 的主要原因。
讓我們來(lái)看看 rust 生態(tài)中,同樣是基于 signal 來(lái)實(shí)現(xiàn)的響應(yīng)式框架 Leptos 是如何在語(yǔ)法設(shè)計(jì)上解決 solid 的割裂問(wèn)題的。
首先,一個(gè)非常巧妙的設(shè)計(jì)就是,在 rsx 中,狀態(tài)傳入的括號(hào)中,直接接收的就是一個(gè)函數(shù)
#[component]fn App() -> impl IntoView { let (count, set_count) = create_signal(0); view! { <div>{count}</div> })
這里類似于 React 的 render props
這樣看著就非常的舒服。因?yàn)槁暶鞯?count 是一個(gè)函數(shù),模板渲染中需要的也是一個(gè)函數(shù),語(yǔ)法表現(xiàn)就很一致,按照這個(gè)設(shè)計(jì),我們就可以不用寫(xiě) count() 了。
這個(gè)小的語(yǔ)法設(shè)計(jì)細(xì)節(jié)的調(diào)整,讓整個(gè)語(yǔ)法都變得更加一致。
當(dāng)我們更新時(shí)
set_count.update(|count| *count += 1)
當(dāng)我們要往子組件中傳遞參數(shù)時(shí)
<ProgressBar progress=count />
當(dāng)語(yǔ)法規(guī)則發(fā)生一些簡(jiǎn)單的調(diào)整,我們會(huì)發(fā)現(xiàn),在大多數(shù)情況下,count 的使用都保持了一致性,而不是像 solid 那樣在不同的場(chǎng)景之下有不同的行為。
當(dāng)然,如果我們要在邏輯中獲取到 count 的值時(shí),仍然需要使用 count() 來(lái)達(dá)到目的。不過(guò)這在語(yǔ)義上是沒(méi)有沖突的。
let double = move || count() * 2;
與 solid 一樣,這段代碼類似于計(jì)算屬性,這個(gè)匿名函數(shù)也會(huì)被收集成為一個(gè)依賴,從而讓 double 也具備響應(yīng)性。
當(dāng)我們往組件內(nèi)部傳參數(shù)時(shí),rust 可以通過(guò)定義參數(shù)宏來(lái)接收和設(shè)置參數(shù)的類型、默認(rèn)值等。
#[component]pub fn ProgressBar( #[prop(default = 100)] max: u16, #[prop(into)] progress: Signal<i32>) -> impl IntoView { view! { <progress max=max value=progress /> }}
這個(gè)東西類似于面向?qū)ο笾械难b飾器,是給函數(shù)/屬性提供額外能力的一種語(yǔ)法。
他有如下幾種用法。
#[attribute="value"]#[attribute(key="value")]#[attribute(value)]
例如,我們將一個(gè)普通函數(shù)定義為一個(gè)組件,則對(duì)該函數(shù)使用如下的宏定義。
#[component]
接收一個(gè)參數(shù) max,默認(rèn)值為 100。
#[prop(default = 100)]max: u16,
支持任意類型的值傳入,然后調(diào)用 .into() 去轉(zhuǎn)化。
#[prop(into)]progress: Signal<i32>
因此,有了這個(gè)宏的幫助,我們的 progress 屬性可以接收一個(gè)響應(yīng)式屬性,也可以接收一個(gè)普通屬性。通過(guò)這種方式解決了 solid 在語(yǔ)法設(shè)計(jì)上面臨的困境。
<ProgressBar progress=count /><ProgressBar progress=|| 100 />
拋開(kāi) rust 的上手難度不談,在語(yǔ)法設(shè)計(jì)上,Leptos 的語(yǔ)法設(shè)計(jì)我認(rèn)為比 solid 要精妙得多。這是一種更成熟的語(yǔ)法構(gòu)思。
但是響應(yīng)式方案本身在語(yǔ)法上確實(shí)存在挑戰(zhàn),例如在 Solid 中還存在更嚴(yán)重的問(wèn)題就是使用解構(gòu)語(yǔ)法會(huì)導(dǎo)致數(shù)據(jù)失去響應(yīng)性,因此最終也只能靠各種編譯手段盡量抹平差異。但黑科技加多了,一不小心就在重新設(shè)計(jì)語(yǔ)法了。因此到目前為止,我依然更喜歡 React,他的語(yǔ)法設(shè)計(jì)足夠簡(jiǎn)潔,編譯手段的侵入性足夠小,更符合 JavaScript 的語(yǔ)法邏輯。
本文鏈接:http://www.tebozhan.com/showinfo-26-90668-0.html來(lái)自 Rust 生態(tài)的強(qiáng)烈沖擊?談?wù)?Leptos 在語(yǔ)法設(shè)計(jì)上的精妙之處
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com