與時俱進的Reactivity

玄學醬發表於2017-10-18
本文講的是與時俱進的 Reactivity,

近十年來,響應式程式設計的興起給 JavaScript 帶來了暴風雨式的進化改革,前端開發極大地從其簡潔性中獲益,使用者介面隨著資料變化實時響應,淘汰掉更新UI時大量易出錯的程式碼。然而,在它變得更加流行的同時,已有的工具和技術並不總是跟上當代瀏覽器功能,比如 Web API、語言能力以及效能優化演算法、可擴充套件能力、簡化的語法和持續穩定性。在本文中,讓我們以一個新庫—— Alkali 為背景,展示一些已有的新技術、方法和功能。

接下來我們將要介紹的技術,包含了渲染佇列、基於 pull 的細粒度響應,ES6 的響應式生成器和表示式,響應式原生 Web 元件,還有雙向資料流。這些技術不僅僅只是一時興起的程式設計方法,它們是採納了已有的瀏覽器技術並結合深入的研究和開發的作用產物,造就了更佳的效能、更簡潔的程式碼、與新元件更好的協調性以及更好的封裝。 
那麼我們將看幾個既簡單又具有宣告性的例子 (你也可以直接看看這個更完整的例子: Alkali todo-mvc application )。它們使用了標準的原生結構,還有或許能夠用到的重要的特性:在資源消耗最低的基礎上能夠快速展示。這些前沿技術的確帶來了可擴充套件的好處、高效率和可觀的效益。在各種庫層出不窮的情況下,最具有預見性和穩定性的庫結構,就是直接架構在基於標準的瀏覽器元素(或元件) API 上的。

Push-Pull Reactivity (響應式 Push-Pull)

擴充套件響應式程式設計的關鍵是資料流的架構。一種原生的響應式方法就是使用簡單的觀察者或者監聽者模式,將每一次更新以判斷流的形式,推送至每個對應的監聽器中。這種快速響應會在任何多步狀態發生更新的情況下,造成很多不必要的重複的中間判斷,從而導致過度計算。一種更具擴充套件性的方法則是使用基於 pull 的方式,只在下游觀察者( Observable )請求或者拉取最新值時(懶載入式)計算。訂閱者( Observer )在被通知依賴資料的改動後,可以採用 de-bouncing 或者 queuing 的方式請求資料。

基於 pull 的方法也可以結合快取使用。一旦資料計算完成,結果就可以被快取,然後上游發生改變發出通知,就會使下游快取的資料失效,從而保證資料的實時性。這種基於 pull 的響應式快取失效方案不但遵循和 REST 一樣的設計架構以及網路的可擴充套件設計,而且也遵循現代瀏覽器渲染流程的架構。

然而,當場景正在漸進式更新當前狀態時,對於某些事件,更推薦使用『 推送 』方式,當逐步增加、刪除、更新集合中的元素時,它是個非常有效的方法,並且與其他一起混合搭配使用會更好哦,比如:資料主要是從觀察者處拉取的,但增量更新可以通過實時資料流作為優化被推送。

Queued Rendering (渲染佇列)

想要通過基於響應式 Pull 在響應式的 app 中以提高應用效率,關鍵就是確保渲染的執行消耗最小。通常情況下,應用程式的多個部分可能都在更新狀態,如果渲染是同步的,任何狀態變化都立即執行,這很容易導致介面抖動,並且執行效率低。通過排隊渲染我們可以確保即使多個狀態發生變化,渲染依然是最小化的。

排隊行為或消除抖動是一種相對常見並且出名的技術。然而,對於優化排隊渲染,瀏覽器實際上給通用的消除抖動方法提供了一個極好的替代。由於它的名字叫 requestAnimationFrame ,所以常被認為是動畫相關的庫,但實際上這個新的API在渲染佇列狀態改變方面表現的相當完美。它是巨集觀事件的 Task ,所以任何微小的 Task ,比如解析度低的將被允許首先載入完成。考慮到最後的渲染,選項卡/瀏覽器的可見性,當前負載等等,它還允許瀏覽器來確定精確的最佳時機來渲染新的變化。它在可見的休眠狀態下可以立即執行回撥(通常是毫秒級),在適當的幀速率在順序呈現的情況下,當一個頁面/選項卡隱藏的時候甚至可以完全延遲(執行)。事實上,通過 requestAnimationFrame 渲染佇列狀態的改變,當檢視需要更新時再渲染,我們實際上和那些當代瀏覽器使用的優化渲染流、精確時機以及序列/路徑相同。這種方法確保了我們和瀏覽器以一種互補的方式去進行高效、及時的渲染,並避免了額外的佈局和重繪。

這可以被認為是兩個階段的渲染方法。第一階段是對事件處理器的響應,我們更新規範化資料來源,進而使依賴這些資料的衍生資料或者元件失效。無效UI元件都是排隊等候渲染。第二階段是渲染階段,檢索必要的資料並渲染。

Alkali 通過 渲染器物件 渲染佇列,實時與響應式的資料輸入和對應的元素相關聯(在 alkali 中稱為『變數』),然後通過requestAnimationFrame 機制重新渲染佇列狀態。這意味著任意資料繫結都與渲染佇列相連。這也可以通過例項化一個Variable物件,並將其與一個元素關聯(這裡我們建立一個greeting)來表明。示例程式碼如下:

import { Variable, Div } from `alkali`

// 建立一個變數
var greeting = new Variable(`Hello`)
// 建立一個 div ,裡面與變數相關聯
body.appendChild(new Div(greeting)) // 注意,這是一個標準的 div 元素
// 現在變數的更新會實時相應到 div 中
greeting.put(`Hi`)
// 這裡的渲染機制會在 div 中排隊渲染
greeting.put(`Hi again`)

這裡的 div 使用了 requestAnimationFrame 機制,將隨時自動更新 div 的狀態改變,並且多個更新不會導致多個渲染,只有最後一個狀態將會被渲染。

Granular Reactivity (細粒度的響應)

單純的響應式程式設計允許單個訊號或變數被使用及通過系統傳遞。然而,由於有利於維持大家對指令式程式設計的熟悉狀態,一些基於 diff 的響應式框架也變得很受歡迎,如使用虛擬 DOM 的 ReactJS 。這些框架能夠讓大家繼續採用命令式程式碼編寫應用程式的方式編寫程式。當應用程式任意的狀態改變時,元件只是重新渲染,一旦完成了,則將元件的輸出與先前的輸出比較不同之處,來確定更改。與顯式資料流產生一些特定明確的變化更新到渲染過的 UI 上不同的是, diff 是將重新執行的輸出結果與先前的狀態進行比較。

雖然使用這種開發很方便也能夠產生我們熟悉的示範程式碼,但是它犧牲了巨大的記憶體和效能。響應式對比需要一個完整副本的渲染輸出和複雜的對比演算法來確定差異來減輕過度 DOM 重寫。這個虛擬 DOM 通常需要2到3倍的記憶體使用和對比演算法增加類似的開銷相比才能直接確定 DOM 的改變。

另一方面,真正的響應式程式設計顯式地定義了可以改變的變數或值、以及它們變化時對它們的值的連續輸出。這並不需要任何額外的開銷或對比演算法,因為輸出是直接被程式碼裡定義好的聯絡所指定的。

程式可調式性得益於細粒度的功能活性程式碼流。除錯指令式程式設計涉及重構的條件和重建程式碼塊的步驟,需要複雜的推理評估狀態值得改變(以及它如何會是錯誤的)。函式式的響應流可以執行靜態檢查,在任何時候任何地方我們都可以看到完整的與 UI 輸出相對應的各自的不獨立的輸入圖。

還有,使用真正的響應式程式設計技術不是一個深奧或用來賣弄學問的電腦科學技術,它其實在程式的可擴充套件性、提升速度、加快響應能力、便於除錯應用程式流有著顯著的好處。

Canonical and Reversible Data (雙向資料流的規範)

在細粒度的 Reactivity 中甚至可以將明確的資料流的傳遞方向逆轉,也就是達到雙向繫結成為可能,這樣下載流資料的消費者,如輸入元素可以請求上傳資料變化,不需要額外的配置連線或必要的邏輯。這使它非常容易與表單的輸入控制元件建立繫結的形式。

響應式程式設計的一個重要原則是『來源的真實一致性』,有一個明確的規範來區別規範的資料來源和派生資料。響應式的資料可以稱為一個的指向性的資料。這對資料管理是至關重要的。如果同步多個資料狀態,並沒有明確的來源和派生資料,會使資料管理混亂,導致多種宣告管理問題。

單向型的資料隨著集中式的資料改變而改變,它與響應式的資料改變有聯絡,是一種適當有向型的圖形資料。很不幸,單向型的資料流根本上意味著資料的消費者可能必須手動連線到資料來源,也就是這是典型的違反了本地化原則還有逐步降低了封裝性,這樣導致越來越多的獨立元件之間糾纏不清,使得開發愈加繁重。

然而,一個有向的規範的資料並不必要只能命令資料改變通過圖形聯絡這一種方法。通過使用細粒度的 Reactivity ,就可以支援雙向資料流。通過雙向資料性,資料的導向性仍然可以被保留,只需在下游資料發生變更或新增時發出通知即可,而相比曾經,上游資料的變化被定義為資料改變發起的請求(在未來實現中,是可撤銷的)。衍生資料的改變請求依然可以實現,只要它含有反向轉換傳遞請求原始資料(雙向資料遍歷或轉換通常被稱為一個 lens 的功能性術語)。規範化資料的變化仍然發生在資料來源上,即使被下游資料消費者初始化過或者請求過。具有了這樣的清晰的據流特性,有向型的規範資料和衍生資料都會被保留下來,維護狀態的一致性,並同時允許封裝的獨立資料實體之間互動,不管它們是否是是衍生資料。實際上,這簡化了開發使用者的輸入和表單的管理,還有助於輸入元件的封裝。

與時俱進的 DOM 擴充套件 (『 Web 元件』)

學習程式設計要具有遠見,程式碼的可維護性是至關重要的,你的程式碼要在 JavaScript 的生態系統中隨著眾多新技術的不斷湧現還能夠做到可維護,這是極具挑戰性的。三年後哪家新框架能夠閃耀奪目?從過往的歷史來看,這是很難預測的。在這種雜亂的情況下我們怎麼發展?最可靠的方法是減少依賴特定 API 庫,並最大化我們的依賴標準瀏覽器 API 和架構。使用新興元件 API 和功能(就是『 Web 元件』)才更加可行。

響應式結構的最佳定義不應該是規定一個特定的元件體系結構,應靈活地使用原生或第三方元件,這樣才能在未來的發展中最大化地生存。然而,儘管我們極力降低耦合,某種程度的耦合可能是有用的。特別是當能夠直接使用變數作為值或屬性的輸入,無疑是比建立資料繫結後再獲的資料來的方便。與元素或元件生命週期的整合、當元素被刪除或分離時通知,便於自動清理的依賴性和監聽機制,為了防止記憶體洩漏,減少資源消耗,簡化元件使用。

此外,當今的瀏覽器使得 Web 元件整合與原生元素的整合完全可行。如今可以從現有的 HTML 原型上定義真正基於 dom 的定製類,通過響應式可勘測變數的建構函式,配合 MutationObserver 介面(和未來潛在的 Web 元件回撥)讓我們能夠監控元素是什麼時候分離的(或者附加上的)。ES5 的 getter / setter 同樣很好地表明瞭允許我們適當地擴充套件和重定製原生元素的樣式屬性。

Alkali 定義了一系列明確的 DOM 建構函式和類來支援這些功能。這些類是原生 DOM 類的最小擴充套件,它的建構函式引數支援輸入變數控制屬性,還支援變數自動清理。結合使用懶載入的或者基於響應式 Pull 的Reactivity,這意味著元素的資料改變動態可見,一旦資料分離,將不再觸發任何通過其依賴的輸入的判定。這就導致一個元素的建立和擴充套件會自動自己清除監聽器。例如:

let greetingDiv = new Div(greeting)
body.appendChild(greetingDiv)
// greeting 的改變會自動建立一個繫結監聽
...
body.removeChild(greetingDiv)
// greeting的繫結/監聽會被清理掉

Reactive Generators (響應式生成器)

不光是 Web API 在響應式程式設計方法中提供了重大的改進,ECMAScript 語言它本身就有著激動人心的新功能,它的語法的優化使得更容易編寫響應式程式碼。其中一個強大的新特性是生成器( generators ),它提供了一個優雅的和直觀的程式碼流互動的語法。也許處理響應式資料的最大的不便就是 JavaScript 是經常需要回撥函式來處理狀態改變。然而,ECMAScript 新的生成器函式能夠暫停,恢復,並重新啟動一個函式,它可以應用響應式資料的輸入通過標準的順序語法,還可暫停和重新開始獲取任何的非同步輸入。生成器的控制器還可以自動訂閱依賴輸入,當輸入變化則重新執行該函式。這種控制函式的執行使得生成器能夠利用收益成為可能( leveraged to yield 雙關語!下文中提到的函式 yield ),通過直觀和淺顯易懂的語法就能夠控制複雜的變數組合輸入。

Generator 曾被賦予眾望,希望能夠像承諾中的那樣淘汰掉 Callback 回撥,並且支援直觀的順序語法。但是 Generator 不僅在暫停或者恢復一個非同步輸入方面發揮出色,還能夠在任何輸入值改變的時候立刻重啟。這隻需在任何輸入變數的前面使用操作符 yield 就可輕鬆做到,它還允許相應的程式碼監聽其變數的變化,並返回當前變數的值可獲取時的表示式。

讓我們來看看這是如何完成的,在 Alkali 中,生成器函式可以作為輸入變數的轉換,想要使用 react 建立一個想響應式函式能夠輸出一個新的複合變數。 react 函式充當生成器的控制器來控制響應式變數。下面來看一個分步講解舉例:

let a = new Variable(2)
let aTimesTwo = react(function*() {
  return 2 * yield a
})

react 控制器負責處理所提供的 Generator 的啟動。一個 Generator 函式返回一個 iterator 用來互動, react 負責啟動 iterator。當 Generator 計算到 yield 操作符出現時才會執行,這裡程式碼會直接與 yield 操作符相遇,然後將 yield 操作符從 iterator 得到的返回值交給 react 函式處理。在這種情況下, a 變數將被返回給 react 函式,這就使得 react 函式身兼多職有木有。

首先,它可以訂閱或監聽所提供的響應式變數(如果它是的話),所以它在任何改變發生時都能夠通過重新執行的方式立即做出響應。第二,它可以得到當前狀態或反應變數的值,當 resume 時它可以返回 yield 表示式的結果。最終返回前, react函式可以檢查這個響應式變數是否是非同步的、是否持有約定值,如果必要還可等待約定值返回之後恢復執行函式。一旦拿到當前的狀態,生成器函式就會恢復執行 2 的值,將它返回給 yield a 表示式。如果有更多的 yield 表示式,它們會順序執行,並以同樣的方式解決。在這種情況下,生成器將返回一個 4 ,然後結束生成器序列(直到 a 變化或重複執行)。

通過 react 函式,這個程式碼的執行被封裝在另一個複合的響應式變數中,任何變數的變化不會觸發重新執行操作,直到下游資料訪問或請求它執行。

Alkali 生成器函式還可以直接使用在元素建構函式中定義一個渲染功能,它在任何輸入值發生變化時都會自動重新執行。在這兩種情況下,我們在所有變數前面使用前面提到的 yield

import { Div, Variable } from `alkali`
let a = new Variable(2)
let b = new Variable(4)
new Div({
  *render() {
    this.textContent = Math.max(yield a, yield b)
  }
})

這建立了一個文字內容為4的textContent(兩個輸入的最大值),我們可以更新其中任一變數,它將重新執行。

a.put(5)

a 現在的內容將被更新為 5 .

生成器還不是普遍相容所有的瀏覽器(比如 IE 瀏覽器和 Safari 就不支援),但是生成器可以搭載或者在其他工具模擬下實現(比如 Babel 或其他工具)。

Properties and Proxies (屬性和代理)

Reactivity 響應式地繫結到物件的屬性上是很重要的一個方面。但是封裝屬性的更改通知,需要的不僅僅是當前的標準屬性訪問返回的值。因此,響應式地繫結屬性或變數需要更詳細的語法。

然而,ECMAScript另一個激動人心的新特性是代理,它允許我們定義一個物件用來攔截所有屬性訪問和修改自定義功能。這是很強大的特性,可用於通過普通屬性訪問返回 reactive 屬性變數,更方便不說,reactive 物件也是使用慣用的語法。

不幸的是代理不像 Babel 那麼容易通過程式碼編譯器模擬。模擬代理不僅需要 transpiling 代理建構函式本身,還需要任何程式碼訪問代理,所以模擬器沒有了原生語言的支援是不完整的,它會執行莫名緩慢並且程式碼臃腫,由於需要大量的執行 transpiration ,過濾應用程式的每個屬性。但更有針對性地執行 transpiration 也是可行的。讓我們來看看。

Reactive 表示式

ECMAScript不斷推進的同時,Babel 及其外掛等工具也在與時俱進,這給我們很大機會來建立新的編譯語言特性。當生成器可以很酷炫地使用 Babel 外掛建立一個函式去執行非同步操作和響應式地立即執行的操作,使用 ECMAScript語法將屬性繫結,程式碼可以轉化為完全響應式的資料流。這就比簡單的執行 re-execution 要複雜很多,比如表示式的輸出與輸入之間可定義一些操作,比如可逆操作符,響應式的屬性,還有使用簡單的慣用的表示式可以生成響應式的任務。

這裡有一個獨立的專案 ,它使用了基於 Alkali 的外掛 Babel 轉換響應式表示式。使用這個我們可以編寫一個表示式用 react作為引數呼叫/操作符:

let aTimes2 = react(a * 2)

這裡的 aTimes2 的值與輸入的變數 a 的乘法運算值相繫結。如果我們改變 a 的值(使用 a.put() 就可改變它的值),aTimes2 將會自動更新值。事實上由於我們使用了完美定製的 react 操作符,所以這個資料還是可逆的。我們可以為aTimes2 指定一個新的值,比如 10 ,那麼 a 的值將自動更新為 5

如上所述,代理模擬整個程式碼庫幾乎是不可能的,但是在我們響應式表示式中呢,響應式變數編譯屬性的語法去控制屬性的就是灑灑水的小事啦。還有更厲害的,其他的操作符還可以將變數 transpile 成可逆的。例如,我們可以寫複雜的純響應式程式碼的組合:

let obj, foo
react(
  obj = {foo: 10}, //我們可以建立新的響應式物件
  foo = obj.foo, //得到一個響應式物件的屬性
  aTimes2 = foo //將它賦值給 aTimes2 (繫結到上邊的表示式中)
  obj.foo = 20 //更新物件(就會響應式地將值通過 foo , aTimes2 傳遞給 a )
)
a.valueOf() // -> 10

##技術要與時俱進

Web 開發一直是在不斷變化和進步,它的每一次進步都激動人心。Reactivity 是當今應用程式中先進的程式設計理念,它隨著新技術的發展和現代瀏覽器功能的不斷進化,它的語言和 API 也在與時俱進。他們一起使用過可以使 Web 開發朝前邁進。對於未來的發展中的無限可能我是滿滿的激動,並希望這些想法能夠實現,未來的新工具對於 Web 開發的改進我拭目以待。

Alkali 已被我們的工程師團隊使用, 在 Doctor Evidence 網站中我們用它開發的。我們一直在努力探索構建互動式和響應的工具,它在這個網站中負責查詢和分析臨床醫學研究的大型資料集。這是一個有趣的挑戰,要保證流暢的使用者介面的同時還要處理複雜和龐大的資料,它其中的許多方法對我們很有用,我們採用新的瀏覽器技術開發我們的網路軟體。沒有別的,我們只是希望 Alkali 可以作為一個例子來激勵 Web 開發更進一步。





原文釋出時間為:2016年10月31日

本文來自雲棲社群合作伙伴掘金,瞭解相關資訊可以關注掘金網站。


相關文章