Notes Of React
React渲染过程
注:15版本之前
- React.createElement 创建虚拟DOM
- React.render 渲染虚拟DOM
- 通过虚拟DOM创建真实DOM,添加属性、样式、事件等
- 把真实DOM挂载到指定的容器中
react中setState的更新机制(16版本之前)
- 同步更新和异步更新
- 合并更新和不合并更新
- 传入函数不会合并更新
默认:异步合并更新
不在React上下文中,则是同步更新
setState是微任务还是普通任务?
setState本质上是同步执行,state也是同步更新,表现上面是异步的
是否了解过 React 的整体渲染流程? 里面主要有哪些阶段?
爹考答案:
React 整体的渲染流程可以分为两大阶段,分别是render阶段和commit 阶段。
render 阶段里面会经由调度器和协调器处理,此过程是在内存中运行,是异步可中断的。
commit 阶段会由渲染器进行处理,根据副作用进行UI的更新,此过程是同步不可中断的,否则会造成 UI和数据显示不一致。
调度器
调度器的主要工作就是调度任务,让所有的任务有优先级的概念,这样的话紧急的任务可以优先执行。Scheduler 实际上在浏览器的API 中是有原生实现的,这个 API 叫做 requestidleCallback,但是由于兼容性问题,React 放弃了使用这个 API,而是自己实现了一套这样的机制,并且后期会把 Scheduler 这个包单独的进行发布,变成一个独立的包。这就意味Scheduler 不仅仅是只能在 React中使用,后面如果有其他的项目涉及到了任务调度的需求,都可以使用这个 scheduler。
协调器
协调器是 Render 的第二阶段工作。该阶段会采用深度优先的原则遍历并且创建一个一个的 FiberNode,并将其串联在一起,在遍历时分为了"递”与 归”两个阶段,其中在“递”阶段会执行 beginwork 方法,该方法会根据传入的 FiberNode 创建下一级 FiberNode。而“归”阶段则会执行 CompleteWork 方法,做一些副作用的收集,生成真实DOM。
渲染器
渲染器的工作主要就是将各种副作用 (flags 表示)commit 到宿主环境的UI中。整个阶段可以分为三个子阶段,分别是 BeforeMutation 阶段、Mutation 阶段和 Layout 阶段。
是否了解过 React 的架构?新的Fiber 架构相较于之前的 Stack 架构有什么优势?
参考答案: React v15及其之前的架枸:
- Reconciler(协调器):VDOM 的实现,负责根据自变量变化计算出 UI变化
- Renderer (渲染器):负责将 U 变化渲染到宿主环境中
这种架构称之为 Stack 架构,在 Reconciler 中,mount 的組件会调用mountcomponent,update 的组件会调用updateComponent, 这两个方法都会递归更新子组件,更新流程一旦开始,中途无法中断。
但是随看应用规模的逐渐增大,之前的架构模式无法再满足"快速响应"这一需求,主要受限于如下两个方面:
- CPU瓶颈:由于 VDOM 在进行差异比较时,采用的是递归的方式,JS计算会消耗大最的时间,从而导致动画、还有一些需要实时更新的内容产生视觉上的卡顿。
- IO瓶颈:由于各种基于“自变量”变化而产生約更新任务没有优先級的概念,因此在某些更新任务(例如文本框的输入) 有稍微的延迟,对于用户来讲也是非常敏感的,会让用户产生卡顿的感觉。
新的架构称之为 Fiber 架构:
- scheduler (调度器):调度任务的优先級,高优先級任务会优先进入到 Reconciler
- Reconciler(协调器):VDOM 的实现,负责根据自变量变化计算出UI变化
- Renderer(渲染器):负责将UI变化渲染到宿主环境中
首先引入了 Fiber的概念,通过一个对象来描述一个 DOM 节点,但是和之前方案不同的地方在于,每个Fiber 对象之间通过链表的方式来进行串联。 通过 child 来指向子元素,通过 sibling 指向兄弟元素,通过 return 来指向父元素。
在新架构中,Reconciler中的更新流程从递归变为了“可中断的循环过程”。每次循环都会调用 shouldYield 判断当前的 Timeslice 是否有剩余时间,没有剩余时间则暂停更新流程,将主线程还给渲染流水线,等待下一个宏任务再继续执行。这样就解决了 CPU 的瓶颈问题。
另外在新架构中还 入了 Scheduler 调度器,用来调度任务的优先级,从而解决了 I/O 的瓶颈问题。
谈一谈你对 React 中 Fiber 的理解以及什么是Fiber 双缓冲?
参考答案: Fiber 可以从三个方面去理解:
- FiberNode 作为一种架构:在React v15 以及之前的版本中,Reconceiler 采用的是递归的方式,因此被称之为 StackReconciler,到了 React v16 版本之后,引入了 Fiber, Reconceiler 也从 Stack Reconciler 变为了 Fiber Reconceiler, 各个FiberNode 之间通过链表的形式串联了起来。
- FiberNode 作为一种数据类型:Fiber 本质上也是一个对象,是之前虛拟 DOM 对象(React 元素,createElement 的返回值)的一种升级版本,每个Fiber 对象里面会包含 React 元素的类型,周围链接的 FiberNode,DOM 相关信息。
- FiberNode 作为动态的工作单元:在每个 FiberNode 中,保存了“本次更新中该 React 元素变化的数据、要执行的工作(增、删、改、更新Ref、副作用等)"等信息。
所谓 Fiber 双缓冲树 ,指的是在内存中构建两颗树,并直接在内存中进行替换的技术。在React 中使用 Wip Fiber Tree 和 Current Fiber Tree 这两颗树来实现更新的逻辑。Wip Fiber Tree 在内存中完成更新,而 Current Fiber Tree 是最终要渲染的树,两颗树通过alternate 指针相互指向,这样在下一次渲染的时候,直接复用 Wip Fiber Tree 作为下一次的渲染树,而上一次的渲染树又作为新的Wip Fiber Tree, 这样可以加快 DOM 节点的替换与更新。
React diff 算法有没有了解过?为什么不使用Vue中的双端比较算法?
diff 计算发生在更新阶段,当第一次渲染完成后,就会产生 Fiber 树,再次渲染的时候 (更新),就会拿新的JSX 对象(vdom)和旧的 FiberNode 节点进行一个对比,再决定如何来产生新的 FiberNode,它的目标是尽可能的复用己有的Fiber 节点。这个就是diff 算法。
在React 中整个 diff 分为单节点diff 和多节点diff。
所谓单节点是指新的节点为单一节点,但是旧节点的数量是不一定的。
单节点 diff 是否能够复用遵循如下的顺序
- 判断key 是否相同
- 如果更新前后均未设置 key,则key 均为 null,也属于相同的情况
- 如果key 相同,进入步骤二
- 如果 key 不同,则无需判断 type,结果为不能复用(有兄弟节点还会去遍历兄弟节点)
- 如果 key 相同,再判断type 是否相同
- 如果 type 相同,那么就复用
- 如果 type 不同,则无法复用(并且兄弟节点也一并标记为删除)
多节点diff 会分为两轮遍历
第一轮遍历会从前往后进行遍历,存在以下三种情况:
- 如果新旧子节点的key 和type 都相同,说明可以复用
- 如果新旧子节点的key 相同,但是type不相同,这个时候就会根据 ReactElement 来生成一个全新的fiber,旧的fiber 被放入到 deletions 数组里面,回头统一删除。但是注意,此时遍历并不会终止。
- 如果新旧子节点的key 和type 都不相同,结束遍历如果第一轮遍历被提前终止了,那么意味着还有新的JSX 元素或者旧的 FiberNode 没有被遍历,因此会采用第二轮遍历去处理。
第二轮遍历会遇到三种情况:
- 只剩下旧子节点:将旧的子节点添加到 deletions 数组里面直接删除掉(刪除的情況)
- 只剩下新的JSX 元素:根据 ReactElement 元素来创建 FiberNode 节点(新增的情况)
- 新旧子节点都有剩余:会将剩余的 FiberNode 节点放入一个map 里面,遍历剩余的新的J5X 元素,然后从 map 中去寻找能够复用的 FiberNode 节点,如果能够找到,就拿来复用。(移动的情况)如果不能找到,就新增呗。然后如果剩余的JSX 元素都遍历完了,map结构中还有剩余的 Fiber 节点,就将这些 Fiber 节点添加到 deletions 数组里面,之后统一做删除操作
整个 diff 算法最最核心的就是两个字"复用”。
- React 不使用双端 diff 的原因: 由于双端diff需要向前查找节点,但每个 FibenNode 节点上都没有反向指针,即前一个FiberNode 通过 sibing属性指向后一个FiberNode,只能从前往后遍历,而不能反过来,因此该算法无法通过双端搜索来进行优化。
React想看下现在用这种方式能走多远,如果这种方式不理想,以后再考虑实现双端 diff。 React 认为对于列表反转和需要进行双端搜素的场景是少见的,所以在这一版的实现中,先不对bad case 做额外的优化。
是否了解过 React 中的lane 模型?为什么要从之前的 expirationTime 模型转换为 lane 模型?
在React 中有一套独立的粒度更细的优先级算法,这就是lane 模型。
这是一个基于位运算的算法,每一个lane 是一个32bit Integer ,不同的优先级对应了不同的lane,越低的位代表越高的优先级。
早期的 React 并没有使用lane 横型,而是采用的的基于 expirationTime 桢型的算法,但是这种算法揉合了“优先级”和”批"这两个概念,限制了模型的表达能力。优先级算法的本质是"为update 排序”,但 expiration Time 模型在完成排序的同时还默认的划定了"批”。
使用lane 模型就不存在这个问题,因为是基于位运算,所以在批的划分上会更加的灵活。