React 合成事件 SyntheticEvent

合成事件在 react 中的机制

1.原生事件冒泡到 document
2.document 执行事件监听回调,把原生事件在 dispatchEvent 里派发合成事件
3.通过 event.target 找到组件和元素
4.dispatchEvent 中执行 batchUpdate batchUpdate (fn, a) => fn(a), fn 是组件元素绑定的方法,a 是 event 合成事件实例
5.react 会在事件池 eventPool 中重复使用 event 实例。

React事件初探

为什么要用合成事件

  1. 浏览器兼容,顶层事件代理机制,能报保证事件冒泡一致性,可以跨浏览器执行
  2. 更好的跨平台,不同平台事件模拟成合成事件
  3. document 事件代理只在内存中开辟了一块空间,节省资源同时减少了dom操作,提高性能
  4. 对于新添加的元素也会有之前的事件
  5. 避免频繁解绑, 只在组件销毁时解绑
  6. 方便事件的统一管理和事务机制

react 合成事件流程

合成事件在 react 中的表现

合成事件对象模拟了 event.preventDefault event.stopPropagation 方法,同时为了提高性能在事件池重复使用 event 对象,每次重复使用后都会把 event 对象信息清空,在 setState 和异步 api 中可以使用 event.persist 方法或暂存值 onChange={({ value }) => handle(value)} 的方式获取正确的属性。
Synthetic Events in React
event.persist()
React SyntheticEvent reuse
官方 event-pooling 应用示例

合成事件源码分析

dispatch 分发事件

顶层监听

export function trapBubbledEvent(
  topLevelType: DOMTopLevelEventType,
  element: Document | Element,
) {
  if (!element) {
    return null;
  }
  const dispatch = isInteractiveTopLevelEventType(topLevelType)
    ? dispatchInteractiveEvent
    : dispatchEvent;
  // 原生 dom 事件监听
  addEventBubbleListener(
    element, // document 对象
    getRawEventName(topLevelType),
    // Check if interactive and wrap in interactiveUpdates
    dispatch.bind(null, topLevelType),
  );
}

dispatchEvent, 这里的 bookkeeping 是重用的,与 eventpool 重用相似。

export function dispatchEvent(
  topLevelType: DOMTopLevelEventType,
  nativeEvent: AnyNativeEvent,
) {
  if (!_enabled) {
    return;
  }

  const nativeEventTarget = getEventTarget(nativeEvent); // 找到 event.target 触发事件的元素
  let targetInst = getClosestInstanceFromNode(nativeEventTarget); // 找到 fiber 实例
  if (
    targetInst !== null &&
    typeof targetInst.tag === 'number' &&
    !isFiberMounted(targetInst)
  ) {
    // If we get an event (ex: img onload) before committing that
    // component's mount, ignore it for now (that is, treat it as if it was an
    // event on a non-React tree). We might also consider queueing events and
    // dispatching them after the mount.
    targetInst = null;
  }
  // 复用 bookKeeping,保存了事件触发的相关实例信息
  const bookKeeping = getTopLevelCallbackBookKeeping(
    topLevelType,
    nativeEvent,
    targetInst,
  );

  try {
    // Event queue being processed in the same cycle allows
    // `preventDefault`.
    batchedUpdates(handleTopLevel, bookKeeping); // 批量处理的方式进行分发
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping); // 推入 pool
  }
}

batchedUpdates(handleTopLevel, bookKeeping); 中的 handleTopLevel

// EventPluginHub.js
function handleTopLevel(bookKeeping) {
  let targetInst = bookKeeping.targetInst;
  // ... 确定 bookKeeping 上的组件信息
  for (let i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    runExtractedEventsInBatch(  // 在触发的组件实例上执行批量事件
      bookKeeping.topLevelType,
      targetInst,
      bookKeeping.nativeEvent,
      getEventTarget(bookKeeping.nativeEvent), // 统一不同浏览器的事件名
    );
  }
}

runExtractedEventsInBatch 最终会执行到 executeDispatchesAndRelease 方法

/**
 * Dispatches an event and releases it back into the pool, unless persistent.
 * dispatch 事件并将其释放回池中,除非是持久的。
 * @param {?object} event Synthetic event to be dispatched.
 * @param {boolean} simulated If the event is simulated (changes exn behavior)
 * @private
 */
const executeDispatchesAndRelease = function(
  event: ReactSyntheticEvent,
  simulated: boolean,
) {
  if (event) {
    executeDispatchesInOrder(event, simulated);
    // 如果合成事件没有 persist , 才推入到 eventPool 中进行复用
    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};

SyntheticEvent 合成事件对象

SyntheticEvent.js

function SyntheticEvent(
  dispatchConfig,
  targetInst,
  nativeEvent,
  nativeEventTarget,
) {
  // ...
  persist: function() {
    this.isPersistent = functionThatReturnsTrue;
  },

  isPersistent: functionThatReturnsFalse,
// ...
function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
  const EventConstructor = this;
  if (EventConstructor.eventPool.length) { // 重用合成事件
    const instance = EventConstructor.eventPool.pop();
    EventConstructor.call(
      instance,
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeInst,
    );
    return instance;
  }
  return new EventConstructor( // 如何合成事件持久化了则创建新的合成事件
    dispatchConfig,
    targetInst,
    nativeEvent,
    nativeInst,
  );
}
// 只在未持久化 isPersistent 为 false 才用到
function releasePooledEvent(event) {
  const EventConstructor = this;
  invariant(
    event instanceof EventConstructor,
    'Trying to release an event instance into a pool of a different type.',
  );
  event.destructor(); // 重置合成事件,属性全设置为 null
  if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
    EventConstructor.eventPool.push(event); //
  }
}

function addEventPoolingTo(EventConstructor) {
  EventConstructor.eventPool = []; // 事件池
  EventConstructor.getPooled = getPooledEvent; // 获取事件池事件进行使用和复用
  EventConstructor.release = releasePooledEvent; // 发布事件到事件池中
}
}
export default SyntheticEvent;

总结

合成事件是 react 模拟原生 dom 事件所有能力的一个事件对象,用于兼容浏览器方便 react 统一管理

react 合成事件是通过模拟不同浏览器事件差异,顶层监听在 document 上保证了事件冒泡的统一性

当事件原生 dom 事件触发冒泡至 document 时,react 通过 event.target 找到事件触发的组件实例,并 dispatchEvent 派发合成事件 event ,把 event 通过 batchUpdates 交由绑定事件的处理函数。

react 会重复使用合成 event,如果 event 已经 persisted 则不会推入 eventPool 中每次处理 handle 时都会重新生成一个 event。

react 合成事件兼容模拟浏览器事件差异,使用事件代理方式节省了内存只开辟一块空间,在组件销毁时解绑,避免频繁解绑方便事件的统一管理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,408评论 4 371
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,690评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,036评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,726评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,123评论 3 296
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,037评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,178评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,964评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,703评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,863评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,333评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,658评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,374评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,195评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,988评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,167评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,970评论 2 279