React 合成事件的设计原理 2

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

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

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

总流程

合成事件的设计原理:

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

这一篇处在第2点,介绍合成事件对象创建的源码、原理。

原生事件的问题

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

例如:

  • 没按标准实现,各浏览器实现不一致(例:movementXrelatedTargetIE 只给 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模拟接口的继承。

相关推荐
JamesGosling6662 小时前
详解 Vue 3.6 Vapor Mode:从原理到问题,看透 VDOM 逐步退场的底层逻辑
前端·vue.js
一个很帅的帅哥2 小时前
Vue中的hash模式和history模式
前端·vue.js·history模式·hash模式
进阶的鱼3 小时前
React+ts+vite脚手架搭建(三)【状态管理篇】
前端·javascript·react.js
By北阳3 小时前
Less resolver error:‘~antd/es/style/themes/index.less‘ wasn‘t found.
前端·elasticsearch·less
西洼工作室3 小时前
SSE与轮询技术实时对比演示
前端·javascript·css
IT_陈寒4 小时前
Vite 5.0 性能优化实战:3 个关键配置让你的构建速度提升50%
前端·人工智能·后端
excel5 小时前
Vue2 动态添加属性导致页面不更新的原因与解决方案
前端
GISer_Jing8 小时前
明天好好总结汇总分析博客
前端·javascript·面试
做运维的阿瑞10 小时前
Windows 环境下安装 Node.js 和 Vue.js 框架完全指南
前端·javascript·vue.js·windows·node.js