歡迎來我的博客閱讀:「「譯」JavaScript框架的探索與變遷」
譯者言
近幾年可謂是 JavaScript 的大爆炸紀元,各種框架類庫層出不窮,它們給前端帶來一個又一個的新思想。從以前我們用的 jQuery 直接操作 DOM,到 BackboneJS、Dojo 提供監聽器的形式,在到 Ember.js、AngularJS 數據綁定的理念,再到現在的 React、Vue 虛擬 DOM 的思想。都是在當前 Web 應用日益復雜的時代,對于如何處理「應用狀態」與「用戶界面」之間如何更新的問題,帶來更先進的解決方案。
本文是一篇從技術上,以數據變更和UI同步為方向,循序漸進的講述 JavaScript 框架如何演進過來的。
本篇文章,給了我一個更加高緯度的視角,來看待 JavaScript 這些個框架。
正文
在 2015 年,JavaScript 框架的選擇并不少。在 Angular,Ember,React,Backbone 以及它們眾多的競爭者中,有足夠多的選擇。
雖然可以通過不少方面來對比這些框架的不同,但是最讓人感興趣的是它們分別如何管理狀態(state)的。特別的,通過思考這些框架分別如何處理狀態變化是很有用的。它們都提供了什么樣的工具讓你把這些變化呈現給用戶?
如何處理應用狀態(app state)與用戶界面(user interface)之間的同步,長期以來都是用戶界面開發如此復雜的主要原因?,F在,我們有幾個不同的處理方案。本文探索以下:Ember 的數據綁定,Angular 的臟檢查、React 的虛擬DOM以及它與不可變數據結構(immutable data structures)之間的聯系。
數據映射 Projecting Data
我們首先討論程序內部的狀態與屏幕所看到的內容之間的映射。你把各種諸如 object,arrays,strings,以及 numbers 轉換成一顆由諸如 texts、forms、links、buttons 和 images 組成的樹狀結構。在 Web 中,前者通常指 JavaScript 中的數據結構,而后者指的是 DOM (Document Object Model)
我們經常稱這個過程為渲染(rendering),你可以想象這個過程是從數據模型到用戶界面的一個映射。當你把數據渲染成一個模板,你得到的是一個 DOM(或者說 HTML)。
[圖片上傳失敗...(image-37a043-1518421446682)]
這個過程本身已經足夠簡單了,數據模型到用戶界面之間的映射,并不總是那么的瑣碎。它基本只是一個接受輸入然后直接輸出的函數。
在我們需要考慮數據開始隨著時間而變化的時候,這件事就變得更有挑戰性了。當用戶進行操作或者其它某些操作導致數據產生變化的時候,用戶界面需要呈現出這些變化。而且,由于重新構建 DOM 樹的代價是極其昂貴的,我們要盡可能產生小的影響。
[圖片上傳失敗...(image-f1252f-1518421446682)]
因為狀態產生了變化,這比只是一次性渲染用戶界面變得更加難。這就到了以下解決方案開始表演的時候了。
服務器渲染 Server-Side Rendering
宇宙是永恒不變的,沒有任何變化
在 JavaScript 新紀元之前,你的 Web 應用的任何交互都會觸發一趟服務器的環繞旅行。每一個點擊和每一個表單提交都會卸載當前頁面,一個請求發送到服務器,服務器響應一個新的頁面,然后瀏覽器重新渲染。
[圖片上傳失敗...(image-b7a7c6-1518421446682)]
這種方式不需要前端管理任何的狀態(state)。就前端范疇而言,當一些事情發生了(后端返回的數據),整個過程就結束了。就算有狀態,那也只是后端的范疇。前端只是由 HTML 和 CSS 構成,也許有時候會有些 JavaScript 撒在表面調味。
從前端來說,這是一個很簡單的實現方式,但也是一個很慢的方式。每一個交互并不僅僅觸發UI的重渲染,還涉及服務器的數據查詢以及服務端渲染。
大多數人已經不再這樣做了,我們可以在服務器端初始化我們的應用,然后轉移到前端來做狀態的管理(這也是 isomorphic JavaScript 致力于的。)。已經有人在類似的更復雜的設計思想中取得成功。
JS第一代革命:手動重渲染
我不知道哪些需要渲染的,你來告訴我。
第一代革命的 JavaScript 框架,如:Backbone.js, Ext JS 以及 Dojo。第一次在瀏覽器端引入了數據模型(Data Model)的概念,代替了以前那些直接操作 DOM 的輕量級的腳本代碼。這意味著你終于可以在瀏覽器端管理狀態了。當數據模型的上下文改變時,你需要做一些工作,讓改變呈現在用戶界面中。
這些框架的體系能分離你的模型和界面代碼,但同時也留下了一大部分同步的工作給你。你可以監聽某類事件的發生,但是你有義務去計算如何重新渲染以及如何落實到用戶界面中。
[圖片上傳失敗...(image-f224ad-1518421446682)]
基于這種模型,作為開發者,你需要考慮大量的性能問題。由于你能控制什么時候和怎么處理更新,你可以從中做任意的做一些調整。這經常會面臨一些權衡:簡單的處理導致大面積的頁面更新,或者強性能的處理來更新一小塊頁面。
Ember.js: 數據綁定
由于我在控制你的模型和試圖,我會確切知道如何重新渲染。
當應用狀態改變的時候,手動處理渲染工作,無可避免的增加了復雜度。很多框架旨在解決這個問題,Ember.js 就是其中之一。
Ember,像 Backbone 一樣,當數據模型改變的時候會觸發某個事件。不同之處在于 Ember 同時提供了一些方法來接收這些事件。你可以把 UI 綁定到數據模型中,這意味著有一個監聽器綁定到了 UI 上。該監聽器當收到事件的時候,知道如何更新 UI。
[圖片上傳失敗...(image-b83a4f-1518421446682)]
這是一個高效率的機制。盡管設置全部的監聽器需要在初始化時多出一些工作,但是之后就能保證同步狀態時的最小影響。當狀態產生變化時, 只有真正需要更新的部分才會發生改變。
這種方式最大的犧牲是 Ember 需要時刻盯著數據模型。這意味著你需要通過 Ember 的 API 封裝你的數據,以及你要更新數據的時候是使用 foo.set('x',42)
而不是 foo.x = 42
,以此類推。
在未來 ES6 的 Proxies 可能會對這種模式產生一定的幫助。它讓 Ember 可以通過裝飾 object 來綁定那些監聽器的代碼。這就不用像傳統方式那樣重寫 object 的 setter 方法了。
AngularJS:臟檢查
我不知道什么更新了,所以當更新的時候,我只能檢查所有的東西。
AngularJS 類似于 Ember,當狀態改變的時候,必須人工去處理。但不同的是,AngularJS 從不同的角度來解決問題。
當你在 Angular 模板中引用你的數據,例如這樣的語句 {{foo.x}}
,Angular 不僅僅只是渲染數據,而且會這個特定的數據創建一個觀察者。如此,只要你的應用中發生任何變化,Angular 都會檢查這個觀察者檢視著的數據是否發生了改變。如果發生了改變,就會重新渲染這個數據對應的用戶界面。這個過程稱作臟檢查(Dirty Checking)。
[圖片上傳失敗...(image-336488-1518421446682)]
這種監聽改變的風格最大的好處就是,你可以在你的數據模型中使用任何姿勢。Angular 對此沒有任何限制,它不關心這個。沒有基礎的對象需要擴展,也沒有 API 需要調用。
但壞處就是現在數據模型沒有任何內建的檢測手段告訴告訴框架哪些東西發生了改變,框架對是否或者哪里發生了改變沒有任何洞察力。這意味著數據模型需要通過外部來監聽改變,而 Angular 就是這樣子做的:所有觀察者在任何時間發生的任何改變,都需要被執行一次。點擊事件,HTTP 響應,timeout 方法的觸發,對于這些,觀察者都需要執行一遍。
經常去執行所有觀察者,這聽起來像是性能的噩夢,但是它令人驚訝的快。這主要是因為在檢查到任何改變之前,沒有 DOM 的操作過程,而原生的 JavaScript 引用對象的檢查平均消耗的性能是廉價的。但是當你要處理大量的 UI 或者經常性觸發重新渲染,那么額外的性能優化手段就變得很有必要了。
Ember 和 Angular 都即將得益于即將到來的標準:ECMAScript7 的 Object.observe 功能,很適合 Angular。它提供了原生的 API 給你用來監聽對象屬性的變化。盡管這樣,Angular 不需要支持所有的用例,因為 Angular 的觀察者相對于簡單的監聽對象屬性,可以做到的更好。
即將到來的 Angular 2 在檢測改變這件事上帶來了很多有趣的更新,最近 Victor Savkin 的一篇文章有介紹到。
關于這個主題,也可以看:Victor's ng-conf talk
React: 虛擬 DOM
我不知道到底哪些發生了變化,所以我只能重新渲染所有東西,然后看一下有哪些不同。
React 有很多有趣的特性,但是我們討論的最有趣的特性是虛擬 DOM。
像 Angular 一樣,React 不會對數據模型進行限制,而是讓你使用你認為合適的任何對象和數據結構。那么,它是如何在存在改變的情況下使 UI 保持最新呢?
React 所做的是有效的把我們帶回服務器渲染時代,當時我們還不關心狀態變化:每當某處發生改變的時候,它會從頭重新渲染整個 UI。這可以顯著的簡化 UI 的代碼。大部分情況,你不會關心如何在 React 中維護狀態。就像服務器渲染一樣,渲染一次就算了。當組件需要變更時,它只能再次重新渲染。組價的初始化渲染和更細它的數據之間,沒有任何區別。
如果故事就這么結束的話,它看起來的確非常低效。然而,React 在重新渲染方面,有點特殊。
當 React 進行重新渲染時,它首先會渲染到虛擬 DOM 中,這不是一個實際的 DOM 對象的圖。而是一個輕量級的,有純粹的 object 和 array 組成的純 JavaScript 的數據結構,它代表著一個真實的 DOM 對象的圖。
然后,一個獨立的進程會根據虛擬 DOM 的結構來創建那些在屏幕上顯示的真實的 DOM 元素。
[圖片上傳失敗...(image-597817-1518421446682)]
之后,當變化發生的時候,一個新的虛擬 DOM 會被從頭到尾創建出來。這個新的虛擬 DOM 將映射出數據模型的新的狀態。現在 React 在手上有兩個虛擬 DOM:一個新的,一個舊的。然后會對兩個虛擬 DOM 進行一個對比算法,得出它們之間的一組變化。有且只有這些更改會被應用到真實 DOM 中:此元素已添加,此屬性以改變,等等。
[圖片上傳失敗...(image-f8edb3-1518421446682)]
所以 React 起碼至少有一個好處,就是你不用追蹤變化了。你只需要每次重新渲染整個 UI ,然后無論改變了什么最終都會得到相應的結果。React 的虛擬 DOM 對比算法,能讓你做到這一點,并且最大限度的節省昂貴的 DOM 操作。
Om: 不可改變的數據結構
我確切的知道哪些沒有改變。
雖然 React 的虛擬 DOM 相當的塊,但是當你的 UI 非常龐大或者經常性渲染的時候(例如:每秒高達 60 次),它依然會面臨瓶頸。
問題在于,真的沒辦法每次都渲染出整個虛擬 DOM,除非你引入一些方法來控制數據模型的改變,就像 Ember 做的一樣。
一種控制變化的辦法是 不可改變的,持久化的數據結構。這些看起來似乎很適合使用在 React 的虛擬 DOM 中,正如 David Nolen 在 Om 庫中所做的 工作 那樣,一個構建于 React 和 ClojureScript 之上的庫。
有一點關于不可改變數據結構的是,顧名思義,你永遠不能改變它,只能產生新的版本。如果你想改變一個對象的屬性,你只能新建一個對象和屬性,因為你不能改變已經存在的那一個。由于持久化數據結構的工作方式,這比聽起來更加有效率。
這意味著在檢測變化方面,當 React 組件都只由不可變數據組成的時候,只有一個逃生窗口:當你重新渲染一個組件時,組件的狀態仍然指向上次渲染時的相同數據結構,你就可以跳過這次重新渲染。你可以使用該組件的先前的虛擬 DOM 以及源自該組件的整個組件樹。沒有必要進一步挖掘,因為在這個狀態中所有東西都不可能改變。
[圖片上傳失敗...(image-9dfd4a-1518421446682)]
就像 Ember 一樣,像 Om 的這種庫不允許在你的數據中使用舊的 JavaScript 對象圖。你必須在不可變數據結構中構建你的數據模型,從而才能在其中得到好處。我會贊同這樣的做法,因為這一次你這樣做并不是為了取悅框架本身。你這樣做只是因為這是一個又簡單又好的方式去管理你的應用狀態。使用不可變數據結構的主要好處,并不是提升渲染性能,而是簡化你的應用結構。
雖然 Om 和 ClojureScript 已經講 React 和不可變數據結構融合起來,但是他們并不是圈子里面的唯一組合。而僅僅使用 React 和 Facebook 的 Immutable-js 是完全可能的。這個庫的作者 Lee Byron 在最近的一次 React.js 為主題的會議中進行了一個 精彩的介紹。
同時我建議看一下 Rich Hickey's 的 Persistent Data Structures And Managed References, 去了解狀態管理的方法。
我自己現在一直在為不可變數據數據結構 寫詩,但我絕對沒有預見到它會進入前端 UI 框架行列。它看起來似乎不遺余力的發生著,而 Angular 的人 正在為支持這個而努力著。
總結
檢測變化時 UI 開發中的核心問題,而 JavaScript 框架們以各種方式解決這個問題。
EmberJS 能在它們發生變化的時候檢測到,因為它控制著你的數據模型 API,并且可以在你調用它的時候觸發事件。
Angular.js 是事后進行檢測, 它通過重新運行你已經在 UI 中注冊的所有數據綁定,來檢測它們的值是否已經發生變化。
React 的檢測方法是通過把整個 UI 重新渲染成一個虛擬 DOM,然后和舊的版本進行對比。無論改變了什么,都可以給真實 DOM 打上個補丁。
React 和 不可變數據結構的組合,對比純粹的 React 有所增強,通過快速的在組件樹中標記不可變的節點。因為組件內的變化是不被允許的。但是,這不是主要出于性能的原因,而是由于它對整個應用程序體系結構有積極的影響。