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
,其中会调用handleTopLevel
,handleTopLevel
会调用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
是由topLevelType
从topLevelEventsToDispatchConfig
数组中获取的配置,在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
属性上,将原生事件对应的DOM
即nativeEventTarget
存储在合成事件的target属性上。原生事件对象上type
、eventPhase
、bubbles
、cancelable
、defaultPrevented
、isTrusted
存储在合成事件相同名称的属性上。app
从getPooledEvent
函数与合成事件对象的数据结构可知,React
合成的SyntheticEvent
采用了池的思想,从而达到节约内存,避免频繁的建立和销毁事件对象的目的。函数
这个是在特定的事件插件的extractEvents
函数中调用EventConstructor.getPooled
获取合成事件以后进行处理函数的收集。即以下调用accumulateTwoPhaseDispatches(event)
性能
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
accumulateTwoPhaseDispatches(event);
复制代码
accumulateTwoPhaseDispatches
中收集事件处理函数的调用栈为:
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
,并将当前处理函数push
到event._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
为上一步中传入的executeDispatchesAndReleaseTopLevel
,arr``为processingEventQueue
。arr
若是不是数组直接执行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
中的if
,event.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)
,执行事件处理函数。
fiber
树上模拟捕获与冒泡事件,即先模拟捕获阶段,从远古祖先节点document
开始向事件触发的目标节点遍历,遍历的过程搜集须要在捕获阶段触发的事件处理函数以及对应的节点并存入合成事件对象的相关属性;而后模拟冒泡阶段,从事件触发的目标节点开始向远古祖先节点document
开始遍历,遍历的过程搜集须要在冒泡阶段触发的事件处理函数以及对应的节点并存入以前存储了捕获阶段的函数与节点的相关属性的数组中;在两个遍历之初会遍历一遍事件目标节点以上的树并以此存入一个数组中,而后捕获和冒泡都是遍历这个数组。经过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
将数组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
。
var a=[1,2],b=[3,4]
a.concat(b) // 返回新的数组,由a与b数组组成
复制代码
这里apply
能够节省内存,不须要建立新的数组。
该函数保持传入的第二个参数不发生改变
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];
}
复制代码