关于javascript:React-入门儿

12次阅读

共计 30018 个字符,预计需要花费 76 分钟才能阅读完成。

作者:凹凸曼 - 风魔小次郎

谁都没有看见过风,更不用说你和我了。然而当纸币在飘的时候,咱们晓得那是风在数钱。

React 影响着咱们工作的方方面面,咱们每天都在应用它,只窥其表却难以窥其里。正所谓看不如写,本篇文章的目标就是从原理层面探索 React 是如何工作的。

工具

在写文章之前,为了不便了解,我筹备了一个懒人调试仓库 simple_react,这个仓库将 benchmark 用例(只有两个 ^ ^)和 React 源码独特放在 src 文件夹中,通过 snowpack 进行热更新,能够间接在源码中退出 log 和 debuger 进行调试。当然这里的“源码”并不是真的源码,因为 React 源码中充斥着巨量的 dev 代码和不明确的性能函数,所以我对源码进行了整顿,用 typescript 对类型进行了标准,删除了大量和外围流程无关的代码(当然也误删了一些无关的 ^ ^)。

如果你只是心愿理解 React 的运行流程而不是写一个能够用的框架的话,那么这个仓库齐全能够满足你学习的须要。当然,这个仓库基于 React16.8,尽管这个版本并不包含以后的航道模型 Lane 等新个性,然而是我集体认为比较稳定且更适宜浏览的一个版本。

(如果心愿调试残缺的源码,也能够参考 拉取源码 通过 yarn link 来进行 debug)

文章构造

  1. fiber 架构设计及首次渲染流程
  2. 事件委托机制
  3. 状态的更新
  4. 工夫片

在理解 React 是如何工作之前,咱们应该确保理解几点无关 React 的基础知识。

Why Framework

首先,咱们须要晓得应用框架对于开发的意义是什么。如果咱们还处于远古期间应用纯 JS 的阶段,每次数据的扭转都会引发组件的展现状态扭转,因而咱们须要去手动的操作 DOM。如果在某一秒内,数据异步的间断扭转了几十次,依据展现逻辑咱们也须要间断对 DOM 进行几十次批改。频繁的 DOM 操作对网页性能的影响是很大的,当然,创立 DOM 元素和批改 DOM 元素的属性都不过分耗费性能,次要在于每次将新的 DOM 插入 document 都会导致浏览器从新计算布局属性,以及各个视图层、合并、渲染。所以,这样的代码性能是非常低下的。

能够试想这样一个场景。对于一个前端列表组件而言,当存在 3 条数据的时候展现 3 条,当存在 5 条数据的时候展现 5 条。也就是说 UI 的出现在某种程度上必然会和数据存在某种逻辑关系。如果 JS 可能感知到要害数据的扭转,应用一种高效的形式将 DOM 改写成与数据绝对应的状态。那么于开发者而言,就能够专一于业务逻辑和数据的扭转,工作效率也会大幅提高。

所以,框架 最外围的性能之一就是 高效地 达成 UI 层和数据层的对立。

React 哲学

React 自身并不是框架,React 只是一个 JavaScript 库,他的作用是通过组件构建用户界面,属于 MVC 利用中的 View 视图层。React 通过 props 和 state 来简化要害数据的存储,对于一个 react 组件函数而言,在 1 秒内可能被执行很屡次。而每一次被执行,数据被注入 JSX,JSX 并不是实在的 DOM,在 React 中会被转换成 React.createElement(type, props, children) 函数,执行的后果就是 ReactElement 元素,也即是 虚构 DOM,用来形容在浏览器的某一帧中,组件应该被出现为什么样子。

Virtual Dom

VirtualDom 并非 React 专属,就像 redux 也能够在非 React 环境下应用一样,它们只是一种设计的思路。

事实上,React 在应用 fiber 架构之前的 Virtual Dom 和 diff 过程要绝对直观一些。然而在引入了 fiber 架构之后整个流程变得简短,如果单纯想理解 VirtualDom 和 diff 过程的原理也能够通过 simple-virtual-dom 这个仓库来学习。

VirtualDom 的实质是利用 JS 变量 对实在 DOM 进行形象,既然每一次操作 DOM 都可能触发浏览器的重排耗费性能,那么就能够应用 VirtualDom 来缓存以后组件状态,对用户交互和数据的变动进行批次解决,间接计算出每一帧页面应该出现的最终状态,而这个状态是以 JS 变量 的模式存在于内存中的。所以通过 VirtualDom 既可能保障用户看到的每一帧都响应了数据的变动,又能节约性能保障浏览器不呈现卡顿。

第一次渲染 First Render

首先咱们应该留神到 React(浏览器环境) 代码的入口 render 函数

ReactDOM.render(<App />, domContainer)

这个 render 过程中,React 须要做到的是依据用户发明的 JSX 语法,构建出一个虚构的树结构(也就是 ReactElement 和 Fiber)来示意用户 冀望中 页面中的元素构造。当然对于这个过程绝对并不简单(误),因为此时的 document 内还是一片虚无。就思路上而言,只须要依据虚构 DOM 节点生成实在的 DOM 元素而后插入 document,第一次渲染就算圆满完成。

createReactElement

通常咱们会通过 Babel 将 JSX 转换为一个 JS 执行函数。例如咱们在 React 环境下用 JSX 中写了一个题目组件

<h1 className='title'>
<div>Class Component</div>
</h1>

那么这个组件被 Babel 转换之后将会是

React.createElement('h1', { className: 'title'}, [React.createElement('div', null, [ 'Class Component']
])

传统编译考究一个 JSON 化,当然 JSX 和 React 也没有什么关系,JSX 只是 React 举荐的一种拓展语法。当然你也能够不必 JSX 间接应用 React.createElement 函数,然而比照下面的两种写法你就也能晓得,应用纯 JS 的心智老本会比扼要可见的 JSX 高多少。咱们能够看出,React.createElement 须要接管 3 个参数,别离是 DOM 元素的标签名,属性对象以及一个子元素数组,返回值则是一个 ReactElement 对象。

事实上,JSX 编译后的 json 构造自身就是一个对象,即便不执行 React.createElement 函数也曾经初步能够应用了。那么在这个函数中咱们做了什么呢。

一个 ReactElement 元素次要有 5 个要害属性,咱们都晓得要构建成一个页面须要通过 html 形容元素的类型和构造,通过 style 和 class 去形容元素的款式出现,通过 js 和绑定事件来触发交互事件和页面更新。

所以最重要的是第一个属性,元素类型 type。如果这个元素是一个纯 html 标签元素,例如 div,那么 type 将会是字符串 div,如果是一个 React 组件,例如

function App() {
return (<div>Hello, World!</div>)
}

那么 type 的值将会指向 App 函数,当然 Class 组件 也一样(家喻户晓 ES6 的 Class 语法自身就是函数以及原型链形成的语法糖)

第二个属性是 props,咱们在 html 标签中写入的大部分属性都会被收集在 props 中,例如 id、className、style、children、点击事件等等。

第三个第四个属性别离是 keyref,其中 key 在数组的解决和 diff 过程中有重要作用,而 ref 则是援用标识,在这里就先不做过多介绍。

最初一个属性是 $$typeof,这个属性会指向 Symbol(React.element)。作为 React 元素的惟一标识的同时,这个标签也承当了平安方面的性能。咱们曾经晓得了所谓的 ReactElement 其实就是一个 JS 对象。那么如果有用户歹意的向服务端数据库中存入了某个有侵入性功能的 伪 React 对象,在理论渲染过程中被当做页面元素渲染,那么将有可能威逼到用户的平安。而 Symbol 是无奈在数据库中被存储的,换句话说,React 所渲染的所有元素,都必须是由 JSX 编译的领有 Symbol 标识的元素。(如果在低版本不反对 Symbol 的浏览器中,将会应用字符串代替,也就没有这层安顿爱护了)

ok,接下来回到 render 函数。在这个函数中到底产生了什么呢,简略来说就是创立 Root 构造。

enqueueUpdate

从设计者的角度,依据 繁多职责准则 开闭口准则 须要有与函数体解耦的数据结构来通知 React 应该怎么操作 fiber。而不是首次渲染写一套逻辑,第二次渲染写一套逻辑。因而,fiber 上有了更新队列 UpdateQueue 和 更新链表 Update 构造

如果查看一下相干的定义就会发现,更新队列 updateQueue 是多个更新组成的链表构造,而 update 的更新也是一个链表,至于为什么是这样设计,试想在一个 Class Component 的更新函数中间断执行了 3 次 setState,与其将其作为 3 个更新挂载到组件上,不如提供一种更小粒度的管制形式。一句话概括就是,setState 级别的小更新合并成一个状态更新,组件中的多个状态更新在组件的更新队列中合并,就可能计算出组件的新状态 newState

对于首次渲染而言,只须要在第一个 fiber 上,挂载一个 update 标识这是一个首次渲染的 fiber 即可。

// 更新根节点
export function ScheduleRootUpdate (
  current: Fiber,
  element: ReactElement,
  expirationTime: number,
  suspenseConfig: SuspenseConfig | null,
  callback?: Function
) {
  // 创立一个 update 实例
  const update = createUpdate(expirationTime, suspenseConfig)
  // 对于作用在根节点上的 react element
  update.payload = {element}

  // 将 update 挂载到根 fiber 的 updateQueue 属性上
  enqueueUpdate(
    current,
    update
  )

  ScheduleWork(
    current,
    expirationTime
  )
}

Fiber

作为整个 Fiber 架构 中最外围的设计,Fiber 被设计成了链表构造。

  • child 指向以后节点的第一个子元素
  • 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 (<div onClick={handleClick}>O2</div>
)
}

如果咱们须要异步的获取这次点击事件在屏幕中的地位并且做出相应解决,那么在 setTimeout 中是否达到目标呢。

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

当然 React 也提供了应用异步事件对象的解决方案,它提供了一个 persist 函数,能够让事件不再进入事件池。(在 React17 中为了解决某些 issue,曾经重写了合成事件机制,事件不再由 document 来代理,官网的说法是合成事件不再由事件池治理,也没有了 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<ReactSyntheticEvent>(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

状态的更新

之前咱们曾经看懂了 React 的事件委托机制,那么不如在一次点击事件中尝试批改组件的状态来更新咱们的页面。

首先康康 setState 是如何工作的,咱们晓得 this.setState 是 React.Component 类中的办法:

/**
* @description 更新组件 state
* @param {object | Function} partialState 下个阶段的状态
* @param {?Function} callback 更新结束之后的回调
*/
Component.prototype.setState = function (partialState, callback) {
 if (!(isObject(partialState) ||
   isFunction(partialState) ||
   isNull
 )) {console.warn('setState 的第一个参数应为对象、函数或 null')
   return
 }
 this.updater.enqueueSetState(this, partialState, callback, 'setState')
}

看起来外围步骤就是触发挂载在实例上的一个 updater 对象。默认的,updater 会是一个展位的空对象,尽管实现了 enqueueSetState 等办法,然而这些办法外部都是空的。

// 咱们初始化这个默认的 update,真正的 updater 会被 renderer 注入
this.updater = updater || ReactNoopUpdateQueue
export const ReactNoopUpdateQueue = {
 /**
  * 查看组件是否曾经挂载
  */
 isMounted: function (publishInstance) {
   // 初始化 ing 的组件就别挂载不挂载了
   return false
 },

 /**
  * 强制更新
  */
 enqueueForceUpdate: function (publishInstance, callback, callerName) {console.warn('enqueueForceUpdate', publishInstance)
 },

 /**
  * 间接替换整个 state, 通常用这个或者 setState 来更新状态
  */
 enqueueReplaceState: function (
   publishInstance,
   completeState,
   callback,
   callerName
 ) {console.warn('enqueueReplaceState', publishInstance)
 },

 /**
  * 批改局部 state
  */
 enqueueSetState: function (
   publishInstance,
   partialState,
   callback,
   callerName
 ) {console.warn('enqueueSetState', publishInstance)
 }
}

还记得咱们在 render 的过程中,是通过执行 Component.render() 来取得一个类组件的实例,当 React 失去了这个实例之后,就会将实例的 updater 替换成真正的 classComponentUpdater:

function adoptClassInstance (
 workInProgress: Fiber,
 instance: any
): void {
 instance.updater = classComponentUpdate
 ...
}

刚刚咱们触发了这个对象中的 enqueueSetState 函数,那么能够看看实现:

const classComponentUpdate = {
 isMounted,
 /**
  * 触发组件状态的更新
  * @param inst ReactElement
  * @param payload any
  * @param callback 更新完结之后的回调
  */
 enqueueSetState(
   inst: ReactElement,
   payload: any,
   callback?: Function
 ) {
   // ReactElement -> fiber
   const fiber = getInstance(inst)
   // 以后工夫
   const currentTime = requestCurrentTime()
   // 获取以后 suspense config
   const suspenseConfig = requestCurrentSuspenseConfig()
   // 计算以后 fiber 节点的工作过期工夫
   const expirationTime = computeExpirationForFiber(
     currentTime,
     fiber,
     suspenseConfig
   )

   // 创立一个 update 实例
   const update = createUpdate(expirationTime, suspenseConfig)
   update.payload = payload
   // 将 update 装载到 fiber 的 queue 中
   enqueueUpdate(fiber, update)
   // 安顿工作
   ScheduleWork(fiber, expirationTime)
 },
 ...
}

显然,这个函数的作用就是取得类组件对应的 fiber,更新它在任务调度器中的过期工夫(领导给了新工作,天然要定新的 Deadline),而后就是创立一个新的 update 工作装载到 fiber 的工作队列中。最初通过 ScheduleWork(通知任务调度器来工作了,连忙干活)要求从这个 fiber 开始和谐,至于和谐和更新的步骤咱们在第一次渲染中曾经有了大抵的理解。

顺带提一提 Hooks 中的 useState。网络上有挺多解说 hook 实现的文章曾经讲得很全面了,咱们只须要搞清楚以下几点问题。

Q1. 函数组件不像类组件一样领有实例,数据存储在哪里

A1. 任何以 ReactElement 为粒度的组件都须要围绕 fiber,数据存储在 fiber.memorizedState 上

Q2. useState 的实现

A2. 如果你听过了 useState 那么你就应该听过 useReducer,如果听过 reducer 就应该晓得 redux。首先,useState 的实质就是 useReducer 的语法糖。咱们都晓得构建一个状态库须要一个 reducer,useState 就是当 reducer 函数为 a => a 时的非凡状况。

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {return typeof action === 'function' ? action(state) : action
}

function updateState<S>(initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {return updateReducer<S, (() => S) | S, any>(basicStateReducer, initialState)
}

Q3. 为什么 Hooks 的程序和个数不容许扭转

A3. 每次执行 Hooks 函数须要取出上一次渲染时数据的最终状态,因为构造是链表而不是一个 Map,所以这些最终状态也会是有序的,所以如果个数和秩序扭转会导致数据的错乱。

工夫调度机制

尽管往年过期工夫 expirationTime 机制曾经被淘汰了,然而不论是航道模型还是过期工夫,实质上都是工作优先级的不同体现模式。

在探索运行机制之前咱们须要晓得一个问题就是,为什么工夫片的性能会优于同步计算的性能。此处借用司徒正美老师文章中的例子。

试验 1,通过 for 循环一次性向 document 中插入 1000 个节点

function randomHexColor(){return "#" + ("0000"+ (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
}
setTimeout(function() {
   var k = 0;
   var root = document.getElementById("root");
   for(var i = 0; i < 10000; i++){
       k += new Date - 0 ;
       var el = document.createElement("div");
       el.innerHTML = k;
       root.appendChild(el);
       el.style.cssText =  background:${randomHexColor()};height:40px ;
   }
}, 1000);

试验 2,进行 10 次 setTimeout 分批次操作,每次插入 100 个节点

function randomHexColor() {return "#" + ("0000" + (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
}
var root = document.getElementById("root");
setTimeout(function () {function loop(n) {
       var k = 0;
       console.log(n);
       for (var i = 0; i < 100; i++) {
           k += new Date - 0;
           var el = document.createElement("div");
           el.innerHTML = k;
           root.appendChild(el);
           el.style.cssText =  background:${randomHexColor()};height:40px ;
       }
       if (n) {setTimeout(function () {loop(n - 1);
           }, 40);
       }
   }
   loop(100);
}, 1000);

雷同的后果,第一个试验破费了 1000 ms,而第二个试验仅仅破费了 31.5 ms。

这和 V8 引擎的底层原理无关,咱们都晓得浏览器是单线程,一次性须要做到 GUI 描述,事件处理,JS 执行等多个操作时,V8 引擎会优先对代码进行执行,而不会对执行速度进行优化。如果咱们略微给浏览器一些工夫,浏览器就可能进行 JIT,也叫热代码优化。

简略来说,JS 是一种解释型语言,每次执行都须要被编译成字节码能力被运行。然而如果某个函数被屡次执行,且参数类型和参数个数始终保持不变。那么这段代码会被辨认为 热代码 ,遵循着“万物皆可空间换工夫”的准则,这段代码的字节码会被缓存,下次再次运行的时候就会间接被运行而不须要进行耗时的解释操作。也就是 解释器 + 编译器 的模式。

做个比喻来说,咱们工作不能始终蛮干,必须要给本人一些工夫进行反思和总结,否则工作速度和效率始终是线性的,人也不会有提高。

还记得在 WorkLoop 函数中,每次解决完一个 fiber 都会跳出循环执行一次 shouldYield 函数进行判断,是否应该将执行权交还给浏览器解决用户工夫或是渲染。看看这个 shouldYield 函数的代码:

// 以后是否应该阻塞 react 的工作
function shouldYield (): boolean {
 // 获取以后的工夫点
 const currentTime = getCurrentTime()

 // 查看工作队列中是否有工作须要执行
 advanceTimers(currentTime)

 // 取出工作队列中工作优先级最高的工作
 const firstTask = peek(taskQueue)

 // 以下两种状况须要 yield
 // 1. 当前任务队列中存在工作,且第一个工作的开始工夫还没到,且过期工夫小于当前任务
 // 2. 处于固定的浏览器渲染工夫区间
 return (
   (
     currentTask !== null &&
     firstTask !== null &&
     (firstTask as any).startTime <= currentTime &&
     (firstTask as any).expirationTime < currentTask.expirationTime
   )
   // 以后处于工夫片的阻塞区间
   || shouldYieldToHost())
}

决定一个工作以后是否应该被执行有两个因素。

  1. 这个工作是否非执行不可,正所谓所有的不论是不是先问为什么都是耍流氓。如果到期工夫还没到,为什么不先把线程空进去留给可能的高优先级工作呢。
  2. 如果多个工作都非执行不可,那么工作的优先级是否是以后队列中最高的。

如果一个工作的过期工夫曾经到了必须执行,那么这个工作就应该处于 待执行队列 taskQueue 中。相同这个工作的过期工夫还没到,就能够先放在 提早列表 中。每一帧完结的时候都会执行 advanceTimer 函数,将一些提早列表中到期的工作取出,插入待执行队列。

可能是出于最佳实际思考,待执行队列是一个小根堆构造,而提早队列是一个有序链表。

回忆一下 React 的任务调度要求,当一个新的优先级更高的工作产生,须要可能打断之前的工作并插队。也就是说,React 须要维持一个始终有序的数组数据结构。因而,React 自实现了一个小根堆,然而这个小根堆无需像堆排序的后果一样整体有序,只须要保障每次进行 push 和 pop 操作之后,优先级最高的工作可能达到堆顶。

所以 shouldYield 返回 true 的一个要害条件就是,以后 taskQueue 堆中的堆顶工作的过期工夫曾经到了,那么就应该暂停工作交出线程使用权。

那么待执行的工作是如何被执行的呢。这里咱们须要先理解 MessageChannel 的概念。Message

Channel 的实例会领有两个端口,其中第一个端口为发送信息的端口,第二个端口为接管信息的端口。当接管到信息就能够执行指定的回调函数。

const channel = new MessageChannel()
// 发送端
const port = channel.port2
// 接收端
channel.port1.onmessage = performWorkUntilDeadline // 在肯定工夫内尽可能的解决工作

每当待执行工作队列中有工作的时候,就会通过 Channel 的发送端发送一个空的 message,当接收端异步地接管到这个信号的时候,就会在一个工夫片内尽可能地执行工作。

// 记录任一时间片的完结时刻
let deadline = 0

// 单位工夫切片长度
let yieldInterval = 5

// 执行工作直到用尽以后工夫片闲暇工夫
function performWorkUntilDeadline () {if (scheduledHostCallback !== null) {
   // 如果有打算工作,那么须要执行

   // 以后工夫
   const currentTime = getCurrentTime()

   // 在每个工夫片之后阻塞(5ms)
   // deadline 为这一次工夫片的完结工夫
   deadline = currentTime + yieldInterval

   // 既然能执行这个函数,就代表着还有工夫残余
   const hasTimeRemaining = true

   try {
     // 将以后阻塞的工作打算执行
     const hasMoreWork = scheduledHostCallback(
       hasTimeRemaining,
       currentTime
     )

     if (!hasMoreWork) {
       // 如果没有工作了, 清空数据
       isMessageLoopRunning = false
       scheduledHostCallback = null
     } else {
       // 如果还有工作,在以后工夫片的结尾发送一个 message event
            // 接收端接管到的时候就将进入下一个工夫片
       port.postMessage(null)
     }
   } catch (error) {port.postMessage(null)
     throw(error)
   }
 } else {
   // 压根没有工作,不执行
   isMessageLoopRunning = false
 }
}

咱们在之前说过,阻塞 WorkLoop 的条件有两个,第一个是工作队列的第一个工作还没到工夫,第二个条件就是 shouldYieldToHost 返回 true,也就是处于工夫片期间。

// 此时是否是【工夫片阻塞】区间
export function shouldYieldToHost () {return getCurrentTime() >= deadline
}

总结一下,工夫调度机制其实就是 fiber 遍历工作 WorkLoop 和调度器中的工作队列抢夺线程使用权的过程。不过区别是前者齐全是同步的过程,只会在每个 while 的间隙去询问 调度器:我是否能够继续执行上来。而在调度器拿到线程使用权的每个工夫片中,都会尽可能的解决工作队列中的工作。

传统武术考究点到为止,以上内容,就是这次 React 原理的全副。在文章中我并没有放出大量的代码,只是放出了一些片段用来佐证我对于源码的一些认识和观点,文中的流程只是一个循序思考的过程,如果须要查看更多细节还是应该从源码动手。

当然文中的很多观点带有主观色调,并不一定就正确,同时我也不认为网络上的其余文章的说法就和 React 被设计时的初衷完全一致,甚至 React 源码中的很多写法也未必完满。不论浏览什么代码,咱们都不要神话它,而是应该辩证的去对待它。总的来说,功过 91 开。

前端世界并不需要第二个 React,咱们学习的意义并不是为了证实咱们对这个框架有如许理解。而是通过窥探这些顶级工程师的实现思路,去欠缺咱们本人的逻辑体系,从而成为一个更加谨严的人。


欢送关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。

正文完
 0