做者: 凹凸曼-风魔小次郎javascript
谁都没有看见过风,更不用说你和我了。可是当纸币在飘的时候,咱们知道那是风在数钱。
React 影响着咱们工做的方方面面,咱们天天都在使用它,只窥其表却难以窥其里。正所谓看不如写,本篇文章的目的就是从原理层面探究 React 是如何工做的。css
在写文章以前,为了方便理解,我准备了一个懒人调试仓库 simple_react ,这个仓库将 benchmark 用例(只有两个 ^ ^)和 React 源码共同放在 src 文件夹中,经过 snowpack 进行热更新,能够直接在源码中加入 log 和 debuger 进行调试。固然这里的“源码”并非真的源码,由于 React 源码中充斥着巨量的 dev 代码和不明确的功能函数,因此我对源码进行了整理,用 typescript 对类型进行了规范,删除了大量和核心流程无关的代码(固然也误删了一些有关的 ^ ^)。html
若是你只是但愿了解 React 的运行流程而不是写一个能够用的框架的话,那么这个仓库彻底能够知足你学习的须要。固然,这个仓库基于 React16.8 ,虽然这个版本并不包括当前的航道模型 Lane 等新特性,可是是我我的认为比较稳定且更适合阅读的一个版本。前端
(若是但愿调试完整的源码,也能够参考 拉取源码 经过 yarn link 来进行 debug)java
在了解 React 是如何工做以前,咱们应该确保了解几点有关 React 的基础知识。node
首先,咱们须要知道使用框架对于开发的意义是什么。若是咱们还处于远古时期使用纯 JS 的阶段,每次数据的改变都会引起组件的展现状态改变,所以咱们须要去手动的操做 DOM 。若是在某一秒内,数据异步的连续改变了几十次,根据展现逻辑咱们也须要连续对 DOM 进行几十次修改。频繁的 DOM 操做对网页性能的影响是很大的,固然,建立 DOM 元素和修改 DOM 元素的属性都不过度消耗性能,主要在于每次将新的 DOM 插入 document 都会致使浏览器从新计算布局属性,以及各个视图层、合并、渲染。因此,这样的代码性能是十分低下的。react
能够试想这样一个场景。对于一个前端列表组件而言,当存在 3 条数据的时候展现 3 条,当存在 5 条数据的时候展现 5 条。也就是说 UI 的呈如今某种程度上必然会和数据存在某种逻辑关系。若是 JS 可以感知到关键数据的改变,使用一种高效的方式将 DOM 改写成与数据相对应的状态。那么于开发者而言,就能够专一于业务逻辑和数据的改变,工做效率也会大幅提升。git
因此, 框架 最核心的功能之一就是 高效地 达成 UI 层和数据层的统一。github
React 自己并非框架, React 只是一个 JavaScript 库,他的做用是经过组件构建用户界面,属于 MVC 应用中的 View 视图层。 React 经过 props 和 state 来简化关键数据的存储,对于一个 react 组件函数而言,在 1 秒内可能被执行不少次。而每一次被执行,数据被注入 JSX , JSX 并非真实的 DOM ,在 React 中会被转换成 React.createElement(type, props, children)
函数,执行的结果就是 ReactElement 元素 ,也便是 虚拟 DOM ,用来描述在浏览器的某一帧中,组件应该被呈现为何样子。typescript
VirtualDom 并不是 React 专属,就像 redux 也能够在非 React 环境下使用同样,它们只是一种设计的思路。
事实上, React 在使用 fiber 架构以前的 Virtual Dom 和 diff 过程要相对直观一些。可是在引入了 fiber 架构以后整个流程变得冗长,若是单纯想了解 VirtualDom 和 diff 过程的原理也能够经过 simple-virtual-dom 这个仓库来学习。
VirtualDom 的本质是利用 JS 变量 对真实 DOM 进行抽象,既然每一次操做 DOM 均可能触发浏览器的重排消耗性能,那么就可使用 VirtualDom 来缓存当前组件状态,对用户交互和数据的变更进行批次处理,直接计算出每一帧页面应该呈现的最终状态,而这个状态是以 JS 变量 的形式存在于内存中的。因此经过 VirtualDom 既可以保证用户看到的每一帧都响应了数据的变化,又能节约性能保证浏览器不出现卡顿。
首先咱们应该注意到 React(浏览器环境) 代码的入口 render 函数
ReactDOM.render(<App />, domContainer)
这个 render 过程当中, React 须要作到的是根据用户创造的 JSX 语法,构建出一个虚拟的树结构(也就是 ReactElement 和 Fiber )来表示用户 指望中 页面中的元素结构。固然对于这个过程相对并不复杂(误),由于此时的 document 内仍是一片虚无。就思路上而言,只须要根据虚拟 DOM 节点生成真实的 DOM 元素而后插入 document ,第一次渲染就算圆满完成。
一般咱们会经过 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 、点击事件等等。
第三个第四个属性分别是 key
和 ref
,其中 key
在数组的处理和 diff 过程当中有重要做用,而 ref
则是引用标识,在这里就先不作过多介绍。
最后一个属性是 $$typeof
,这个属性会指向 Symbol(React.element)
。做为 React 元素的惟一标识的同时,这个标签也承担了安全方面的功能。咱们已经知道了所谓的 ReactElement 其实就是一个 JS 对象。那么若是有用户恶意的向服务端数据库中存入了某个有侵入性功能的 伪 React 对象,在实际渲染过程当中被当作页面元素渲染,那么将有可能威胁到用户的安全。而 Symbol
是没法在数据库中被存储的,换句话说, React 所渲染的全部元素,都必须是由 JSX 编译的拥有 Symbol
标识的元素。(若是在低版本不支持 Symbol 的浏览器中,将会使用字符串替代,也就没有这层安排保护了)
ok,接下来回到 render 函数。在这个函数中到底发生了什么呢,简单来讲就是建立 Root
结构。
从设计者的角度,根据 单一职责原则 和 开闭口原则 须要有与函数体解耦的数据结构来告诉 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 被设计成了链表结构。
若是是 React16 以前的树状结构,就须要经过 DFS 深度遍从来查找每个节点。而如今只须要将指针按照 child → sibling → return 的优先级移动,就能够处理全部的节点
这样设计还有一个好处就是在 React 工做的时候只须要使用一个全局变量做为指针在链表中不断移动,若是出现用户输入或其余优先级更高的任务就能够 暂停 当前工做,其余任务结束后只须要根据指针的位置继续向下移动就能够继续以前的工做。指针移动的规律能够概括为 自顶向下,从左到右 。
康康 fiber 的基本结构
其中
stateNode 表明这个 fiber 节点对应的真实状态
接下来是初次渲染的几个核心步骤,由于是初次渲染,核心任务就是将首屏元素渲染到页面上,因此这个过程将会是同步的。
由于笔者是土货没学过英语,百度了下发现是 准备干净的栈 的意思。结合了下流程,能够看出这一步的做用是在真正工做以前作一些准备,例如初始化一些变量,放弃以前未完成的工做,以及最重要的—— 建立双向缓冲变量 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 }
这里简称 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。
在工做循环中,将会执行一个 while
语句,每执行一次循环,都会完成对一个 fiber
节点的处理。在 workLoop 模块中有一个指针 workInProgress 指向当前正在处理的 fiber ,它会不断向链表的尾部移动,直到指向的值为 null ,就中止这部分工做, workLoop 的部分也就结束了。
每处理一个 fiber 节点都是一个工做单元,结束了一个工做单元后 React 会进行一次判断,是否须要暂停工做检查有没有更高优先级的用户交互进来。
function workLoopConcurrent() { // 执行工做直到 Scheduler 要求咱们 yield while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); } }
跳出条件只有:
可是咱们如今讨论的是第一次渲染,触屏渲染的优先级高于一切,因此并不存在第二个限制条件。
function workLoopSync () { // 只要没有完成reconcile就一直执行 while(workInProgress !== null) { workInProgress = performUnitOfWork(workInProgress as Fiber) } }
单元工做 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 个点击事件呢。事件委托的收益就是:
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 逐层向上触发的方式,捕获过程也是同理。
咱们都知道正常的事件触发流程是:
处于事件 阶段是一个 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() ) }
决定一个任务当前是否应该被执行有两个因素。
若是一个任务的过时时间已经到了必须执行,那么这个任务就应该处于 待执行队列 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),不定时推送文章。