react事件系統之事件觸發

NodeNote,持續更新中react相關庫源碼淺析react ts3 項目react

概述,背景

<div id="app"></div>
<div onclick="alert(1)">原生</div>

class APP extends React.Component{
   render(){
       return (
           <div>
               <Header/>
           </div>
       )
   }
}

class Header extends React.Component{
   clickHandler(){
       console.log("click")
   }
   render(){
       return (
           <div>
               <div onClick={this.clickHandler.bind(this)} a={1}>
                   this is Header
               </div>
               <p onClick={this.clickHandler.bind(this)} a={1}>
                   this is Header
               </p>
           </div>
       )
   }
}
ReactDOM.render(
   <APP/>,
   document.getElementById('app')
);
複製代碼

上述的組件中點擊事件的觸發過程以下:git

document監聽到某個 DOM上冒泡上來的點擊事件以後,調用 document上的處理函數 dispatchInteractiveEvent

function dispatchInteractiveEvent(topLevelType, nativeEvent) {
  interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}
複製代碼

interactiveUpdates函數的做用是執行:dispatchEvent(topLevelType, nativeEvent)github

dispatchEvent(topLevelType, nativeEvent)
緣由以下:
export function interactiveUpdates(fn, a, b) {
 return _interactiveUpdatesImpl(fn, a, b);
}
let _interactiveUpdatesImpl = function(fn, a, b) {
 return fn(a, b);
};
複製代碼

dispatchEvent會調用batchedUpdates,其中會調用handleTopLevelhandleTopLevel會調用runExtractedEventsInBatch數組

function runExtractedEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    // 合成事件的生成以及在fiber樹上經過模擬捕獲與冒泡收集事件處理函數與對應節點並存儲到合成事件的相關屬性上
    var events = extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    // 開始執行合成事件上的相關屬性存儲的執行事件處理函數
    runEventsInBatch(events);
}
複製代碼

runExtractedEventsInBatch函數中會先將事件名稱topLevelType、對應的react元素實例targetInst、原生事件對象nativeEvent以及事件做用的DOM傳入extractEvents中,extractEvents函數會遍歷事件插件數組plugins,並經過傳入的事件名稱topLevelType選擇對應的plugin,並調用該plugin上的extractEvents,生成合成事件SyntheticEvent,並收集事件觸發的目標節點以及以上的祖先節點須要觸發的事件處理函數和對於的fiber分別存入合成事件的_dispatchListeners_dispatchInstances屬性上,捕獲階段的函數與節點在數組靠前位置,祖先節點‘越老’其事件處理函數以及該節點在數組中的位置越靠前;冒泡階段的函數與節點在數組靠後位置,祖先節點‘越老’其事件處理函數以及該節點在數組中的位置越靠後;bash

合成事件對象

runExtractedEventsInBatch函數中合成事件對象的邏輯:
var events = ex:tractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
複製代碼

首先看合成事件對象的生成

對於點擊事件調用的是simpleeventplugin上的extractEvents函數,該函數會傳入的參數是dispatchConfig, targetInst, nativeEvent, nativeEventTarget,其中dispatchConfig是由topLevelTypetopLevelEventsToDispatchConfig數組中獲取的配置,在simpleeventplugin.extractEvents函數中會調用以下代碼根據事件配置dispatchConfig將事件對應的react元素實例、原生事件、原生事件對應的DOM封裝成爲一個合成事件。數據結構

var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget)
複製代碼

EventConstructor.getPooled實際就是react\packages\events\SyntheticEvent.js下的getPooledEvent函數,從其代碼中能夠看到會從事件池中去取一個合成事件對象,而後利用這個對象用新的 dispatchConfig,targetInst,nativeEvent,nativeInst從新初始化便可;若是事件池爲空,則新建立一個合成事件對象。閉包

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,
  );
}
複製代碼
從數據結構看合成事件與原生事件的關係
function SyntheticEvent(
  dispatchConfig,
  targetInst,
  nativeEvent,
  nativeEventTarget,
) {
  this.dispatchConfig = dispatchConfig;
  this._targetInst = targetInst;
  this.nativeEvent = nativeEvent;

  const Interface = this.constructor.Interface;
  for (const propName in Interface) {
    if (!Interface.hasOwnProperty(propName)) {
      continue;
    }
    const normalize = Interface[propName];
    if (normalize) {
      this[propName] = normalize(nativeEvent);
    } else {
      if (propName === 'target') {
        this.target = nativeEventTarget;
      } else {
        this[propName] = nativeEvent[propName];
      }
    }
  }

  const defaultPrevented =
    nativeEvent.defaultPrevented != null
      ? nativeEvent.defaultPrevented
      : nativeEvent.returnValue === false;
  if (defaultPrevented) {
    this.isDefaultPrevented = functionThatReturnsTrue;
  } else {
    this.isDefaultPrevented = functionThatReturnsFalse;
  }
  this.isPropagationStopped = functionThatReturnsFalse;
  return this;
}
複製代碼

能夠看到原生事件存儲在合成事件對象的nativeEvent屬性上,目標react元素實例存儲在_targetInst屬性上,dispatchConfig存儲在dispatchConfig屬性上,將原生事件對應的DOMnativeEventTarget存儲在合成事件的target屬性上。原生事件對象上typeeventPhasebubblescancelabledefaultPreventedisTrusted存儲在合成事件相同名稱的屬性上。app

爲何事件池會提升性能

getPooledEvent函數與合成事件對象的數據結構可知,React合成的SyntheticEvent採用了池的思想,從而達到節約內存,避免頻繁的建立和銷燬事件對象的目的。函數

而後收集事件處理函數與對應節點並存儲到合成事件對象上

這個是在特定的事件插件的extractEvents函數中調用EventConstructor.getPooled獲取合成事件以後進行處理函數的收集。即以下調用accumulateTwoPhaseDispatches(event)性能

var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
accumulateTwoPhaseDispatches(event);
複製代碼

accumulateTwoPhaseDispatches中收集事件處理函數的調用棧爲:

traverseTwoPhase的代碼在 以前的文章中有分析,

export function traverseTwoPhase(inst, fn, arg) {
  const path = [];
  //將inst的父節點入棧,數組最後的爲最遠的祖先
  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }
  let i;
  //從最遠的祖先開始向inst節點捕獲執行fn
  for (i = path.length; i-- > 0; ) {
    fn(path[i], 'captured', arg);
  }
    //從inst節點開始向最遠的祖先節點冒泡執行fn
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}
複製代碼

在虛擬DOM樹中其實就是fiber樹,將當前事件觸發的目標節點開始向上遍歷的祖先節點挨個存入path中,而後從祖先節點開始向目標節點進行遍歷對應的就是從數組的從尾向頭開始遍歷(這裏模擬的是捕獲,因此從祖先節點開始向下遍歷),這個遍歷過程當中,將遍歷到的當前節點、合成事件對象、表明捕獲仍是冒泡階段的標誌做爲參數傳入accumulateDirectionalDispatches,在其中執行listenerAtPhase獲取該綁定在該節點上須要在捕獲階段觸發的事件處理函數,而後將獲取到的事件處理函數listener與事件上的存儲處理函數數組event._dispatchListeners傳入accumulateInto,並將當前處理函數pushevent._dispatchListeners中;一樣調用accumulateInto將捕獲階段中當前綁定了須要捕獲階段觸發的事件的節點存儲到event._dispatchInstances,至此accumulateDirectionalDispatches執行完畢,也就是收集到了全部須要捕獲階段執行的事件處理函數與相對應的節點分別存儲在合成事件對象上的_dispatchListeners_dispatchInstances上。捕獲階段,父節點以及其處理函數位於數組的開頭部分,模擬捕獲的事件觸發順序。

按照上述邏輯,冒泡階段,對應與在traverseTwoPhase中的第二個for循環,會依次調用accumulateDirectionalDispatches對事件觸發的目標節點以及以上的父節點進行事件處理函數與綁定了事件處理函數的節點的收集,並將這些函數與節點分別添加在合成事件的_dispatchListeners_dispatchInstances上。

function accumulateDirectionalDispatches(inst, phase, event) {
    {
        !inst ? warningWithoutStack$1(false, 'Dispatching inst must not be null') : void 0;
    }
    var listener = listenerAtPhase(inst, event, phase);
    if (listener) {
        event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
        event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
    }
}
// 獲取捕獲階段的事件名registrationName
function listenerAtPhase(inst, event, propagationPhase) {
    var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
    return getListener(inst, registrationName);
}    
// 開始根據registrationName在當前節點上的props中獲取對應的事件處理函數
function getListener(inst, registrationName) {
    var listener = void 0;

    // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
    // live here; needs to be moved to a better place soon
    var stateNode = inst.stateNode;
    if (!stateNode) {
        // Work in progress (ex: onload events in incremental mode).
        return null;
    }
    var props = getFiberCurrentPropsFromNode(stateNode);
    if (!props) {
        // Work in progress.
        return null;
    }
    listener = props[registrationName];
    if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
        return null;
    }
    !(!listener || typeof listener === 'function') ? invariant(false, 'Expected `%s` listener to be a function, instead got a value of `%s` type.', registrationName, typeof listener) : void 0;
    return listener;
}
複製代碼

至此對於下面的結構:

<div id='div1' onClick={this.clickHandler1.bind(this)} a={1}>
    <div id='div2' onClick={this.clickHandler2.bind(this)} a={1}>
        <div id='div3' onClick={this.clickHandler3.bind(this)} a={1}>
            this is Header
        </div>
    </div>
</div>
複製代碼

合成事件上的_dispatchListeners_dispatchInstances上分別爲:

_dispatchListener = [
    clickHandler1,
    clickHandler2,
    clickHandler3
]
_dispatchInstances = [
    id爲'div1'的fiberNode,
    id爲'div2'的fiberNode,
   id爲'div3'的fiberNode
]
複製代碼

執行合成事件對象上的事件處理函數

runExtractedEventsInBatch 執行合成事件對象上的事件處理函數的邏輯:
runEventsInBatch(events);
複製代碼

觸發過程的函數調用棧以下:

通過上述的邏輯以後,會從頭至尾的調用合成事件對象上 _dispatchListeners上的事件處理函數。到此本文重點在弄清楚了合成事件對象和原生事件對象的關係,以及如何收集 fiber樹上的事件處理函數。至於如何執行,是明天研究的內容了。固然仍是接着這個寫吧。 從 runEventsInBatch開始,最後調用合成事件上的事件處理函數:

//將新的合成事件對象添加到原來的對象隊列中,而後進入下一個處理環節forEachAccumulated
export function runEventsInBatch(events) {
  if (events !== null) {
    // 將當前生成的合成事件對象或者合成事件對象數組添加到以前的合成事件對象隊列中,構成新的隊列
    eventQueue = accumulateInto(eventQueue, events);
  }
  //  將新的合成事件對象隊列eventQueue做爲正在處理的隊列processingEventQueue,並將前者清空
  const processingEventQueue = eventQueue;
  eventQueue = null;
  if (!processingEventQueue) {return;}
  //進入下一步
  forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
}
複製代碼

runEventsInBatch新的合成事件對象合併到原來的對象隊列中,而後進入下一個處理環節forEachAccumulated,可見react的每一個函數的縝密,每一步都添加了對應的錯誤處理機制。forEachAccumulated函數源碼:

function forEachAccumulated(arr,cb,scope,) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}
複製代碼

forEachAccumulated會對傳入的arr數組的元素挨個執行cb(arr[i]),這裏的cb爲上一步中傳入的executeDispatchesAndReleaseTopLevelarr``爲processingEventQueuearr若是不是數組直接執行cb(arr) ,所以主要看executeDispatchesAndReleaseTopLevel的邏輯:

const executeDispatchesAndReleaseTopLevel = function(e) {
  return executeDispatchesAndRelease(e);
};
const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
  if (event) {
    executeDispatchesInOrder(event);

    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};

複製代碼

先看第二步:這裏先看if中的ifevent.isPersistent返回的始終是false,所以這裏始終會執行release重置event上的屬性值,並添加到事件對象池的空餘位置:

react\packages\events\SyntheticEvent.js
//重置event上的屬性值,並添加到事件對象池的空餘位置
function releasePooledEvent(event) {
  const EventConstructor = this;
  //
  event.destructor();
  if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
    EventConstructor.eventPool.push(event);
  }
}
//合成事件對象實例重置
destructor: function() {
    const Interface = this.constructor.Interface;
    for (const propName in Interface) {
        this[propName] = null;
    }
    this.dispatchConfig = null;
    this._targetInst = null;
    this.nativeEvent = null;
    this.isDefaultPrevented = functionThatReturnsFalse;
    this.isPropagationStopped = functionThatReturnsFalse;
    this._dispatchListeners = null;
    this._dispatchInstances = null;
});
複製代碼

接着看關鍵的第一步,executeDispatchesInOrder是如何執行合成事件對象上的事件處理函數的:

export function executeDispatchesInOrder(event) {
  const dispatchListeners = event._dispatchListeners;
  const dispatchInstances = event._dispatchInstances;
  if (Array.isArray(dispatchListeners)) {
    for (let i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}
複製代碼

從上面代碼的循環能夠看到,遍歷event._dispatchListeners上的監聽器,首先經過合成事件對象上的isPropagationStopped()來判斷是否阻止捕獲和冒泡階段中當前事件的進一步傳播,若是有則日後的事件處理函數都沒法被執行。若是沒有阻止傳播,那麼會調用 executeDispatch執行事件處理函數,最終將合成事件對象上的_dispatchListeners_dispatchInstances清空。如今來看executeDispatch

function executeDispatch(event, listener, inst) {
  const type = event.type || 'unknown-event';
  //獲取當前fiber對應的真實DOM
  event.currentTarget = getNodeFromInstance(inst);
  //進入下一步
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}
複製代碼

executeDispatch會將當前傳入的這個fiber的真實DOM存儲在合成事件對象上的currentTarget屬性上,而後將合成事件對象與當前fiber對應的事件處理函數一塊兒傳入invokeGuardedCallbackAndCatchFirstError

react\packages\shared\ReactErrorUtils.js

function invokeGuardedCallbackAndCatchFirstError(name,func,context,a,b,c,d,e,f) {
  invokeGuardedCallback.apply(this, arguments);
}
export function invokeGuardedCallback(name,func,context,a,b,c,d,e,f) {
  invokeGuardedCallbackImpl.apply(reporter, arguments);
}
//在開發環境下invokeGuardedCallbackImpl = invokeGuardedCallbackDev;
//invokeGuardedCallbackDev中涉及到如何處理錯誤,保證了系統的健壯性——TODO
//react\packages\shared\invokeGuardedCallbackImpl.js
let invokeGuardedCallbackImpl = function(name,func,context,a,b,c,d,e,f){
  const funcArgs = Array.prototype.slice.call(arguments, 3);
  try {
    func.apply(context, funcArgs);
  } catch (error) {
    this.onError(error);
  }
};
複製代碼

最終經過invokeGuardedCallbackImpl中調用func.apply(context, funcArgs),執行事件處理函數。

總結

  1. 合成事件對象的初始化
  2. fiber樹上模擬捕獲與冒泡事件,即先模擬捕獲階段,從遠古祖先節點document開始向事件觸發的目標節點遍歷,遍歷的過程蒐集須要在捕獲階段觸發的事件處理函數以及對應的節點並存入合成事件對象的相關屬性;而後模擬冒泡階段,從事件觸發的目標節點開始向遠古祖先節點document開始遍歷,遍歷的過程蒐集須要在冒泡階段觸發的事件處理函數以及對應的節點並存入以前存儲了捕獲階段的函數與節點的相關屬性的數組中;在兩個遍歷之初會遍歷一遍事件目標節點以上的樹並以此存入一個數組中,而後捕獲和冒泡都是遍歷這個數組。
  3. 將新的合成事件對象添加到已有的事件對象隊列中,而後開始依次執行隊列中每一個合成事件對象上存儲的事件處理函數數組。

彩蛋bind

改裝事件監聽器函數:

經過addEventListener添加的事件處理函數只有一個輸入參數爲event事件。 以下:

element.addEventListener(eventType, listener, true);
複製代碼

如今的需求是: 當事件觸發的時候,想調用下面這個函數

function dispatchInteractiveEvent(type, event) {
    ...
}
複製代碼

最容易想到的方法是:

const listener = function(event){
    // 一些和type變量有關的邏輯
        ...
    dispatchInteractiveEvent(type, event)
}
element.addEventListener(eventType, listener, true);
複製代碼

利用bind實現:

// 一些和type變量有關的邏輯
    ...
const listener = dispatchInteractiveEvent.bind(null, type);
element.addEventListener(eventType, listener, true);
複製代碼

這二者的區別:

bind實現能夠將type的邏輯放到外層做用域,而且在事件觸發以前type就計算好了。而第一種方法事件觸發調用回調的時候才計算type,這個時候必然形成內存泄漏,由於type的邏輯可能涉及到外層做用域的局部變量。bind實現,type已經計算好了,因此垃圾回收機制會自動回收不用的變量。

總的一句話就是:第一種形成了閉包的效果。

彩蛋apply巧妙拼接兩個數組

利用apply將數組b添加到數組a後面

var a=[1,2],b=[3,4]
a.push.apply(a,b) // 返回數組a的長度
a // [1, 2, 3, 4]
b // [3, 4]
複製代碼

apply在這裏主要的做用是能夠將數組做爲參數傳入,而後一個一個對數組中的元素執行a.push(b[i]),這裏this必須指向a

與concat的不一樣

var a=[1,2],b=[3,4]
a.concat(b) // 返回新的數組,由a與b數組組成
複製代碼

總結

這裏apply能夠節省內存,不須要建立新的數組。

來源react源碼的accumulateInto函數

該函數保持傳入的第二個參數不發生改變

function accumulateInto(
  current,
  next,
) {
  if (current == null) {
    return next;
  }
  if (Array.isArray(current)) {
    if (Array.isArray(next)) {
      current.push.apply(current, next);
      return current;
    }
    current.push(next);
    return current;
  }

  if (Array.isArray(next)) {
    return [current].concat(next);
  }

  return [current, next];
}
複製代碼
相關文章
相關標籤/搜索