著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。
文章不好写,要是有帮助别忘了点赞,收藏~ 你的鼓励是我继续挖干货的的动力🔥。
另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~
总流程
合成事件的设计原理:
- 给容器绑定统一的事件监听器
- 创建合成事件对象
- 收集Fiber事件 (详细)
- 事件回调的派发
这一篇处在第2点,介绍合成事件对象创建的源码、原理。
原生事件的问题
nativeEvent 原生事件并不是稳定的一致集合,React 不能直接把它暴露给开发者。
例如:
- 没按标准实现,各浏览器实现不一致(例:
movementX
、relatedTarget
IE 只给fromElement/toElement
,W3C 叫relatedTarget
)。 - 旧版本浏览器没实现(例:早期 WebKit 没有
pageX/pageY
)。 - 属性名一样,但是属性值的含义不一致(例:IE 的
button
值和 W3C 完全不同。IE 左键=1,右键=2;标准是左键=0,右键=2)。 - ...
React根据w3c上的接口来实现合成事件,合成事件上的属性都是根据接口来的,
![]() |
![]() |
---|---|
![]() |
![]() |
在JavaScript中使用assign
模拟接口继承:
js
/**
* @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);
var UIEventInterface = assign({}, EventInterface, {
view: 0,
detail: 0
});
/**
* @interface MouseEvent
* @see http://www.w3.org/TR/DOM-Level-3-Events/
*/
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);
声明好合成事件的构造函数后,根据原生事件的类型实例化它:
js
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
});
}
js
//例子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:
js
{
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
返回 基础合成事件构造函数 (SyntheticBaseEvent
)。
SyntheticBaseEvent
就像 干细胞,可以变成任何细胞,例如:基础合成事件 变成 鼠标合成事件。
var SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface)
-- 相当于 --> var SyntheticMouseEvent = function SyntheticBaseEvent(){} 基础合成事件构造器,构建鼠标合成事件。
默认值只有两种情况
基础的合成事件上的值都是0,this[_propName] = nativeEvent[_propName];把原生事件上的值同步到合成事件上。
不是0,一定是函数(设计好的),this[_propName] = normalize(nativeEvent);调用函数传入原生事件来来得到值。
js
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 标准化的事件对象。
做的是转化、映射工作,把原生事件对象转成符合标准的合成事件,合成事件并不是一套独立的事件系统。
实现思路:1. 根据w3c的接口和继承关系,声明出合成事件的接口和接口属性。2. 使用assign模拟接口的继承。