React 事件系统的设计原理 2

173 阅读3分钟

著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。

文章不好写,要是有帮助别忘了点赞,收藏,评论 ~ 你的鼓励是我继续挖干货的的动力🔥。

另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~

总流程

事件系统的设计原理:

  1. 给容器绑定统一的事件监听器
  2. ✅ 创建合成事件对象
  3. 收集Fiber事件(详细、推荐阅读)
  4. 事件回调的派发

在收集事件前,创建了合成事件,合成事件上应该有哪些属性呢?这就是本篇的主题。

原生事件的问题

nativeEvent 原生事件并不是稳定的一致集合,React 不能直接把它暴露给开发者。例如:

  • 没按标准实现,各浏览器实现不一致(例:movementXrelatedTargetIE 只给 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就是不同的类型的合成事件。