本系列的上一篇文章《React.Component損害了復用性?》探討了如何在前端開發(fā)中編寫可復用的界面元素菇用。本篇文章將從性能和算法的角度比較 Binding.scala 和其他框架的渲染機制澜驮。
Binding.scala 實現(xiàn)了一套精確數(shù)據(jù)綁定機制,通過在模板中使用 bind
和 for
/yield
來渲染頁面惋鸥。你可能用過一些其他 Web 框架杂穷,大多使用臟檢查或者虛擬 DOM 機制。和它們相比卦绣,Binding.scala 的精確數(shù)據(jù)綁定機制使用更簡單耐量、代碼更健壯、性能更高滤港。
ReactJS虛擬DOM的缺點
比如廊蜒, ReactJS 使用虛擬 DOM 機制,讓前端開發(fā)者為每個組件提供一個 render
函數(shù)溅漾。render
函數(shù)把 props
和 state
轉換成 ReactJS 的虛擬 DOM山叮,然后 ReactJS 框架根據(jù) render
返回的虛擬 DOM 創(chuàng)建相同結構的真實 DOM。
每當 state
更改時添履,ReactJS 框架重新調用 render
函數(shù)屁倔,獲取新的虛擬 DOM 。然后暮胧,框架會比較上次生成的虛擬 DOM 和新的虛擬 DOM 有哪些差異锐借,進而把差異應用到真實 DOM 上。
這樣做有兩大缺點:
- 每次
state
更改叔壤,render
函數(shù)都要生成完整的虛擬 DOM瞎饲,哪怕state
改動很小,render
函數(shù)也會完整計算一遍炼绘。如果render
函數(shù)很復雜嗅战,這個過程就會白白浪費很多計算資源。 - ReactJS 框架比較虛擬 DOM 差異的過程俺亮,既慢又容易出錯驮捍。比如,你想要在某個
<ul>
列表的頂部插入一項<li>
脚曾,那么 ReactJS 框架會誤以為你修改了<ul>
的每一項<li>
东且,然后在尾部插入了一個<li>
。
這是因為 ReactJS 收到的新舊兩個虛擬 DOM 之間相互獨立本讥,ReactJS 并不知道數(shù)據(jù)源發(fā)生了什么操作珊泳,只能根據(jù)新舊兩個虛擬 DOM 來猜測需要執(zhí)行的操作鲁冯。自動的猜測算法既不準又慢,必須要前端開發(fā)者手動提供 key
屬性色查、shouldComponentUpdate
方法薯演、componentDidUpdate
方法或者 componentWillUpdate
等方法才能幫助 ReactJS 框架猜對。
AngularJS的臟檢查
除了類似 ReactJS 的虛擬 DOM 機制秧了,其他流行的框架跨扮,比如 AngularJS 還會使用臟檢查算法來渲染頁面。
類似 AngularJS 的臟檢查算法和 ReactJS 有一樣的缺點验毡,無法得知狀態(tài)修改的意圖衡创,必須完整重新計算 View 模板。除此之外晶通,AngularJS 更新 DOM 的范圍往往會比實際所需大得多璃氢,所以會比 ReactJS 還要慢。
Binding.scala的精確數(shù)據(jù)綁定
Binding.scala 使用精確數(shù)據(jù)綁定算法來渲染 DOM 录择。
在 Binding.scala 中拔莱,你可以用 @dom
注解聲明數(shù)據(jù)綁定表達式。@dom
會自動把 =
之后的代碼包裝成 Binding
類型隘竭。
比如:
@dom val i: Binding[Int] = 1
@dom def f: Binding[Int] = 100
@dom val s: Binding[String] = "content"
@dom
既可用于 val
也可以用于 def
塘秦,可以表達包括 Int
、 String
在內的任何數(shù)據(jù)類型动看。
除此之外尊剔,@dom
方法還可以直接編寫 XHTML,比如:
@dom val comment: Binding[Comment] = <!-- This is a HTML Comment -->
@dom val br: Binding[HTMLBRElement] = <br/>
@dom val seq: Binding[BindingSeq[HTMLBRElement]] = <br/><br/>
這些 XHTML 生成的 Comment 和 HTMLBRElement 是 HTML Node 的派生類菱皆。而不是 XML Node须误。
每個 @dom
方法都可以依賴其他數(shù)據(jù)綁定表達式:
val i: Var[Int] = Var(0)
@dom val j: Binding[Int] = 2
@dom val k: Binding[Int] = i.bind * j.bind
@dom val div: Binding[HTMLDivElement] = <div>{ k.bind.toString }</div>
通過這種方式,你可以編寫 XHTML 模板把數(shù)據(jù)源映射為 XHTML 頁面仇轻。這種精確的映射關系京痢,描述了數(shù)據(jù)之間的關系,而不是 ReactJS 的 render
函數(shù)那樣描述運算過程篷店。所以當數(shù)據(jù)發(fā)生改變時祭椰,只有受影響的部分代碼才會重新計算,而不需要重新計算整個 @dom
方法疲陕。
比如:
val count = Var(0)
@dom def status: Binding[String] = {
val startTime = new Date
"本頁面初始化的時間是" + startTime.toString + "方淤。按鈕被按過" + count.bind.toString + "次。按鈕最后一次按下的時間是" + (new Date).toString
}
@dom def render = {
<div>
{ status.bind }
<button onclick={ event: Event => count := count.get + 1 }>更新狀態(tài)</button>
</div>
}
以上代碼可以在ScalaFiddle實際運行一下試試蹄殃。
注意携茂,status
并不是一個普通的函數(shù),而是描述變量之間關系的特殊表達式诅岩,每次渲染時只執(zhí)行其中一部分代碼讳苦。比如带膜,當 count
改變時,只有位于 count.bind
以后的代碼才會重新計算医吊。由于 val startTime = new Date
位于 count.bind
之前钱慢,并不會重新計算,所以會一直保持為打開網頁首次執(zhí)行時的初始值卿堂。
有些人在學習 ReactJS 或者 AngularJS 時,需要學習 key
懒棉、 shouldComponentUpdate
草描、 $apply
、 $digest
等復雜概念策严。這些概念在 Binding.scala 中根本不存在穗慕。因為 Binding.scala 的 @dom
方法描述的是變量之間的關系。所以妻导,Binding.scala 框架知道精確數(shù)據(jù)綁定關系逛绵,可以自動檢測出需要更新的最小部分。
結論
本文比較了虛擬 DOM 倔韭、臟檢查和精確數(shù)據(jù)綁定三種渲染機制术浪。
這三種機制中,Binding.scala 的精確數(shù)據(jù)綁定機制概念更少寿酌,功能更強胰苏,性能更高。我將在下一篇文章中介紹 Binding.scala 如何在渲染 HTML 時靜態(tài)檢查語法錯誤和語義錯誤醇疼,從而避免 bug 硕并。
相關鏈接
- Binding.scala 項目主頁
- Binding.scala ? TodoMVC 項目主頁
- Binding.scala ? TodoMVC DEMO
- Binding.scala ? TodoMVC 以外的其他 DEMO
- JavaScript 到 Scala.js 移植指南
- Scala.js 項目主頁
- Scala API 參考文檔
- Scala.js API 參考文檔
- Scala.js DOM API 參考文檔
- Binding.scala快速上手指南
- Binding.scala API參考文檔
- Binding.scala 的 Gitter 聊天室
更多精彩洞見,請關注微信公眾號:ThoughtWorks