从原理层面探究 React 是如何工作的(1)

  • return 指向当前节点的父元素

  • sibling 指向同级的下一个兄弟节点

如果是 React16 之前的树状结构,就需要通过 DFS 深度遍历来查找每一个节点。而现在只需要将指针按照 child → sibling → return 的优先级移动,就可以处理所有的节点

这样设计还有一个好处就是在 React 工作的时候只需要使用一个全局变量作为指针在链表中不断移动,如果出现用户输入或其他优先级更高的任务就可以 暂停 当前工作,其他任务结束后只需要根据指针的位置继续向下移动就可以继续之前的工作。指针移动的规律可以归纳为 自顶向下,从左到右 。

康康 fiber 的基本结构

其中

  • tag fiber 的类型 ,例如函数组件,类组件,原生组件, Portal 等。

  • type React 元素 类型 详见上方 createElement。

  • alternate 代表双向缓冲对象(看后面)。

  • effectTag 代表这个 fiber 在下一次渲染中将会被如何处理。例如只需要插入,那么这个值中会包含 Placement ,如果需要被删除,那么将会包含 Deletion 。

  • expirationTime 过期时间,过期时间越靠前,就代表这个 fiber 的优先级越高。

  • firstEffectlastEffect 的类型都和 fiber 一样,同样是链表结构,通过 nextEffect 来连接。代表着即将更新的 fiber 状态

  • memorizeStatememorizeProps 代表在上次渲染中组件的 props 和 state 。如果成功更新,那么新的 pendingProps 和 newState 将会替代这两个变量的值

  • ref 引用标识

  • stateNode 代表这个 fiber 节点对应的真实状态

    • 对于原生组件,这个值指向一个 dom 节点(虽然已经被创建了,但不代表就被插入了 document )
  • 对于类组件,这个值指向对应的类实例

  • 对于函数组件,这个值指向 Null

  • 对于 RootFiber,这个值指向 FiberRoot (如图)

接下来是初次渲染的几个核心步骤,因为是初次渲染,核心任务就是将首屏元素渲染到页面上,所以这个过程将会是同步的。

PrepareFreshStack—


因为笔者是土货没学过英语,百度了下发现是 准备干净的栈 的意思。结合了下流程,可以看出这一步的作用是在真正工作之前做一些准备,例如初始化一些变量,放弃之前未完成的工作,以及最重要的—— 创建双向缓冲变量 WorkInProgress

let workInProgress: Fiber | null = null

export function prepareFreshStack (

root: FiberRoot,

expirationTime: number

) {

// 重置根节点的finishWork

root.finishedWork = null

root.finishedExpirationTime = ExpirationTime.NoWork

if (workInProgress !== null) {

// 如果已经存在了WIP,说明存在未完成的任务

// 向上找到它的root fiber

let interruptedWork = workInProgress.return

while (interruptedWork !== null) {

// unwindInterruptedWork // 抹去未完成的任务

unwindInterruptedWork(interruptedWork)

interruptedWork = interruptedWork.return

}

}

workInProgressRoot = root

// 创建双向缓冲对象

workInProgress = createWorkInProgress(root.current, null, expirationTime)

renderExpirationTime = expirationTime

workInProgressRootExitStatus = RootExitStatus.RootImcomplete

}

双向缓冲变量 WorkInProgress

=====================

这里简称 WIP 好了,与之对应的是 current , current 代表的是当前页面上呈现的组件对应的 fiber 节点,你可以将其类比为 git 中的 master 分支,它代表的是已经对外的状态。而 WIP 则代表了一个 pending 的状态,也就是下一帧屏幕将要呈现的状态,就像是从 master 拉出来的一个 feature 分支,我们可以在这个分支上做任意的更改。最终协调完毕,将 WIP 的结果渲染到了页面上,按照页面内容对应 current 的原则, current 将会指向 WIP ,也就是说, WIP 取代了之前的 current ( git 的 master 分支)。

在这之前 current 和 WIP 的 alternate 字段分别指向彼此。

那么 WIP 是如何被创造出来的呢:

// 根据已有 fiber 生成一个 workInProgress 节点

export function createWorkInProgress (

current: Fiber,

pendingProps: any,

expirationTime

): Fiber {

let workInProgress = current.alternate

if (workInProgress === null) {

// 如果当前fiber没有alternate

// tip: 这里使用的是“双缓冲池技术”,因为我们最多需要一棵树的两个实例。

// tip: 我们可以自由的复用未使用的节点

// tip: 这是异步创建的,避免使用额外的对象

// tip: 这同样支持我们释放额外的内存(如果需要的话

workInProgress = createFiber(

current.tag,

pendingProps,

current.key,

current.mode

)

workInProgress.elementType = current.elementType

workInProgress.type = current.type

workInProgress.stateNode = current.stateNode

workInProgress.alternate = current

current.alternate = workInProgress

} else {

// 我们已经有了一个 WIP

workInProgress.pendingProps = pendingProps

// 重置 effectTag

workInProgress.effectTag = EffectTag.NoEffect

// 重置 effect 链表

workInProgress.nextEffect = null

workInProgress.firstEffect = null

workInProgress.lastEffect = null

}

可以看出 WIP 其实就是继承了 current 的核心属性,但是去除了一些副作用和工作记录的 干净 的 fiber。

工作循环 WorkLoop

=============

在工作循环中,将会执行一个 while 语句,每执行一次循环,都会完成对一个 fiber 节点的处理。在 workLoop 模块中有一个指针 workInProgress 指向当前正在处理的 fiber ,它会不断向链表的尾部移动,直到指向的值为 null ,就停止这部分工作, workLoop 的部分也就结束了。

每处理一个 fiber 节点都是一个工作单元,结束了一个工作单元后 React 会进行一次判断,是否需要暂停工作检查有没有更高优先级的用户交互进来。

function workLoopConcurrent() {

// 执行工作直到 Scheduler 要求我们 yield

while (workInProgress !== null && !shouldYield()) {

workInProgress = performUnitOfWork(workInProgress);

}

}

跳出条件只有:

  1. 所有 fiber 都已经被遍历结束了

  2. 当前线程的使用权移交给了外部任务队列

但是我们现在讨论的是第一次渲染,触屏渲染的优先级高于一切,所以并不存在第二个限制条件。

function workLoopSync () {

// 只要没有完成reconcile就一直执行

while(workInProgress !== null) {

workInProgress = performUnitOfWork(workInProgress as Fiber)

}

}

PerformUnitOfWork & beginWork—


单元工作 performUnitOfWork 的主要工作是通过 beginWork 来完成。beginWork 的核心工作是通过判断 fiber.tag 判断当前的 fiber 代表的是一个类组件、函数组件还是原生组件,并且针对它们做一些特殊处理。这一切都是为了最终步骤:操作真实 DOM 做准备,即通过改变 fiber.effectTag 和 pendingProps 告诉后面的 commitRoot 函数应该对真实 DOM 进行怎样的改写。

switch (workInProgress.tag) {

// RootFiber

case WorkTag.HostRoot:

return updateHostRoot(current as Fiber, workInProgress, renderExpirationTime)

// class 组件

case WorkTag.ClassComponent: {

const Component = workInProgress.type

const resolvedProps = workInProgress.pendingProps

return updateClassComponent(

current,

workInProgress,

Component,

resolvedProps,

renderExpirationTime

)

}

}

此处就以 Class 组件为例,查看一下具体是如何构建的。

之前有提过,对于类组件而言, fiber.stateNode 会指向这个类之前构造过的实例。

// 更新Class组件

function updateClassComponent (

current: Fiber | null,

workInProgress: Fiber,

Component: any,

nextProps,

renderExpiration: number

) {

// 如果这个 class 组件被渲染过,stateNode 会指向类实例

// 否则 stateNode 指向 null

const instance = workInProgress.stateNode

if (instance === null) {

// 如果没有构造过类实例

} else {

// 如果构造过类实例

}

// 完成 render 的构建,将得到的 react 元素和已有元素进行调和

const nextUnitOfWork = finishClassComponent(

current,

workInProgress,

Component,

shouldUpdate,

false,

renderExpiration

)

return nextUnitOfWork

如果这个 fiber 并没有构建过类实例的话,就会调用它的构建函数,并且将更新器 updater 挂载到这个类实例上。(处理 setState 逻辑用的,事实上所有的类组件实例上的更新器都是同一个对象,后面会提到)

if (instance === null) {

// 这个 class 第一次渲染

if (current !== null) {

// 删除 current 和 WIP 之间的指针

current.alternate = null

workInProgress.alternate = null

// 插入操作

workInProgress.effectTag |= EffectTag.Placement

}

// 调用构造函数,创造新的类实例

// 给予类实例的某个指针指向更新器 updater

constructClassInstance(

workInProgress,

Component,

nextProps,

renderExpiration

)

// 将属性挂载到类实例上,并且触发多个生命周期

mountClassInstance(

workInProgress,

Component,

nextProps,

renderExpiration

)

}

如果实例已经存在,就需要对比新旧 props 和 state ,判断是否需要更新组件(万一写了 shouldComponentUpdate 呢)。并且触发一些更新时的生命周期钩子,例如 getDerivedStateFromProps 等等。

else {

// 已经 render 过了,更新

shouldUpdate = updateClassInstance(

current,

workInProgress,

Component,

nextProps,

renderExpiration

)

}

属性计算完毕后,调用类的 render 函数获取最终的 ReactElement ,打上 Performed 标记,代表这个类在本次渲染中已经执行过了。

// 完成Class组件的构建

function finishClassComponent (

current: Fiber | null,

workInProgress: Fiber,

Component: any,

shouldUpdate: boolean,

hasContext: boolean,

renderExpiration: number

) {

// 错误 边界捕获

const didCaptureError = false

if (!shouldUpdate && !didCaptureError) {

if (hasContext) {

// 抛出问题

return bailoutOnAlreadyFinishedWork(

current,

workInProgress,

renderExpiration

)

}

}

// 实例

const instance = workInProgress.stateNode

let nextChildren

nextChildren = instance.render()

// 标记为已完成

workInProgress.effectTag |= EffectTag.PerformedWork

// 开始调和 reconcile

reconcileChildren(

current,

workInProgress,

nextChildren,

renderExpiration

)

return workInProgress.child

}

调和过程

如果还记得之前的内容的话,我们在一切工作开始之前只是构建了第一个根节点 fiberRoot 和第一个无意义的空 root ,而在单个元素的调和过程 reconcileSingleElement 中会根据之前 render 得到的 ReactElement 元素构建出对应的 fiber 并且插入到整个 fiber 链表中去。

并且通过 placeSingleChild 给这个 fiber 的 effectTag 打上 Placement 的标签,拥有 Placement 标记后这里的工作就完成了,可以将 fiber 指针移动到下一个节点了。

// 处理对象类型(单个节点)

const isObjectType = isObject(newChild) && !isNull(newChild)

// 对象

if (isObjectType) {

switch (newChild.$$typeof) {

case REACT_ELEMENT_TYPE: {

// 在递归调和结束,向上回溯的过程中

// 给这个 fiber 节点打上 Placement 的 Tag

return placeSingleChild(

reconcileSingleElement(

returnFiber,

currentFirstChild,

newChild,

expirationTime

)

)

}

// 还有 Fragment 等类型

}

}

// 如果这时子元素是字符串或者数字,按照文字节点来处理

// 值得一提的是,如果元素的子元素是纯文字节点

// 那么这些文字不会被转换成 fiber

// 而是作为父元素的 prop 来处理

if (isString(newChild) || isNumber(newChild)) {

return placeSingleChild(

reconcileSingleTextNode(

returnFiber,

currentFirstChild,

‘’ + newChild,

expirationTime

)

)

}

// 数组

if (isArray(newChild)) {

return reconcileChildrenArray(

returnFiber,

currentFirstChild,

newChild,

expirationTime

)

}

文章篇幅有限,对于函数组件和原生组件这里就不做过多介绍。假设我们已经完成了对于所有 WIP 的构建和调和过程,对于第一次构建而言,我们需要插入大量的 DOM 结构,但是到现在我们得到的仍然是一些虚拟的 fiber 节点。

所以,在最后一次单元工作 performUnitOfWork 中将会执行 completeWork ,在此之前,我们的单元工作是一步步向尾部的 fiber 节点移动。而在 completeWork 中,我们的工作将是自底向上,根据 fiber 生成真实的 dom 结构,并且在向上的过程中将这些结构拼接成一棵 dom 树。

export function completeWork (

current: Fiber | null,

workInProgress: Fiber,

renderExpirationTime: number

): Fiber | null {

// 最新的 props

const newProps = workInProgress.pendingProps

switch (workInProgress.tag) {

case WorkTag.HostComponent: {

// pop 该 fiber 对应的上下文

popHostContext(workInProgress)

// 获取 stack 中的当前 dom

const rootContainerInstance = getRootHostContainer()

// 原生组件类型

const type = workInProgress.type

if (current !== null && workInProgress.stateNode !== null) {

// 如果不是初次渲染了,可以尝试对已有的 dom 节点进行更新复用

updateHostComponent(

current,

workInProgress,

type as string,

newProps,

rootContainerInstance

)

} else {

if (!newProps) {

throw new Error(‘如果没有newProps,是不合法的’)

}

const currentHostContext = getHostContext()

// 创建原生组件

let instance = createInstance(

type as string,

newProps,

rootContainerInstance,

currentHostContext,

workInProgress

)

// 将之前所有已经生成的子 dom 元素装载到 instance 实例中

// 逐步拼接成一颗 dom 树

appendAllChildren(instance, workInProgress, false, false)

// fiber 的 stateNode 指向这个 dom 结构

workInProgress.stateNode = instance

// feat: 这个函数真的藏得很隐蔽,我不知道这些人是怎么能注释都不提一句的呢→_→

// finalizeInitialChildren 作用是将props中的属性挂载到真实的dom元素中去,结果作为一个判断条件被调用

// 返回一个bool值,代表是否需要auto focus(input, textarea…)

if (finalizeInitialChildren(instance, type as string, newProps, rootContainerInstance, currentHostContext)) {

markUpdate(workInProgress)

}

}

}

}

return null

}

构建完毕后,我们得到了形如下图,虚拟 dom 和 真实 dom,父元素和子元素之间的关系结构

截止到当前,调和 reconcile 工作已经完成,我们已经进入了准备提交到文档 ready to commit 的状态。其实从进入 completeUnitOfWork 构建开始,后面的过程就已经和时间片,任务调度系统没有关系了,此时一切事件、交互、异步任务都将屏气凝神,聆听接下来 dom 的改变。

// 提交根实例(dom)到浏览器真实容器root中

function commitRootImpl (

root: FiberRoot,

renderPriorityLevel: ReactPriorityLevel

) {

// 因为这次是整个组件树被挂载,所以根 fiber 节点将会作为 fiberRoot 的 finishedWork

const finishedWork = root.finishedWork

// effect 链表,即那些将要被插入的原生组件 fiber

let firstEffect = finishedWork.firstEffect

let nextEffect = firstEffect

while (nextEffect !== null) {

try {

commitMutationEffects(root, renderPriorityLevel)

} catch(err) {

throw new Error(err)

}

}

}

在 commitMutationEffects 函数之前其实对 effect 链表还进行了另外两次遍历,分别是一些生命周期的处理,例如 getSnapshotBeforeUpdate ,以及一些变量的准备。

// 真正改写文档中dom的函数

// 提交fiber effect

function commitMutationEffects (

root: FiberRoot,

renderPriorityLevel: number

) {

// @question 这个 while 语句似乎是多余的 = =

while (nextEffect !== null) {

// 当前fiber的tag

const effectTag = nextEffect.effectTag

// 下方的switch语句只处理 Placement,Deletion 和 Update

const primaryEffectTag = effectTag & (

EffectTag.Placement |

EffectTag.Update |

EffectTag.Deletion |

EffectTag.Hydrating

)

switch (primaryEffectTag) {

case EffectTag.Placement: {

// 执行插入

commitPlacement(nextEffect)

// effectTag 完成实名制后,要将对应的 effect 去除

nextEffect.effectTag &= ~EffectTag.Placement

}

case EffectTag.Update: {

// 更新现有的 dom 组件

const current = nextEffect.alternate

commitWork(current, nextEffect)

}

}

nextEffect = nextEffect.nextEffect

}

}

截至此刻,第一次渲染的内容已经在屏幕上出现。也就是说,真实 DOM 中的内容不再对应此时的 current fiber ,而是对应着我们操作的 workInProgress fiber ,即函数中的 finishedWork 变量。

// 在 commit Mutation 阶段之后,workInProgress tree 已经是真实 Dom 对应的树了

// 所以之前的 tree 仍然是 componentWillUnmount 阶段的状态

// 所以此时, workInProgress 代替了 current 成为了新的 current

root.current = finishedWork

一次点击事件

======

如果你是一个经常使用 React 的打工人,就会发现 React 中的 event 是“阅后即焚的”。假设这样一段代码:

import React, { MouseEvent } from ‘react’

function TestPersist () {

const handleClick = (

event: MouseEvent<HTMLElement, globalThis.MouseEvent>

) => {

setTimeout(() => console.log(‘event’, event))

}

return (

O2

)

}

如果我们需要异步的获取这次点击事件在屏幕中的位置并且做出相应处理,那么在 setTimeout 中能否达到目的呢。

答案是否定的,因为 React 使用了 事件委托 机制,我们拿到的 event 对象并不是原生的 nativeEvent ,而是被 React 挟持处理过的合成事件 SyntheticEvent ,这一点从 ts 类型中也可以看出, 我们使用的 MouseEvent 是从 React 包中引入的而不是全局的默认事件类型。在 handleClick 函数同步执行完毕的一瞬间,这个 event 就已经在 React 事件池中被销毁了,我们可以跑这个组件康一康。

当然 React 也提供了使用异步事件对象的解决方案,它提供了一个 persist 函数,可以让事件不再进入事件池。(在 React17 中为了解决某些 issue ,已经重写了合成事件机制,事件不再由 document 来代理,官网的说法是合成事件[4]不再由事件池管理,也没有了 persist 函数)

那,为什么要用事件委托呢。还是回到那个经典的命题,渲染 2 个 div 当然横着写竖着写都没关系,如果是 1000 个组件 2000 个点击事件呢。事件委托的收益就是:

  1. 简化了事件注册的流程,优化性能。

  2. dom 元素不断在更新,你无法保证下一帧的 div 和上一帧中的 div 在内存中的地址是同一个。既然不是同一个,事件又要全部重新绑定,烦死了(指浏览器)。

ok,言归正传。我们点击事件到底发生了什么呢。首先是在 React 的 render 函数执行之前,在 JS 脚本中就已经自动执行了事件的注入。

事件注入—


事件注入的过程稍微有一点复杂,不光模块之间有顺序,数据也做了不少处理,这里不 po 太详细的代码。可能有人会问为啥不直接写死呢,浏览器的事件不也就那么亿点点。就像 Redux 不是专门为 React 服务的一样, React 也不是专门为浏览器服务的。文章开头也说了 React 只是一个 javascipt 库,它也可以服务 native 端、桌面端甚至各种终端。所以根据底层环境的不同动态的注入事件集也是非常合理的做法。

当然注入过程并不重要,我们需要知道的就是 React 安排了每种事件在 JSX 中的写法和原生事件的对应关系(例如 onClick 和 onclick ),以及事件的优先级。

/* ReactDOM环境 */

// DOM 环境的事件 plugin

const DOMEventPluginOrder = [

‘ResponderEventPlugin’,

‘SimpleEventPlugin’,

‘EnterLeaveEventPlugin’,

‘ChangeEventPlugin’,

‘SelectEventPlugin’,

‘BeforeInputEventPlugin’,

];

// 这个文件被引入的时候自动执行 injectEventPluginOrder

// 确定 plugin 被注册的顺序,并不是真正引入

EventPluginHub.injectEventPluginOrder(DOMEventPluginOrder)

// 真正的注入事件内容

EventPluginHub.injectEventPluginByName({

SimpleEventPlugin: SimpleEventPlugin

})

这里以 SimpleEventPlugin 为例,点击事件等我们平时常用的事件都属于这个 plugin。

// 事件元组类型

type EventTuple = [

DOMTopLevelEventType, // React 中的事件类型

string, // 浏览器中的事件名称

EventPriority // 事件优先级

]

const eventTuples: EventTuple[] = [

// 离散的事件

// 离散事件一般指的是在浏览器中连续两次触发间隔最少 33ms 的事件(没有依据,我猜的)

// 例如你以光速敲打键盘两次,这两个事件的实际触发时间戳仍然会有间隔

[ DOMTopLevelEventTypes.TOP_BLUR, ‘blur’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_CANCEL, ‘cancel’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_CHANGE, ‘change’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_CLICK, ‘click’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_CLOSE, ‘close’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_CONTEXT_MENU, ‘contextMenu’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_COPY, ‘copy’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_CUT, ‘cut’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_DOUBLE_CLICK, ‘doubleClick’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_AUX_CLICK, ‘auxClick’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_FOCUS, ‘focus’, DiscreteEvent ],

[ DOMTopLevelEventTypes.TOP_INPUT, ‘input’, DiscreteEvent ],

]

那么,这些事件的监听事件是如何被注册的呢。还记得在调和 Class 组件的时候会计算要向浏览器插入什么样的 dom 元素或是要如何更新 dom 元素。在这个过程中会通过 diffProperty 函数对元素的属性进行 diff 对比,其中通过 ListenTo 来添加监听函数

大家都知道,最终被绑定的监听事件一定是被 React 魔改过,然后绑定在 document 上的。

function trapEventForPluginEventSystem (

element: Document | Element | Node,

topLevelType: DOMTopLevelEventType,

capture: boolean

): void {

// 生成一个 listener 监听函数

let listener

switch (getEventPriority(topLevelType)) {

case DiscreteEvent: {

listener = dispatchDiscreteEvent.bind(

null,

topLevelType,

EventSystemFlags.PLUGIN_EVENT_SYSTEM

)

break

}

default: {

listener = dispatchEvent.bind(

null,

topLevelType,

EventSystemFlags.PLUGIN_EVENT_SYSTEM

)

}

}

// @todo 这里用一个getRawEventName转换了一下

// 这个函数就是 →_→

// const getRawEventName = a => a

// 虽然这个函数什么都没有做

// 但是它的名字语义化的说明了这一步

// 目的是得到浏览器环境下addEventListener第一个参数的合法名称

const rawEventName = topLevelType

// 将捕获事件listener挂载到根节点

// 这两个部分都是为了为了兼容 IE 封装过的 addEventListener

if (capture) {

// 注册捕获事件

addEventCaptureListener(element, rawEventName, listener)

} else {

// 注册冒泡事件

addEventBubbleListener(element, rawEventName, listener)

}

}

大家应该都知道 addEventListener 的第三个参数是控制监听捕获过程 or 冒泡过程的吧

ok,right now,鼠标点了下页面,页面调用了这个函数。开局就一个 nativeEvent 对象,这个函数要做的第一件事就是知道真正被点的那个组件是谁,其实看了一些源码就知道, React 但凡有什么事儿第一个步骤总是找到需要负责的那个 fiber 。

首先,通过 nativeEvent 获取目标 dom 元素也就是 dom.target

const nativeEventTarget = getEventTarget(nativeEvent)

export default function getEventTarget(nativeEvent) {

// 兼容写法

let target = nativeEvent.target || nativeEvent.srcElement || window

// Normalize SVG

// @todo

return target.nodeType === HtmlNodeType.TEXT_NODE ? target.parentNode : target

}

那么如何通过 dom 拿到这个 dom 对应的 fiber 呢,事实上, React 会给这个 dom 元素添加一个属性指向它对应的 fiber 。对于这个做法我是有疑问的,这样的映射关系也可以通过维护一个 WeekMap 对象来实现,操作一个 WeakMap 的性能或许会优于操作一个 DOM 的属性,且后者似乎不太优雅,如果你有更好的想法也欢迎在评论区指出。

每当 completeWork 中为 fiber 构造了新的 dom,都会给这个 dom 一个指针来指向它的 fiber

// 随机Key

const randomKey = Math.random().toString(36).slice(2)

// 随机Key对应的当前实例的Key

const internalInstanceKey = ‘__reactInternalInstance$’ + randomKey

// Key 对应 render 之后的 props

const internalEventHandlersKey = ‘__reactEventHandlers$’ + randomKey

// 对应实例

const internalContianerInstanceKey = ‘__reactContainer$’ + randomKey

// 绑定操作

export function precacheFiberNode (

hostInst: object,

node: Document | Element | Node

): void {

node[internalInstanceKey] = hostInst

}

// 读取操作

export function getClosestInstanceFromNode (targetNode) {

let targetInst = targetNode[internalInstanceKey]

// 如果此时没有Key,直接返回null

if (targetInst) {

return targetInst

}

// 省略了一部分代码

// 如果这个 dom 上面找不到 internalInstanceKey 这个属性

// 就会向上寻找父节点,直到找到一个拥有 internalInstanceKey 属性的 dom 元素

// 这也是为什么这个函数名要叫做 从 node 获取最近的 (fiber) 实例

return null

}

此时我们已经拥有了原生事件的对象,以及触发了事件的 dom 以及对应的 fiber ,就可以从 fiber.memorizedProps 中取到我们绑定的 onClick 事件。这些信息已经足够生成一个 React 合成事件 ReactSyntheticEvent 的实例了。

React 声明了一个全局变量 事件队列 eventQueue ,这个队列用来存储某次更新中所有被触发的事件,我们需要让这个点击事件入队。然后触发。

// 事件队列

let eventQueue: ReactSyntheticEvent[] | ReactSyntheticEvent | null = null

export function runEventsInBatch (

events: ReactSyntheticEvent[] | ReactSyntheticEvent | null

) {

if (events !== null) {

// 存在 events 的话,加入事件队列

// react 自己写的合并数组函数 accumulateInto

// 或许是 ES3 时期写的吧

eventQueue = accumulateInto(eventQueue, events)

}

const processingEventQueue = eventQueue

// 执行完毕之后要清空队列

// 虽然已经这些 event 已经被释放了,但还是会被遍历

eventQueue = null

if (!processingEventQueue) return

// 将这些事件逐个触发

// forEachAccumulated 是 React 自己实现的 foreach

forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel)

}

// 触发一个事件并且立刻将事件释放到事件池中,除非执行了presistent

const executeDispatchesAndRelease = function (event: ReactSyntheticEvent) {

if (event) {

// 按照次序依次触发和该事件类型绑定的所有 listener

executeDispatchesInOrder(event)

}

// 如果没有执行 persist 持久化 , 立即销毁事件

if (!event.isPersistent()) {

(event.constructor as any).release(event)

}

}

可以看到合成事件的构造函数实例上挂载了一个函数 release ,用来释放事件。我们看一看 SyntheticEvent 的代码,可以发现这里使用了一个事件池的概念 eventPool 。

Object.assign(SyntheticEvent.prototype, {

// 模拟原生的 preventDefault 函数

preventDefault: function() {

this.defaultPrevented = true;

const event = this.nativeEvent;

if (!event) {

return;

}

if (event.preventDefault) {

event.preventDefault();

} else {

event.returnValue = false;

}

this.isDefaultPrevented = functionThatReturnsTrue;

},

// 模拟原生的 stopPropagation

stopPropagation: function() {

const event = this.nativeEvent;

if (!event) {

return;

}

if (event.stopPropagation) {

event.stopPropagation();

} else {

event.cancelBubble = true;

}

this.isPropagationStopped = functionThatReturnsTrue;

},

/**

  • 在每次事件循环之后,所有被 dispatch 过的合成事件都会被释放

  • 这个函数能够允许一个引用使用事件不会被 GC 回收

*/

persist: function() {

this.isPersistent = functionThatReturnsTrue;

},

/**

  • 这个 event 是否会被 GC 回收

*/

isPersistent: functionThatReturnsFalse,

/**

  • 销毁实例

  • 就是将所有的字段都设置为 null

*/

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;

},

});

React 在构造函数上直接添加了一个事件池属性,其实就是一个数组,这个数组将被全局共用。每当事件被释放的时候,如果线程池的长度还没有超过规定的大小(默认是 10 ),那么这个被销毁后的事件就会被放进事件池

// 为合成事件构造函数添加静态属性

// 事件池为所有实例所共用

function addEventPoolingTo (EventConstructor) {

EventConstructor.eventPool = []

EventConstructor.getPooled = getPooledEvent

EventConstructor.release = releasePooledEvent

}

// 将事件释放

// 事件池有容量的话,放进事件池

function releasePooledEvent (event) {

const EventConstructor = this

event.destructor()

if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {

EventConstructor.eventPool.push(event)

}

}

我们都知道单例模式,就是对于一个类在全局最多只会有一个实例。而这种事件池的设计相当于是 n 例模式,每次事件触发完毕之后,实例都要还给构造函数放进事件池,后面的每次触发都将复用这些干净的实例,从而减少内存方面的开销。

// 需要事件实例的时候直接从事件池中取出

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

)

}

如果在短时间内浏览器事件被频繁触发,那么将出现的现象是,之前事件池中的实例都被取出复用,而后续的合成事件对象就只能被老老实实重新创建,结束的时候通过放弃引用来被 V8 引擎的 GC 回收。

回到之前的事件触发,如果不特地将属性名写成 onClickCapture 的话,那么默认将被触发的就会是冒泡过程。这个过程也是 React 模拟的,就是通过 fiber 逐层向上触发的方式,捕获过程也是同理。

我们都知道正常的事件触发流程是:

  1. 事件捕获

  2. 处于事件

  3. 事件冒泡

处于事件 阶段是一个 try-catch 语句,这样即使发生错误也会处于 React 的错误捕获机制当中。我们真正想要执行的函数实体就是在此被触发:

export default function invodeGuardedCallbackImpl<

A,

B,

C,

D,

E,

F,

Context

(

name: string | null,

func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,

context?: Context,

a?: A,

b?: B,

c?: C,

d?: D,

e?: E,

f?: F,

): void {

const funcArgs = Array.prototype.slice.call(arguments, 3)

try {

func.apply(context, funcArgs)

} catch (error) {

this.onError(error)

}

}

类与函数

====

当我们使用类组件或是函数组件的时候,最终目的都是为了得到一份 JSX 来描述我们的页面。那么其中就存在着一个问题—— React 是如何分辨函数组件和类组件的。

虽然在 ES6 中,我们可以轻易的看出 Class 和 函数的区别,但是别忘了,我们实际使用的往往是 babel 编译后的代码,而类就是函数和原型链构成的语法糖。可能大部分人最直接的想法就是,既然类组件继承了 React.Component ,那么应该可以直接使用类类型判断就就行:

App instanceof React.Component

当然, React 采用的做法是在原型链上添加一个标识

Component.prototype.isReactComponent = {}

源码中需要判断是否是类组件的时候,就可以直接读取函数的 isReactComponent 属性时,因为在函数(也是对象)自身找不到时,就会向上游原型链逐级查找,直到到达 Object.prototype 对象为止。

为什么 isReactComponent 是一个对象而不是布尔以及为什么不能用 instanceOf [5]

状态的更新

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
,而后续的合成事件对象就只能被老老实实重新创建,结束的时候通过放弃引用来被 V8 引擎的 GC 回收。

回到之前的事件触发,如果不特地将属性名写成 onClickCapture 的话,那么默认将被触发的就会是冒泡过程。这个过程也是 React 模拟的,就是通过 fiber 逐层向上触发的方式,捕获过程也是同理。

我们都知道正常的事件触发流程是:

  1. 事件捕获

  2. 处于事件

  3. 事件冒泡

处于事件 阶段是一个 try-catch 语句,这样即使发生错误也会处于 React 的错误捕获机制当中。我们真正想要执行的函数实体就是在此被触发:

export default function invodeGuardedCallbackImpl<

A,

B,

C,

D,

E,

F,

Context

(

name: string | null,

func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,

context?: Context,

a?: A,

b?: B,

c?: C,

d?: D,

e?: E,

f?: F,

): void {

const funcArgs = Array.prototype.slice.call(arguments, 3)

try {

func.apply(context, funcArgs)

} catch (error) {

this.onError(error)

}

}

类与函数

====

当我们使用类组件或是函数组件的时候,最终目的都是为了得到一份 JSX 来描述我们的页面。那么其中就存在着一个问题—— React 是如何分辨函数组件和类组件的。

虽然在 ES6 中,我们可以轻易的看出 Class 和 函数的区别,但是别忘了,我们实际使用的往往是 babel 编译后的代码,而类就是函数和原型链构成的语法糖。可能大部分人最直接的想法就是,既然类组件继承了 React.Component ,那么应该可以直接使用类类型判断就就行:

App instanceof React.Component

当然, React 采用的做法是在原型链上添加一个标识

Component.prototype.isReactComponent = {}

源码中需要判断是否是类组件的时候,就可以直接读取函数的 isReactComponent 属性时,因为在函数(也是对象)自身找不到时,就会向上游原型链逐级查找,直到到达 Object.prototype 对象为止。

为什么 isReactComponent 是一个对象而不是布尔以及为什么不能用 instanceOf [5]

状态的更新

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-rGxEbryY-1715813316377)]

[外链图片转存中…(img-ZxkJYaQi-1715813316378)]

[外链图片转存中…(img-MDoWjLPj-1715813316378)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值