Skip to content

Latest commit

 

History

History
233 lines (181 loc) · 7.91 KB

dispatch-event.md

File metadata and controls

233 lines (181 loc) · 7.91 KB

源码解析二十二 事件触发

上节说到,会把 dispatch()绑定到原生DOM上,每次触发事件时,调用的都是dispatch函数

dispatch

function dispatchEvent(topLevelType: TopLevelType, nativeEvent: AnyNativeEvent) {
  if (!_enabled) {
    return
  }

  const nativeEventTarget = getEventTarget(nativeEvent)
  let targetInst: Fiber = getClosestInstanceFromNode(nativeEventTarget)

  if (targetInst !== null && isNumber(targetInst.tag) && !isFiberMounted(targetInst)) {
    targetInst = null
  }

  const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst)

  try {
    batchedUpdates(handleTopLevel, bookKeeping)
  } catch (e) {
    console.error(e)
    releaseTopLevelCallbackBookKeeping(bookKeeping)
  }
}

先通过event对象拿到target和相应的fiber实例,然后封装成一个bookKeeping对象,这里同样使用了对象池来优化创建的过程,在getTopLevelCallbackBookKeeping中根据对象池的大小取出或者新建对象,在事件完成后使用releaseTopLevelCallbackBookKeeping释放对象并放回到事件池中

关键在于这里的batchedUpdates,这个函数位于schedule中,是react里实现异步setState的核心所在。在执行事件前,将isBatchingUpdates这个标志位设为了true。这个标志位在requestWork中有用到

function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  addRootToSchedule(root, expirationTime)
  if (isRendering) {
    return
  }

  if (isBatchingUpdates) {
    if (isUnbatchingUpdates) {
      nextFlushedRoot = root
      nextFlushedExpirationTime = Sync
      performWorkOnRoot(root, Sync, false)
    }
    return
  }

  if (expirationTime === Sync) {
    performSyncWork() // 同步
  } else {
    scheduleCallbackWithExpirationTime(expirationTime) // 异步
  }
}

回顾requestWork,如果isBatchingUptestrue的话,就直接中断掉,不往下继续执行。所以渲染在事件函数没执行完之前都不会进行,一直到整个事件函数都结束掉,在finally中重启整个任务,所以如果在同一个事件监听函数中多次setState,则只是将它们放到updateQueue的末尾。React通过这个异步机制避免了频繁setState触发渲染造成的性能浪费

function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates
  isBatchingUpdates = true

  try {
    return fn(a)
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork()
    }
  }
}

handleTopLevel

由于事件函数中可能会操作DOM,导致与初始渲染时的节点缓存不一致,所以在进入核心逻辑之前,先构建一个关于ancestor数组,防止任何的嵌套组件导致的bug。构建完ancestor数组后,我们可以利用现有的信息去生成event

function handleTopLevel(bookKeeping: BookKeeping) {
  let targetInst: Fiber = bookKeeping.targetInst
  let ancestor: Fiber = targetInst

  do {
    if (!ancestor) {
      bookKeeping.ancestors.push(ancestor)
      break
    }

    const root = findRootContainerNode(ancestor)

    if (!root) {
      break
    }

    bookKeeping.ancestors.push(ancestor)
    ancestor = getClosestInstanceFromNode(root)
  } while (ancestor)

  bookKeeping.ancestors.forEach((target: Fiber) => {
    targetInst = target

    runExtractedEventsInBatch(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent))
  })
}

获取SyntheticEvent

前面介绍plugin时提到过,每个plugin都有着统一的extractEvents函数,用于生成SyntheticEvent

整个获取SyntheticEvent的过程也就是遍历已经injectplugin,调用它们各自的extractEvents拿到SyntheticEvent,那么,extractEvents是如何生成event的呢?具体的逻辑如下:

  • 通过传入的事件名,选择对应的具体的SyntheticEvent子类,如touch事件对应的SyntheticEventSyntheticTouchEvent
  • 通过事件池拿到event对象
  • 模拟冒泡、捕获的过程,按照顺序取出props的事件监听函数及相应的 fiber ,绑定到event的 _dispatchListeners 和 _dispatchInstances 上
  • 返回event

冒泡、捕获处理

在获得SyntheticEvent的过程中,pluginextractEvents都调用了accumulateTwoPhaseDispatches对冒泡、捕获做了处理,如图

function accumulateTwoPhaseDispatches(events: SyntheticEvent) {
  function callback(event: SyntheticEvent) {
    if (event && event.dispatchConfig.phasedRegistrationNames) {
      traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event) // 捕获和冒泡
    }
  }

  forEachAccumulated(events, callback)
}

export function traverseTwoPhase(inst: Fiber, fn: Function, arg: SyntheticEvent) {
  const path = []
  while (inst) {
    path.push(inst)
    inst = getParent(inst)
  }

  let i: number
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg)
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg)
  }
}

traverseTwoPhase中对当前fiber往上遍历直到fiberRoot,并存储到path中,之后按照 捕获->冒泡 的顺序对每个pathfiber调用accumulateDirectionalDispatches

function accumulateDirectionalDispatches(inst: Fiber, phase: Phases, event: SyntheticEvent) {
  const listener = listenAtPhase(inst, event, phase)

  if (listener) {
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener)
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst)
  }
}

在这个函数中,根据传入的参数,拿到fiber props的事件处理函数和相应的fiber,并挂载到event_dispatchListeners_dispatchInstances

function getListener(inst: Fiber, registrationName: string): Function {
  let listener: Function = null

  const { stateNode } = inst
  if (!stateNode) {
    return null
  }

  const props = getFiberCurrentPropsFromNode(stateNode)
  if (!props) {
    return null
  }

  listener = props[registrationName]

  if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
    return null
  }

  return listener
}

function listenAtPhase(inst: Fiber, event: SyntheticEvent, phase: Phases) {
  const registrationName = event.dispatchConfig.phasedRegistrationNames[phase]
  return getListener(inst, registrationName)
}

执行事件函数

由于在生成event时已经把props里跟事件有关的信息注册到event_dispatchListeners_dispatchInstances上,所以,我们需要遍历_dispatchListeners,执行相应的事件函数listener(event),由于这里是直接执行,所以在我们写的事件函数中,如果使用了this且没有强绑定,在调用时会拿不到对应的this

function executeDispatchesInOrder(event: SyntheticEvent) {
  const dispatchListeners = event._dispatchListeners
  const dispatchInstances = event._dispatchInstances

  if (Array.isArray(dispatchListeners)) {
    for (let i = 0; i < dispatchListeners.length; i++) {
      // 判断是否停止冒泡
      if (event.isPropagationStopped()) {
        break
      }

      executeDispatch(event, dispatchListeners[i], dispatchInstances[i])
    }
  } else if (dispatchListeners) {
    executeDispatch(event, dispatchListeners, dispatchInstances as Fiber)
  }
  event._dispatchListeners = null
  event._dispatchInstances = null
}

function executeDispatch(event: SyntheticEvent, listener: Function, inst: Fiber) {
  event.currentTarget = getNodeFromInstance(inst)
  listener(event)
  event.currentTarget = null
}

export {
  executeDispatchesInOrder,
}

到这里,整个事件的触发过程就结束了,回到batchedUpdatesfinally,把相应的标记位置回false,开始渲染