经典的事件系统分成两大块,绑定事件与分派事件,在浏览器中,分派事件很少人会直接dispatchEvent。因为创建一个DOM 事件是非常复杂的事情,不同的事件对象对应不同的事件构造器,传参也五花八门。因为分派事件基本上用户行为触发,比如我们点击了某个元素,恰逢在这上方绑定了点击事件,于是触发了。
React的绑定事件是在JSX 中进行,换言之, render时,props的onXXX事件就被收集起来,进行绑定。在jQuery时,人们发明了事件委托,将可以冒泡的事件挂在document或window对象上,进行统一的监听,实现高性能的事件系统。现在我们可以无视IE8 ,因此对不可冒泡的事件可以使用事件捕获,对能冒泡的事件使用事件冒泡,也是放在顶层对象 上进行监听。
function trapBubbledEvent(topLevelType, element) { //by 司徒正美
trapEventForPluginEventSystem(element, topLevelType, false);
}
function trapCapturedEvent(topLevelType, element) {
trapEventForPluginEventSystem(element, topLevelType, true);
}
大家可能觉得很奇怪,浏览器的dom.addEventListener(type, fn, capture)
怎么也要三个参数, 原因是fn是一个统一调度的方法,因此可省掉一个。而冒泡与捕获则通过不同的方法做区分,实际干活的是trapEventForPluginEventSystem。
trapEventForPluginEventSystem根据事件名进行分大三类,DiscreteEvent,UserBlockingEvent,ContinuousEvent,选出不同的事件派发器。
function trapEventForPluginEventSystem(element, topLevelType, capture) { //by 司徒正美
var listener;
switch (getEventPriority(topLevelType)) {
case DiscreteEvent:
listener = dispatchDiscreteEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
break;
case UserBlockingEvent:
listener = dispatchUserBlockingUpdate.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
break;
case ContinuousEvent: //by 司徒正美
default:
listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
break;
}
var rawEventName = getRawEventName(topLevelType);
if (capture) {
addEventCaptureListener(element, rawEventName, listener);
} else {
addEventBubbleListener(element, rawEventName, listener);
}
}
DiscreteEvent 离散事件. 例如blur、focus、 click、 submit、 touchStart. 这些事件都是离散触发的。
UserBlockingEvent 用户阻塞事件. 例如touchMove、mouseMove、scroll、drag、dragOver等等。这些事件会'阻塞'用户的交互。
ContinuousEvent 连续事件。例如load、error、loadStart、abort、animationEnd. 这个优先级最高,也就是说它们应该是立即同步执行的,这就是Continuous的意义,是持续地执行,不能被打断。
源码里有一个怒长的表,对所有常用事件进行归类
var eventTuples = [// Discrete events
[TOP_BLUR, 'blur', DiscreteEvent], [TOP_CANCEL, 'cancel', DiscreteEvent], [TOP_CLICK, 'click', DiscreteEvent], [TOP_CLOSE, 'close', DiscreteEvent], [TOP_CONTEXT_MENU, 'contextMenu', DiscreteEvent], [TOP_COPY, 'copy', DiscreteEvent], [TOP_CUT, 'cut', DiscreteEvent], [TOP_AUX_CLICK, 'auxClick', DiscreteEvent], [TOP_DOUBLE_CLICK, 'doubleClick', DiscreteEvent], [TOP_DRAG_END, 'dragEnd', DiscreteEvent], [TOP_DRAG_START, 'dragStart', DiscreteEvent], [TOP_DROP, 'drop', DiscreteEvent], [TOP_FOCUS, 'focus', DiscreteEvent], [TOP_INPUT, 'input', DiscreteEvent], [TOP_INVALID, 'invalid', DiscreteEvent], [TOP_KEY_DOWN, 'keyDown', DiscreteEvent], [TOP_KEY_PRESS, 'keyPress', DiscreteEvent], [TOP_KEY_UP, 'keyUp', DiscreteEvent], [TOP_MOUSE_DOWN, 'mouseDown', DiscreteEvent], [TOP_MOUSE_UP, 'mouseUp', DiscreteEvent], [TOP_PASTE, 'paste', DiscreteEvent], [TOP_PAUSE, 'pause', DiscreteEvent], [TOP_PLAY, 'play', DiscreteEvent], [TOP_POINTER_CANCEL, 'pointerCancel', DiscreteEvent], [TOP_POINTER_DOWN, 'pointerDown', DiscreteEvent], [TOP_POINTER_UP, 'pointerUp', DiscreteEvent], [TOP_RATE_CHANGE, 'rateChange', DiscreteEvent], [TOP_RESET, 'reset', DiscreteEvent], [TOP_SEEKED, 'seeked', DiscreteEvent], [TOP_SUBMIT, 'submit', DiscreteEvent], [TOP_TOUCH_CANCEL, 'touchCancel', DiscreteEvent], [TOP_TOUCH_END, 'touchEnd', DiscreteEvent], [TOP_TOUCH_START, 'touchStart', DiscreteEvent], [TOP_VOLUME_CHANGE, 'volumeChange', DiscreteEvent], // User-blocking events
[TOP_DRAG, 'drag', UserBlockingEvent], [TOP_DRAG_ENTER, 'dragEnter', UserBlockingEvent], [TOP_DRAG_EXIT, 'dragExit', UserBlockingEvent], [TOP_DRAG_LEAVE, 'dragLeave', UserBlockingEvent], [TOP_DRAG_OVER, 'dragOver', UserBlockingEvent], [TOP_MOUSE_MOVE, 'mouseMove', UserBlockingEvent], [TOP_MOUSE_OUT, 'mouseOut', UserBlockingEvent], [TOP_MOUSE_OVER, 'mouseOver', UserBlockingEvent], [TOP_POINTER_MOVE, 'pointerMove', UserBlockingEvent], [TOP_POINTER_OUT, 'pointerOut', UserBlockingEvent], [TOP_POINTER_OVER, 'pointerOver', UserBlockingEvent], [TOP_SCROLL, 'scroll', UserBlockingEvent], [TOP_TOGGLE, 'toggle', UserBlockingEvent], [TOP_TOUCH_MOVE, 'touchMove', UserBlockingEvent], [TOP_WHEEL, 'wheel', UserBlockingEvent], // Continuous events
[TOP_ABORT, 'abort', ContinuousEvent], [TOP_ANIMATION_END, 'animationEnd', ContinuousEvent], [TOP_ANIMATION_ITERATION, 'animationIteration', ContinuousEvent], [TOP_ANIMATION_START, 'animationStart', ContinuousEvent], [TOP_CAN_PLAY, 'canPlay', ContinuousEvent], [TOP_CAN_PLAY_THROUGH, 'canPlayThrough', ContinuousEvent], [TOP_DURATION_CHANGE, 'durationChange', ContinuousEvent], [TOP_EMPTIED, 'emptied', ContinuousEvent], [TOP_ENCRYPTED, 'encrypted', ContinuousEvent], [TOP_ENDED, 'ended', ContinuousEvent], [TOP_ERROR, 'error', ContinuousEvent], [TOP_GOT_POINTER_CAPTURE, 'gotPointerCapture', ContinuousEvent], [TOP_LOAD, 'load', ContinuousEvent], [TOP_LOADED_DATA, 'loadedData', ContinuousEvent], [TOP_LOADED_METADATA, 'loadedMetadata', ContinuousEvent], [TOP_LOAD_START, 'loadStart', ContinuousEvent], [TOP_LOST_POINTER_CAPTURE, 'lostPointerCapture', ContinuousEvent], [TOP_PLAYING, 'playing', ContinuousEvent], [TOP_PROGRESS, 'progress', ContinuousEvent], [TOP_SEEKING, 'seeking', ContinuousEvent], [TOP_STALLED, 'stalled', ContinuousEvent], [TOP_SUSPEND, 'suspend', ContinuousEvent], [TOP_TIME_UPDATE, 'timeUpdate', ContinuousEvent], [TOP_TRANSITION_END, 'transitionEnd', ContinuousEvent], [TOP_WAITING, 'waiting', ContinuousEvent]];
addEventBubbleListener与addEventCaptureListener就是对DOM 的addEvenListener的简单封装。但要注意的是,这两个方法都是使用注入方式实现的。换言之,react-native中,由于打包的文件不一样,它对应的实现也不一样
function addEventBubbleListener(element, eventType, listener) {
element.addEventListener(eventType, listener, false); //by 司徒正美
}
function addEventCaptureListener(element, eventType, listener) {
element.addEventListener(eventType, listener, true); //by 司徒正美
}
然后我们终于有机会看dispatchDiscreteEvent,dispatchUserBlockingUpdate与dispatchEvent。
function dispatchDiscreteEvent(topLevelType, eventSystemFlags, nativeEvent) {
flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, nativeEvent);
}
//by 司徒正美
function dispatchUserBlockingUpdate(topLevelType, eventSystemFlags, nativeEvent) {
runWithPriority(UserBlockingPriority,
dispatchEvent.bind(null, topLevelType, eventSystemFlags, nativeEvent));
}
派发离散事件分两部分,第一个是flushDiscreteUpdatesIfNeeded,你跟踪进去是flushDiscreteUpdates,它会搞定之前积攒的DiscreteEvent与useEffect回调。第二个是discreteUpdates,它会为React 的调度叠加一个DiscreteEventContext 上下文,并执行runWithPriority,这时看来它与dispatchUserBlockingUpdate无异,只是做了一个前置处理。
function discreteUpdates(fn, a, b, c) {
var prevExecutionContext = executionContext;
executionContext |= DiscreteEventContext;
try {
// Should this
return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c));
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
runWithPriority对调度器的影响主要有两个,一个是fiber节点的expirationTime属性,涉及到fiber对DOM的刷新,另一个是fiber节点上的回调的执行(setState, forceUpdate, hooks),它们会转交scheduleCallback方法处理。scheduleCallback相当于一个setTimeout,根据priorityLevel,延迟执行事件。scheduleCallback详看这里
https://github.com/facebook/react/blob/master/packages/scheduler/src/Scheduler.jsgithub.compriorityLevel能立即转换成callbackPriority,默认为NoPriority 90。然后运行过程中变成以下5种。
ImmediatePriority 99 IMMEDIATE_PRIORITY_TIMEOUT=-1;
UserBlockingPriority 98 USER_BLOCKING_PRIORITY=250
NormalPriority 97 NORMAL_PRIORITY_TIMEOUT=5000
LowPriority 96 LOW_PRIORITY_TIMEOUT=10000
IdlePriority 85 LOW_PRIORITY_TIMEOUT=10000
每一个fiber都分配一个expirationTime属性(其实有多种expirationTime属性),它是大于当时的毫秒数。但调度器执行时,就计算出当前的毫秒数now, 然后now - fiber.expirationTime <= 0,那么这fiber就可以更新了,其priorityLevel会改成ImmediatePriority。否则它只会创建一个effect记录用户的操作(如更新了某个属性啦,对它做删除啦)。
好了,我们知道runWithPriority的重要性了,那么我们需要获取其他priorityLevel值。
我们会发现flushControlled,deferredUpdates,flushSync会包含 runWithPriority(NormalPriority, fn)逻辑。 syncUpdates, flushSyncCallback则包含runWithPriority(ImmediatePriority, fn)的逻辑。
此外,SuspenseComponent也会影响到expirationTime的计算(默认分配为LOW_PRIORITY_EXPIRATION)
,最后通过 内部 inferPriorityFromExpirationTime的方法计算priorityLevel值。
本文React版本为 16.10.2
总结: 多年之前,人们说到fiber,只是模糊地联想到Time slicing与Suspense这两个单词。而经过多次迭代,我们终于能揭开fiber的真面目。 React的时间切片,只是它更新的一种表现,实质上是由每个fiber的expirationTime所决定,而fiber的expirationTime又来自priorityLevel,priorityLevel则来自用户的UI操作,不同的事件,带来三种不同的priorityLevel。而悬停,则只为某个fiber带来第四种priorityLevel——LowPriority。用户代码出现问题,被catch住时,出现第五种priorityLevel——IdlePriority。