為什麼幾乎所有的GUI界面都採用事件驅動編程模型?
不知道與事件驅動模型相提並論的還有哪些方式,還有與這些方式相比為什麼都選擇了事件驅動模型。
我覺得是因為一般GUI的用戶輸入頻率比較低,如果每個部分不停做輪詢去獲取用戶輸入,浪費時間又可能比較難處理。而事件處理則是由中央的系統把輸入事件派送給需要的部分。
這問題也問得很準確,僅是「幾乎所有」GUI系統是這樣,也有像Unity的「immediate mode GUI」,如ocornut/imgui · GitHub。對於較多動態的GUI,如遊戲,每幀更新是常態,順便處理輸入也不是大問題。不過如Unity GUI遇到的一個大問題,這種GUI通常只能由程序員製作,和一般GUI可以用專門的編輯器製作不一樣,所以Unity 4.6 就增加了傳統式的GUI系統。剛好最近實現了一版事件模型,GUI 的邏輯處理幾乎都由事件驅動,我認為比較重要的原因有:
- 事件模型就是典型的"職責鏈模式"(Chain-of-responsibility pattern),看看職責鏈的優勢就知道了:
- 可動態增加減少接收者,可動態調整接收優先順序
- 每個接收對象都有機會響應事件,每個事件可由多個對象同時處理
- 對於處理不了的事件,接收對象可以不做處理,減少外部耦合
- 對於已經處理的事件,接收對象可以選擇停止派發,也可以選擇繼續派發
- 事件模型還充分應用了"監聽器模式":
- 解耦事件發送者和接收者
- 每個事件可觸發多種邏輯
- 還有很重要一點:單線程的應用如果想處理外部事件,都離不開消息循環,而事件模型和消息循環是天生一對。
- 桌面上,最廣為人知的消息循環恐怕就是Windows了,WinMain 方法都是一個死循環,不斷的讀取並處理Windows消息,大家都玩過你懂的。(派發給窗體)
- 網頁上,JavaScript 應用也是單線程的,靠瀏覽器的消息循環驅動各種事件和回調。(派發給DOM)
個人理解是 UI 框架面臨的核心問題是 UI 更新流暢度和對輸入響應的速度。前者是 UI 更新的幀率和 每幀的 timing (尤其是 UI 動畫)決定的, 後者是由從用戶輸入到完成 UI 更新的延遲決定的。這要求 UI 框架對應用的運行擁有最大限度的控制權,以準確的把握每個幀的渲染時機。
而基於事件回調的方式,回調函數結束後程序的控制權可以快速回到 UI 框架。只要回調函數不阻塞或引起崩潰,執行時長超出預期等情況 UI 框架都可以處理的。但如果讓開發者去決定何時將控制權交還給 UI 框架,比如 runOnce() 之類的,對開發者的要求就太高了。
從UI Toolkit的角度的話,其實有很多research system用了不同於傳統的event based method的方法來handle input.
其中比較有代表性的就是Amulet [1,2] 和Garnet [3, 4], 用了interactor的概念,把UI里input和output的處理徹底分開,具體細節可以看reference里的兩篇paper。
還有一個比較有意思的model是subArctic [5],細節實現也可以看對應的paper。
//TODO: 等有空的時候再具體寫一下
References:
[1] Brad A. Myers and David Kosbie. "Reusable Hierarchical Command Objects," Proceedings CHI"96: Human Factors in Computing Systems. Vancouver, BC, Canada. April 14-18, 1996.[2] Brad A. Myers, Richard G. McDaniel, Robert C. Miller, Alan S. Ferrency, Andrew Faulring, Bruce D. Kyle, Andrew Mickish, Alex Klimovitski and Patrick Doane. "The Amulet Environment: New Models for Effective User Interface Software Development," IEEE Transactions on Software Engineering, Vol. 23, no. 6. June, 1997. pp. 347-365.
[3] Brad A. Myers. "A New Model for Handling Input," ACM Transactions on Information Systems, vol. 8, No. 3. July, 1990. pp. 289-320.
[4] Brad A. Myers, Dario A. Giuse, Roger B. Dannenberg, Brad Vander Zanden, David S. Kosbie, Ed Pervin, Andrew Mickish, and Philippe Marchal. "Garnet: Comprehensive Support for Graphical, Highly-Interactive User Interfaces," IEEE Computer. vol. 23, no. 11. November, 1990. pp. 71-85.
[5] Scott E. Hudson, Jennifer Mankoff, and Ian Smith. 2005. Extensible input handling in the subArctic toolkit. In Proceedings of the SIGCHI Conference on Human Factors in Computing Systems (CHI "05). ACM, New York, NY, USA, 381-390.因為GUI不是用戶代碼「畫出來」的,而是窗口管理器根據用戶代碼的請求,加上自行的判斷來繪製的。窗口管理器獨佔對屏幕像素的控制權,並視情況拒絕或無視用戶代碼的請求。同時,窗口管理器需要繪製應用局部細節的時候,會發消息給客戶代碼要求補充細節數據
由於用戶程序需要響應窗口管理器的數據要求(一般來說,不及時響應的話窗口管理器就把未知細節塗白),因此用戶程序必須要有一個IPC響應埠,而絕大多數OS將其設計成消息隊列這種IPC因為界面是Reactive的,道法自然。
因為底層,也就是win32api就是事件驅動的,靠一個循環不斷處理消息調用回調。上層的庫與其保持一致的話,寫起來方便簡潔。
至於底層為什麼會是這樣,解耦啊,便於動態調整啊之類的看其他答案即可,就不再贅述了。事件的queue模型不需要額外的線程同步開銷。
事件驅動的模型,是實際的模型。
中斷的GUI表現形式就是事件,事件驅動當然就最簡單高效
比較好奇除了事件驅動,還有其它模式嗎?難道不是用戶單擊滑鼠,然後給出事件?或者用戶敲擊鍵盤然後通知處理?
一個原因是認識和表達(世界)的時候是只能一個個部分的認識和感官。GUI的時候也是一個個組件的畫。
另外一個原因是記憶和意識的熵減的確定化過程和整體的熵增加有關,很容易體現在時間的箭頭上。而什麼點上發生什麼事是最直觀的時間看法。
每個組件的響應頻率不一樣,用組件自身去反轉控制訂閱事件,是比較有效率的做法。
更加動態化的響應要求和更快的硬體發展趨勢都需要 狀態(state)更少的方式。GUI方式也在變化以適應新的需求,比如interactor ,data binding, rx, flux等
雖然世界根本不會管人到底怎麼想的,開發者作為人來說,肯定要對自己好,所以表層暴露的使用介面,以及開發人群的選擇是有所偏向的。
總而言之,事件驅動是直觀的,也有狀態需要更好維護的要求,不好的做法會有所變化。如果按照推薦用法的話,WPF/Silverlight/WinJS/GacUI用的是data binding+MVVM,MFC和angular.js用的都是變數交換,QT、Delphi和.net的Windows Forms用的是事件機制。
其實按照這三種常用的用法,架構設計的高低之分,立刻就明白。
實現就兩種方式,一個是中斷,一個是輪詢。對於UI這種來說,天然的就是硬體中斷啊。輪詢那效率多低啊。
For example,奴隸線程幹完活,給 UI 線程發個消息,UI 可以更新顯示狀態。
Signal programming為了解耦合,使顯示與邏輯分離。
多線程的GUI框架特別容易死鎖,所以很多設計採用的單線程的實現方式,避免死鎖。
單線程又有個致命的弱點,容易阻塞,所以人們當然想辦法解決阻塞的問題,就產生了事件驅動模型。事件驅動 模型 底層實現 是 事件輪詢 + 非同步IO,非同步IO可以避免阻塞。
代碼層面,語句同步執行是可以接受的,因為非常快。
IO層面,IO函數同步執行是不可以接受的,因為慢,影響交互。想像一下,你點擊一個按鈕提交數據到服務端,GUI界面就必須等待服務端返回才能繼續響應,因為單線程。
這樣避免多線程死鎖解決了,單線程阻塞解決了,而這一編程模型又有利於程序設計,所以這種模式就很流行。WPF的推薦用法是用MVVM模式實現數據驅動,當然WPF也可以像WinForm一樣使用事件驅動。
你要知道當初Windows原生GUI就是事件驅動,靠著回調函數處理系統事件的……
為啥用回調?性能問題就不贅述。為啥用事件驅動?事件其實就是一種很靈活的介面,可以用一個固定的介面表示出各種各樣的調用要求,只要求程序員按照事件對應的協議處理傳進來的函數參數。
事件驅動可以理解成一個IPC,系統「跨進程」調用用戶提供的處理函數,消息則是IPC的協議,用戶按照消息處理用戶自己的數據,分工明確。後來各種庫對這種協議做了個淺淺的封裝,誕生了大名鼎鼎的MFC,俗稱「沒飯吃」。隨著Borland的崛起,微軟與之競爭,導致了RAD編程開發工具的流行一時,delphi、vb都是當時很流行的工具和編程語言。同時降低了GUI的編程門檻,讓我們當時這些小學生都能進行編程這樣高大上的工作。這些工具仍然擺脫不了事件驅動的影子,因為Windows的GUI編程模型就是這樣的。
隨著互聯網的發展,前端和後端的界限分的越來越清晰,程序員的分工越來越明顯,而早年程序員前端後端基本都是一個人/同一個團隊來完成的。因此界面和邏輯分離的需求越來越明顯,於是誕生了WPF等應用程序框架,用各種xml表示界面(html?)。C#就成了WEB開發中的後端語言(php?)和部分前端語言(js?)。為了提供更好的響應體驗
推薦閱讀:
※有誰可以介紹一些團隊任務分配管理軟體?
※如何編寫一個硬體模擬器?
※像QQ遊戲這樣的界面應該用什麼框架開發比較好?
※作為一枚程序猿,你有或見過哪些奇葩的編程習慣?
※開發人員買 MacBook Pro 好還是 MacBook Air 好?