著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。
文章不好写,要是有帮助别忘了点赞,收藏,评论 ~ 你的鼓励是我继续挖干货的的动力🔥。
另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~
总流程
事件系统的设计原理:
在收集事件前,创建了合成事件,合成事件上应该有哪些属性呢?这就是本篇的主题。
原生事件的问题
nativeEvent 原生事件并不是稳定的一致集合,React 不能直接把它暴露给开发者。例如:
- 没按标准实现,各浏览器实现不一致(例:
movementX、relatedTargetIE 只给fromElement/toElement,W3C 叫relatedTarget)。 - 浏览器没实现(例:早期 WebKit 没有
pageX/pageY)。 - 属性名一样,但是属性值的含义不一致(例:IE 的
button值和 W3C 完全不同。IE 左键=1,右键=2;标准是左键=0,右键=2)。 - ...
w3c上的接口
例如点击事件的原生事件是 PointerEvent,继承链:PointerEvent → MouseEvent → UIEvent → Event。
合成事件的实现
React根据w3c的接口声明合成事件,合成事件上的属性是根据接口来的。
使用JavaScript的assign模拟接口继承。
/**
* @interface Event
* @see http://www.w3.org/TR/DOM-Level-3-Events/
* 事件接口
*/
var EventInterface = {
eventPhase: 0,
bubbles: 0,
cancelable: 0,
timeStamp: function (event) {
return event.timeStamp || Date.now();
},
defaultPrevented: 0,
isTrusted: 0
};
var SyntheticEvent = createSyntheticEvent(EventInterface);
//UI事件接口,继承事件
var UIEventInterface = assign({}, EventInterface, {
view: 0,
detail: 0
});
/**
* @interface MouseEvent
* @see http://www.w3.org/TR/DOM-Level-3-Events/
* 点击事件接口,继承UI事件
*/
var MouseEventInterface = assign({}, UIEventInterface, {
screenX: 0,
screenY: 0,
clientX: 0,
clientY: 0,
pageX: 0,
pageY: 0,
ctrlKey: 0,
shiftKey: 0,
altKey: 0,
metaKey: 0,
getModifierState: getEventModifierState,
button: 0,
buttons: 0,
relatedTarget: function (event) {
if (event.relatedTarget === undefined) return event.fromElement === event.srcElement ? event.toElement : event.fromElement;
return event.relatedTarget;
},
movementX: function (event) {
if ('movementX' in event) {
return event.movementX;
}
updateMouseMovementPolyfillState(event);
return lastMovementX;
},
movementY: function (event) {
if ('movementY' in event) {
return event.movementY;
}
// Don't need to call updateMouseMovementPolyfillState() here
// because it's guaranteed to have already run when movementX
// was copied.
return lastMovementY;
}
});
var SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface);
根据事件的类型实例化接口
function extractEvents$4(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
var SyntheticEventCtor = SyntheticEvent;
//根据原生事件类型 选择 合成事件
switch (domEventName) {
...
case 'click':
case 'auxclick':
case 'dblclick':
case 'mousedown':
case 'mousemove':
case 'mouseup':
case 'mouseout':
case 'mouseover':
case 'contextmenu':
SyntheticEventCtor = SyntheticMouseEvent;//选择接口
break;
...
}
//实例化事件对象
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
dispatchQueue.push({
event: _event,
listeners: _listeners //事件回调
});
}
//例子1: 创建鼠标合成事件
var SyntheticEventCtor = SyntheticMouseEvent;
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
dispatchQueue.push({
event: _event,
listeners: _listeners
});
// 例子2:
var event = new SyntheticEvent('onSelect', 'select', null, nativeEvent, nativeEventTarget);
dispatchQueue.push({
event: event,
listeners: listeners
});
创建出的event
{
altKey: false
bubbles: true
button: 0
buttons: 0
cancelable: true
clientX: 236
clientY: 441
ctrlKey: false
currentTarget: null
defaultPrevented: false
detail: 1
eventPhase: 3
getModifierState: ƒ modifierStateGetter(keyArg)
isDefaultPrevented: ƒ functionThatReturnsFalse()
isPropagationStopped: ƒ functionThatReturnsFalse()
isTrusted: true
metaKey: false
movementX: 0
movementY: 0
nativeEvent: PointerEvent {isTrusted: true, pointerId: 1, width: 1, height: 1, pressure: 0, …}
pageX: 236
pageY: 441
relatedTarget: null
screenX: 236
screenY: 563
shiftKey: false
target: button
timeStamp: 8484.39999961853
type: "click"
view: Window {window: Window, self: Window, document: document, name: '', location: Location, …}
_reactName: "onClick"
_targetInst: null
[[Prototype]]: Object
}
createSyntheticEvent
createSyntheticEvent返回 基础合成事件构造函数 (SyntheticBaseEvent)。
function createSyntheticEvent(Interface) {
function SyntheticBaseEvent() {
//从Interface获得接口
var normalize = Interface[_propName];
//给event赋值,this是SyntheticBaseEvent的实例化
this[_propName] = normalize(nativeEvent);//执行函数计算
this[_propName] = nativeEvent[_propName];//nativeEvent原生事件
}
return SyntheticBaseEvent;
}
SyntheticBaseEvent就像干细胞,可以变成任何其他类型的细胞,传入不同的接口就是不同的合成事件。
var SyntheticEvent = createSyntheticEvent(Interface)
= function SyntheticBaseEvent(){Interface}
var SyntheticEvent = createSyntheticEvent(EventInterface);
var SyntheticUIEvent = createSyntheticEvent(UIEventInterface);
var SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface);、
...
event的赋值
从前面的接口MouseEventInterface也能看出来,event一开始是没有值的,默认值是0和函数。
值是0,this[_propName] = nativeEvent[_propName]把原生事件上的值同步到合成事件上。
不是0,一定是函数,this[_propName] = normalize(nativeEvent)执行函数计算值。
function createSyntheticEvent(Interface) {
function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent;
this.target = nativeEventTarget;
this.currentTarget = null;
for (var _propName in Interface) {
if (!Interface.hasOwnProperty(_propName)) {
continue;
}
var normalize = Interface[_propName];
if (normalize) {// 0或函数
this[_propName] = normalize(nativeEvent);
} else {
this[_propName] = nativeEvent[_propName];
}
}
var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
if (defaultPrevented) {
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
this.isDefaultPrevented = functionThatReturnsFalse;
}
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
// preventDefault、stopPropagation
assign(SyntheticBaseEvent.prototype, {
preventDefault: function () {
this.defaultPrevented = true;
var event = this.nativeEvent;
if (!event) {
return;
}
if (event.preventDefault) {
event.preventDefault(); // $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
stopPropagation: function () {
var event = this.nativeEvent;
if (!event) {
return;
}
if (event.stopPropagation) {
event.stopPropagation(); // $FlowFixMe - flow is not aware of `unknown` in IE
} else if (typeof event.cancelBubble !== 'unknown') {
// The ChangeEventPlugin registers a "propertychange" event for
// IE. This event does not support bubbling or cancelling, and
// any references to cancelBubble throw "Member not found". A
// typeof check of "unknown" circumvents this issue (and is also
// IE specific).
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
},
/**
* We release all dispatched `SyntheticEvent`s after each event loop, adding
* them back into the pool. This allows a way to hold onto a reference that
* won't be added back into the pool.
*/
persist: function () {// Modern event system doesn't use pooling.
},
/**
* Checks if this event should be released back into the pool.
*
* @return {boolean} True if this should not be released, false otherwise.
*/
isPersistent: functionThatReturnsTrue
});
//返回SyntheticBaseEvent构造器
return SyntheticBaseEvent;
}
总结
React 把不同浏览器的原生事件属性 统一映射成 W3C 标准接口。开发者拿到的始终是 W3C 标准化的事件对象。
做的是转化、映射工作,把原生事件对象转成符合标准的合成事件,合成事件并不是React自己定义的事件。
实现思路:1. 根据w3c的接口和继承关系,声明出合成事件的接口和接口属性。2. 使用assign模拟接口的继承。传入不同的Interface就是不同的类型的合成事件。