Kraken中事件通道原理分析
前言
客戶端開發中,跨平臺和動態性已是老生常談的話題了,也誕生了ReactNative、Weex、Flutter等大前端方向的技術。
Kraken作為一款上層基於W3C標準實現,底層基於Flutter渲染的高效能渲染引擎,同時兼顧了跨平臺和動態化的特性。對業務的快速迭代起到了很關鍵的作用。
其中事件的註冊與分發在Flutter和JS的互動中算是其中比較典型的場景,今天就事件通道的原理跟大家分享一下學習Kraken原始碼的一些收穫。
方案簡介
首先簡單介紹一下該方案的結構,對事件通道的場景有個概念。
如下圖在Flutter頁面中內嵌了很多JS卡片元件,這些元件的佈局結構由js程式碼提供,服務端下發,通過渲染引擎將js元件翻譯成widget元件,插入widget樹中交由Flutter渲染。
本文要介紹的就是該場景下使用者手指從按下滑動到抬起過程中事件是如何從Flutter側傳遞到js側並由js消費的
事件通道
架構圖
kraken事件通道整體分為三層架構(JS業務層、Flutter容器層、C++引擎層)、兩條鏈路(註冊、分發)
流程概述
如下圖綠色為註冊流程,紅色為分發流程:
註冊:
-
1. C++側將eventType和callback進行繫結,分發時通過eventType回撥callback
-
2. 通過FFI方式給Flutter側傳送事件註冊指令
-
3. Flutter側根據id找到對應的Element,對eventType、Element、RenderObject等進行繫結,為分發做準備
分發:
-
1. Pointer事件在RenderObject樹中正常流轉到Kraken根節點
-
2. 根據註冊流程中提前繫結好的eventType、Element、RenderObject關係,找出當前路徑上的Element並梳理出已註冊的事件型別,遍歷路徑上的Element
-
3. Flutter和C++側都維護了一個一一對應的Element樹,每對Element擁有唯一id,通過id找到對應C++側的Element
-
4. 找到Element在註冊時eventType繫結的callback,進行回撥
經過上述的流程介紹,相信大家在腦中對事件通道已經有了一個整體的認知,下面將從原始碼角度進行分析:大家可以從Kraken官網下載原始碼一步步跟著理解
註冊
JS側
基於W3C標準,JS側註冊事件的程式碼和前端開發一樣,下面是js中事件註冊監聽的示例程式碼:
btn.addEventListener(eventType, callback)
C++側
解析到JS程式碼中的addEventListener語句時,C++側做了下面兩件事:
-
1. 傳送名為addEvent的UICommand指令給flutter側,指令內容主要有:targetId(Element節點唯一id,Flutter側可通過該targetId找到對應的Element元件)、指令名稱、eventType。
-
2. 將eventType作為key,callbackList作為value維護到集合m_eventListenerMap中,後續事件分發可通過eventType找到所有callbackList進行回撥
JSValue EventTarget::addEventListener(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) { // ... eventTargetInstance->m_context->uiCommandBuffer()->addCommand(eventTargetInstance->m_eventTargetId, UICommand::addEvent, args_01, nullptr); eventTargetInstance->m_eventListenerMap.add(eventType, JS_DupValue(ctx, callback)); // ... }
flutter側
-
1. 在事件註冊之前有類似createElement的方法建立Element,在Flutter側會維護一個Element集合,key為targetId,值為Element節點:(其中Element繼承自Node,Node又繼承自EventTarget,這裡的設計跟Flutter中RenderObject繼承自HitTestTarget一樣)
//維護Element節點和targetId的集合 Map<int, EventTarget> _eventTargets = <int, EventTarget>{};
//Node類定義 class Node extends EventTarget implements RenderObjectNode,LifecycleCallbacks {} //Element類定義 class Element extends Node with ElementBase,ElementEventMixin,ElementOverflowMixin {}
-
2. 接收到addEvent指令,對應會呼叫Flutter側的addEvent方法:
void addEvent(int targetId, String eventType) { if (!_existsTarget(targetId)) return; // 根據targetId獲取EventTarget dom.EventTarget target = _getEventTargetById<dom.EventTarget>(targetId)!; if (target != null) { BindingBridge.listenEvent(target, eventType); } }
// 呼叫EventTarget.addEventListener方法將事件型別和EventTarget進行繫結 // 此處的_dispatchBindingEvent在事件分發階段會聊到,暫時理解成用於事件處理的即可 static void listenEvent(EventTarget eventTarget, String type) { eventTarget.addEventListener(type, _dispatchBindingEvent); }
-
3. 注意此處的eventTarget實際型別是Element,通過上面的Element類定義可以看出Element繼承自EventTarget的同時又通過with關鍵字繼承了ElementEventMixin,ElementEventMixin重寫了addEventListener方法,所以我們先看ElementEventMixin.addEventListener方法:
@override void addEventListener(String eventType, EventHandler handler) { //根據上述繼承關係分析,此處super會先呼叫EventTarget的addEventListener方法 super.addEventListener(eventType, handler); RenderBoxModel? renderBox = renderBoxModel; if (renderBox != null) { ensureEventResponderBound(); } }
-
4. EventTarget中的addEventListener:
void addEventListener(String eventType, EventHandler eventHandler) { if (_disposed) return;//是否disposed //取出當前EventTarget中該事件型別的handler集合 List<EventHandler>? existHandler = _eventHandlers[eventType]; if (existHandler == null) { _eventHandlers[eventType] = existHandler = []; } //新增handler existHandler.add(eventHandler); }
其中_eventHandlers是EventTarget中維護事件型別以及handler的集合:
final Map<String, List<EventHandler>> _eventHandlers = {}; //其中EventHandler的定義如下,是一個入參為Event的function: typedef EventHandler = void Function(Event event);
-
5. super部分父類的邏輯講完了,回到ElementEventMixin.addEventListener方法中繼續看ensureEventResponderBound方法,其中只需要關注第6行將自身與RenderBox.getEventTarget進行繫結,便於後面能方便拿到EventTarget物件
void ensureEventResponderBound() { // Must bind event responder on render box model whatever there is no event listener. RenderBoxModel? renderBox = renderBoxModel; if (renderBox != null) { // Make sure pointer responder bind. renderBox.getEventTarget = getEventTarget; } } EventTarget getEventTarget() { return this; }
至此,從JS程式碼addEventListener開始進行的事件註冊流程暫告一段落,
小結
經過上述程式碼的分析,事件註冊其實可以簡單理解成各種繫結:
-
• eventType和callback的繫結
-
• EventTarget、eventType、EventHandler的繫結
-
• EventTarget和RenderBox.getEventTarget繫結
接下來分析事件分發的過程:
分發
Flutter指標事件簡介
在此之前需要先了解一下Flutter原生的指標事件是如何流轉的,所謂指標事件主要是指使用者手指在螢幕按下、移動和抬起的Pointer事件,另外我們常說的單機、雙擊、長按等事件屬於手勢事件,是對指標事件的一種封裝。針對Flutter原生的指標事件網上有很多原始碼分析的文章,本文就不對Flutter原生指標事件擴充套件介紹了,下面是Flutter原生指標事件的原始碼流程圖:
Flutter側
-
1. 根據對Flutter原生事件分發的瞭解,最終處理事件的是HitTestTarget.handleEvent()方法,RenderObject繼承自HitTestTarget,各個Node節點自身並沒有重寫handleEvent,所以直接找到Kraken元件最外層的RenderObject,檢視_KrakenRenderObjectWidget.createRenderObject方法,該方法中返回的RenderObject是RenderViewportBox
-
2. RenderViewportBox.handleEvent方法:
// RenderViewportBox類的定義 class RenderViewportBox extends RenderProxyBox with RenderObjectWithControllerMixin, RenderEventListenerMixin { ///... @override void handleEvent(PointerEvent event, HitTestEntry entry) { super.handleEvent(event, entry as BoxHitTestEntry); // Add pointer to gesture dispatcher. GestureDispatcher.instance.handlePointerEvent(event); if (event is PointerDownEvent) { // Set event path at begin stage and reset it at end stage on viewport render box. GestureDispatcher.instance.resetEventPath(); } } ///... }
-
3. RenderViewportBox通過with繼承了RenderEventListenerMixin,上述方法中super呼叫了RenderEventListenerMixin.handleEvent方法:
@override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { assert(debugHandleEvent(event, entry)); // Set event path at begin stage and reset it at end stage on viewport render box. // And if event path existed, it means current render box is not the first in path. if (getEventTarget != null) { if (event is PointerDownEvent) { // Store the first handleEvent the event path list. if (GestureDispatcher.instance.getEventPath().isEmpty) { GestureDispatcher.instance.setEventPath(getEventTarget!()); } } } super.handleEvent(event, entry); }
上述方法中最關鍵的就是setEventPath方法:其中入參getEventTarget就是之前事件註冊流程中最後將Element自身賦值給了getEventTarget,看一下setEventPath方法:
void setEventPath(EventTarget target) { _eventPath = target.eventPath; }
eventPath顧名思義就是事件路徑,指從葉子節點到根節點路徑上的所有節點組成的集合:
List<EventTarget> get eventPath { List<EventTarget> path = []; EventTarget? current = this; while (current != null) { path.add(current); // 冒泡遍歷父節點 current = current.parentEventTarget; } return path; }
-
4. RenderEventListenerMixin.handleEvent方法執行完後,繼續回到RenderViewportBox.handleEvent方法中,看看GestureDispatcher.instance.handlePointerEvent(event)是如何處理指標事件的:
void handlePointerEvent(PointerEvent event) { TouchPoint touchPoint = _toTouchPoint(event); if (event is PointerDownEvent) { // 收集路徑中所有註冊監聽的事件型別,事件是否分發取決於事件型別是否在這裡 _gatherEventsInPath(); _addPointerDownEventToMatchedRecognizers(event); // 將路徑上的葉子節點儲存下來,事件的分發都是從葉子節點開始的 _target = _eventPath.isNotEmpty ? _eventPath.first : null; if (_target != null) { // 將EventTarget和事件進行繫結,key為事件id,value為eventTarget _bindEventTargetWithTouchPoint(touchPoint, _target!); } // 將事件儲存到集合中,後續分發時從集合中取出事件 _addPoint(touchPoint); } // 處理指標事件 _handleTouchPoint(touchPoint); // up和cancel事件解綁 if (event is PointerUpEvent || event is PointerCancelEvent) { _removePoint(touchPoint); _unbindEventTargetWithTouchPoint(touchPoint); } }
-
• 4.1 PointerDownEvent事件中通過_gatherEventsInPath方法將eventPath上所有節點註冊的事件型別收集起來儲存在_eventsInPath中:
void _gatherEventsInPath() { // Reset the event map when start a new gesture. _eventsInPath.clear(); //遍歷_eventPath for (int i = 0; i < _eventPath.length; i++) { EventTarget eventTarget = _eventPath[i]; //遍歷EventTarget中事件處理集合中的key(事件名稱) eventTarget.getEventHandlers().keys.forEach((eventType) { _eventsInPath[eventType] = true; }); } }
-
• 4.2 PointerDownEvent事件中將葉子節點儲存到_target中,後續的連續事件都是從葉子節點開始分發
-
• 4.3 呼叫_bindEventTargetWithTouchPoint方法將EventTarget和事件進行繫結
-
• 4.4 呼叫_addPoint方法將事件快取到集合中,後續會遍歷該集合進行事件處理
-
• 4.5 _handleTouchPoint事件處理
void _handleTouchPoint(TouchPoint currentTouchPoint) { String eventType; if (currentTouchPoint.state == PointState.Down) { eventType = EVENT_TOUCH_START; } else if (currentTouchPoint.state == PointState.Move) { eventType = EVENT_TOUCH_MOVE; } else if (currentTouchPoint.state == PointState.Up) { eventType = EVENT_TOUCH_END; } else { eventType = EVENT_TOUCH_CANCEL; } // 這裡的_eventsInPath就是上面_gatherEventsInPath方法收集到的路徑上註冊的所有的事件型別,只有註冊了的才會分發 if (_eventsInPath.containsKey(eventType)) { TouchEvent e = TouchEvent(eventType); if (eventType == EVENT_TOUCH_MOVE) { // 16ms的卡口,每16ms只能有一個move事件被分發 _throttler.throttle(() { // 取出事件對應的EventTarget進行分發,呼叫EventTarget.dispatchEvent(e) _pointTargets[currentTouchPoint.id]?.dispatchEvent(e); }); } else { // 取出事件對應的EventTarget進行分發,呼叫EventTarget.dispatchEvent(e) _pointTargets[currentTouchPoint.id]?.dispatchEvent(e); } } }
-
• 4.6 PointerUpEvent和PointerCancelEvent事件時執行解綁操作
-
5. 上述_handleTouchPoint方法最後都呼叫了EventTarget.dispatchEvent方法來進行事件分發,我們來看下里面是如何處理的:
其中呼叫了_dispatchEventInDOM方法,從_eventHandlers中取出eventType註冊的所有handler進行回撥,同時冒泡將事件傳遞給父元件
void dispatchEvent(Event event) { if (_disposed) return; // 將自身賦值給target event.target = this; _dispatchEventInDOM(event); } void _dispatchEventInDOM(Event event) { String eventType = event.type; // _eventHandlers是EventTarget中維護事件型別以及handler的集合,前面有介紹 List<EventHandler>? existHandler = _eventHandlers[eventType]; if (existHandler != null) { // Modify currentTarget before the handler call, otherwise currentTarget may be modified by the previous handler. event.currentTarget = this; for (EventHandler handler in existHandler) { handler(event); } event.currentTarget = null; } // 冒泡將事件分發給父元件 if (event.bubbles && !event.propagationStopped) { parentEventTarget?._dispatchEventInDOM(event); } }
-
6. 大家是不是很好奇這裡的handler到底是什麼,往上翻到Flutter側註冊流程第2點中有這一段程式碼:
// 呼叫EventTarget.addEventListener方法將事件和EventTarget進行繫結 // 此處的_dispatchBindingEvent在事件分發階段會聊到,暫時理解成用於事件處理的即可 static void listenEvent(EventTarget eventTarget, String type) { eventTarget.addEventListener(type, _dispatchBindingEvent); }
當時對_dispatchBindingEvent留一個懸念,其型別就是EventHandler,就是上面呼叫的handler,看看裡面做了什麼:
// Dispatch the event to the binding side. void _dispatchBindingEvent(Event event) { Pointer<NativeBindingObject>? pointer = event.currentTarget?.pointer; int? contextId = event.target?.contextId; if (contextId != null && pointer != null) { emitUIEvent(contextId, pointer, event); } }
該方法主要就是通過FFI的方式將事件分發給C++層:
void emitUIEvent(int contextId, Pointer<NativeBindingObject> nativeBindingObject, Event event) { if (KrakenController.getControllerOfJSContextId(contextId) == null) { return; } DartDispatchEvent dispatchEvent = nativeBindingObject.ref.dispatchEvent.asFunction(); Pointer<Void> rawEvent = event.toRaw().cast<Void>(); bool isCustomEvent = event is CustomEvent; Pointer<NativeString> eventTypeString = stringToNativeString(event.type); int propagationStopped = dispatchEvent(contextId, nativeBindingObject, eventTypeString, rawEvent, isCustomEvent ? 1 : 0); event.propagationStopped = propagationStopped == 1 ? true : false; freeNativeString(eventTypeString); }
C++側
事件分發到C++側的入口函式是NativeEventTarget.dispatchEventImpl
繼續追朔最終呼叫了EventTargetInstance::internalDispatchEvent:
其中的m_eventListenerMap是在addEventListener註冊事件時賦值的,對eventType和callback進行了繫結;分發時,根據eventType從m_eventListenerMap中取出所有的callback通過JS_Call進行回撥
bool EventTargetInstance::internalDispatchEvent(EventInstance* eventInstance) { // ... if (m_eventListenerMap.contains(eventType)) { const EventListenerVector* vector = m_eventListenerMap.find(eventType); for (auto& eventHandler : *vector) { _dispatchEvent(eventHandler); } } // ... } // Dispatch event listeners writen by addEventListener auto _dispatchEvent = [&eventInstance, this](JSValue handler) { if (!JS_IsFunction(m_ctx, handler)) return; if (eventInstance->propagationImmediatelyStopped()) return; /* 'handler' might be destroyed when calling itself (if it frees the handler), so must take extra care */ JS_DupValue(m_ctx, handler); // The third params `thisObject` to null equals global object. JSValue returnedValue = JS_Call(m_ctx, handler, JS_NULL, 1, &eventInstance->jsObject); JS_FreeValue(m_ctx, handler); m_context->handleException(&returnedValue); m_context->drainPendingPromiseJobs(); JS_FreeValue(m_ctx, returnedValue); };
小結
Flutter事件分發從Kraken根節點RenderViewportBox.handleEvent開始,首先找到葉子節點到根節點路徑上的所有節點eventPath,然後從葉子節點開始冒泡向上分發給eventPath路徑上所有節點進行處理,Flutter側只是將事件的處理按照順序通過FFI方式分發給C++側,JS業務程式碼中寫的callback事件回撥方法在註冊階段就已經在C++側和事件型別進行了繫結,C++側根據事件型別取出callback通過JS_Call進行回撥
其他如手勢相關以及C++側具體的實現邏輯由於篇幅有限就不展開了,有興趣的小夥伴可以去 Kraken官網 下載開原始碼檢視。
結語
通過對事件傳遞流程的學習,我們可以一窺Kraken各架構層之間的通訊方式,舉一反三我們也可以理解除事件註冊外的其他UICommand指令如createElement、removeNode、setStyle的排程方式,同時也可以自定義各種事件。對我們理解Kraken終端容器的整體架構思想有所幫助。
基於Kraken終端容器的設計思想,閒魚正在調研和實踐終端容器架構,致力於推進客戶端和前端技術的融合與演進,一起期待一下吧!
- 大終端領域的新物種-KUN
- 一次夜間介面超時的解決過程
- 如何寫出有效的單元測試
- Kraken中事件通道原理分析
- 我在閒魚做搭建——魔魚搭投編輯器介紹
- Flutter富文字編輯器系列文章3——互動篇
- Flutter富文字編輯器系列文章3——互動篇
- 打造Flutter高效能富文字編輯器——渲染篇
- 節日獻禮:Flutter圖片庫重磅開源!
- 節日獻禮:Flutter圖片庫重磅開源!
- 關於閒魚測試資料構造,我有幾條心得
- 關於閒魚測試資料構造,我有幾條心得
- 打造Flutter高效能富文字編輯器——協議篇
- 打造Flutter高效能富文字編輯器——協議篇
- 閒魚前端技術體系的背後——魔魚(良心推薦,從思路到實踐)
- 閒魚如何保障交易鏈路質量
- Flutter 音影片開發的新思路
- 實效性與準確性的背後:多系統資料聚合展示
- Flutter滑動體驗對齊原生-滑動曲線篇
- 閒魚搜尋-成交寬度優化實踐