React系列-React15-16对比

198 阅读9分钟

声明: 引用自: www.teqng.com/2021/09/10/…

本文仅为记笔记

前置

为什么需要新增 fiber 结构呢

首先把 vdom 转成 fiber,之后再渲染 fiber

vdom 转 fiber 的过程叫做 reconcile,最后增删改真实 dom 的过程叫做 commit。

为什么要做这样的转换呢?

因为 vdom 只有子节点 children 的引用,没有父节点 parent 和其他兄弟节点 sibling 的引用,这导致了要一次性递归把所有 vdom 节点渲染到 dom 才行,不可打断。

万一打断了会怎么样呢?因为没有记录父节点和兄弟节点,那只能继续处理子节点,却不能处理 vdom 的其他部分了。

所以 React 才引入了这种 fiber 的结构,也就是有父节点 return、子节点 child、兄弟节点 sibling 等引用,可以打断,因为断了再恢复也能找到后面所有没处理过的节点。

渲染帧

帧: 动画过程中,每一幅静止的画面叫做帧。

帧率:即每秒钟播放的静止画面的数量。

帧时长:每一幅静止的画面的停留时间。

丢帧:当某一帧时长高于平均帧时长。

  • 一般来说浏览器刷新率在 60hz,渲染一帧时长必须控制在16.67ms(1000/60 = 16.67ms)
  • 如果渲染超过该时间,对用户视觉上来说,会出现卡顿现象,即丢帧。

解决丢帧

JS 运算是占用渲染的时间的。

JS线程和渲染线程本质上都是消息队列中的一个task, 其中一个执行会阻塞另一个

如果只是图层的位置,透明度发生变化 如:translate3d, opacity 不会涉及重绘重排,直接在合成层交给gpu 独立渲染,不涉及渲染线程,它和js 执行是独立的。

如何解决丢帧问题呢

  1. 一帧空闲时处理,利用 requestIdleCallback 处理任务。

    window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

  2. 对高耗时的任务,进行分步骤处理。

reqestIdleCallback 启发

将一个大任务分割成N个小任务,在每一帧有空余的时间情况下,逐步去执行小任务。

React 15 架构的缺陷

React 15 的协调器采用的是递归更新的方式

特性: 同步更新

架构

分为两层:

  • Stack Reconciler(协调器)
  • Renderer(渲染)
 数据更新 ------------->  Stack Reconciler -----------------> Renderer
                                     负责找出变化的组件                   负责将变化的组件渲染到页面上
                                     1. jsx to vdom;             将其所有的变更一次性的更新到Dom上
                                     2. diff对比前后vdom;
                                     3. 找出需要更新的vdom;
                                     4. 通知Render更新;

缺陷出现在 Stack Reconciler 递归更新。

因为递归一旦开始无法终止,知道遍历完整棵树,才能将主线程释放。

如果主线程有用户操作 Event Handlers 或者 动画渲染 等操作,就必须等到主线程释放,才能被响应。

无法快速响应丢帧

工作方式

深度优先遍历+递归遍历的路径

总结

  1. React 15 对创建和更新节点的处理,是通过递归。
  2. 递归,在未完成对整棵树的遍历前,是不会停止的。
  3. 该任务一直占用浏览器主线程,导致无法响应优先级更高的任务。
  4. 故,浏览器渲染超过临界时间,从视觉上来看就会卡死。

主动思考

  1. 将一个大的任务分成N个小人物.

  2. if 一帧里没有优先级更高的任务, 则执行自己.

    else 有其他优先级高的任务, 优先执行其他.

    if 等一帧有空闲了, 再执行自己.

    else 下一帧.

那么

如何将任务拆分呢?

如何判断优先级呢?

如何判断一帧空闲时,再执行?

Fiber 架构

我们先来对比两张图片

A任务进入执行区域,

在执行过程中,有个更高优先级的任务B,

但是因为先来后到,此时任务B无法被执行,而暂时被挂起,只能等待执行。

等A任务执行完之后,才能执行任务B。

  1. 任务A进入执行区域。
  2. 在执行任务A的过程中,有个更高优先级任务B,请求被执行。
  3. 考虑到任务B优先级高,则将任务A没有执行完成的部分 Stash 暂存。
  4. 任务B被执行。当任务B被执行完成后,去执行剩余没有完成的任务A。

核心关注

并发,调度

Concurrency 并发: 有能力优先处理更高优事务,同时对正在执行的中途任务可暂存,待高优任务完成后,再去执行。

Scheduler 协调调度: 暂存未执行任务,等待时间成熟后,再去安排执行剩下未完成的任务。

考虑所有任务都可以被并发执行,就需要有个协调任务的调度算法。

调用栈 & 虚拟调用栈帧

如果使用调用栈,在这里就看起来很不合理。

因为浏览器是利用调用栈来管理函数执行顺序的,那么是如何做到某任务都入栈了,但是因为中途有其他事儿,就被中断。而且还能再中断后,接着后续再执行。

问题突然间就变成了:pause a function call (暂停对一个函数的调用)

巧了,像generator 和 浏览器 debugger 就可以做到中断函数调用。

但是考虑到可中断渲染,并可重回构造。React 自行实现了一套体系叫做:React Fiber 架构

React Fiber核心:自行实现 虚拟栈帧

That’s the purpose of React Fiber. Fiber is reimplementation of the stack, specialized for React components. You can think of a single fiber as a virtual stack frame* .*

React 16+ 架构

架构

分为三层:

  • Scheduler (调度器)
  • Fiber Reconclier (协调器)
  • Renderer (渲染)

数据结构

Fiber 的数据结构有三层信息:实例属性,构建属性,工作属性。

  1. 实例属性:例如组件类型
this.tag = tag; // 组件类型
this.key = key;
this.elementType = null; 
  1. 构建属性:(return, child, sibling)
this.return = null // 指向父节点
this.child = null  // 指向孩子
this.sibling = null //指向兄弟

构建流程:

  1. 分为同步或者异步更新。
  2. 增加的异步更新,使用 shouldYield 来判断是否需要中断。

考虑如下代码:

<div id="linjiayu">123</div>
<script type="text/babel">
    const App = () => {
        const [sum, onSetSum] = React.useState(0)
        return (
            <div id="app 1">
                <h1 id="2-1 h1">标题 h1</h1>
                <ul id="2-2 ul"> 
                    <li id="3-1 li" onClick={() => onSetSum(d => d + 1)}>点击 h2</li>
                    <li id="3-2 li">{sum}</li>
                </ul>
                <h3 id="2-3 h3">标题 h3</h3>
            </div>
        )
    }
​
    ReactDOM.render(<App />,  document.getElementById('linjiayu') );
</script>

  1. 创建 fiberNode FiberRootNode

  2. 创建 fiberNode rootFiber (即示例中

    进入循环工作区域, workInProgress (工作指针指向 rootFiber

  3. 创建 fiberNode App

    beginWork() -> 只有一个子节点 -> workInProgress (工作指针指向App)

  4. 创建 fiberNode div

    beginWork() -> 有多个子节点 -> workInProgress (工作指针指向div)

  5. 构建孩子们节点

    按照 1 -> 2 -> 3 顺序将每个节点创建。

  6. workInProgress (工作指针指向h1)

    beginWork() -> 没有子节点 -> completeUnitOfWork() -> 有兄弟节点,继续 ...

工作属性

  1. 【数据】数据的变更会导致UI层的变更。
  2. 【协调】为了减少对DOM 的操作,通过Reconclier 进行 diff 查找, 并将需要变更的节点打上标签,变更路径保留在 effectList 里。
  3. 【调度】 待变更内容要由 Scheduler 优先级处理

故。涉及到diff等查找操作,是需要有个高效手段来处理前后变化,即双缓存机制。

Reconciler & Scheduler

链表结构即可支持随时中断的诉求

中断机制

中断机制是一种非常重要的解决资源共享的手段,对于操作系统而言,它已经是一个必不可少的功能。

有了中断机制,中断服务后,不同任务就能实现间断执行的可能,如何实现多任务的合理调度,就需要一个调度任务来进行处理,这通常代表着操作系统。

例如:当一个任务A被执行到一半时,被中断机制强制中断,此时操作系统需要对当前任务A进行现场保护,如:寄存器数据,然后切换到下一个任务B, 当任务A再次被调度时,操作系统需要还原之前任务A的现场信息,从而保证任务A能继续执行下一半任务。

调度过程中如何保证被中断任务的信息不被破坏时一个非常重要的功能

浏览器提供的 RequestIdleCallback 机制,类似 中断服务注册机制,注册后我们只要合适的时间进行释放,就能实现 中断 效果,对于不同任务之间切换,在中断后,需要考虑现场保护和现场还原。

早期的React 是同步渲染,实际上是一个递归过程,递归可能会回来长的调用栈,这会给现场保护和还原变得复杂。

React Filber 的做法是将递归过程拆成一系列小任务(filber) 转换成 线性的链表结构, 此时现场保护只需要保存下一个任务结构信息即可,所以拆分的任务上需要扩展额外信息,该结构记录着任务执行时所需要的信息。

{
 stateNode,
 child,
 return,
 sibling,
 expirationTime
 ...
}

我们简述下架构中的两个核心模块

  • Reconciler(协调) : 负责找出变化的组件。
  • Scheduler (调度) : 负责找出高优任务。

双缓存机制

上面两行代码,就是双缓存机制的核心代码

至多有两颗 Fiber Tree

  1. current fiber tree
  2. workInProgress fiber tree

即在屏幕上已建立的 fiber tree 和 因为数据变化重新在内存中创建的 fiber tree

它们之间是通过 alternate 指针 建立链接。

简单地说:

  1. workInProgress fiber 的创建,是否可复用 current fiber 的节点。《详细看Diff算法》
  2. workInProgress fiber tree 将确定要变更的节点,渲染到屏幕上。
  3. workInProgress fiber tree 晋升为 current fiber

\