React源码学习:合成事件 SyntheticEvent

React17之前与React17+在事件处理上的区别,本文讲的是17之前的合成事件
更新于: 2022-06-08 01:26:14

背景

React17 已经放弃了这个特性~这篇文章权当留作纪念了。

但是这个设计思想还是有意义的,值得大家学习。

发音<来源: 百度> 

Synthetic
美 / sɪnˈθetɪk

一段 React 里的事件代码

<Button onClick={ 
    event =>{ 
     console.log('user click button') 
    }
  }> 
  Click Me
</Button>

什么是合成事件(上例中的event)

  • 这是React在原生的DOM事件上的一层封装,称为SyntheticEvent(合成事件)
  • SyntheticEvent 是一类对象,它们都拥有以下属性/方法
boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type

SyntheticEvent 有两个主要特点

总结:React SyntheticEvent 封装并兼容了浏览器DOM事件,采用了事件池的机制提升性能同时带来了不能异步使用的弊端 —— 还好有2个简单的解决方案。

  • 兼容各种主流浏览器的DOM事件
  • 事件池机制(坑: 很多异步场景取不到事件的来源)

如果你听说过线程池、Java字符串常量池等“池”的概念,你应该就能秒懂事件池的意思(编程理念很多数都是共通的)

事件池可以形象地理解为有个池子里装满了SyntheticEvent对象,程序有需要时会从池中取出一些使用,使用完后再放回池中。

事件池机制意味着 SyntheticEvent对象会被缓存且反复使用,目的是提高性能,减少创建不必要的对象。当SyntheticEvent对象被收回到事件池中时,属性会被抹除、重置为null。

因此,我们在写React事件回调函数的时候切记不能将 event 用于异步操作 —— 当异步操作真正执行的时候,SyntheticEvent对象有可能已经被重置(release)了

import React, { Component } from "react";

class TextInput extends Component {
  state = {
    editionCounter: 0,
    value: this.props.defaultValue,
  }
  // 由于 setState 是异步操作,event.target.value 在运行时可能已经被重置了
  handleChange = event => 
    this.setState(prevState => ({ value: event.target.value, editionCounter: prevState.editionCounter + 1 }));

  render() {
    return (
      <span>Edited {this.state.editionCounter} times</span>
      <input
        type="text"
        value={this.state.value}
        onChange={this.handleChange} // WRONG!
      />
    )
  }
}

解决异步问题

  1. 使用 event.persist() 方法
  2. 及时缓存所需的event属性值

persist 的直译过来是持久化,即 event.persist() 方法会将当前 event 踢出事件池,因此属性值可以一直存在而不会被重置。

这种方案缺点很明显 —— 放弃了SyntheticEvent事件池的性能优势,使用不当的时候可能引起性能问题

import React, { Component } from "react";

class TextInput extends Component {
  state = {
    editionCounter: 0,
    value: this.props.defaultValue,
  }
  
  handleChange = event => {
    event.persist();  // 持久化
    this.setState(prevState => ({ value: event.target.value, editionCounter: prevState.editionCounter + 1 }));
  }

  render() {
    return (
      <span>Edited {this.state.editionCounter} times</span>
      <input
        type="text"
        value={this.state.value}
        onChange={this.handleChange}
      />
    )
  }
}
import React, { Component } from "react";

class TextInput extends Component {
  state = {
    editionCounter: 0,
    value: this.props.defaultValue,
  }
  
  handleChange = event => {
    const value = event.target.value; // value这个本地变量已经保存了目标值
    this.setState(prevState => ({ value, editionCounter: prevState.editionCounter + 1 }));
  }

  render() {
    return (
      <span>Edited {this.state.editionCounter} times</span>
      <input
        type="text"
        value={this.state.value}
        onChange={this.handleChange}
      />
    )
  }
}

官方文档解读

  • SyntheticEvent 实例将被传递给你的事件处理函数,它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()
  • 取得原生事件对象:如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可。合成事件与浏览器的原生事件不同,也不会直接映射到原生事件
  • 事件池:从 v17 开始,e.persist() 将不再生效,因为 SyntheticEvent 不再放入事件池中。
  • 从 v0.14 开始,事件处理器返回 false 时,不再阻止事件传递。你可以酌情手动调用 e.stopPropagation() 或 e.preventDefault() 作为替代方案。
  • 从 React 17 开始,onScroll 事件在 React 中不再冒泡
    • 这与浏览器的行为一致,并且避免了当一个嵌套且可滚动的元素在其父元素触发事件时造成混乱。
    • aric: 以前自己开发槿的时候,强制冒泡,在处理一个 onScroll 的事件就无法利用框架级的事件来完成了

关于事件池

事件池的理解

stopPropagation以及stopImmediatePropagation 区别

  • event.stopPropagation 阻止向上冒泡,但是本元素其余的监听函数还是会执行
document.addEventListener('click', e => {
    e.stopPropagation() // 不再向上冒泡,但还是会打印2
    console.log(1)
})
document.addEventListener('click', e => {
    console.log(2)
})
复制代码
  • event.stopImmediatePropagation 阻止向上冒泡,同时本元素其余的监听函数不会执行
document.addEventListener('click', e => {
    e.stopImmediatePropagation() // 不再向上冒泡,同时2不会打印
    console.log(1)
})
document.addEventListener('click', e => {
    console.log(2)
})

官方-事件池

  • 此文章仅适用于 React 16 及更早版本、React Native。
  • Web 端的 React 17 不使用事件池。
  • React 17 中移除了 “event pooling(事件池)“,它并不会提高现代浏览器的性能,甚至还会使经验丰富的开发者一头雾水:

SyntheticEvent 对象会被放入池中统一管理

这意味着 SyntheticEvent 对象可以被复用,当所有事件处理函数被调用之后,其所有属性都会被置空。例如,以下代码是无效的:

function handleChange(e) {
  // This won't work because the event object gets reused.
  setTimeout(() => {
    console.log(e.target.value); // Too late!
  }, 100);
}

如果你需要在事件处理函数运行之后获取事件对象的属性,你需要调用 e.persist()

function handleChange(e) {
  // Prevents React from resetting its properties:
  e.persist();

  setTimeout(() => {
    console.log(e.target.value); // Works
  }, 100);
}

React v17 删除 event-pool 原理

ReactFeatureFlags

这个过程是分2步实现的:

  • ReactFeatureFlags.enableModernEventSystem 用这种方式去启用
  • 在后面再去掉这个 flag 来真正替换这个功能
import { enableModernEventSystem } from 'shared/ReactFeatureFlags';
去掉 pool event 功能

事件池实现相关代码

简化后的代码,可以知道的点:

  • DEFAULT_POOL_SIZE:默认创建10个可复用对象
  • Klass.instancePool.length < Klass.poolSize 当 size 数量没达到 pool 上限的时候,继续向 pool 里添加对象
  • 实际上,React 很多类是用这种方式实现的,事件池也是其中的一个场景
'use strict';

var invariant = require('invariant');

var oneArgumentPooler = function (copyFieldsFrom) {
  var Klass = this;
  if (Klass.instancePool.length) {
    var instance = Klass.instancePool.pop();
    Klass.call(instance, copyFieldsFrom);
    return instance;
  } else {
    return new Klass(copyFieldsFrom);
  }
};

var standardReleaser = function (instance) {
  var Klass = this;
  invariant(
    instance instanceof Klass,
    'Trying to release an instance into a pool of a different type.'
  );
  instance.destructor();
  if (Klass.instancePool.length < Klass.poolSize) {
    Klass.instancePool.push(instance);
  }
};

var DEFAULT_POOL_SIZE = 10;
var DEFAULT_POOLER = oneArgumentPooler;

var addPoolingTo = function (CopyConstructor, pooler) {
  // Casting as any so that flow ignores the actual implementation and trusts
  // it to match the type we declared
  var NewKlass = CopyConstructor;
  NewKlass.instancePool = [];
  NewKlass.getPooled = pooler || DEFAULT_POOLER;
  if (!NewKlass.poolSize) {
    NewKlass.poolSize = DEFAULT_POOL_SIZE;
  }
  NewKlass.release = standardReleaser;
  return NewKlass;
};

var PooledClass = {
  addPoolingTo: addPoolingTo,
  oneArgumentPooler: oneArgumentPooler
};

module.exports = PooledClass;

一个带事件池的源码

事件池还未移除的版本

参考