摘要:是前端開(kāi)發(fā)領(lǐng)域新興的方法論體系,它繼承了與編程理念,在技術(shù)上有不少創(chuàng)新。但專利與開(kāi)源協(xié)議是平行的兩個(gè)世界,改底層也不大容易解決問(wèn)題。此外,要求在中結(jié)合各屬性的是否變化,判斷是否該觸發(fā)更新。
ReRest (Reactive Resource State Transfer) 是前端開(kāi)發(fā)領(lǐng)域新興的方法論體系,它繼承了 MVVM 與 FRP 編程理念,在技術(shù)上有不少創(chuàng)新。本文從專利稿修改而來(lái),主要介紹 ReRest 原理與若干實(shí)踐經(jīng)驗(yàn)。
?
說(shuō)明:文章作者授權(quán)任何組織或個(gè)人,在不更改原文內(nèi)容(包括本段)的前提下,可以自由轉(zhuǎn)載本文。點(diǎn)擊下載本文 PDF 格式
?
1. 前言前陣子 React 附加專利條件的開(kāi)源協(xié)議鬧得沸沸揚(yáng)揚(yáng),國(guó)內(nèi)外有多家大公司開(kāi)始棄用 React,我們也深感困惑,是否該將 shadow-widget 全盤(pán)改寫(xiě),很猶豫。讓底層脫離 React。但專利與開(kāi)源協(xié)議是平行的兩個(gè)世界,改底層也不大容易解決問(wèn)題。Facebook 擁有虛擬 DOM 方面的專利,preact、vue 都可能涉嫌侵權(quán),通過(guò)修改底層代碼來(lái)規(guī)避還是挺難的。
后來(lái)我們決定自己申請(qǐng)專利,以便今后萬(wàn)一用到,手頭有個(gè)專利可為 shadow-widget 增加話語(yǔ)權(quán)。當(dāng)權(quán)利要求書(shū)完稿時(shí),F(xiàn)acebook 宣布 React 回歸真正的 MIT 開(kāi)源協(xié)議了,真是大喜訊!我們不必?fù)?dān)心專利的風(fēng)險(xiǎn)了,為自家申請(qǐng)專利不再必要 —— 我們創(chuàng)建 shadow-widget 技術(shù)平臺(tái),但無(wú)意借此盈利,源碼開(kāi)放出來(lái)讓大家都受益。(PS:不必感謝,如果覺(jué)得這項(xiàng)目對(duì)您有用,上 github 為我們加星吧)
本文從專利申請(qǐng)稿改寫(xiě)而來(lái),內(nèi)容有壓縮,要不文章太長(zhǎng)了,另外還增加了可視化編程實(shí)踐相關(guān)的若干內(nèi)容。公布此文還有一個(gè)目的,防止他人偷偷拿我們的技術(shù)申請(qǐng)專利,如果以后真發(fā)現(xiàn)有人這么干了,本文是憑證,大家可以提請(qǐng)專利無(wú)效,把別人的保護(hù)條款廢掉。
說(shuō)明:本文完稿時(shí),Shadow Widget 最新版本為 v1.1.2,產(chǎn)品用戶手冊(cè)對(duì)技術(shù)實(shí)現(xiàn)有更詳細(xì)介紹。
2. 背景近些年 Web 前端技術(shù)發(fā)展,可以說(shuō)是框架橫飛的時(shí)代,雖然十年前網(wǎng)頁(yè)還正常能打開(kāi),IE 還是那個(gè)頑固的 IE,但前端開(kāi)發(fā)卻已經(jīng)歷翻天覆地的變化。近來(lái)比較搶眼的是 React 框架,F(xiàn)acebook 開(kāi)創(chuàng)性的實(shí)踐了兩種技術(shù):虛擬 DOM 與 Functional Reactive Programming(FRP,函數(shù)式響應(yīng)型編程),這兩種技術(shù)幾乎已成現(xiàn)代前端框架的標(biāo)準(zhǔn)配置。
Facebook 在虛擬 DOM 上原創(chuàng)較多,鉆研很深入,這項(xiàng)技術(shù)也可以說(shuō)很成熟了。FRP 在 React 的實(shí)現(xiàn)就是那個(gè) FLUX 框架,它不是 Facebook 首創(chuàng),在 React 中用起來(lái)也有點(diǎn)磕磕碰碰,尤其在調(diào)和指令式風(fēng)格與函數(shù)式風(fēng)格方面,并不順暢。
另外,盡管十年來(lái) Web 開(kāi)發(fā)技術(shù)發(fā)展很快,但在可視化開(kāi)發(fā)方面仍然進(jìn)展緩慢,所有主流框架都在界面的形式化描述上做文章,Angular 與 Vue 擴(kuò)展了標(biāo)簽屬性,增加不少控制指令,React 則全盤(pán)引入 JSX 描述方式,他們無(wú)一例外的都要求大家,一行行寫(xiě)腳本去定義界面,而不是 20 年前在 Delphi 與 VB 就已出現(xiàn)的可視化、所見(jiàn)即所得的開(kāi)發(fā)方式。
本文所提的 ReRest 編程方法,是適應(yīng) Web 可視化開(kāi)發(fā)要求,融合虛擬 DOM 與 FRP 技術(shù),并克服它們應(yīng)用于主流框架的若干不足,而提出的通用型解決方案。ReRest 方法在 shadow-widget 平臺(tái)有一些實(shí)踐,已取得良好效果。
3. ReRest 要點(diǎn)ReRest 全稱為 REactive REsource State Transfer,譯為 “響應(yīng)式資源狀態(tài)遷移”,與本概念相關(guān)的提法還有:
ReRest framework,ReRest 框架
ReRest based programming,基于 ReRest 的編程
ReRest-ful design,ReRest 風(fēng)格設(shè)計(jì)
光從字面上看,“響應(yīng)式資源狀態(tài)遷移” 不大好理解,就像縮寫(xiě)為 REST 的 “Representational State Transfer”,表現(xiàn)層狀態(tài)轉(zhuǎn)移,只看文字,是不大容易搞清楚講的是啥。
ReRest 提倡以 “資源” 的觀點(diǎn)展開(kāi)設(shè)計(jì),將針對(duì)資源的操作規(guī)格化,統(tǒng)一抽象成 4 類操作,在程序開(kāi)發(fā)過(guò)程中,可視界面的功能塊分解設(shè)計(jì)是一個(gè)維度,基于資源狀態(tài)變遷所帶來(lái)的單向數(shù)據(jù)流,構(gòu)成另一個(gè)維度,兩個(gè)維度共同形成一個(gè)正交矩陣,這種開(kāi)發(fā)方式有效平衡了指令式與函數(shù)式兩種設(shè)計(jì)風(fēng)格,集兩者優(yōu)勢(shì)于一身。
ReRest 理念與 REST 有某種相似性。REST 核心含義是用 URL 定位資源,用 HTTP 動(dòng)詞描述操作,它要求服務(wù)側(cè)提供的 RESTful API 中,只使用名詞來(lái)指定資源,原則上不使用動(dòng)詞,“資源” 概念可以說(shuō)是 REST 架構(gòu)的處理核心,針對(duì)資源的操作有 GET, POST, PUT, DELETE 等 HTTP 動(dòng)詞。在 ReRest 框架中,界面可視控件的屬性數(shù)據(jù)視作資源,依據(jù) shadow-widget 實(shí)踐,“資源(Resource)” 則指 React Component 的屬性數(shù)據(jù)。
理解基于 ReRest 的編程,須把握兩個(gè)重點(diǎn):Component 管界面呈現(xiàn),Resource 管數(shù)據(jù)流。前者適用靜態(tài)思維,更偏指令式風(fēng)格,后者適用動(dòng)態(tài)思維,更偏函數(shù)式風(fēng)格。
4. 兩種思維模式主流的前端框架一直并存靜態(tài)與動(dòng)態(tài)兩種思維模式,舉例來(lái)說(shuō),Vue 與 Angular 更多采用靜態(tài)思維模式,界面是可描述的,React 更多的用動(dòng)態(tài)思維,界面是可編程的,JSX 看上去也是一種表述形式,但它本質(zhì)是一段 javascript 代碼,你很難將它 “去編程化” —— 把 JSX 從上下文環(huán)境摳出來(lái)獨(dú)立使用,事情將變得毫無(wú)意義。
我們不必爭(zhēng)論這兩種模式孰優(yōu)孰劣,兩者都有顯著優(yōu)點(diǎn)。F.S.菲茨杰拉德曾說(shuō):檢驗(yàn)一流智力的標(biāo)準(zhǔn),就是頭腦中能同時(shí)存在兩種相反的想法,但仍保持行動(dòng)能力。
況且,在前端開(kāi)發(fā)中,該采用靜態(tài)思維或動(dòng)態(tài)思維的條件還算清晰。比如開(kāi)發(fā)一個(gè)網(wǎng)頁(yè),大塊功能的界面設(shè)計(jì)應(yīng)采用靜態(tài)思維,比方,在頂部放一個(gè)工具條,左側(cè)放導(dǎo)航,中間放內(nèi)容;簡(jiǎn)單界面設(shè)計(jì)應(yīng)以靜態(tài)思維為主,因?yàn)榻缑娼M件很少動(dòng)態(tài)替換;而應(yīng)對(duì)復(fù)雜功能,應(yīng)以動(dòng)態(tài)思維為主,既然 JS 代碼可以控制一切,局部界面用 JSX 定義會(huì)很爽。越是動(dòng)態(tài)變化的界面,應(yīng)該越傾向于用動(dòng)態(tài)的、編程性思維。
Angule 靜態(tài)思維過(guò)重,React 動(dòng)態(tài)思維過(guò)重,都不好,Vue 從靜態(tài)走向動(dòng)態(tài),易用且適應(yīng)復(fù)雜變化,應(yīng)該說(shuō)它正前進(jìn)在正確道路上,只是,Vue 兼容兩種風(fēng)格并非一開(kāi)始就統(tǒng)籌規(guī)劃了,工具復(fù)雜性不容易降下來(lái)。
5. 從 FRP 到 FLUX,再到 ReRestReRest 在前人已有經(jīng)驗(yàn)基礎(chǔ)上,提出更優(yōu)方法,然后驗(yàn)證,結(jié)合實(shí)踐再調(diào)整、優(yōu)化,React 生態(tài)鏈上系列工具的實(shí)踐是其中最重要的經(jīng)驗(yàn)基礎(chǔ)。
如果只把 React 看作虛擬 DOM 庫(kù),它無(wú)疑是一項(xiàng)偉大的發(fā)明,作為 DOM 節(jié)點(diǎn)對(duì)應(yīng)物,可按任意方式使用它。你完全可以在 React 基礎(chǔ)上擴(kuò)展出像 Vue 那樣的指令式描述系統(tǒng),甚到回退到 jQuery 方式也行(偷偷告訴你一個(gè)關(guān)鍵點(diǎn),用 node.__reactInternalInstance$XXX 能反查 React Component),用 React 搭建 MVVM 也完全可能,React 團(tuán)隊(duì)在 SoC(關(guān)注度分離)方面分寸把握得很好。
React 工具鏈普遍遵從濃重的函數(shù)式編程風(fēng)格,從函數(shù)式拓展命令式較為容易,但反過(guò)來(lái)就困難得多。就像許多編程語(yǔ)言,都從 LISP 普系吸收營(yíng)養(yǎng),相對(duì)來(lái)說(shuō),函數(shù)式編程更反映事物的本原,從此出發(fā)更容易理順具有復(fù)雜關(guān)系的框架系統(tǒng)。
由于上面原因,ReRest 的實(shí)踐性探索從 React 開(kāi)始,而不是 Vue 或其它工具。
5.1 理解 React 的 FRP 機(jī)制FRP 是響應(yīng)式編程一種范式,由不斷變化的數(shù)據(jù)驅(qū)動(dòng)界面持續(xù)更新,界面更新中,或用戶操作(如鼠標(biāo)點(diǎn)擊)中又產(chǎn)生新的數(shù)據(jù)流,再驅(qū)動(dòng)界面更新,如此循環(huán)往復(fù)。觸發(fā)界面更新的數(shù)據(jù)流也稱事件流,因?yàn)樗男袨榉绞接幸恍┫薅ǎ皇浅R?guī)數(shù)據(jù)流動(dòng),它至少要求單流向、細(xì)粒度、按 tick 觸發(fā)。
我們不妨把網(wǎng)頁(yè)界面的更新過(guò)程,理解成眾多 “驅(qū)動(dòng)更新的時(shí)間片” 的集合,一個(gè)時(shí)間片稱為一個(gè) tick,各 tick 可能前后緊挨著,但兩個(gè) tick 之間至少都有 “調(diào)度間隙”。就像下面 process2 函數(shù)緊隨 process1 執(zhí)行,用 setTimeout(process2,0) 延時(shí) 0 秒,這兩函數(shù)之間就產(chǎn)生 “調(diào)度間隙” 了。
function process1() { console.log("in process1"); setTimeout( function process2() { console.log("in process2"); },0); }
數(shù)據(jù)變化導(dǎo)致界面更新(即 React 的 render() 調(diào)用),界面更新又觸發(fā)數(shù)據(jù)變化,如果沒(méi)有調(diào)度間隙,系統(tǒng)可能陷入無(wú)限遞歸,遞歸結(jié)果必然爆棧。React 的 FLUX 框架首先要讓數(shù)據(jù)單向流動(dòng),只要有 “調(diào)度間隙” 區(qū)隔,即使數(shù)據(jù)變化與界面更新無(wú)限制的互為觸發(fā),都算單向流動(dòng)。
React 以兩種機(jī)制保障數(shù)據(jù)單向流動(dòng),一是讓 props 只讀,二是 setState() 延后一個(gè)調(diào)度間隙執(zhí)行。后者好理解,前者 “props 只讀” 是間接生效的,因?yàn)?props 與 state 同時(shí)決定 Component 界面如何表現(xiàn),但更改 props 屬性只能在父節(jié)點(diǎn)的 render() 函數(shù)中進(jìn)行,你得用 ownerComp.setState() 觸發(fā)父節(jié)點(diǎn)再次 render(),所以,不管你怎么用,都會(huì)插入 “調(diào)度間隙” 的。
此外,React 要求在 shouldComponentUpdate() 中結(jié)合各屬性的 immutable 是否變化,判斷是否該觸發(fā) render 更新。總之,上述機(jī)制支持了 FRP 編程以下要求:按時(shí)間切片驅(qū)動(dòng)界面更新,各切片保持細(xì)粒度,讓每次更新最小化、無(wú)關(guān)聯(lián)。
5.2 改造雙源驅(qū)動(dòng)由父節(jié)點(diǎn)決定如何更新的 props.attr,與節(jié)點(diǎn)自己就能決定的 state.attr,兩者共同定義 Component 的界面表現(xiàn),所以 props 與 state 合稱為 “雙源”,只是原生 React 是 “隱式雙源”,ReRest 框架要把它改造成 “顯式雙源”。
實(shí)現(xiàn)原理大致如下:
引入一個(gè)與 props.attr 及 state.attr 對(duì)等的集合:duals.attr
該集合中的 attr 把 props.attr 自動(dòng)記錄到 state.attr,通過(guò) duals.attr 讀寫(xiě)接口,可等效實(shí)現(xiàn)對(duì)相應(yīng) state.attr 的存取,即:讀 duals.attr 等效于讀 state.attr,寫(xiě)操作 duals.attr = value 等效于執(zhí)行 this.setState({attr:value})。
提供 this.defineDual() 讓用戶手工注冊(cè) duals 屬性
系統(tǒng)還將傳給標(biāo)簽內(nèi)置屬性(如 name,href,src 等)自動(dòng)注冊(cè)為 duals 屬性,此舉方便了編程,否則大量屬性手工編碼去注冊(cè)很麻煩。
由 defineDual() 實(shí)現(xiàn) setter 回調(diào)的捆綁
比如調(diào)用 this.defineDual("a",setter) 注冊(cè)后,對(duì)它賦值 this.duals.a = value,將自動(dòng)觸發(fā) setter(value,oldValue) 回調(diào)。
經(jīng)上述改造,更改 Component 自身的 props 就不必繞轉(zhuǎn)到父節(jié)點(diǎn)去做了,比如,用類似comp.duals.name = "new_name" 語(yǔ)句直接賦值就好。
這么變動(dòng)將帶來(lái)一個(gè)重大影響:上層 FLUX 機(jī)制可以捊直了做。如何實(shí)現(xiàn) FLUX,官方給出了框架建議,React 說(shuō)我只管虛擬 DOM,如何搭 FLUX 是上層的事,Redux 說(shuō),我來(lái)管這事,增加 action,增加 reducer,增加 store,不過(guò)異步的事你自己解決。什么是 action 呢?就是事件化數(shù)據(jù),什么是 reducer 呢?就是事件處理函數(shù),什么是 store 呢?那個(gè) Component 限制了數(shù)據(jù)讀寫(xiě),還搞不清關(guān)聯(lián)子節(jié)點(diǎn)、父節(jié)點(diǎn)在哪,自個(gè)弄一數(shù)據(jù)集就是 stroe。結(jié)果,Redux 繞了很大一個(gè)彎,說(shuō)把事情解決了,但用戶仍報(bào)怨寫(xiě)異步很難受呀,這么繞的東西不難受就鬼了!
ReRest 的對(duì)策很簡(jiǎn)單,最直接。事件化數(shù)據(jù)就是可偵聽(tīng)的 duals 屬性嘛,事件處理函數(shù)就是 duals 的 setter 回調(diào),理不清父子從屬關(guān)系,就弄一個(gè) W 樹(shù)吧,把各節(jié)點(diǎn)串起來(lái),用 this.componentOf() 按相對(duì)路徑(或絕對(duì)路徑)直接找,至于 store,哪有必要,Component 自身就是 store 嘛!
5.3 資源化ReRest 嘗試讓 Web 開(kāi)發(fā)回歸事物本原,網(wǎng)頁(yè)開(kāi)發(fā)主要處理兩樣?xùn)|西:開(kāi)發(fā)界面、與服務(wù)器交換數(shù)據(jù),它與 Delphi、Qt 等 GUI 開(kāi)發(fā)工具不該有太大差別,為什么 React 就不能支持 MVVM 呢?MVC 難以適應(yīng)標(biāo)簽化的界面表達(dá)形式,但用 MVVM 是沒(méi)問(wèn)題的。
常規(guī)所見(jiàn)即所得開(kāi)發(fā)工具,界面設(shè)計(jì)的主體過(guò)程是:拖入一個(gè)樣板創(chuàng)建界面組件,選中它對(duì)修改某些屬性,再拖入樣板創(chuàng)建其它組件,設(shè)屬性,重復(fù)操作直至組裝出復(fù)雜界面。外觀設(shè)計(jì)差不多就這些,剩下工作主要是功能實(shí)現(xiàn),實(shí)現(xiàn)類似如何接收鍵盤(pán)輸入,如何響應(yīng)按鈕點(diǎn)擊等函數(shù)定義。
原生 React 之所以離常規(guī)可視化設(shè)計(jì)很遠(yuǎn),主要是 Component 屬性成員級(jí)別的設(shè)計(jì)還不夠好,少一層可靜態(tài)依賴的錨點(diǎn),過(guò)早套上高度動(dòng)態(tài)變遷的事件流了,所有東西都動(dòng)態(tài)變化,可視設(shè)計(jì)是無(wú)法支持的。在 ReRest 設(shè)計(jì)理念中,凡 Component 屬性中公開(kāi)供控制,或供配置的,都應(yīng)視作 “資源”,“資源” 是靜態(tài)化的概念,就像 RESTful 要求 URL 要用名詞表達(dá)資源,動(dòng)作統(tǒng)一由 HTTP 的 GET, POST, PUT 等表達(dá)一樣,將 Component 屬性 “資源化”,才是問(wèn)題解決之道。
就 shadow-widget 已有實(shí)踐而言,ReRest 所謂的資源,專指 Component 的靜態(tài)屬性(即 comp.props.attr)與雙源屬性(即 comp.duals.attr)。
React 對(duì) Component 渲染組裝在 render 函數(shù)中完成,組裝過(guò)程是一段 JS 代碼,因?yàn)?JS 代碼可以任意書(shū)寫(xiě),如何組裝會(huì)非常靈活。而靈活是一把雙刃劍,功能雖然強(qiáng)大了,但缺少穩(wěn)定形態(tài),對(duì)建立 MVVM 框架與可視化開(kāi)發(fā)都不利。
ReRest 希望將渲染過(guò)程,改造成開(kāi)發(fā)主體依賴于對(duì) “資源” 的操作,當(dāng)然,這里的 “資源” 是動(dòng)作化了的,也就是,讀寫(xiě)資源會(huì)自動(dòng)觸發(fā)預(yù)設(shè)的關(guān)聯(lián)動(dòng)作。換一句話來(lái)說(shuō),ReRest 想把 render 函數(shù)改造成一種固定格式,不必再通過(guò)寫(xiě)一段過(guò)程代碼實(shí)施控制,而改成對(duì)若干 duals.attr 讀寫(xiě),以此驅(qū)動(dòng)渲染過(guò)程的定制處理。
ReRest 對(duì)渲染的 “資源化” 改造過(guò)程,本質(zhì)是將過(guò)程控制邏輯,挪到 “資源” 附屬的動(dòng)作函數(shù)中書(shū)寫(xiě)。
5.4 渲染臨界區(qū)如下示例:
01 render() { 02 // 進(jìn)入渲染臨界區(qū) 03 渲染臨界區(qū)的過(guò)程處理 ... 04 // 退出渲染臨界區(qū) 05 06 固定程式的其它 render 處理 ... 07 }
“渲染臨界區(qū)” (Rendering Critical Section) 中的代碼(上面 03 行)用來(lái)驅(qū)動(dòng)本 Component 各個(gè) duals.attr 附屬動(dòng)作。上面 06 行,讓附屬動(dòng)作處理后的結(jié)果生效,完成渲染輸出。經(jīng)此改造,用戶不必再定義各 Component 的 render() 函數(shù)。
在 “渲染臨界區(qū)” 執(zhí)行的代碼有特別要求,其一,render 函數(shù)因?yàn)橛?React 內(nèi)核發(fā)起,使用有一些限制,比如 render 過(guò)程中再次觸發(fā)更新、用 ReactDOM.findDOMNode 查找 node 節(jié)點(diǎn)等,不過(guò),隨著 React 版本優(yōu)化,這些限制逐漸變少(比如目前版本在 render 函數(shù)中調(diào)用 findDOMNode 不再報(bào)錯(cuò)了)。其二,ReRest 資源的行為函數(shù)如何被調(diào)用,在臨界區(qū)中與臨界區(qū)外有差別,下文馬上介紹。
5.5 資源的行為定義ReRest 區(qū)分兩類資源,只讀資源(即 comp.props.attr)與可寫(xiě)資源(即 comp.duals.attr),對(duì)于前者,在 Component 生存周期內(nèi),只支持 “讀” 行為,而對(duì)于后者,支持讀、寫(xiě)、setter 處理、listen 處理共 4 種行為。
這 4 種行為含義如下:
讀
從 props.attr 直接讀取,或從 duals.attr 讀取由系統(tǒng)返回 state.attr 的值。
寫(xiě)
對(duì) duals.attr 賦值,系統(tǒng)除了把值賦給 state.attr 外,還觸發(fā)相應(yīng)的 “setter處理” 與 “l(fā)isten處理”。
setter 處理
這個(gè) setter 就是 defineDual(attr,setter) 的 setter 回調(diào)函數(shù)。對(duì)于同一 Component 的同一 attr,可以調(diào)用多次 defineDual() 注冊(cè)多個(gè)回調(diào)函數(shù),給 attr 賦值后,各回調(diào)函數(shù)依次被調(diào),調(diào)用順序與注冊(cè)順序相同。
listen 處理
對(duì)一個(gè)已存在 comp.duals.attr,可調(diào)用 comp.listen(attr,fn) 登記一項(xiàng)偵聽(tīng),當(dāng) attr 值發(fā)生變化后,系統(tǒng)會(huì)自動(dòng)調(diào)用 fn(value,oldValue)。同一 comp.duals.attr 支持在多處偵聽(tīng),我們可以為兩個(gè)(或多個(gè)) duals.attr 建立偵聽(tīng)關(guān)聯(lián),一處更新,其它地方也聯(lián)動(dòng)更新。
setter 處理與 listen 處理的適用場(chǎng)合有明顯差別,setter 函數(shù)只在渲染臨界區(qū)的處理過(guò)程中被調(diào)用,listen 函數(shù)在觸發(fā)后(即更改 duals 屬性值)必定延后一個(gè) “調(diào)度間隙” 才被執(zhí)行,所以它必然不在任何節(jié)點(diǎn)的渲染臨界區(qū)內(nèi)執(zhí)行。
在同一節(jié)點(diǎn)的渲染臨界區(qū)內(nèi),setter 函數(shù)可被連續(xù)調(diào)用,當(dāng)前節(jié)點(diǎn)中不同 duals.attr 的 setter,或同一 attr 的 setter 可串連執(zhí)行,這意味著,臨界區(qū)內(nèi)對(duì)當(dāng)前節(jié)點(diǎn) duals.attr 賦值可能會(huì)引發(fā)遞歸重入,各次 setter 調(diào)用之間沒(méi)有 “調(diào)度間隙” 區(qū)隔。比如對(duì) comp1.duals.attr1 修改,導(dǎo)致 comp1.duals.attr2 與 comp2.duals.attr3 修改,而 comp1.duals.attr2 修改可能再導(dǎo)致 comp1.duals.attr1 修改,這時(shí)對(duì) comp1.duals.attr1 賦值可能導(dǎo)致該屬性的 setter 函數(shù)遞歸調(diào)用,而引發(fā)的 comp2.duals.attr3 更改卻是延后一個(gè) “調(diào)度間隙” 的,因?yàn)?comp2 的雙源屬性 setter 函數(shù)將在 comp2 的臨界區(qū)被調(diào)用。
setter 與 listen 處理反映了兩類資源聯(lián)動(dòng)的需求,常規(guī)情況下,隔一個(gè) “調(diào)度間隙” 可確保數(shù)據(jù)單向流動(dòng),而特殊情況下,對(duì)于緊密相關(guān)的資源聯(lián)動(dòng),如果總有 “調(diào)度間隙” 隔著,顯然會(huì)影響運(yùn)行效率,上述機(jī)制保留了重入式 setter 回調(diào)是有意義的。
6. 范式變換Redux 是 React 生態(tài)鏈中提供 FLUX 框架的一個(gè)典型工具,有代表性,接下來(lái)介紹范式變換與它有關(guān)。
Redux 以 “Action” 的觀點(diǎn)展開(kāi)設(shè)計(jì)(其它 FLUX 工具也大都如此),ReRest 則要求以 “Resource” 的觀點(diǎn)展開(kāi)設(shè)計(jì),Action 是動(dòng)態(tài)的動(dòng)作,Resource 是靜態(tài)的資源,兩者差別可用 “非 RESTful” 風(fēng)格與“RESTful” 的差別來(lái)類比。基于這兩種觀點(diǎn)的設(shè)計(jì)存在范式變換關(guān)系,下面我們用 Redux 與 shadow-widget 的 FLUX 實(shí)現(xiàn)差異為例,展開(kāi)說(shuō)明。
6.1 單 Store 變多 Store拿 Redux 用戶手冊(cè)提到的 Todo 例子來(lái)說(shuō),增加一條 todo 記錄,基于 Action 觀點(diǎn)會(huì)先設(shè)計(jì)一個(gè) Action 定義:
const ADD_TODO = "ADD_TODO"; var actTodo = { type: ADD_TODO, text: "Build my first Redux app" };
然后,設(shè)計(jì)一個(gè) reducer 響應(yīng)這個(gè) Action:
function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false, }]; // ... } // ... }
Redux 采用單一的大 Store 結(jié)構(gòu),ReRest 要求的資源卻是小數(shù)據(jù),相當(dāng)于把 Redux 的大 Store 分割成許多小塊,一個(gè)小塊就是一個(gè)資源。針對(duì) todo 列表,資源項(xiàng)用 duals.todoList 表示,指定它的初值是空數(shù)組。
this.defineDual("todoList",null,[]);
然后如下代碼添加一條 todo 記錄,就對(duì)等實(shí)現(xiàn)了上述 reducer 功能:
utils.update(this,"todoList", {$push: [{ text: "Build my first Redux app", completed: false, }]});
ReRest 的 Store 具備兩個(gè)特點(diǎn):
采用多 Store(與 reflux 類似),Store 實(shí)體與 Component 重合。
由于數(shù)據(jù)流動(dòng)設(shè)計(jì)針對(duì) Component 下的屬性展開(kāi),為方便理解,ReRest 的 Store 也可視為雙層結(jié)構(gòu),第一層是 Component 實(shí)體,第二層是 Component 下視作 resource 的屬性定義,包括 props.attr 與 duals.attr。
Component 下的 resource,本質(zhì)是數(shù)據(jù),與 Store 同屬一類,Redux 的 reducer 定義,對(duì)應(yīng) ReRest 變成 4 種資源行為定義(讀、寫(xiě)、setter、listen),而 Redux 的 Action 則弱化成一條操作資源的常規(guī)語(yǔ)句。強(qiáng)調(diào)一句,Redux 設(shè)計(jì)用 Action 提綱挈領(lǐng),ReRest 設(shè)計(jì)用 Resource 提綱挈領(lǐng),弱化 Action 是很自然的事,因?yàn)橄嚓P(guān)操作可以隨時(shí)添加,抓住數(shù)據(jù)定義才是核心本質(zhì)。Redux 編程中,給 Action 指定一個(gè)常量名,再定義 Action 結(jié)構(gòu),然后用 switch..case 到處判斷 action.type,就沒(méi)人覺(jué)得煩嗎?
6.2 數(shù)據(jù)定義用作事件偵聽(tīng)一個(gè) duals.attr 后,偵聽(tīng)函數(shù)就是事件處理函數(shù),F(xiàn)LUX 框架要求的 Dispatcher 可以簡(jiǎn)化,比如我們用 duals.receivedData = data 表示接收到外部一條指令,對(duì)它賦值即觸發(fā)偵聽(tīng)它的事件處理函數(shù)馬上被調(diào)。
如果對(duì) duals.receivedData 賦值時(shí),新舊值沒(méi)有變化,系統(tǒng)將忽略觸發(fā)偵聽(tīng)函數(shù)。要是不想忽略,調(diào)整一下數(shù)據(jù)定義,比如用 duals.receivedData = [data,ex.time()],加一個(gè)時(shí)間戳,就保證每次對(duì) duals.receivedData 賦值,都能觸發(fā)偵聽(tīng)函數(shù)了。
盡管 ReRest 聚焦于如何配置資源,duals.attr 的組織形式很簡(jiǎn)單,卻完整支持事件流機(jī)制,包括多源頭偵聽(tīng),等全部事件來(lái)齊后再觸發(fā)回調(diào)函數(shù),例如:
utils.waitAll(comp1,"attr1",comp2,"attr2", function(value1,value2) { // do something ... });6.3 渲染器
如果一個(gè)節(jié)點(diǎn)的結(jié)構(gòu)比較穩(wěn)定,比如它渲染輸出的標(biāo)簽名不變,其子節(jié)點(diǎn)構(gòu)成也不變,這時(shí),對(duì)該節(jié)點(diǎn)的屬性做 “資源化” 改造很容易。但如果節(jié)點(diǎn)結(jié)構(gòu)不穩(wěn)定,比如,有時(shí)單節(jié)點(diǎn),隨時(shí)變?yōu)槎鄬庸?jié)點(diǎn),甚至有時(shí)輸出的標(biāo)簽名也在變。我們還得另尋方法實(shí)現(xiàn)資源化定義,解決對(duì)策便是 “渲染器”。
在 React 中內(nèi)容組裝在 render() 函數(shù)進(jìn)行,通常由 comp.setState() 驅(qū)動(dòng)render() 函數(shù)反復(fù)調(diào)用。render 是動(dòng)作,按資源化方式理解,把它變名詞,是 rendering,就是渲染器,我們假想 render() 由一個(gè)渲染器驅(qū)動(dòng),渲染器內(nèi)部用一個(gè)計(jì)數(shù)器(記為 id__)控制渲染刷新,比如:comp.duals.id__ = 2 賦值導(dǎo)致 render() 被調(diào)用,運(yùn)行 comp.setState({attr:value}) 也促使 render() 調(diào)用,而且 id__ 會(huì)自動(dòng)取新值。也就是說(shuō),每次 render() 運(yùn)行,渲染器的計(jì)數(shù)器都會(huì)自動(dòng)取不同值,等效于執(zhí)行 duals.id__ = value 語(yǔ)句。
按如下方式注冊(cè) duals.id__:
01 this.defineDual("id__", function(value,oldValue) { 02 // this.state["tagName."] = "div"; 03 // this.state.attr1 = xxx; 04 // this.duals.attr2 = xxx; 05 06 // prepare jsx_list ... 07 // var jsx_list = [... ]; 08 07 // utils.setChildren(this,jsx_list); 10 });
上述渲染器 duals.id__ 的 setter 函數(shù),我們稱為 idSetter 函數(shù),這種以 “渲染器資源” 指代 render() 渲染過(guò)程的定義形式,稱為 idSetter 定義。
在 idSetter 函數(shù)中編寫(xiě)代碼,等效于在 render() 編程,可以隨意組裝子節(jié)點(diǎn),然后用 utils.setChildren() 設(shè)進(jìn)去。還可修改當(dāng)前節(jié)點(diǎn)的 state.attr, duals.attr,甚至節(jié)點(diǎn)的標(biāo)簽名也可以改,如上面 02 行代碼。
借助 duals.attr 的資源化形式(包括 duals.id__ 渲染器),ReRest 實(shí)現(xiàn)了 render() 渲染過(guò)程的范式變換。現(xiàn)有實(shí)踐表明,基于 ReRest 的編程與 React 原生方式等效,表達(dá)能力近乎等同。
7. 可視化設(shè)計(jì)與 MVVM 框架為了支持可視化編程,像 JSX 這種與 JS 代碼混寫(xiě)的界面描述方式需要改進(jìn),因?yàn)榻缑嬖O(shè)計(jì)應(yīng)獨(dú)立進(jìn)行。在可視化設(shè)計(jì)器中,被設(shè)計(jì)的界面,不能像產(chǎn)品正常運(yùn)行那樣表現(xiàn)功能,鼠標(biāo)點(diǎn)擊在可視化設(shè)計(jì)器中表示選擇一個(gè)構(gòu)件,接下來(lái)要配置它的屬性,而對(duì)于正式運(yùn)行的產(chǎn)品,可能是按鈕點(diǎn)擊、跳轉(zhuǎn)鏈接點(diǎn)擊等,所以,基于 ReRest 的編程,要求我們改用一種 “功能定義可選捆綁” 的界面描述方式。
shadow-widget 采用 “轉(zhuǎn)義標(biāo)簽” 描述界面,界面的功能實(shí)現(xiàn)則在投影類或 idSetter 函數(shù)中實(shí)施,這兩者分開(kāi)定義。產(chǎn)品正常運(yùn)行時(shí),在頁(yè)面導(dǎo)入初始化階段,兩者自動(dòng)捆綁,讓類似 onClick 在 JS 實(shí)現(xiàn)的功能定義,與用 “轉(zhuǎn)義標(biāo)簽” 描述的界面結(jié)合。但在可視化設(shè)計(jì)狀態(tài),功能定義缺省被忽略(注:也可以不忽略,但要用特殊方式定義)。
上述 “轉(zhuǎn)義標(biāo)簽”,就是用類似 desc
,或用類似 title 描述行內(nèi)標(biāo)簽 。上述 “投影類”,與 “idSetter 定義” 等效,都用來(lái)定義 Component 節(jié)點(diǎn)的行為。限于本文篇幅,這三項(xiàng)我們不展開(kāi)介紹。
前面介紹的資源化改造,還支持了 MVVM 框架在 React 技術(shù)體系中得以實(shí)現(xiàn),MVVM 要求數(shù)據(jù)屬性能夠雙向綁定,duals.attr 的 getter/setter 支持了此項(xiàng)要求。如下圖,ViewModel 就是投影定義與 idSetter 定義,View 是各 Component 從虛擬 DOM 反映到真實(shí) DOM 的界面表現(xiàn),而 Model 是數(shù)據(jù)模型,對(duì)于前端開(kāi)發(fā),Model 通常很簡(jiǎn)單,一般就是各 Component 的 props.attr 與 duals.attr 規(guī)格定義,只有少數(shù)需對(duì)數(shù)據(jù)做轉(zhuǎn)換、存盤(pán)、備份等特殊處理的,才會(huì)額外設(shè)計(jì)一個(gè) Model 實(shí)體。
MVVM 可視為 MVC 框架在前端環(huán)境的最佳適配,它也是可視設(shè)計(jì)的基礎(chǔ)。可視化設(shè)計(jì)的主體過(guò)程是在創(chuàng)建 Component 構(gòu)件后,在線設(shè)置它的 props.attr 與 duals.attr 屬性值。正因?yàn)?MVVM 中 ViewModel 是雙向綁定的,屬性取值與界面表現(xiàn)才能自動(dòng)保持一致,這也是 MVC 框架不能適應(yīng)前端可視化開(kāi)發(fā),而 MVVM 適應(yīng)得很好的主要原因。
多說(shuō)一句,屬性取值與界面表現(xiàn)并非簡(jiǎn)單的直接對(duì)應(yīng)關(guān)系,而是屬性取值變更要關(guān)聯(lián)一系列變化,須有自動(dòng) setter 調(diào)用的機(jī)制才行。舉例來(lái)說(shuō),設(shè)置一個(gè)按鈕的 duals.disabled 為真,不止是設(shè)置 DOM 節(jié)點(diǎn)的 disabled 屬性,還要讓按鈕外觀變灰,再改換 cursor 配置為 "not-allowed"。
8. 函數(shù)式風(fēng)格相比 Angular 與 Vue,React 生態(tài)鏈上各工具普遍追求純正的函數(shù)式開(kāi)發(fā),這既與 React 團(tuán)隊(duì)傾向性推動(dòng)有關(guān),也與 React 技術(shù)特征有關(guān),越傾向函數(shù)式開(kāi)發(fā)就越適應(yīng)它的 FLUX 模型。
8.1 函數(shù)式是 FRP 編程的天然姻親FLUX 框架是 FRP 編程理念(Functional Reactive Programming)的一種實(shí)現(xiàn),一個(gè)重要技術(shù)路徑是,以 CPS 風(fēng)格(Continuation-Passing Style)應(yīng)對(duì)響應(yīng)式接續(xù)處理。
函數(shù)式編程正是 CPS 變換的最佳載體。舉一個(gè)簡(jiǎn)單例子,如下提供 Email 輸入,當(dāng)輸入內(nèi)容不合郵箱格式時(shí),右側(cè)圖標(biāo)出現(xiàn)告警圖標(biāo),底部還有詳細(xì)提示。
響應(yīng)式編程的做法是,用戶持續(xù)輸入文本,內(nèi)容是否合規(guī)隨即校驗(yàn),校驗(yàn)與輸入同時(shí)進(jìn)行,校驗(yàn)結(jié)果并不打斷用戶輸入。這么理解,手工輸入形成持續(xù)的數(shù)據(jù)流,各次數(shù)據(jù)都驅(qū)動(dòng)一次校驗(yàn)處理,校驗(yàn)對(duì)于輸入來(lái)說(shuō)是異步推進(jìn)的。假定用戶輸入合法的 Email 地址后,系統(tǒng)用它自動(dòng)向服務(wù)器查詢進(jìn)一步信息,比如得到用戶別名、上次登錄時(shí)間等,這些信息用來(lái)輔助下一步表單填寫(xiě)。可以這么編碼:
01 comp.listen("validation", function(value,oldValue) { 02 if (value == "success") { 03 var sEmail = comp.duals.email; 04 utils.ajax( { 05 url: "/users/" + encodeURIComponent(sEmail), 06 success: function(data) { 07 // ... 08 }, 09 }); 10 } 11 });
這里 01 行與 06 行調(diào)用都是 CPS 風(fēng)格,實(shí)際調(diào)用雖是異步,但代碼寫(xiě)一起,上下文變量共享。這種代碼風(fēng)格在響應(yīng)式編程中大量使用,不難看出,函數(shù)式是 FRP 編程的必然選擇。
8.2 ReRest 中的函數(shù)式編程雖然 “資源” 是靜態(tài)化的概念,但 ReRest 對(duì)資源的動(dòng)作定義,仍是可適用 CPS 的函數(shù)方式,并未破壞整體函數(shù)式風(fēng)格。簡(jiǎn)單這么理解,前面所提 ReRest 資源化,實(shí)質(zhì)是提供了 帶錨點(diǎn)的函數(shù)式編程,錨點(diǎn)依附于 Component 實(shí)體而存在。所以,在可視設(shè)計(jì)器中,創(chuàng)建 Component 后,資源錨點(diǎn)(即 props.attr 與 duals.attr)就存在了,這讓所見(jiàn)即所得的在線配置因此成為可能。換一種說(shuō)法,相當(dāng)于 ReRest 在原設(shè)計(jì)基礎(chǔ)上,插入一排方便思考、易于可視設(shè)計(jì)的 “抓手”。
用來(lái)實(shí)現(xiàn) Component 功能定義的投影類,以對(duì)象方式編碼,屬于命令式風(fēng)格。而與之對(duì)等提供功能的 idSetter 定義,是函數(shù)式的,如下舉例:
this.defineDual("id__", function(value,oldValue) { if (oldValue == 1) { // init process just after all duals-attr registed } if (value <= 2) { if (value == 1) { // init process, same to getInitialState() // this.setEvent({$onClick:fn}); // this.defineDual("attr",fn); // ... } else if (value == 2) { // same to componentDidMount() // ... } else if (value == 0) { // same to componentWillUnmount() // ... } return; } // other render process ... });
前面已介紹 idSetter 如何組裝渲染內(nèi)容,既然渲染器每次計(jì)數(shù)變化代表一次渲染調(diào)用,那能不能留出幾個(gè)特殊計(jì)數(shù)值表達(dá) Component 狀態(tài)變化呢?idSetter 確實(shí)這么做了,比如上面代碼,計(jì)數(shù)值為 0 是初始狀態(tài),變?yōu)?1 是 Component 的雙源屬性尚未預(yù)備的初始化狀態(tài),相當(dāng)于 getInitialState(),變?yōu)?2 是 componentDidMount() 狀態(tài),再變回 0 表示馬上要返回初態(tài),對(duì)應(yīng)于 componentWillUnmount()。這樣,一個(gè)完整的 React Class 定義,我們用一個(gè) idSetter 函數(shù)就表達(dá)了,實(shí)現(xiàn)了命令式風(fēng)格的函數(shù)式表達(dá)。
idSetter 函數(shù)既適應(yīng)可視化設(shè)計(jì)時(shí)界面描述與功能定義分離,還適應(yīng)函數(shù)式編程。比如當(dāng)有多層 Component 嵌套時(shí),你可以將里層 Component 的行為定義任意 “Lifting State Up” 到外層 Component 的函數(shù)空間。
8.3 Lifting State Up采用 JSX 描述界面時(shí),行為定義與虛擬 DOM 描述混在一起,這時(shí)僅依賴 props.attr 逐層傳遞實(shí)現(xiàn)數(shù)據(jù)共享方式,用起來(lái)很不方便。React 官方介紹提供一種 “上舉 State” 的解決方案,以輸入溫度值判斷是否達(dá)到沸點(diǎn)為例,參見(jiàn) Lifting State Up。
將上舉 State 用在 ReRest 編程中,除了收獲 React 官方所提幾個(gè)好處,還有兩項(xiàng)特別收益。其一,原有 React 基于一個(gè)過(guò)程組織渲染內(nèi)容,而 ReRest 主體是基于 duals.attr 資源驅(qū)動(dòng)渲染,跨節(jié)點(diǎn) listen 更容易,處理邏輯也更清晰;其二,定義節(jié)點(diǎn)行為的 idSetter 是函數(shù),原生 React Class 定義要用 class MyClass extends React.Component {} 方式,層層嵌套使用時(shí),肯定沒(méi)有 idSetter 用得方便。
如果仔細(xì)琢磨 “Lifting State Up” 方案,大家不難發(fā)現(xiàn),上舉 State 解決了部分 Reflux 或 Redux 已支持的需求,被上舉共享的 state 其實(shí)也是一種 Store 數(shù)據(jù)。
9. 可視化設(shè)計(jì)實(shí)踐ReRest 編程在 shadow-widget 平臺(tái)的實(shí)踐已持續(xù)一年多時(shí)間,多個(gè)項(xiàng)目采用了 ReRest 編程,較典型的有 pinp-blog 與 shadow-bootstrap。在這一年多時(shí)間里,shadow-widget 底層庫(kù)也在 ReRest 實(shí)踐推動(dòng)下不斷完善,尤其是 idSetter 與可計(jì)算表達(dá)式方面,優(yōu)化幅度較大。
在接下來(lái)幾節(jié),我們補(bǔ)充介紹前文尚未涉及的,與實(shí)踐相關(guān)的若干知識(shí)與編程體驗(yàn)。
9.1 正交框架分析模式先介紹 “功能塊” Functionarity Block(簡(jiǎn)稱 FB)的概念。一組 Component 節(jié)點(diǎn)合起來(lái)提供某專項(xiàng)功能,稱為一個(gè) FB。以上面提到 Lifting State Up 判斷溫度是否達(dá)到沸點(diǎn)為背景,我們可以開(kāi)發(fā)兩個(gè)功能塊,其一是配置溫度格式(config FB),用來(lái)配置當(dāng)前采用攝氏 Celsius 還是華氏 Fahrenheit 作計(jì)量單位,其二是計(jì)算沸點(diǎn)(calculator FB),提供輸入框,判斷輸入溫度是否達(dá)到沸點(diǎn)。
后一 FB 的界面如下:
編寫(xiě) FB 代碼塊如下:
(function() { // functionarity block: calculator var scaleNames = { c:"Celsius", f:"Fahrenheit" }; var selfComp = null, verdictComp = null; idSetter["calculator"] = function(value,oldValue) { // ... }; })();
一個(gè) FB 宜用一個(gè)函數(shù)包裹,主要為了構(gòu)造獨(dú)立的命名空間(Namespace),本功能塊內(nèi)共享的變量在這個(gè)地方定義,比如上面代碼中 scaleNames, selfComp, verdictComp 變量,把命名空間獨(dú)立出來(lái),也防止 FB 內(nèi)部使用的變量污染外部全局空間。
既然一個(gè) FB 內(nèi)某些 Component 很常用,把它定義成 FB 內(nèi)共享的變量會(huì)更方便。
var selfComp = null, verdictComp = null; idSetter["calculator"] = function(value,oldValue) { if (value <= 2) { if (value == 1) { // init selfComp = this; // ... } else if (value == 2) { // mount verdictComp = this.componentOf("verdict"); // ... } else if (value == 0) { // unmount selfComp = verdictComp = null; } return; } };
產(chǎn)品開(kāi)發(fā)明顯可分兩個(gè)階段:界面可視化設(shè)計(jì)與功能實(shí)現(xiàn),在前一階段,應(yīng)考慮有哪些 FB 功能塊可分解,再針對(duì)各 FB 設(shè)計(jì)界面,按用戶使用習(xí)慣逐級(jí)擺放各構(gòu)件,各層構(gòu)件都是 W 樹(shù)中節(jié)點(diǎn)。以上述 config 與 calculator 功能塊為例,我們畫(huà)出 FB 分布為橫軸,W 樹(shù)為縱軸的示例圖。
之后進(jìn)入開(kāi)發(fā)第二階段:功能實(shí)現(xiàn)。這時(shí)要解決數(shù)據(jù)如何在 FB 之間流動(dòng),前一功能塊 config 配置當(dāng)前采用哪種溫度格式,記錄到 duals.scale,后一功能塊 calculator 根據(jù)自身 duals.scale 配置指示界面如何顯示,并決定用 100 度還是 212 度判斷沸點(diǎn),兩個(gè) scale 屬性的數(shù)據(jù)流向如下圖,我們只需讓后一 duals.scale 偵聽(tīng)前一 duals.scale,即實(shí)現(xiàn)兩者自動(dòng)同步。
本處舉例比較簡(jiǎn)單,復(fù)雜些產(chǎn)品的設(shè)計(jì)過(guò)程大致也是這幾個(gè)步驟。
總結(jié)一下,整個(gè) HTML 頁(yè)面是一顆 DOM 樹(shù),是縱向的(上圖縱軸),將這顆樹(shù)劃分為若干 FB 功能塊(上圖橫軸),劃分過(guò)程主要依據(jù) MVVM 逐步拆解;而處理各功能塊之間的橫向聯(lián)系,則以 FRP 思路為主導(dǎo)。這一縱一橫的思考方式,我們稱為 “正交框架” 分析模式。
可視化設(shè)計(jì)時(shí),提供在線配置的最小單位是各 Component 的 props.attr 與 duals.attr,就是 ReRest 所說(shuō)的 “資源” 項(xiàng)。而處理各 FB 之間數(shù)據(jù)如何流動(dòng)的思考起點(diǎn),也是這類 “資源” 項(xiàng),MVVM 與 FRP 分析的交匯處正是 ReRest 資源化的落腳點(diǎn)。
9.2 Component 屬性定位的變化props.attr 是只讀的,用來(lái)驅(qū)動(dòng)本節(jié)點(diǎn)組織渲染數(shù)據(jù),凡涉及狀態(tài)變化的要用 state.attr,然后同樣用 props 驅(qū)動(dòng)子節(jié)點(diǎn)的內(nèi)容更新。現(xiàn)有 React 生態(tài)鏈上各類工具對(duì) props.attr 定位似乎只有兩項(xiàng):一是用作 Component 的入口驅(qū)動(dòng)數(shù)據(jù),二是以只讀特性保障數(shù)據(jù)單向流動(dòng)。
shadow-widget 對(duì) props 與 state 的使用定位做了優(yōu)化。其一,用 duals.attr 表達(dá)一個(gè) Component 對(duì)外公開(kāi)的控制接口,不再建議用 setState() 動(dòng)態(tài)更新 “非自身節(jié)點(diǎn)” 的數(shù)據(jù)了,相應(yīng)的 state.attr 也收縮到 “只供 Component 內(nèi)部編程” 時(shí)使用,類似于用作私有變量。其二,props.attr 當(dāng)入口驅(qū)動(dòng)數(shù)據(jù)的定位沒(méi)變,但刨去轉(zhuǎn)換成 duals.attr 與事件函數(shù),剩下的常規(guī)屬性在生存周期內(nèi)被看作常量,在節(jié)點(diǎn) unmount 之前不會(huì)變化。
這兩點(diǎn)定位調(diào)整的背后有深刻原因,開(kāi)發(fā)理念變了。在 React 支持的虛擬 DOM 庫(kù)級(jí)別,各 Component 所有屬性都是對(duì)等的,無(wú)差別,虛擬節(jié)點(diǎn)無(wú)需識(shí)別各項(xiàng)屬性的語(yǔ)法含義,在底層這么處理沒(méi)問(wèn)題,因?yàn)樽鳛榈讓訋?kù),只聚焦節(jié)點(diǎn)虛擬化。但對(duì)于上層應(yīng)用,須區(qū)分各屬性的語(yǔ)義,現(xiàn)實(shí)應(yīng)用中,各節(jié)點(diǎn)總具備一定 “性狀” 的。比如,你想表達(dá)一段文本就創(chuàng)建
節(jié)點(diǎn);如果創(chuàng)建了 節(jié)點(diǎn),也意味著你將在它下面掛入 節(jié)點(diǎn);如果創(chuàng)建 節(jié)點(diǎn),通常連帶 type 屬性也算作 “性狀” 一部分,type="text" 文本框,type="checkbox" 是選項(xiàng)框,兩者形態(tài)差異巨大,文本框要用 node.value 取輸入字串,選項(xiàng)框則用 node.checked。
所以,上層應(yīng)用宜將各節(jié)點(diǎn)的固有性狀,視作生存期內(nèi)不變的常量,動(dòng)態(tài)變化的納入 duals,用作控制量。反之,如果不承認(rèn)節(jié)點(diǎn)固有性狀,就不會(huì)有 MVVM 框架形式,可視設(shè)計(jì)器也無(wú)法支持通過(guò)拖入樣板來(lái)創(chuàng)建 Component。比如假設(shè)你創(chuàng)建的是 shadow-widget 還將 className 分裂成 props.className 與 duals.klass 兩個(gè)屬性,用 className 表達(dá)固有類定義,在構(gòu)件的生存期內(nèi)不變,用 klass 表達(dá)可變的狀態(tài)量。 我們先看一個(gè)事實(shí),Bootstrap 提供的 50 多個(gè)組件中,大部分由多層節(jié)點(diǎn)構(gòu)成,或者使用時(shí)要求與其它組件搭配,一個(gè)節(jié)點(diǎn)表達(dá)完整功能的只是少數(shù),而且都只提供簡(jiǎn)單功能,像 Label、Badge 等,這類組件約占總量十分之一。可以說(shuō),現(xiàn)實(shí)中的前端開(kāi)發(fā),父子 Component 組合是常態(tài),是主流。 Shadow Widget 有很多機(jī)制讓父子節(jié)點(diǎn)關(guān)聯(lián)起來(lái),主要有: 把所有存活的構(gòu)件(已掛載且未卸載)串接成一顆 W 樹(shù),樹(shù)中各節(jié)點(diǎn)能方便的互相引用 提供導(dǎo)航面板把多個(gè)構(gòu)件封裝起來(lái),形成一組,組內(nèi)構(gòu)件用 "./" 相對(duì)路徑索引 上面提到 FB 功能塊的編碼,建立塊內(nèi)共用 Namespace,讓功能緊密相關(guān)的父子節(jié)點(diǎn)共享變量 用 $for, $if, $else 等指令描述動(dòng)態(tài)節(jié)點(diǎn),層層嵌套的 callspace 支持在下級(jí)節(jié)點(diǎn)直接引用上級(jí)各層節(jié)點(diǎn)的各種屬性 支持 $trigger 機(jī)制觸發(fā)相鄰節(jié)點(diǎn)的動(dòng)作定義 React 讓 props 屬性只讀的深刻根源是:解決數(shù)據(jù)依賴性。解決依賴性的同時(shí),順帶保證數(shù)據(jù)在父子節(jié)點(diǎn)之間要單向流動(dòng)。節(jié)點(diǎn)創(chuàng)建有先有后,具有從屬關(guān)系的兩個(gè)節(jié)點(diǎn),子節(jié)點(diǎn)必然在父節(jié)點(diǎn)之后創(chuàng)建,并且 unmount 必在父節(jié)點(diǎn)之前,也就是,子節(jié)點(diǎn)依賴于父節(jié)點(diǎn)而存在,子節(jié)點(diǎn)的數(shù)據(jù)也依賴于父節(jié)點(diǎn)的屬性先行賦值。所以,React 設(shè)計(jì)了數(shù)據(jù)傳遞要借助 props 逐層進(jìn)行,原則上屬性數(shù)據(jù)跨層不可見(jiàn)(先撇開(kāi) context 不談,那是補(bǔ)救性設(shè)計(jì),官方并不推薦你用)。 子節(jié)點(diǎn)依賴于父節(jié)點(diǎn),但反過(guò)來(lái)不是,依賴是單向的,但 React 生態(tài)鏈上諸多工具,都按 “隔絕依賴” 來(lái)處理了,相當(dāng)于忽略了單向依賴存在。舉例來(lái)說(shuō),比方我們要設(shè)計(jì)下圖 DropdownBtn 與 SplitBtn 兩種按鈕,兩者功能基本一樣,外觀有差別,怎么實(shí)現(xiàn)呢? 外層節(jié)點(diǎn)用 this.isSplitBtn 指示按鈕是否為 SplitBtn,然后里層節(jié)點(diǎn)根據(jù) isSplitBtn 取值,繪制不同外觀的按鈕。如果按 “隔絕依賴” 來(lái)處理,只能借助 props 屬性層層傳遞 isSplitBtn,隔了幾層就傳幾層;如果按 “單向依賴” 來(lái)處理,里層哪個(gè)節(jié)點(diǎn)需要要區(qū)分 isSplitBtn,就往上層查找,看看 props.isSplitBtn 取什么值。這兩種處理方式差別很大,前者忽略了主從構(gòu)件的天然關(guān)系,以暴露接口的代價(jià)實(shí)現(xiàn)功能,把無(wú)關(guān)節(jié)點(diǎn)都牽扯進(jìn)當(dāng)來(lái)傳手,就像打排球的一傳、二傳、三傳,當(dāng)功能組合較多時(shí),顯得很繞。 從子節(jié)點(diǎn)向上查找,分析一級(jí)(或多級(jí))父節(jié)點(diǎn)的屬性特點(diǎn),從而確定它自身所處的場(chǎng)景,進(jìn)而讓當(dāng)前節(jié)點(diǎn)應(yīng)對(duì)不同場(chǎng)景表現(xiàn)不同功能。我們管這種場(chǎng)景推導(dǎo)過(guò)程叫 “場(chǎng)景自省”,如上介紹,向上追溯的 “場(chǎng)景自省” 是安全的,因?yàn)樽庸?jié)點(diǎn)若存活,父節(jié)點(diǎn)必然還存活,反過(guò)來(lái)從父節(jié)點(diǎn)查子節(jié)點(diǎn)則不行。 現(xiàn)有 React 生態(tài)鏈上諸多主流工具都很繞,不像 shadow-widget 那么直接,主要表現(xiàn)以下幾個(gè)方面。 其一,主流工具普遍忽視父子節(jié)點(diǎn)的主從關(guān)系是隱含豐富信息的,把所有 Component 擺同等位置來(lái)解決跨節(jié)點(diǎn)數(shù)據(jù)傳遞問(wèn)題。 源頭在于 Facebook 官方的 FLUX 框架有缺陷,F(xiàn)LUX 在虛擬 DOM 的上層實(shí)現(xiàn),但它繼續(xù)無(wú)視 Component 屬性帶語(yǔ)義特性,都無(wú)差別對(duì)待。借助 Dispatcher 分發(fā) Action,構(gòu)造獨(dú)立的 Store,統(tǒng)一處理各 Action 消息。另設(shè) Store 與 Action 另行驅(qū)動(dòng)的過(guò)程,相當(dāng)于換個(gè)地方重建各節(jié)點(diǎn)的場(chǎng)景信息。 其二,這些工具普遍過(guò)于依賴函數(shù)式風(fēng)格,靜態(tài)化概念只停留在 Component 層面,沒(méi)往下探一層。各 Component 互相關(guān)聯(lián),形成網(wǎng)格,這網(wǎng)格直接用函數(shù)式編程去編織了。因?yàn)榇a量沒(méi)減,該做的事情一件不少,重建場(chǎng)景的各個(gè)處理環(huán)節(jié)又衍生不少概念,比較繞。基于 ReRest 的編程則將 Component 下的屬性視作資源,把靜態(tài)化概念深入一層,然后在 “資源粒子” 層面,用函數(shù)式風(fēng)格編織網(wǎng)格。這樣更直接了當(dāng),也符合開(kāi)發(fā)者思考習(xí)慣。 shadow-bootstrap 項(xiàng)目按 ReRest 理念去實(shí)踐的,該項(xiàng)目核心功能是將 Bootstrap 往 shadow-widget 平臺(tái)適配。與之類似,業(yè)界還有一個(gè)知名項(xiàng)目 react-bootstrap,把 Bootstrap 往 React 適配。這兩項(xiàng)目的功能對(duì)等,封裝的組件幾乎能一一對(duì)應(yīng),如果對(duì)比兩者源碼,shadow-bootstrap 明顯簡(jiǎn)潔許多,react-bootstrap 不容易讀,繞來(lái)繞去的。最終代碼 minify 后,前者 103 Kb,而后者 213 Kb,整整多出一倍。前者開(kāi)發(fā)只用一個(gè)多月,后者遠(yuǎn)不止這個(gè)投入,當(dāng)我們的框架沒(méi)那么繞時(shí),生產(chǎn)力是大幅提升的。 長(zhǎng)期以來(lái) GUI 開(kāi)發(fā)工具與 Web 前端工具是兩條獨(dú)立主線,并行發(fā)展。MFC、Delphi、VB、WxWidget、Qt 等歸入前者,沒(méi)人將前端開(kāi)發(fā)也視作 GUI 一類,不過(guò),大概沒(méi)人否認(rèn)前端開(kāi)發(fā)主要工作是設(shè)計(jì)圖形用戶界面(Graphical User Interface),就目的而言,前端開(kāi)發(fā)無(wú)疑也是 GUI 開(kāi)發(fā)。 這兩條主線靠攏發(fā)展的時(shí)代已來(lái)臨,虛擬 DOM 技術(shù)結(jié)合 FRP 理念,再結(jié)合 ReRest 資源化改造,基于 MVVM 框架 —— 對(duì)應(yīng)主流 GUI 工具的 MVC —— 的可視化開(kāi)發(fā)已經(jīng)走通了。ReRest 方法論嘗試讓前端開(kāi)發(fā)回歸可視化 GUI 工具序列,其實(shí)踐已在 shadow-widget 平臺(tái)走出第一步,希望這一步對(duì) Web APP 與 Native APP 逐步融合的發(fā)展提供有益經(jīng)驗(yàn)。 ? (本文完) 文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。 轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/89245.html 摘要:前端日?qǐng)?bào)精選桌面通知精讀前端性能優(yōu)化備忘錄聊聊組件間通信的幾種姿勢(shì)到底該如何配置深入理解高階組件中文第期體系調(diào)研報(bào)告前端面試總結(jié)掘金技術(shù)周刊期知乎專欄從試著改進(jìn)可重用做起掘金式數(shù)學(xué)作者眾成翻譯為什么企業(yè)進(jìn)行數(shù)碼變革要用平臺(tái)眾成
2017-10-23 前端日?qǐng)?bào)
精選
HTML5 桌面通知:Notification API精讀《2017前端性能優(yōu)化備忘錄》聊聊Vue.js組件間通信的幾種姿... 摘要:前言非正經(jīng)入門(mén)是相對(duì)正經(jīng)入門(mén)而言的。不過(guò)不要緊,正式學(xué)習(xí)仍需回到正經(jīng)入門(mén)的方式。快速入門(mén)建議先學(xué)會(huì)用拼文寫(xiě)文檔注冊(cè)一個(gè)賬號(hào),把庫(kù)到自己名下,然后用這個(gè)庫(kù)寫(xiě)自己的博客,參見(jiàn)這份介紹。會(huì)用拼文寫(xiě)文章,相當(dāng)于開(kāi)發(fā)已入門(mén)三分之一了。
本系列博文從 Shadow Widget 作者的視角,解釋該框架的設(shè)計(jì)要點(diǎn),既作為用戶手冊(cè)的補(bǔ)充,也從更本質(zhì)角度幫助大家理解 Shadow Widget 為什么這... 摘要:明明如日中天,把它與倒過(guò)來(lái),給加點(diǎn)東西或可與抗衡。在之后,大版本有十?dāng)?shù)個(gè),只有最近推的才回歸正常等后人總結(jié)歷史,無(wú)疑會(huì)把與之間的所有都稱為垃圾。讓網(wǎng)頁(yè)支持所見(jiàn)即得的可視化設(shè)計(jì),是框架的最高形態(tài),以前沒(méi)有類似工具,主要因?yàn)榧夹g(shù)做不到。
好吧,我承認(rèn)我是標(biāo)題黨。React 明明如日中天,把它與 Vue 倒過(guò)來(lái),給 Vue 加點(diǎn)東西或可與 React 抗衡。不過(guò),這兩年 Vue 干的正是這事... 閱讀 2610·2021-11-17 09:33 閱讀 3969·2021-10-19 11:46 閱讀 922·2021-10-14 09:42 閱讀 2266·2021-09-22 15:41 閱讀 4242·2021-09-22 15:20 閱讀 4655·2021-09-07 10:22 閱讀 2320·2021-09-04 16:40 閱讀 827·2019-08-30 15:52 節(jié)點(diǎn),改改屬性就把它變成
列表,可視設(shè)計(jì)就沒(méi)法做了。
相關(guān)文章
2017-10-23 前端日?qǐng)?bào)
React 可視化開(kāi)發(fā)工具 Shadow Widget 非正經(jīng)入門(mén)(之一:React 三宗罪)
介紹一項(xiàng)讓 React 可以與 Vue 抗衡的技術(shù)
發(fā)表評(píng)論
0條評(píng)論
Cciradih
男|高級(jí)講師
TA的文章
閱讀更多
IBM推出新款量子芯片,預(yù)計(jì)兩年內(nèi)擊敗傳統(tǒng)計(jì)算機(jī)
Linphone和MicroSIP軟電話中暴露嚴(yán)重安全漏洞 可致黑客遠(yuǎn)程攻擊
【物聯(lián)網(wǎng)】12.物聯(lián)網(wǎng)服務(wù)器發(fā)送方式(HTTP,WebSocket ,MQTT )
如何訪問(wèn)云主機(jī)數(shù)據(jù)庫(kù)-云主機(jī)跟云數(shù)據(jù)庫(kù)的區(qū)別?
云主機(jī)怎么安裝軟件-如何選擇云主機(jī)部署管理軟件?
4個(gè)實(shí)時(shí)查看臺(tái)風(fēng)路徑的網(wǎng)站平臺(tái)(知曉臺(tái)風(fēng)最新消息必備工具)
??身為在軟件測(cè)試摸爬滾打多年工程師的感悟,寫(xiě)給正在迷茫的你??
React事件