[React] 解析 React 框架底层源码(下)
温馨提示:在写这篇文章的时候其实是图文并茂的,但由于图片都保存在第三方平台的图床中(notion, juejin),搬运到博客也较为麻烦一些,所以博文中就没有图片,如果对图片感兴趣的小伙伴,可以看我的掘金文章,那里有图文并茂的源码解释
目录
1. 饥饿问题
2. 批量更新
3. 高优先级打断低优先级
4. setState 是同步还是异步?
5. 合成事件
6. 生命周期
饥饿问题
什么是饥饿问题: 高优先级一直打断低优先级,低优先级没有机会执行。
如何解决饥饿问题:
第一步: 在每次调度更新都会执行的 ensureRootIsScheduled
调用 markStarvedLanesAsExpired
函数。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
markStarvedLanesAsExpired(root, currentTime);
第二步: 在 markStarvedLanesAsExpired
函数,如果是第一次更新,那会遍历 root.pendingLane 上的 每一个 lane,针对于不同 lane 上的任务,通过调用 computeExpirationTime
为每一个任务计算过期时间。
function computeExpirationTime(lane: Lane, currentTime: number) {
switch (lane) {
case SyncHydrationLane:
case SyncLane:
case InputContinuousHydrationLane:
case InputContinuousLane:
return currentTime + 250;
case DefaultHydrationLane:
case DefaultLane:
case TransitionHydrationLane:
case TransitionLane1:
case TransitionLane16:
return currentTime + 5000;
case RetryLane4:
return NoTimestamp;
case SelectiveHydrationLane:
case IdleHydrationLane:
case IdleLane:
case OffscreenLane:
return NoTimestamp;
}
}
第三步: 下次调度更新再次来到markStarvedLanesAsExpired
函数,仍然去遍历每一条 lane, 对于新的更新任务还是计算过期时间,而对于哪些已经有过期时间的任务,去判断它们是否已经到期。如果已经到期 在 root.expiredLanes
上标记已经过期的 lane
。
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number
): void {
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes;
let lanes = pendingLanes & ~RetryLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
root.expiredLanes |= lane;
}
lanes &= ~lane;
}
}
第四步:开始调度更新 performConcurrentWorkOnRoot
函数。
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
第五步:在 performConcurrentWorkOnRoot
函数中,去通过 includesExpiredLane
函数 检查 root.expiredLanes
上是否有过期的 lane。如果有过期的 lane,也即一直被打断的低优先级赛道。那么会调用 renderRootSync
将低优先级直接提升至同步优先级进行渲染更新。低优先级任务得到了执行。
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
自此解决饥饿问题
批量更新
const App = () => {
const [number, setNumber] = React.useState(1);
console.log("App 组件, 渲染");
const handleClick = () => {
setNumber((pre) => (pre += 1));
setNumber((pre) => (pre += 1));
setNumber((pre) => (pre += 1));
};
return (
<div>
{number}
<div
onClick={handleClick}
>
批量更新
</div>
</div>
);
};
我们发现,触发三次 setNumber
, App 组件 只渲染一次,并且 number
值可以更新为 4。
批量更新原理
第一:当点击的时候, 触发第一次 setNumber, 调用的是 dispatchSetState
, 在 该函数中,将 update
对象 加入到 hook.queue.pending
循环队列当中,然后 调用了熟悉的 scheduleUpdateOnFiber
函数,从根节点开始调度更新。
const dispatchSetState = () => {
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
第二:scheduleUpdateOnFiber
函数 中 调用了 ensureRootIsScheduled
函数。
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number
) {
ensureRootIsScheduled 函数。(root, eventTime);
}
第三: ensureRootIsScheduled
函数中,将 performConcurrentWorkOnRoot
交给 Scheduler
, 进行异步调用。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
第四:异步调用之后,又同步来到了 第二个 setNumber 函数调用。调用的是 dispatchSetState
, 在 该函数中,将 update
对象 加入到 hook.queue.pending
循环队列当中,然后 调用了熟悉的 scheduleUpdateOnFiber
函数,从根节点开始调度更新。
const dispatchSetState = () => {
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
第五:scheduleUpdateOnFiber
函数 中 调用了 ensureRootIsScheduled
函数。
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number
) {
ensureRootIsScheduled 函数。(root, eventTime);
}
第六: ensureRootIsScheduled
函数中,判断新的更新优先级 和 当前已经存在的优先级相等,则不会进行调度更新,进行返回。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
if (existingCallbackPriority === newCallbackPriority) {
return;
}
}
第七:异步调用之后,又同步来到了 第三个 setNumber 函数调用。调用的是 dispatchSetState
, 在 该函数中,将 update
对象 加入到 hook.queue.pending
循环队列当中,然后 调用了熟悉的 scheduleUpdateOnFiber
函数,从根节点开始调度更新。
const dispatchSetState = () => {
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
第八:scheduleUpdateOnFiber
函数 中 调用了 ensureRootIsScheduled
函数。
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number
) {
ensureRootIsScheduled 函数。(root, eventTime);
}
第九: ensureRootIsScheduled
函数中,判断新的更新优先级 和 当前已经存在的优先级相等,则不会进行调度更新,进行返回。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
if (existingCallbackPriority === newCallbackPriority) {
return;
}
}
第十:三次 setNumber()
同步代码执行完毕后,空闲时间时,开始执行由第一次更新产生的调度任务,从根节点开始,深度优先调度 fiber 节点,进行更新。更新到 App 组件时,调用 useState, 更新时调用的 useState, 是 updateReducer
函数,在 updateReducer
函数中,根据三次更新产生的更新队列,计算出新状态 4 -> 提交。
自此 批量更新结束。
高优先级打断低优先级
打断低优先级任务效果展示:
function FunctionComponent() {
const [number, setNumber] = React.useState("1");
React.useEffect(() => {
setNumber((pre) => (pre += "3"));
}, []);
return (
<button
onClick={() => {
setNumber((pre) => (pre += "2"));
}}
>
{number}
</button>
);
}
const element = <FunctionComponent />;
const container = document.getElementById("root");
const root = createRoot(container);
root.render(element);
https://juejin.cn/post/7185092839582203962 , 初次渲染我们已经在这篇文章当中聊过了。
打断低优先级任务原理
第一:挂载之后,执行 useEffect 中的副作用,setNumber((pre) => (pre += "3"))
后, React 从根节点开始调度更新。正准备在浏览器的空闲时间里,去继续调度更新 FunctionComponent
fiber 节点之前,触发了点击事件,所以浏览器的下一帧会先触发点击事件的回调函数,然后再继续调度更新。
浏览器执行 React 代码的时机,是在一帧最后的空闲时间。所以 event 事件会先执行。
第二:event 回调函数中调用 setNumber(setNumber((pre) => (pre += "2)))
, 事实上触发了 dispatchSetState 函数
又调用 scheduleUpdateOnFiber
从 根节点开始执行。
```javascript
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
markUpdateInDevTools(fiber, lane, action); } ```
第三: scheduleUpdateOnFiber
函数中调用 ensureRootIsScheduled
函数,就是在这个函数中实现了打断。打断原理实现如下,existingCallbackNode
就是上一个更新 调度的 performConcurrentWorkOnRoot
函数,Scheduler cancelCallback
函数拿到 existingCallbackNode
将 task.callback
置为 null
。所以当再此调度的时候,发现第一次 task 的 callback
属性已经为 null
了,直接将第一次更新的 task 弹出优先级队列。至此第一次更新被打断。
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}
第四:打断上一次更新之后,紧随其后的就是下一次更新,由于点击事件 的 lane 是同步优先级 1,所以 includesSyncLane
成立,包含同步优先级,所以以同步优先级调用 performSyncWorkOnRoot
-> renderRootSync
-> workLoopSync
。
javascript
// Schedule a new callback.
let newCallbackNode;
if (includesSyncLane(newCallbackPriority)) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
ReactCurrentActQueue.didScheduleLegacyUpdate = true;
}
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
第五:假定又遍历到了该函数节点,又调用了 useState, 事实上是 调用了 updateReducer, 遍历更新队列,由于是以同步优先级调用的。所以第一个 update 会被跳过,直接执行第二个 update 对象,计算出新状态 -> 提交,至此,UI 渲染出了最高优先级的任务。
if (shouldSkipUpdate) {
const clone = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane
);
}
打断低优先级任务
但是仍然没有结束,被打断了的低优先级任务,将高优先级任务提交之后,还需要再 commitRootImpl
函数,调用 ensureRootIsScheduled
函数执行。
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority
) {
// Always call this before exiting `commitRoot`, to ensure that any
// additional work on this root is scheduled.
ensureRootIsScheduled(root, now());
return null;
}
第一:虽然低优先级更新的调度任务取消了,但是该 setNumebr
产生的两个更新对象还在按照交互顺序排列在 hook.queue.pending
当中,由于低优先级的任务被跳过了,所以 baseQueue 还是 +=3 -> += 2 ,baseState 还是 1, 所以最后的结果是 132。
如果不打断,2s 之后去点击按钮, 结果就是动画当中的 132。
问题来了?那什么时候,会出现 123 呢,就是在 2s 之前只要点击了按钮,就会出现 123的结果。
最后的队列执行结果,大家可能有些模糊,可以带着目的去读源码,从目的推到过程,目的是 React 在保证优先级高的任务打断低优先级的任务先执行的同时,也要保证最后的执行结果是正确的。
/*
queue: 1 -> 2 -> 3
1 执行, 2 被跳过 baseQueue 就是 2 -> 3, baseState: 1
1 被跳过, 2, 3执行 baseQueue 就是 1 -> 2 -> 3 baseState: 初始值
*/
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
//获取更新队列
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
/* 1-> 2 -> 3
1 执行, 2 被跳过 baseQueue 就是 2 -> 3
1 被跳过, 2, 3执行 baseQueue 就是 1 -> 2 -> 3
*/
// baseQueue 是上一次被跳过更新的队列
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
// 如果有待生效的队列
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
// 将上一次被跳过的更新的队列进行合并
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 如果有更新
if (baseQueue !== null) {
// We have a queue to process.
// 基本状态
let newState = queue.baseState;
// 新的车道
let newLanes = NoLanes;
// 新的基本状态
let newBaseState = null;
// 新的第一个基本更新
let newFirstBaseUpdate = null;
// 新的最后一个基本更新
let newLastBaseUpdate = null;
// 第一个更新
let update = firstBaseUpdate;
do {
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
// 判断优先级是否足够,如果不够就跳过此更新
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
if (shouldSkipUpdate) {
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
// 如果已经有跳过的更新了,即使优先级再高也需要添到新的基本链表中
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Process this update.
const action = update.action;
if (shouldDoubleInvokeUserFnsInHooksDEV) {
reducer(newState, action);
}
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
setState 是同步还是异步?
legacy
模式 下的 setState
legacy
模式下:也就是 通过 ReactDom.render()
方法来挂载时,setState
在 生命周期函数,事件函数当中是批量异步更新,而在定时器 setTimeout | setInterval, Promsie, addEventListener……
当中是 同步非批量更新。
如果想要在 legacy
模式下的定时器当中获得批量异步更新,通过使用 React
提供的 batchedUpdates
将更新包裹起来就可以强制获得异步批量更新。而事实上在生命周期,事件函数当中,源码默认包裹了 batchedUpdates
所以生命周期,事件函数当中是异步批量更新。
batheUpdate 原理
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// If there were legacy sync updates, flush them at the end of the outer
// most batchedUpdates-like method.
if (
executionContext === NoContext &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
}
batheUpdate
原理:将模式暂时 noMode
改成了 conCurrent
,然后再去执行点击事件,fiber 节点调度到 scheduleFiberOnRoot
就不会被判断 是 noMode
,所以不会同步执行,而是 通过 performConcurrentWorkOnRoot
批量异步更新。
如果是 setTimeout
没有包裹 batheUpdate
的话,判断是 noMode
就会立即同步执行 setTimeout
中的 setState
, 通过 performSyncWorkOnRoot
进行同步更新。
异步批量更新
import * as React from 'react';
import * as ReactDOM from 'react-dom';
class Counter extends React.Component{
state = {number:0}
buttonClick = ()=>{
console.log('buttonClick');
this.setState({number:this.state.number+1});
console.log(this.state.number);
this.setState({number:this.state.number+1});
console.log(this.state.number);
}
divClick = ()=>{
console.log('divClick');
}
render(){
return (
<div onClick={this.divClick} id="counter">
<p>{this.state.number}</p>
<button onClick={this.buttonClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter/>,document.getElementById('root'));
同步非批量
import * as React from 'react';
import * as ReactDOM from 'react-dom';
class Counter extends React.Component{
state = {number:0}
setTimeout(()=>{
this.setState((state)=>({number:state.number+1}),()=>{
console.log(this.state.number);
});
this.setState((state)=>({number:state.number+1}),()=>{
console.log(this.state.number);
});
});
}
render(){
return (
<div onClick={this.divClick} id="counter">
<p>{this.state.number}</p>
<button onClick={this.buttonClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter/>,document.getElementById('root'));
concurrent
模式 下的 setState
concurrent
模式下:也就是通过 ReactDom.creatRoot().render()
方法来挂载时,这里的所有更新 - 生命周期函数,事件函数,定时器 setTimeout | setInterval, Promsie, addEventListener……
都是异步批量更新。
挂载时,不多说,以异步优先级去调度,异步并发更新
更新时,Hook 更新,不多说,大家可以看看之前的文章。
下面我们来聊聊类组件的更新。
第一:由于组件类 extends Components | PureComponents
PureComponents 本质就是(使用 shalowEquall 重写 shouldUpdate方法)
第二:调用类组件的 setState 方法 ,在setState 方法中调用 this.updater.enqueuSetState
方法。
第三:通过实例的 reactInternals 方法找到 对应的 fiber, createUpdate 创建更新对象 update
第四:过 enqueueUpdate将 更新对象 update , push 到 fiber.updateQueue 当中。开始scheduleUpdateOnFiber
。
第五:异步调度到该类组件的 fiber 时,会通过 processUpdateQueue 函数, 更新 state。
processUpdateQueue 函数可以让 React 获得高优先级打断低优先级更新的能力。
第六: 提交,Dom 更新
合成事件
读完收获
学会 Dom 事件流
理解 事件委托
掌握 React 合成事件原理
Dom 事件流
事件流包含三个阶段:
- 事件捕获阶段
- 目标阶段
- 事件冒泡阶段
首先发生的是事件捕获,然后是实际的目标接收到事件,最后阶段是事件冒泡阶段。
<html>
<body>
<div>
<button></button>
</div>
</body>
</html>
事件捕获阶段
事件捕获是先由最上层节点 document
先接收事件, 然后向下传播到具体的节点 document->body->div->button
目标阶段
在目标节点上触发,称为目标阶段
//w3c浏览器:event.target
//IE: event.srcElement
let target = event.target || event.srcElement;
事件冒泡阶段
从目标节点开始 (这里是 button),然后逐级向上传播 button->div->body->document
addEventListener
// useCapture 默认是 fales,当参数是true,则在捕获阶段绑定函数,反之,在冒泡阶段绑定函数,
element.addEventListener(event, function, useCapture)
阻止冒泡
// IE
window.event.cancelBubble = true;
// w3c
event.stopPropagation();
事件代理
事件代理又称之为事件委托, 事件代理是把原本需要绑定在子元素
的事件委托给父元素
,让父元素负责事件监听和处理。
事件代理的好处是有两点:
第一点:可以大量节省内存占用,减少事件注册事件。
第二点:当新增子对象时无需再次对其绑定
为什么父元素能做到事件代理呢? 笔者认为有两点:
第一点 :事件冒泡到父元素,父元素可以订阅到冒泡事件。
第二点:可以通过 event.target
得到目标节点。不然, 父元素怎么针对不同的子节点,进行定制化事件代理。
React 合成事件原理
下面这段代码在 React 18合成事件的打印结果是:
/**
document原生捕获
父元素React事件捕获
子元素React事件捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
*/
const App = () => {
const divRef = useRef();
const pRef = useRef();
const parentBubble = () => {
console.log("父元素React事件冒泡");
};
const childBubble = () => {
console.log("子元素React事件冒泡");
};
const parentCapture = () => {
console.log("父元素React事件捕获");
};
const childCapture = () => {
console.log("子元素React事件捕获");
};
useEffect(() => {
divRef.current.addEventListener(
"click",
() => {
console.log("父元素原生捕获");
},
true
);
divRef.current.addEventListener("click", () => {
console.log("父元素原生冒泡");
});
pRef.current.addEventListener(
"click",
() => {
console.log("子元素原生捕获");
},
true
);
pRef.current.addEventListener("click", () => {
console.log("子元素原生冒泡");
});
document.addEventListener(
"click",
() => {
console.log("document原生捕获");
},
true
);
document.addEventListener("click", () => {
console.log("document原生冒泡");
});
}, []);
return (
<div ref={divRef} onClick={parentBubble} onClickCapture={parentCapture}>
<p ref={pRef} onClick={childBubble} onClickCapture={childCapture}>
事件执行顺序
</p>
</div>
);
};
Mout 阶段: 点击之前
Mout 阶段总结:点击之前 就是在 root 容器上监听了 JavaScript 所有原生事件的冒泡和捕获。
第一:通过调用 SimpleEventPlugin.registerEvents
插件函数来注册事件(在项目当中的 index.tsx 中 调用 createRoot
函数 之前就去注册了。)
import {allNativeEvents} from './EventRegistry';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
SimpleEventPlugin.registerEvents();
SimpleEventPlugin.registerEvents
函数:处理,加工原始事件名字为 React 事件名字,比如 将 click
变为onClick
, 然后调用 registerSimpleEvent
函数 将原始事件名字和 React 事件名字 建立 Map 映射关系,比如 Map {click: onClick}
// 所有原生事件
const simpleEventPluginEvents = [
'abort',
'auxClick',
'click',
// 剩余所有原生事件
];
// React 事件和原始事件的映射 Map
export const topLevelEventsToReactNames = new Map();
SimpleEventPlugin.registerEvents
export function registerSimpleEvents() {
for (let i = 0; i < simpleEventPluginEvents.length; i++) {
const eventName = ((simpleEventPluginEvents[i]: any): string);
const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
}
}
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}
第四:原始事件名字和 React 事件名字 建立 Map 映射关系 之后,在 registerSimpleEvent
函数 调用 registerTwoPhaseEvent
函数, 在 registerTwoPhaseEvent
函数中调用 registerDirectEvent
函数
结果是将所有原生事件名字 加入到 allNativeEvents 数组当中去,比如:[click, dbclick]
。调用两次是因为要在 registrationNameDependencies = {}
映射冒泡和捕获。比如:{onClick: click, onClickCapture: click}
为什么 dependencies 是数组,因为 一个 React 事件可能会对应多个原生事件。
export function registerTwoPhaseEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
): void {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}
// {onClick: click, onClickCapture: click}
export const registrationNameDependencies = {};
export function registerDirectEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
) {
registrationNameDependencies[registrationName] = dependencies;
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}
第五: 在项目当中的 index.tsx 中 调用 createRoot
函数
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(<App/>)
第六:在 createRoot
函数 当中 调用 listenToAllSupportedEvents
函数,并创建 FiberRoot
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType {
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks
);
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
第七:调用 listenToAllSupportedEvents
函数, 见名知意, 监听所有原生事件。 来看看这里是怎么监听。
经过前五步,已经将所有的原生事件都放到了 allNativeEvents 数组中。遍历 allNativeEvents 数组,调用 listenToNativeEvent
函数。
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
allNativeEvents.forEach(domEventName => {
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
}
第八:listenToNativeEvent 调用了 addTrappedEventListener
函数。
第九:addTrappedEventListener
函数 当中,首先通过 createEventListenerWrapperWithPriority
函数 创建了 listenr 监听函数,这个监听函数很重要,注意看!!!!
listenr 监听函数通过 dispatchEvent 去 bind 绑定一些重要参数,返回的一个函数。
这样每次事件触发都可以调用 dispatchEvent 并且携带一些固定的参数。
export function createEventListenerWrapper(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
return dispatchEvent.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
然后调用 addEventBubbleListener
addEventCaptureListener
函数。
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
);
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener
);
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener
);
}
第十:调用 addEventBubbleListener
addEventCaptureListener
函数,真相大白 😀,原来结果就是在 root 根容器上绑定了原生事件的冒泡和捕获事件。
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
) {
target.addEventListener(eventType, listener, false);
retu电击 listener;
}
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}
Mout 阶段总结:点击之前 就是在 root 容器上监听了 JavaScript 所有原生事件的冒泡和捕获。
点击触发
总结:提取所有事件监听的处理函数放到
dispatchQueue
当中。然后,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。上文提到,所有事件的触发,都绑定到了 dispachtEvent 函数上,相当于:
div.addEventListener("click", dispatchEvent.bind(null,
click,
eventSystemFlags,
div#root))
div.addEventListener("dbclick", dispatchEvent.bind(null,
dbclick,
eventSystemFlags,
div#root))
第一:点击触发,调用 dispatchEvent
函数。函数做了至关重要的两件事情。
第一件事情是:通过调用 extractEvents
函数, 提取所有监听的处理函数放到 dispatchQueue
当中。
第二件事情是:通过调用 processDispatchQueue
函数,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。
基于这两件事情,我们看看 Reect 是怎么完成的?
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
第一件事情是:调用 extractEvents
函数,首先根据不同的事件类型名字,去获取不同类别的事件对象 event,也就是React 传递给开发者的 事件对象 event。
然后,在该函数当中调用 accumulateSinglePhaseListeners
函数。这个 accumulateSinglePhaseListeners
函数 做的事情就是 从当前的 Fiebr 节点开始一直向上遍历,找到路径上 fiber 节点的所有绑定事件函数比如:onClick, onClickCapture。经过包装成为 Dispatch 返回给 listeners。
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent
);
然后将 Dispatch 和 匹配的 event 对象,封装成一个对象,加入到 dispatdchQueue
当中去,
dispatchQueue.push({ event, listeners });
extractEvents
函数整体实现
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget
): void {
const reactName = topLevelEventsToReactNames.get(domEventName);
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
// 首先根据不同的事件类型名字,去获取不同类别的事件对象 event,也就是React 传递给开发者的 事件对象 event。
switch (domEventName) {
case "click":
case "mousemove":
SyntheticEventCtor = SyntheticMouseEvent;
break;
case "drop":
SyntheticEventCtor = SyntheticDragEvent;
break;
default:
break;
}
// 通过调用 `extractEvents` 函数, 提取所有监听的处理函数放到 `dispatchQueue` 当中。
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent
);
if (listeners.length > 0) {
const event: ReactSyntheticEvent = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget
);
// Dispatch 和 匹配的 event 对象,封装成一个对象,加入到 `dispatdchQueue` 当中去,
dispatchQueue.push({ event, listeners });
}
}
第二件事情是:通过调用 processDispatchQueue
函数,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。比如:捕获阶段,从最高层节点向下传播,而加入到队列的顺序 是从目标节点开始向上加入的,所以要想模拟捕获,就需要从最后一个节点开始 倒序执行。要想模拟冒泡,就需要从 第一个节点开始,正序执行。
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean
): void {
let previousInstance;
if (inCapturePhase) {
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const { instance, currentTarget, listener } = dispatchListeners[i];
// 这里的判断是因为,React 重写了 阻止冒泡的方法,可以通过 isPropagationStopped 来判断是否阻止
//了冒泡,如果阻止了冒泡,则立即返回。
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
// 执行事件监听函数,传入 Event 对象。
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (let i = 0; i < dispatchListeners.length; i++) {
const { instance, currentTarget, listener } = dispatchListeners[i];
// 这里的判断是因为,React 重写了阻止冒泡的方法,可以通过 isPropagationStopped 来判断是否阻止
//了冒泡,如果阻止了冒泡,则立即返回。
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
// 执行事件监听函数,传入Event 对象。
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
总结:提取所有事件监听的处理函数放到
dispatchQueue
当中。然后,模拟捕获阶段和冒泡阶段的执行流程,去执行所有的监听处理函数。至此,事件触发阶段完成
生命周期
聊生命周期, 就是从类组件生命周期函数的角度来聊 React 的挂载和更新的渲染流程。
mount 阶段
React Reconcil
阶段 会在 beginWork
函数 中挂载类组件, 挂载的时候 再 constructClassInstance
函数中,先 new
类,执行 constructor
构造函数。初始化 state
和 props
。
然后再 updateComponents
中继续执行,执行 mountClassInstance
函数,执行 getDeriveStateFromProps
生命周期函数 ,然后执行 componenWillMount
生命周期函数,新的生命周期已经废除了 componentWillMount
。
if (instance === null) {
resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress);
constructClassInstance(workInProgress, Component, nextProps);
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
}
之后,执行 render
函数,子节点开始进入 reconcil
阶段。
最后进入 commit
阶段,再 mutataion
阶段之后,再 layout
阶段 执行 componentDidMount
生命周期函数,此时 Dom
已经挂载完毕。
自此类组件挂载时的生命周期,执行完毕
update 阶段
更新的时候,调用 setState
, React
又会从调用 scheduleUpdateOnFiber
从根节点开始调度更新,假设已经调度到该类组件。到达了该组件的 reconcil
阶段。reconcil
阶段 会在 beginWork
函数 中更新该类组件, 更新的时候判断 instance !== null, current !== null
, 所以会走 updateClassInstance
函数,在该函数中,先执行 componentWillReceiveProps
生命周期期函数 传入新的 props,新的生命周期中已经废除了 componentWillReceiveProps
, 然后开始执行 processUpdateQueue
函数 进行更新计算新的 state。将计算出的新的 state 和 新的 props 传入并执行getDeriveStateFromProps
生命周期函数,该生命周期返回的状态和新的状态,进行合并。作为 Dom 提交时最终的状态。
然后执行 checkShouldComponentUpdate
函数,在该函数中执行 shouldComponentUpdate
生命周期函数。如果返回 true 则执行 componentWillUpdate
生命周期函数,新的生命周期中已经废除了 componentWillReceiveProps
。之后,执行 render
函数,子节点开始进入 reconcil
阶段。
最后进入 commit
阶段,再 before mutataion
阶段 (Dom 更新前) 传入旧的 state 执行 getSnapshotBeforeUpdate
生命周期函数,再 layout
阶段(Dom 更新后) 执行 componentDidUpdate
生命周期函数(和 componentDidMount
是同一个函数),此时 Dom
已经更新完毕。
自此类组件更新时的生命周期,执行完毕