客户端开发中,跨平台和动态性已是老生常谈的话题了,也诞生了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官网下载源码一步步跟着理解
基于W3C标准,JS侧注册事件的代码和前端开发一样,下面是js中事件注册监听的示例代码:
btn.addEventListener(eventType, callback)
解析到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));
// ...
}
1. 在事件注册之前有类似createElement的方法创建Element,在Flutter侧会维护一个Element集合,key为targetId,值为Element节点:(其中Element继承自Node,Node又继承自EventTarget,这里的设计跟Flutter中RenderObject继承自HitTestTarget一样)
//维护Element节点和targetId的集合
Map _eventTargets = {};
//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(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? existHandler = _eventHandlers[eventType];
if (existHandler == null) {
_eventHandlers[eventType] = existHandler = [];
}
//添加handler
existHandler.add(eventHandler);
}
其中_eventHandlers是EventTarget中维护事件类型以及handler的集合:
final Map> _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原生的指针事件是如何流转的,所谓指针事件主要是指用户手指在屏幕按下、移动和抬起的Pointer事件,另外我们常说的单机、双击、长按等事件属于手势事件,是对指针事件的一种封装。针对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 get eventPath {
List 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? 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? 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, Event event) {
if (KrakenController.getControllerOfJSContextId(contextId) == null) {
return;
}
DartDispatchEvent dispatchEvent = nativeBindingObject.ref.dispatchEvent.asFunction();
Pointer rawEvent = event.toRaw().cast();
bool isCustomEvent = event is CustomEvent;
Pointer eventTypeString = stringToNativeString(event.type);
int propagationStopped = dispatchEvent(contextId, nativeBindingObject, eventTypeString, rawEvent, isCustomEvent ? 1 : 0);
event.propagationStopped = propagationStopped == 1 ? true : false;
freeNativeString(eventTypeString);
}
事件分发到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终端容器的设计思想,闲鱼正在调研和实践终端容器架构,致力于推进客户端和前端技术的融合与演进,一起期待一下吧!