《手写mini React》 合成事件

160 阅读8分钟

这段代码展示了一个实现自定义事件系统的示例,其中涉及合成事件的包装、事件回调路径的收集、捕获和冒泡阶段的实现等。

  1. SyntheticEvent:用于包装原生事件对象,同时提供 stopPropagation 方法,该方法标记阻止事件传播,并调用原生事件对象的 stopPropagation 方法。
  2. collectPaths 函数:用于收集事件路径中的事件回调函数。该函数从当前触发事件的 Fiber 节点开始,沿着树结构向上遍历,查找包含事件回调函数的 DOM 节点的父级 Fiber 节点,将事件回调函数保存在路径中。
  3. triggerEventFlow 函数:用于在捕获和冒泡阶段触发事件回调函数。它根据事件路径中的回调函数,依次执行每一个回调函数,并传入合成事件。如果在捕获或冒泡过程中调用了 stopPropagation 方法,事件的传播会被阻止。
  4. dispatchEvent 函数:根据事件的类型,执行事件的分发过程。它首先创建一个合成事件对象,然后获取根 Fiber 节点(这部分代码对 React 内部结构进行了假设,实际情况会因 React 版本和内部结构变化而有所不同),然后收集事件路径中的事件回调函数,依次在捕获和冒泡阶段触发回调函数。
  5. addEvent 函数:用于为容器元素添加事件监听。它会监听原生事件,当事件触发时,会调用 dispatchEvent 函数,传入事件对象和事件类型,从而触发自定义的事件处理流程。

注意,这段代码是一个简化的示例,演示了如何通过合成事件、事件路径的收集以及捕获和冒泡阶段的模拟来实现一个简单的事件系统。在实际的 React 实现中,事件系统会更加复杂和优化,考虑了更多的情况和性能优化。

// 包装合成事件
class SyntheticEvent {
  constructor(e){
    this.nativeEvent = e;
  }
  stopPropagation(){
    this._stopPropagation = true;
    if(this.nativeEvent.stopPropagation) {
      this.nativeEvent.stopPropagation();
    }
  }
}

// 收集路径中的事件回调函数
const collectPaths = (type, begin) => {
  const paths = [];

  // 不是根FiberNode的话,就一直向上遍历
  while (begin.tag !== 3) {
    const { memoizedProps, tag } = begin;

    // 5代表DOM节点对应FiberNode
    if (tag === 5) {
      const eventName = ("on" +type).toUpperCase();

      // 如果包含对应事件回调,保存在paths中
      if(memoizedProps && Object.keys(memoizedProps).includes(eventName)) {
        const pathNode = {};
        pathNode[type.toUpperCase()] = memoizedProps[eventName];
        paths.push(pathNode);
      }
    }
    begin = begin.return;
  }
  return paths;
}

// 捕获阶段的实现
const triggerEventFlow = (paths, type, se) => {
  // 从后向前遍历
  for(let i = paths.length; i--;) {
    const pathNode = paths[i];
    const callback = pathNode[type];
    if(callback) {
      // 存在回调函数,传入合成事件,执行
      callback.call(null, se);
    }
    if(se._stopPropagation) {
      // 如果执行了se.stopPropagation(),取消接下来的遍历
      break;
    }
  }
}

const dispatchEvent = (e, type, container) => {
  // 包装合成事件
  const se = new SyntheticEvent(e);

  // 第三步:获取FiberNode(你的代码中的比较hack的方法)
  const fiber = container._reactRootContainer._internalRoot.current;

  // 第四步:收集路径中"该事件的所有回调函数"
  const paths = collectPaths(type, fiber);

  // 第四步:捕获阶段的实现
  triggerEventFlow(paths, type + "CAPTURE", se);

  // 第五步:冒泡阶段的实现
  if(!se._stopPropagation) {
    triggerEventFlow(paths.reverse(), type, se);
  }
}

export const addEvent = (container, type) => {
  container.addEventListener(type, (e) => {
    dispatchEvent(e, type.toUpperCase(), container)
  })
}

这部分代码通过模拟事件绑定和触发的过程,实现了一个简化的合成事件系统,其中涉及到事件的注册、事件监听器的提取、合成事件对象的生成和事件回调的调用等步骤。下面是代码的解释:

  1. registerSimpleEvents 函数:根据预先定义好的原生事件和对应的 React 事件名称,将原生事件名称和 React 事件名称的映射存储到 topLevelEventsToReactNames 集合中,并将所有原生事件名称存储到 allNativeEvents 集合中。
  2. listenToAllSupportedEvents 函数:遍历所有原生事件,为容器元素 root 注册所有的事件监听。根据事件的冒泡和捕获阶段,分别调用 listenToNativeEvent 函数绑定事件监听器。
  3. listenToNativeEvent 函数:根据事件名称和是否为捕获阶段,为容器元素添加事件监听器。如果是捕获阶段,第三个参数传入 true,表示事件在捕获阶段传播;如果是冒泡阶段,传入 false,表示事件在冒泡阶段传播。
  4. dispatchEvent 函数:在事件触发时调用,首先获取事件的目标元素 target 和对应的 Fiber 实例 targetInst,然后调用 dispatchEventForPluginEventSystem 函数进行事件的分发。
  5. dispatchEventForPluginEventSystem 函数:提取事件处理函数,生成合成事件对象,并调用 processDispatchQueue 函数处理事件队列。
  6. processDispatchQueue 函数:遍历事件队列 dispatchQueue,根据事件的冒泡和捕获阶段,依次执行事件回调函数。
  7. extractEvents 函数:提取事件处理函数,生成合成事件对象,将事件处理函数和合成事件对象加入到事件队列 dispatchQueue
  8. accumulateSinglePhaseListeners 函数:累积单阶段的事件监听器,根据传入的 Fiber 实例 targetFiber、事件名称 reactName、原生事件类型 nativeType 以及是否为捕获阶段 inCapturePhase,逐级查找监听器,并存入数组 listeners 中。

这段代码模拟了事件的注册、监听和触发的过程,演示了合成事件系统的基本实现思路,但请注意,这是一个非常简化的版本,并没有涵盖 React 内部的所有细节和优化。在实际的 React 实现中,事件系统会更加复杂,考虑了更多的情况和性能优化。

const root = document.getElementById('root')
const elementTree = {
    type: 'div',
    props: {
        onClick: () => { 
            console.log('父元素冒泡')
        },
        onClickCapture: (e) => {
            // e.stopPropagation()
            console.log('父元素捕获')
        }
    },
    children: {
        type: 'button',
        props: {
            onClick: () => { 
                console.log('子元素冒泡')
            },
            onClickCapture: (e) => {
                // e.stopPropagation()
                console.log('子元素捕获')
            }
        },
        children: '点击'
    }
}
function render(element, parentNode) {
    const type = element.type
    let dom = null
    // 如果element是字符串或数字类型
    if(typeof element === 'string' || typeof element === 'number'){
        dom = document.createTextNode(element)
    } else {
        dom = document.createElement(type)
    }
    const returnFiber = parentNode.__reactFiber || null
    const fiber = {
        // 设置fiber的类型
        type,
        // 设置fiber的状态节点为dom
        stateNode: dom,
        // 设置fiber的返回节点为returnFiber
        return: returnFiber
    }
    dom.__reactFiber = fiber
    dom.__reactProps = element.props
    // 如果element有子元素
    if(element.children){
        // 递归调用render函数,将element.children渲染到dom上
        render(element.children, dom)
    }
    // 将dom添加到parentNode的子节点中
    parentNode.appendChild(dom)
}
render(elementTree, root)


// 第一阶段 注册事件 以及 模拟合成事件
// 创建合成事件的工厂函数
function createSyntheticEvent(Interface){
    function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget){
        // 事件所属的React组件名称
        this._reactName = reactName
        // 目标实例
        this._targetInst = targetInst
        // 事件类型
        this.type = reactEventType
        // 原生事件对象
        this.nativeEvent = nativeEvent
        // 原生事件的目标对象
        this.target = nativeEventTarget
        // 当前的事件源,默认为空
        this.currentTarget = null // 当前的事件源
        // 将Interface对象的属性赋值给当前对象
        for(const propName in Interface){
            this[propName] = nativeEvent[propName]
        }

        // 默认阻止事件的默认行为,始终返回false
        this.isDefaultPrevented = () => false
        // 是否停止事件冒泡,始终返回false
        this.isPropagationStopped = () => false

        return this
    }

    Object.assign(SyntheticBaseEvent.prototype, {
        preventDefault(){
            // 设置事件的defaultPrevented属性为true,表示已经阻止了事件的默认行为
            // event.defaultPrevented返回一个布尔值,表明当前事件是否调用了event.preventDefault()方法。
            // 因此在模拟的preventDefault方法里需要手动设置这个属性
            this.defaultPrevented = true
            const event = this.nativeEvent
            if(event.preventDefault){
                event.preventDefault()
            } else {
                // 在IE浏览器中,通过修改event.returnValue来阻止事件的默认行为
                event.returnValue = true; // IE
            }
            // 设置isDefaultPrevented方法返回值为true,表示已经阻止了事件的默认行为
            this.isDefaultPrevented = () => true
        },
        stopPropagation(){
            const event = this.nativeEvent
            if(event.stopPropagation){
                event.stopPropagation()
            } else {
                // 在IE浏览器中,通过修改event.cancelBubble来阻止事件冒泡
                event.cancelBubble = true; // IE
            }
            // 设置isPropagationStopped方法返回值为true,表示已经停止了事件冒泡
            this.isPropagationStopped = () => true
        }
    })

    return SyntheticBaseEvent
}


const MouseEventInterface = {
    clientX: 0,
    clientY: 0
}

const SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface)

// 将浏览器支持的所有原生事件枚举出来,下面的事件两两一对,奇数位代表原生事件名称,
// 偶数位用于模拟react事件名称,比如 'keyDown' 用于 'on' + 'KeyDown',或者 'on' + 'KeyDown' + 'Capture'
const discreteEventPairsForSimpleEventPlugin = [
    'click', 'Click', 
    // 'keydown', 'KeyDown',
    // 'keypress', 'KeyPress',
    // 'keyup', 'KeyUp',
    // 'mousedown', 'MouseDown',
    // 'mouseup', 'MouseUp'
]
const topLevelEventsToReactNames = new Map()
const allNativeEvents = new Set()
// const registrationNameDependencies = {}
/**
 * 注册简单事件
 *
 * @function
 * @returns {void}
 */
function registerSimpleEvents(){
    for(let i = 0; i < discreteEventPairsForSimpleEventPlugin.length; i+=2){
        // 获取原生事件名称
        const topEvent = discreteEventPairsForSimpleEventPlugin[i] // 原生事件名称
        // 获取react事件名称
        const reactName = 'on' + discreteEventPairsForSimpleEventPlugin[i+1] // react事件
        // 将原生事件名称和react事件名称存入集合中
        topLevelEventsToReactNames.set(topEvent, reactName)
        // registrationNameDependencies[reactName] = [topEvent]
        // registrationNameDependencies[reactName + 'Capture'] = [topEvent]
        // 将原生事件添加到所有原生事件集合中
        allNativeEvents.add(topEvent)
    }
}
registerSimpleEvents()


// 第二阶段 事件绑定
// 根据收集的allNativeEvents给容器root注册所有的事件
function listenToAllSupportedEvents(container){
    // 遍历所有原生事件
    allNativeEvents.forEach(domEventName => {
        // 绑定冒泡事件
        listenToNativeEvent(domEventName, false, container)
        // 绑定捕获事件
        listenToNativeEvent(domEventName, true, container)
    })
}
function listenToNativeEvent(domEventName, isCaptruePhaseListener, rootContainerElement){
    // 绑定 domEventName、isCaptruePhaseListener 和 rootContainerElement 参数,并赋值给 listener 变量
    let listener = dispatchEvent.bind(null, domEventName, isCaptruePhaseListener, rootContainerElement)

    // 如果 isCaptruePhaseListener 为真
    if(isCaptruePhaseListener){
        // 在 rootContainerElement 元素上添加指定的事件监听器,指定 true 作为第三个参数,表示事件在捕获阶段传播
        rootContainerElement.addEventListener(domEventName, listener, true)
    } else {
        // 在 rootContainerElement 元素上添加指定的事件监听器,指定 false 作为第三个参数,表示事件在冒泡阶段传播
        rootContainerElement.addEventListener(domEventName, listener, false)
    }
}
listenToAllSupportedEvents(root)

// 第三阶段 事件触发
// 事件触发首先执行的是dispatchEvent
function dispatchEvent(domEventName, isCaptruePhaseListener, targetContainer, nativeEvent){
    // 获取原生的事件源
    const target = nativeEvent.target || nativeEvent.srcElement || window
    
    // 获取fiber实例
    const targetInst = target.__reactFiber;
    
    dispatchEventForPluginEventSystem(
        domEventName,
        isCaptruePhaseListener,
        nativeEvent,
        targetInst,
        targetContainer
    )
}

function dispatchEventForPluginEventSystem(domEventName, isCaptruePhaseListener, nativeEvent, targetInst, targetContainer){
    const nativeEventTarget = nativeEvent.target;
    const dispatchQueue = []
    // 提取事件处理函数,填充 dispatchQueue 数组
    extractEvents(
        dispatchQueue,
        domEventName,
        targetInst,
        nativeEvent,
        nativeEventTarget,
        isCaptruePhaseListener,
        targetContainer
    )

    processDispatchQueue(dispatchQueue, isCaptruePhaseListener)
}

function processDispatchQueue(dispatchQueue, isCaptruePhaseListener){
    // 遍历 dispatchQueue 数组
    for(let i = 0; i < dispatchQueue.length; i++){
        // 解构 dispatchQueue[i] 中的 event 和 listeners
        const { event, listeners } = dispatchQueue[i]

        // 如果 isCaptruePhaseListener 为真
        if(isCaptruePhaseListener){
            // 从 listeners 数组的末尾开始遍历
            for(let i = listeners.length - 1; i >= 0; i--){
                // 解构 listeners[i] 中的 currentTarget 和 listener
                const [ currentTarget, listener ] = listeners[i]
                // 如果 event 的 isPropagationStopped 方法返回真
                if(event.isPropagationStopped()){
                    // 直接返回
                    return
                }
                // 调用 execDispatch 函数,传入 event、listener 和 currentTarget
                execDispatch(event, listener, currentTarget)
            }
        // 如果 isCaptruePhaseListener 为假
        } else {
            // 从 listeners 数组的开头开始遍历
            for(let i = 0; i < listeners.length; i++){
                // 解构 listeners[i] 中的 currentTarget 和 listener
                const [ currentTarget, listener ] = listeners[i]
                // 如果 event 的 isPropagationStopped 方法返回真
                if(event.isPropagationStopped()){
                    // 直接返回
                    return
                }
                // 调用 execDispatch 函数,传入 event、listener 和 currentTarget
                execDispatch(event, listener, currentTarget)
            }
        }
    }
}
function execDispatch(event, listener, currentTarget){
    // 将currentTarget赋值给event对象的currentTarget属性
    event.currentTarget = currentTarget
    // 调用listener函数,并传入event作为参数
    listener(event)
    // 将event对象的currentTarget属性设置为null
    event.currentTarget = null
}

// 提取事件处理函数 生成合成事件对象 填充dispatchQueue数组
function extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, isCaptruePhaseListener, targetContainer){
    const reactName = topLevelEventsToReactNames.get(domEventName)
    let SyntheticEventCtor;
    let reactEventType = domEventName
    // 根据不同的事件,合成事件对象是不一样的,对应不同合成事件构造函数
    switch(domEventName){
        case 'click':
            SyntheticEventCtor = SyntheticMouseEvent;
            break
        default:
            break;
    }

    // 累计单阶段监听器
    const listeners = accumulateSinglePhaseListeners(
        targetInst,
        reactName,
        nativeEvent.type,
        isCaptruePhaseListener
    )

    // 如果有监听器
    if(listeners.length){
        const event = new SyntheticEventCtor(
            reactName, 
            reactEventType, 
            targetInst, 
            nativeEvent,
            nativeEventTarget
        )
        dispatchQueue.push({
            event,
            listeners
        })
    }
}
function accumulateSinglePhaseListeners(targetFiber, reactName, nativeType, inCapturePhase){
    const captureName = reactName + 'Capture'
    const reactEventName = inCapturePhase ? captureName : reactName
    const listeners = []
    let instance = targetFiber

    // 循环遍历实例
    while(instance){
        const stateNode = instance.stateNode
        // 获取实例对应的监听器
        const listener = stateNode.__reactProps[reactEventName]
        if(listener){
            // 将实例、监听器和状态节点存入数组
            listeners.push([instance, listener, stateNode])
        }
        instance = instance.return
    }
    // 返回监听器数组
    return listeners
}