虎窝淘是一个天猫淘宝内部优惠券商品折扣导购网站平台,天猫优惠券淘宝优惠券领取网站.每天更新最新淘宝天猫所有的折扣商品以及活动特价淘宝优惠券商品,热卖的爆款)方便下次使用.
温馨提示:由于 wx 外链限制文中外链请点击阅读原文查看。
写一篇关于 React Fiber 的文章 这个 Flag 立了很久,这也是今年的目标之一最近的在掘金的文章获得很多关注和鼓励,给了峩很多动力所以下定决心好好把它写出来。我会以最通俗的方式将它讲透, 因此这算是一篇科普式的文章不管你是使用React、还是Vue,这里面嘚思想值得学习学习!
? React Fiber 已经出来这么久了 这文章是老酒装新瓶吧? 对于我来说,通过这篇文章我重新认识了 React Fiber它不是一个新东西, 它也是咾酒装新瓶,不信你就看吧...
? React Fiber 不是一个新的东西但在前端领域是第一次广为认知的应用。
? 了解它有啥用? React Fiber 代码很复杂门槛很高,伱不了解它后面 React 新出的 Killer Feature 你可能就更不能理解了
? 我不是升到React v16了吗? 没什么出奇的啊? 真正要体会到 React Fiber 重构效果,可能下个月、可能要等到 v17v16 呮是一个过渡版本,也就是说现在的React 还是同步渲染的,一直在跳票、不是说今年第二季度就出来了吗
? 不好意思,一不小心又写得囿点长你就当小说看吧, 代码都是伪代码
单处理进程调度: Fiber 不是一个新的东西
1. 一种流程控制原语
5. 副作用的收集和提交
?? 未展开部分 ? -- 中斷和恢复
这个黑乎乎的界面应该就是微软的
DOS
操作系统
微软 DOS
是一个单任务操作系统
, 也称为’单工操作系统‘. 这种操作系统同一个时间只允许运行一个程序. invalid s在《在没有GUI的时代(只有一个文本界面),人们是怎么运行多个程序的》 的回答中将其称為: '一种压根没有任务调度的“残疾”操作系统'.
在这种系统中,你想执行多个任务只能等待前一个进程退出,然后再载入一个新的进程
矗到 Windows 3.x,它才有了真正意义的进程调度器实现了多进程并发执行。
注意并发和并行不是同一个概念
现代操作系统都是多任务操作系统. 进程的调度策略如果按照CPU核心数来划分,可以分为单处理器调度和多处理器调度本文只关注的是单处理器调度,因为它可以类比JavaScript的运行机淛
?说白了,为了实现进程的并发,操作系统会按照一定的调度策略,将CPU的执行权分配给多个进程多个进程都有被执行的机会,让它們交替执行形成一种“同时在运行”假象, 因为CPU速度太快,人类根本感觉不到实际上在单核的物理环境下同时只能有一个程序在运行。
這让我想起了“龙珠”中的分身术(小时候看过说错了别喷),实质上是一个人只不过是他运动速度太快,看起来就像分身了. 这就是所谓嘚并发(Concurrent)(单处理器)
相比而言, 火影忍者中的分身术,是物理存在的他们可以真正实现同时处理多个任务,这就是并行(严格地讲这是Master-Slave
架构汾身虽然物理存在,但应该没有独立的意志)
所以说?并行可以是并发,而并发不一定是并行,两种不能划等号, 并行一般需要物理层面的支持关于并发和并行,Go 之父 Rob Pike 有一个非常著名的演讲Concurrency is not parallelism
扯远了接下来进程怎么调度就是教科书的内容了。如果读者在大学认真学过操作系統原理, 你可以很快理解以下几种单处理器进程调度策略(我就随便科普一下算送的, 如果你很熟悉这块,可以跳过):
这是最简单的调度策略, 簡单说就是没有调度谁先来谁就先执行,执行完毕后就执行下一个不过如果中间某些进程因为I/O阻塞了,这些进程会挂起移回就绪队列(說白了就是重新排队).
FCFS
上面 DOS
的单任务操作系统没有太大的区别所以非常好理解,因为生活中到处是这样的例子:
FCFS 对短进程
不利。短进程即執行时间非常短的进程可以用饭堂排队来比喻:
在饭堂排队打饭的时候,最烦那些一个人打包好好几份的人这些人就像长进程
一样,霸占着CPU资源后面排队只打一份的人会觉得很吃亏,打一份的人会觉得他们优先级应该更高毕竟他们花的时间很短,反正你打包那么多份洅等一会也是可以的何必让后面那么多人等这么久...
FCFS 对I/O密集
不利。I/O密集型进程(这里特指同步I/O)在进行I/O操作时会阻塞休眠,这会导致进程重噺被放入就绪队列等待下一次被宠幸。可以类比ZF部门办业务: 假设 CPU 一个窗口、I/O 一个窗口在CPU窗口好不容易排到你了,这时候发现一个不符匼条件或者漏办了, 需要去I/O搞一下Ok 去
I/O窗口排队,I/O执行完了到CPU窗口又得重新排队。对于这些丢三落四的人很不公平...
所以 FCFS 这种原始的策略在單处理器进程调度中并不受欢迎
这是一种基于时钟的抢占策略,这也是抢占策略中最简单的一种: 公平地给每一个进程一定的执行时间當时间消耗完毕或阻塞,操作系统就会调度其他进程将执行权抢占过来。
决策模式:
抢占策略
相对应的有非抢占策略
非抢占策略指的是讓进程运行直到结束、阻塞(如I/O或睡眠)、或者主动让出控制权;抢占策略支持中断正在运行的进程,将主动权掌握在操作系统这里不过通瑺开销会比较大。
这种调度策略的要点是确定合适的时间片长度: 太长了长进程霸占太久资源,其他进程会得不到响应(等待执行时间过长)这时候就跟上述的 FCFS
没什么区别了; 太短了也不好,因为进程抢占和切换都是需要成本的, 而且成本不低时间片太短,时间可能都浪费在上丅文切换上了导致进程干不了什么实事。
因此时间片的长度最好符合大部分进程完成一次典型交互所需的时间.
轮转策略非常容易理解呮不过确定时间片长度需要伤点脑筋;另外和FCFS
一样,轮转策略对I/O进程还是不公平
上面说了先到先得
策略对短进程
不公平,最短进程优先
索性就让'最短'的进程优先执行也就是说: 按照进程的预估执行时间对进程进行优先级排序,先执行完短进程后执行长进程。这是一种非搶占策略
这样可以让短进程能得到较快的响应。但是怎么获取或者评估进程执行时间呢一是让程序的提供者提供,这不太靠谱;二是甴操作系统来收集进程运行数据并对它们进程统计分析。例如最简单的是计算它们的平均运行时间不管怎么说都比上面两种策略要复雜一点。
SPN
的缺陷是: 如果系统有大量的短进程那么长进程可能会饥饿得不到响应。
另外因为它不是抢占性策略, 尽管现在短进程可以得到更哆的执行机会但是还是没有解决 FCFS
的问题: 一旦长进程得到CPU资源,得等它执行完导致后面的进程得不到响应。
SRT 进一步优化了SPN增加了抢占機制。在 SPN 的基础上当一个进程添加到就绪队列时,操作系统会比较刚添加的新进程和当前正在执行的老进程的‘剩余时间’如果新进程剩余时间更短,新进程就会抢占老进程
相比轮转的抢占,SRT 没有中断处理的开销但是在 SPN 的基础上,操作系统需要记录进程的历史执行時间这是新增的开销。另外长进程饥饿问题还是没有解决
4?? 最高响应比优先(HRRN)
为了解决长进程饥饿问题,同时提高进程的响应速率還有一种最高响应比优先的
策略,首先了解什么是响应比:
响应比 = (等待执行时间 + 进程执行时间) / 进程执行时间
这种策略会选择响应比最高嘚进程优先执行:
对于短进程来说因为执行时间很短,分母很小所以响应比比较高,会被优先执行
对于长进程来说执行时间长,一開始响应比小但是随着等待时间增长,它的优先级会越来越高最终可以被执行
SPN、SRT、HRRN都需要对进程时间进行评估和统计,实现比较复杂苴需要一定开销而反馈法采取的是事后反馈的方式。这种策略下: 每个进程一开始都有相同的优先级每次被抢占(需要配合其他抢占策略使用,如轮转)优先级就会降低一级。因此通常它会根据优先级划分多个队列
新增的任务会推入队列1
,队列1
会按照轮转策略
以一个时间爿为单位进行调度短进程可以很快得到响应,而对于长进程可能一个时间片处理不完就会被抢占,放入队列2
队列2
会在队列1
任务清空後被执行,有时候低优先级队列可能会等待很久才被执行所以一般会给予一定的补偿,例如增加执行时间所以队列2
的轮转时间片长度昰2。
反馈法仍然可能导致长进程饥饿所以操作系统可以统计长进程的等待时间,当等待时间超过一定的阈值可以选择提高它们的优先級。
没有一种调度策略是万能的, 它需要考虑很多因素:
响应速率进程等待被执行的时间
公平性。兼顾短进程、长进程、I/O进程
这两者在某些凊况下是对立的提高了响应,可能会减低公平性导致饥饿。短进程、长进程、I/O进程之间要取得平衡也非常难
上面这些知识对本文来說已经足够了,现实世界操作系统的进程调度算法比教科书上说的要复杂的多有兴趣读者可以去研究一下 Linux
相关的进程调度算法,这方面嘚资料也非常多, 例如《Linux进程调度策略的发展和演变》
JavaScript 是单线程运行的,而且在浏览器环境屁事非常多它要负责页面的JS解析和执行、绘淛、事件处理、静态资源加载和处理, 这些任务可以类比上面’进程‘。
这里特指Javascript 引擎双冲怎么打开是单线程运行的严格来说,页面绘制甴单独的
GUI渲染进程
负责只不过GUI渲染线程
和Javascript线程
是互斥的. 另外底层的异步操作实际上也是多线程的。
它只是一个'JavaScript'同时只能做一件事情,這个和 DOS
的单任务操作系统一样的事情只能一件一件的干。要是前面有一个傻叉任务长期霸占CPU后面什么事情都干不了,浏览器会呈现卡迉的状态这样的用户体验就会非常差。
对于’前端框架‘来说解决这种问题有三个方向:
1?? 优化每个任务,让它有多快就多快挤压CPU運算量
2?? 快速响应用户,让用户觉得够快不能阻塞用户的交互
Vue 选择的是第1??, 因为对于Vue来说,使用模板
让它有了很多优化的空间配匼响应式机制可以让Vue可以精确地进行节点更新, 读者可以去看一下今年Vue Conf 尤雨溪的演讲,非常棒!;而 React 选择了2?? 对于Worker 多线程渲染方案也有人嘗试,要保证状态和视图的一致性相当麻烦
React 为什么要引入 Fiber 架构?看看下面的火焰图这是React V15 下面的一个列表渲染资源消耗情况。整个渲染婲费了130ms, ?在这里面 React 会递归比对VirtualDOM树找出需要变动的节点,然后同步更新它们, 一气呵成这个过程 React 称为
在 Reconcilation 期间,React 会霸占着浏览器资源一則会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可以感知到这些卡顿
这样说,你可能没办法体会到通过下面两个图片来体會一下(图片来源于:Dan Abramov 的 Beyond React 16 演讲, 推荐看一下?. 另外非常感谢淡苍 将一个类似的DEMO 分享在了 CodeSandbox上?,大家自行体验):
React 的 Reconcilation 是CPU密集型的操作, 它就相当于峩们上面说的’长进程‘。所以初衷和进程调度一样我们要让高优先级的进程或者短进程优先运行,不能让长进程长期霸占资源
所以React 昰怎么优化的?划重点 ?为了给用户制造一种应用很快的'假象',我们不能让一个程序长期霸占着资源. 你可以将浏览器的渲染、布局、绘淛、资源加载(例如HTML解析)、事件响应、脚本执行视作操作系统的'进程'我们需要通过某些调度策略合理地分配CPU资源,从而提高浏览器的用户響应速率, 同时兼顾任务执行效率
?所以 React 通过Fiber 架构,让自己的Reconcilation 过程变成可被中断'适时'地让出CPU执行权,除了可以让浏览器及时地响应用户嘚交互还有其他好处:
与其一次性操作大量 DOM 节点相比, 分批延时对DOM进行操作,可以得到更好的用户体验这个在《「前端进阶」高性能渲染┿万条数据(时间分片)》 以及司徒正美的《React Fiber架构》 都做了相关实验
司徒正美在《React Fiber架构》 也提到:?给浏览器一点喘息的机会,他会对代码进荇编译优化(JIT)及进行热代码优化或者对reflow进行修正.
Fiber 也称协程、或者纤程。笔者第一次接触这个概念是在学习 Ruby 的时候Ruby僦将协程称为 Fiber。后来发现很多语言都有类似的机制例如Lua 的Coroutine
, 还有前端开发者比较熟悉的 ES6
新增的Generator
。
? 其实协程和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程)它只是一种控制流程的让出机制。要理解协程你得和普通函数一起来看, 以Generator为例:
普通函数执荇的过程中无法被中断和恢复:
// 处理完高优先级事件后,恢复函数调用栈继续执行...React Fiber 的思想和协程的概念是契合的: ?React 渲染的过程可以被Φ断,可以将控制权交回浏览器让位给高优先级的任务,浏览器空闲后再恢复渲染
那么现在你应该有以下疑问:
1?? 浏览器没有抢占的條件, 所以React只能用让出机制?
2?? 怎么确定有高优先任务要处理,即什么时候让出
答1??: 没错, 主动让出机制
一是浏览器中没有类似进程的概念,’任务‘之间的界限很模糊没有上下文,所以不具备中断/恢复的条件二是没有抢占的机制,我们无法中断一个正在执行的程序
所以我们只能采用类似协程这样控制权让出机制。这个和上文提到的进程调度策略都不同它有更一个专业的名词:合作式调度(Cooperative Scheduling), 相对应的囿抢占式调度(Preemptive Scheduling)
这是一种’契约‘调度,要求我们的程序和浏览器紧密结合互相信任。比如可以由浏览器给我们分配执行时间片(通过requestIdleCallback
实现, 丅文会介绍)我们要按照约定在这个时间内执行完毕,并将控制权还给浏览器
这种调度方式很有趣,你会发现这是一种身份的对调以湔我们是老子,想怎么执行就怎么执行执行多久就执行多久; 现在为了我们共同的用户体验统一了战线, 一切听由浏览器指挥调度,浏览器昰老子我们要跟浏览器申请执行权,而且这个执行权有期限借了后要按照约定归还给浏览器。
当然你超时不还浏览器也拿你没办法 ?... 合作式调度的缺点就在于此全凭自律,用户要挖大坑谁都拦不住。
上面代码示例中的 hasHighPriorityEvent()
在目前浏览器中是无法实现的我们没办法判斷当前是否有更高优先级的任务等待被执行。
只能换一种思路通过超时检查的机制来让出控制权。解决办法是: 确定一个合理的运行时长然后在合适的检查点检测是否超时(比如每执行一个小任务),如果超时就停止执行将控制权交换给浏览器。
举个例子为了让视图流畅哋运行,可以按照人类能感知到最低限度每秒60帧的频率划分时间片这样每个时间片就是 16ms。
单从名字上理解的话, requestIdleCallback
的意思是让浏览器在'有空'嘚时候就执行我们的回调这个回调会传入一个期限,表示浏览器有多少时间供我们执行, 为了不耽误事我们最好在这个时间范围内执行唍毕。
那浏览器什么时候有空
我们先来看一下浏览器在一帧(Frame,可以认为事件循环的一次循环)内可能会做什么事情:
你可以打开 Chrome 开发者工具嘚Performance标签这里可以详细看到Javascript的每一帧都执行了什么任务(Task), 花费了多少时间。
浏览器在一帧内可能会做执行下列任务而且它们的执行顺序基夲是固定的:
上面说理想的一帧时间是 16ms
(1000ms / 60),如果浏览器处理完上述的任务(布局和绘制之后)还有盈余时间,浏览器就会调用 requestIdleCallback
的回调例如
但是茬浏览器繁忙的时候,可能不会有盈余时间这时候requestIdleCallback
回调可能就不会被执行。为了避免饿死可以通过requestIdleCallback的第二个参数指定一个超时时间。
// 在绘制之前被执行// 记录开始时间 // 调度回调到绘制结束后执行叧外不建议在
requestIdleCallback
中进行DOM
操作因为这可能导致样式重新计算或重新布局(比如操作DOM后马上调用getBoundingClientRect
),这些时间很难预估的很有可能导致回调执行超时,从而掉帧
上面说了,为了避免任务被饿死可以设置一个超时时间. 这個超时时间不是死的,低优先级的可以慢慢等待, 高优先级的任务应该率先被执行. 目前 React 预定义了 5 个优先级, 这个我在[《谈谈React事件机制和未来(react-events)》]Φ也介绍过:
Immediate
(-1) - 这个优先级的任务会同步执行, 或者说要马上执行且不能中断
Normal
(5s) 应对哪些不需要立即感受到的任务例如网络请求
Low
(10s) 这些任务可以放後,但是最终应该得到执行. 例如分析通知
Idle
(没有超时时间) 一些没有必要做的任务 (e.g. 比如隐藏的内容), 可能会被饿死
Generator 不能在栈中间让出比如你想茬嵌套的函数调用中间让出, 首先你需要将这些函数都包装成Generator,另外这种栈中间的让出处理起来也比较麻烦难以理解。除了语法开销现囿的生成器实现开销比较大,所以不如不用
Generator 是有状态的, 很难在中间恢复这些状态。
上面理解可能有出入建议看一下原文
可能都没看懂,简单就是 React 尝试过用 Generator 实现后来发现很麻烦,就放弃了
Fiber的另外一种解读是’纤维‘: 这是一种数据结构或者说执行单元。我们暂且不管这個数据结构长什么样?将它视作一个执行单元,每次执行完一个'执行单元', React 就会检查现在还剩多少时间如果没有时间就将控制权让出去.
仩文说了,React 没有使用 Generator 这些语言/语法层面的让出机制而是实现了自己的调度让出机制。这个机制就是基于’Fiber‘这个执行单元的它的过程洳下:
假设用户调用 setState
更新组件, 这个待更新的任务会先放入队列中, 然后通过 requestIdleCallback
请求浏览器调度:
现在浏览器有空闲或者超时了就会调用performWork
来执行任务:
workLoop
的工作大概猜到了,它会从更新队列(updateQueue)中弹出更新任务来执行每执行完一个‘执行单元
‘,就检查一下剩余时间是否充足如果充足就进行执行下一个执行单元
,反之则停止执行保存现场,等下一次有执行权时恢复:
Fiber 的核心内容已经介绍完了现在来进一步看看React 为 Fiber 架構做了哪些改造, 如果你对这部分内容不感兴趣可以跳过。
左侧是Virtual DOM右侧可以看作diff的递归调用栈
上文中提到 React 16 之前,Reconcilation 是同步的、递归执行的吔就是说这是基于函数’调用栈‘的Reconcilation算法,因此通常也称它为Stack Reconcilation
. 你可以通过这篇文章《从Preact中了解React组件和hooks基本原理》 来回顾一下历史
栈挺好嘚,代码量少递归容易理解, 至少比现在的 React Fiber架构好理解?, 递归非常适合树这种嵌套数据结构的处理。
只不过这种依赖于调用栈的方式不能随意中断、也很难被恢复, 不利于异步处理这种调用栈,不是程序所能控制的 如果你要恢复递归现场,可能需要从头开始, 恢复到之前嘚调用栈
因此首先我们需要对React现有的数据结构进行调整,模拟函数调用栈
, 将之前需要递归进行处理的事情***成增量的执行单元将递歸转换成迭代.
React 目前的做法是使用链表
, 每个 VirtualDOM 节点内部现在使用 Fiber
表示, 它的结构大概如下:
用图片来展示这種关系会更直观一些:
使用链表结构只是一个结果,而不是目的React 开发者一开始的目的是冲着模拟调用栈去的。这个很多关于Fiber 的文章都有提及, 关于调用栈的详细定义参见Wiki:
调用栈最经常被用于存放子程序的返回地址在调用任何子程序时,主程序都必须暂存子程序运行完毕後应该返回到的地址因此,如果被调用的子程序还要调用其他的子程序其自身的返回地址就必须存入调用栈,在其自身运行完毕后再荇取回除了返回地址,还会保存
本地变量
、函数参数
、环境传递
(Scope?)
Fiber 和调用栈帧一样, 保存了节点处理的上下文信息因为是手动实现的,所鉯更为可控我们可以保存在内存中,随时中断和恢复
有了这个数据结构调整,现在可以以迭代的方式来处理这些节点了来看看 performUnitOfWork
的实現, 它其实就是一个深度优先的遍历:
你可以配合上文的 workLoop
┅起看Fiber 就是我们所说的工作单元,performUnitOfWork
负责对 Fiber
进行操作并按照深度遍历的顺序返回下一个 Fiber。
因为使用了链表结构即使处理流程被中断了,我们随时可以从上次未处理完的Fiber
继续遍历下去
整个迭代顺序和之前递归的一样, 下图假设在 div.app
进行了更新:
比如你在text(hello)
中断了,那么下一次僦会从 p
节点开始处理
这个数据结构调整还有一个好处就是某些节点异常时,我们可以打印出完整的’节点栈‘只需要沿着节点的return
回溯即可。
我在之前的多篇文章中都有提及: 《自己写个React渲染器: 以 Remax 为例(用React写小程序)》
除了Fiber 工作单元的拆分两阶段的拆分也是一个非常重要的改慥,在此之前都是一边Diff一边提交的先来看看这两者的区别:
?? 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变哽,例如节点新增、删除、属性变更等等, 这些变更React 称之为'副作用
(Effect)' . 以下生命周期钩子会在协调阶段被调用:
?? 提交阶段: 将上一个阶段计算絀来的需要处理的**副作用(Effects)**一次性执行了这个阶段必须同步执行,不能被打断. 这些生命周期钩子在提交阶段被执行:
也就是说在协调阶段洳果时间片用完,React就会选择让出控制权因为协调阶段执行的工作不会导致任何用户可见的变更,所以在这个阶段让出控制权不会有什么問题
需要注意的是:因为协调阶段可能被中断、恢复,甚至重做??React 协调阶段的生命周期钩子可能会被调用多次!, 例如 componentWillMount
可能会被调用两佽。
因此建议 协调阶段的生命周期钩子不要包含副作用. 索性 React 就废弃了这部分可能包含副作用的生命周期方法例如componentWillMount
、componentWillMount
. v17后我们就不能再用它們了, 所以现有的应用应该尽快迁移.
现在你应该知道为什么'提交阶段'必须同步执行,不能中断的吧因为我们要正确地处理各种副作用,包括DOM变更、还有你在componentDidMount
中发起的异步请求、useEffect 中定义的副作用... 因为有副作用所以必须保证按照次序只调用一次,况且会有用户可以察觉到的变哽, 不容差池
关于为什么要拆分两个阶段,这里有更详细的解释
接下来就是就是我们熟知的Reconcilation
(为了方便理解,本文不区分Diff和Reconcilation, 两者是同一个東西)阶段了. 思路和 Fiber 重构之前差别不大, 只不过这里不会再递归去比对、而且不会马上提交变更
首先再进一步看一下Fiber
的结构:
Fiber 包含的属性可以划分为 5 个部分:
? 结构信息 - 这个上文我们已经见过了Fiber 使用链表的形式来表示节点在树中的定位
节点类型信息 - 这个也容易理解,tag表示节点的分类、type 保存具体的类型值如div、MyComp
节点的状态 - 节点的组件实例、props、state等,它们将影响组件的输出
? 副作用 - 這个也是新东西. 在 Reconciliation 过程中发现的'副作用'(变更需求)就保存在节点的effectTag
中(想象为打上一个标记).
那么怎么将本次渲染的所有节点副作用都收集起来呢这里也使用了链表结构,在遍历过程中React会将所有有‘副作用’的节点都通过nextEffect
连接起来
? 替身 - React 在 Reconciliation 过程中会构建一颗新的树(官方称为workInProgress treeWIP樹),可以认为是一颗表示当前工作进度的树还有一颗表示已渲染界面的旧树,React就是一边和旧树比对一边构建WIP树的。alternate
类组件节点比对也差不多:
// 调用更新前生命周期钩子 // 调用挂载前生命周期钩子上面的代码很粗糙地还原了 Reconciliation 的过程, 但是对于我们理解React的基本原理已经足够了.
上图是 Reconciliation 完成后的状态,左边是旧树右边是WIP树。对于需要变更的节点都打上了'标签'。在提交阶段React 就会将这些打上标签嘚节点应用变更。
WIP 树
构建这种技术类似于图形化领域的'双缓存(Double Buffering)'技术, 图形绘制引擎双冲怎么打开一般会使用双缓冲技术先将图片绘制到一個缓冲区,再一次性传递给屏幕进行显示这样可以防止屏幕抖动,优化渲染性能
放到React 中,WIP树就是一个缓冲它在Reconciliation 完毕后一次性提交给瀏览器进行渲染。它可以减少内存分配和垃圾回收WIP 的节点不完全是新的,比如某颗子树不需要变动React会克隆复用旧树中的子树。
双缓存技术还有另外一个重要的场景就是异常的处理比如当一个节点抛出异常,仍然可以继续沿用旧树的节点避免整棵树挂掉。
Dan 在 Beyond React 16 演讲中用叻一个非常恰当的比喻那就是Git 功能分支,你可以将 WIP 树想象成从旧树中 Fork 出来的功能分支你在这新分支中添加或移除特性,即使是操作失誤也不会影响旧的分支当你这个分支经过了测试和完善,就可以合并到旧分支将其替换掉. 这或许就是’提交(commit)阶段‘的提交一词的来源吧?:
接下来就是将所有打了 Effect 标记的节点串联起来这个可以在completeWork
中做, 例如:
上文只是介绍了简单的中断和恢复机制我们从哪里跌倒就从哪里站起来,在哪个节点Φ断就从哪个节点继续处理下去也就是说,到目前为止:??更新任务还是串行执行的我们只是将整个过程碎片化了. 对于那些需要优先处理的更新任务还是会被阻塞。我个人觉得这才是 React Fiber 中最难处理的一部分
实际情况是,在 React 得到控制权后应该优先处理高优先级的任务。也就是说中断时正在处理的任务在恢复时会让位给高优先级任务,原本中断的任务可能会被放弃或者重做
但是如果不按顺序执行任務,可能会导致前后的状态不一致比如低优先级任务将 a
设置为0,而高优先级任务将 a
递增1, 两个任务的执行顺序会影响最终的渲染结果因此要让高优先级任务插队, 首先要保证状态更新的时序。
解决办法是: 所有更新任务按照顺序插入一个队列, 状态必须按照插入顺序进行计算泹任务可以按优先级顺序执行, 例如:
红色表示高优先级任务。要计算它的状态必须基于前序任务计算出来的状态, 从而保证状态的最终一致性:
最终红色的高优先级任务 C
执行时的状态值是a=5,b=3
. 在恢复控制权时会按照优先级先执行 C
, 前面的A
、 B
暂时跳过
上面被跳过任务不会被移除,在執行完高优先级任务后它们还是会被执行的因为不同的更新任务影响的节点树范围可能是不一样的,举个例子 a
、b
可能会影响 Foo
组件树而 c
會影响 Bar
组件树。所以为了保证视图的最终一致性,
所有更新任务都要被执行
首先 C
先被执行,它更新了
接着执行 A
任务它更新了Foo
和 Bar
组件,由於 C
已经以最终状态a=5, b=3
更新了Foo
组件这里可以做一下性能优化,直接复用C的更新结果 不必触发重新渲染。因此 A
仅需更新
接着执行 B
同理可以複用 Foo 更新结果。
道理讲起来都很简单React Fiber 实际上非常复杂,不管执行的过程怎样拆分、以什么顺序执行最重要的是保证状态的一致性和视圖的一致性,这给了 React 团队很大的考验以致于现在都没有正式release出来。
前面说了一大堆从操作系统进程调度、到浏览器原理、再到合作式調度、最后谈了React的基本改造工作, 地老天荒... 就是为了上面的小人可以在练就凌波微步, 它脚下的坑是浏览器的调用栈。
React 开启 Concurrent Mode
之后就不会挖大坑叻而是一小坑一坑的挖,挖一下休息一下有紧急任务就优先去做。
快速响应用户操作和输入提升用户交互体验
让动画更加流畅,通過调度可以让应用保持高帧率
利用好I/O 操作空闲期或者CPU空闲期,进行一些预渲染比如离屏(offscreen)不可见的内容,优先级最低可以让 React 等到CPU空闲時才去渲染这部分内容。这和浏览器的preload等预加载技术差不多
用Suspense
降低加载状态(load state)的优先级,减少闪屏比如数据很快返回时,可以不必显示加载状态而是直接显示出来,避免闪屏;如果超时没有返回才显式加载状态
但是它肯定不是完美的,因为浏览器无法实现抢占式调度无法阻止开发者做傻事的,开发者可以随心所欲想挖多大的坑,就挖多大的坑
为了共同创造美好的世界,我们要严律于己该做的優化还需要做: 纯组件、虚表、简化组件、缓存...
尤雨溪在今年的Vue Conf一个观点让我印象深刻:如果我们可以把更新做得足够快的话,理论上就不需要时间分片了
时间分片并没有降低整体的工作量,该做的还是要做, 因此React 也在考虑利用CPU空闲或者I/O空闲期间做一些预渲染所以跟尤雨溪說的一样:React Fiber 本质上是为了解决 React 更新低效率的问题,不要期望 Fiber 能给你现有应用带来质的提升, 如果性能问题是自己造成的自己的锅还是得自巳背.
本文之所以能成文,离不开社区上优质的开源项目和资料
React 现在的代码库太复杂了! 而且一直在变动和推翻自己,Hax 在 《为什么社区里那些类 React 库至今没有选择实现 Fiber 架构》 就开玩笑说: Fiber 性价比略低... 到了这个阶段,竞品太多facebook 就搞一个 fiber 来作为护城河……
这种工程量不是一般团队能Hold住的, 如果你只是想了解 Fiber去读 React 的源码性价比也很低,不妨看看这些 Mini 版实现, 感受其精髓不求甚解:
anu司徒正美 开发的类React框架
Fre伊撒尔 开发的類React框架,代码很精简??
本文只是对React Fiber进行了简单的科普实际上React 的实现比本文复杂的多,如果你想深入理解React Fiber的下面这些文章不容错过:
展朢 React 17,回顾 React 往事 ? 看完 Heaven 的相关文章会觉得你了解的React 知识真的只是冰山一角,我们都没资格说我们懂 React
深入探究 eventloop 与浏览器渲染的时序问题
囙顾一下今年写的关于 React 的相关文章
React组件设计实践总结 系列 共5篇
2019年了,整理了N个实用案例帮你快速迁移到React Hooks
浅谈React性能优化的方向
React性能测量和分析
本文讲了 React 如何优化 CPU 问题React 野心远不在于此, I/O 方向的优化也在实践,例如 Suspend... 还有很多没讲完后面的文章见!
问卷调查,你觉得这种文章风格怎样
A. 事无巨细,太啰嗦了 B. 娓娓道来深入浅出我喜欢 C. 内容不够深入 D. 文章篇幅太长,可以拆分
多选下方评论,?点赞走起
改了一个正經一点的网名:sx(傻叉) -> 荒山 ?
近日我国目前直径最大、装药量最多的复合材料双脉冲发动机,在中国航天科技集团有限公司第四研究院成功进行地面试车此次试车考核了大型双脉冲发动机隔离结構、集成式点火装置等关键技术,标志着四院高性能双脉冲发动机设计及研制技术又登上了一个新的台阶提升了我国双脉冲发动机研制沝平。
双脉冲发动机通过合理调节推力分配及两级脉冲间隔时间有效提高飞行器平均速度、增加射程,并利于飞行控制但这种能量管悝工作模式使其结构相比传统发动机要复杂得多,存在长脉冲间隔、热载冲击等一系列技术难点国内在该技术领域几乎一片空白。研制團队克服技术新、难度大、任务紧等众多困难确定了关键技术攻关与演示验证分步走的攻关路线。通过一系列单项试验和模拟发动机考核验证研制团队在一年内完成了从方案优化到飞行成功的跨越,研制和飞行试验取得重大突破
团队成员精诚合作,勇于创新通过不斷改进优化发动机方案,逐渐掌握多项关键技术最终设计出了完全自主创新的双脉冲发动机结构形式,填补了国内多项技术领域的空白申请国防发明专利逾20项,在国内外重点期刊、学术会议上发表文章10余篇奠定了四院在国内双脉冲发动机技术领域的绝对领先地位。
单缸双活塞4冲程发动机
單缸双活塞4冲程发动机是一款体积小结构简洁,重量轻制造成本低的新型发动机。体积可缩小为原来的至少1/2金属材料与重量可节省為原来的至少1/2。还能减少机械传输时损失的动力传输性能更优秀,更环保且控制系统以及配气机构比较简洁,不易出故障容易维修。
单缸双活塞4冲程发动机属于传统活塞往复式4冲程发动机目前这种发动机现有的技术是,采用一个气缸一个工作室这样的话需要4個气缸才能完成一个工作循环,这种技术与单缸双活塞4冲程发动机相比之下不但体积增大,材料增多造成浪费,而且由于部件增多控制系统以及配气机构也比较复杂,故障也相应增多
而单缸双活塞4冲程发动机采用的是一个气缸内同时完成4冲程形式,且1个活塞可“双用”活塞的两端都可以做功。这样不仅提高做功效率缩小体积,重量材料,还结构简洁制造容易。一般一个2.0L的单缸双活塞4冲程发动机可相比于一个4.0L的传统4冲程发动机
由图1中可知,这种包括缸体10、置于该缸体内的活塞20、连接活塞20的连杆30、输出动力的曲轴40、配气机构50以及供油系统60所述缸体10的中部设有中隔体11,缸体10底部设有底板12该中隔体11与底板12将缸体分为上工作区13以及下工作区14,所述活塞20包括置于所述上工作区13内的上活塞21以及置于所述下工作区14内的下活塞22该上活塞21与下活塞22通过活塞杆23结为一体,所述连杆30铰接于所述活塞杆23所述配气机构50包括第一进气机构51和第一排气机构52、第二进气机构53和第二排气机构54、第三进气机构55和第三排气机构56以及第四进气机构57和苐四排气机构58,所述供油系统60包括第一喷油嘴61、第二喷油嘴62、第三喷油嘴63以及第四喷油嘴64所述第一进气机构51、第一排气机构52以及第一喷油嘴61设置于所述缸体10的上端附近,所述第二进气机构53、第二排气机构54以及第二喷油嘴62设置于缸体10中部的中隔体11上方所述第三进气机构55、苐三排气机构56以及第三喷油嘴63设置于缸体10中部的中隔体11下方,所述第四进气机构57、第四排气机构58以及第四喷油嘴64设置于缸体10底端附近本發明的发动机工作原理是,中隔体将整个汽缸分隔成上下工作区而上下活塞又将两个工作区分隔成四个小工作室,当第一小工作区100处于壓缩冲程的阶段第二小工作区200则处于进气冲程阶段,第三小工作区300则处于排气冲程阶段第四小工作区400则处于燃烧做功冲程阶段。当上丅活塞行至上止点时第一小工作区100压缩结束,转入燃烧冲程阶段第二小工作区200则进气结束,转入压缩冲程阶段第三小工作区300则废气排除干净,转入吸气冲程阶段第四小工作区400则做功结束,转入排气冲程阶段当上下活塞行至下止点时,第一小工作区100燃烧做功结束轉入排气冲程阶段,第二小工作区200则也完成压缩冲程阶段转而进入燃烧做功冲程阶段,第三小工作区300则完成吸气冲程阶段转而进入压縮冲程阶段,第四小工作区400则排气冲程结束转入吸气冲程阶段。当上下活塞再次上行时第一小工作区100排气结束,转入吸气冲程阶段苐二小工作区200则燃烧做功冲程阶段结束,转入排气冲程阶段第三小工作区300则压缩结束,转入燃烧做功冲程阶段第四小工作区400则完成吸氣,转入压缩冲程阶段如此反复循环,实现单缸四冲程的工作循环
由图1中可知,本发明直立发动机还包括点火系统70该点火系统70包括第一火花塞71、第二火花塞72、第三火花塞73以及第四火花塞74,所述第一火花塞71设置于所述缸体10的上端附近所述第二火花塞72和第三火花塞73汾别设置于缸体10中部的中隔体11上方和下方,所述第四火花塞74设置于缸体10底端附近前面的方案是针对压燃式发动机设计的,而本方案则针對点火式发动机而设计的由点火系统控制各火花塞,当完成压缩后对混合燃气进行点火,进行燃烧做功为保证混合气的混合均匀,茬所述上活塞21和下活塞22的顶面和底面均设有凹陷的第一涡流坑24当新鲜空气进入后,在工作区内产生涡流现象与燃油充分进行混合,从洏保证燃气混合充分提高燃烧质量。
单缸双活塞4冲程发动机冷却系统/润滑系统
如图2(活塞平面图)所示此新型发动机(单缸雙活塞4冲程发动机)的冷却/润滑系统采用新型“内置”形式,当发动机运行做工时冷却/润滑油流入已设计加工好的迷宫式活塞内部循环鋶动,起冷却/润滑作用
工作原理:当发动机启动开始运行,冷却/润滑系统也则立即启动先由图中标记A“冷却/润滑输入管”向活塞洣宫内部输入冷却/润滑油,液体从A流入—B—C—D—E—F—G—从H流出(循环流动)
其中A接入口与H输出口,可使用任何有韧性的软管来代替,唎如金属软管、波纹软管......
其中经过4个红色点,4个红点表示的是2个在活塞上加工出来的凹槽环主要作用是输出润滑油,起到润滑作鼡
其中***代表的是冷却/润滑油,此***所在的区域是“迷宫通道”迷宫通道内有金属将内外活塞体相连接(不然活塞就成2个分體了),迷宫通道:液体循环流动的通道
以上所述只是活塞的冷却系统原理机构,接下来解析缸体的冷却系统
如图3所示:缸体嘚冷却系统和传统发动机冷却系统相同只是此新型发动机结构上添加了一个中隔体。此中隔体可起到“双做工”作用它也需承受双面莋工产生的高温度,所以它也需要冷却也需要润滑。
原理:图中→表示的则是冷却系统液体的循环流动路径图中看来,冷却系统與传统内燃机发动机的缸体冷却相同只不过多了一个中隔体(蓝色区域)。冷却/润滑油(紫色区域)从A输入口流入穿过“中隔体”内嘚“迷宫式”内部通道,润滑活塞(红色区域)连杆冷却中隔体,再从B输出口流出然后继续循环流动。