文章目录
此篇文章是在学习一步一步实现
fiber
架构的同时,从另外一个由总到分的角度来总结
fiber
架构的实现思路。文章末尾有一些学习参考文章可以借鉴。
fiber架构是什么?它解决了什么问题?
当我们项目过于复杂,渲染树过于庞大的时候,那么我们的递归渲染会耗时很长,而且很难被中断,fiber 的主要原理就是让我们在 diff 的过程中可以被中断,去处理更高优先级的事件如:用户事件或者动画,这样让浏览器的渲染更加流畅。
fiber 的核心思想,实现 fiber 我们需要做到什么?如何做?
React 16.0
之前,我们在渲染的过程中,通过去遍历一整棵 虚拟 dom
树来更新变化,我们很难中断,并且无法标记中断来持续工作。那么 fiber 架构将递归 diff 拆分成一个一个小任务,并且随时可中断,利用浏览器的空闲时间来执行,当处理完更高优先级任务后回到中断点继续执行;
要实现这样的机制,fiber做了什么,这里我总结了几点:
- 将原有的 vdom 树结构变成一个新的链表结构的树,每个节点都标记了它的(
child,sibling,return,
分别代表节点的第一个字节点、兄弟节点、父节点),这样可以随时中断,下次从中断处继续执行; - fiber的核心思想是将一个庞大的任务拆分成一个个小的任务块,利用浏览器的空闲时间来执行,那么如何拆分,如何协调?
React
以虚拟dom
节点为维度对任务进行拆分,即一个虚拟dom节点对应一个任务,采用深度优先遍历的规则进行协调; - 从根节点开始调度和渲染的过程可以分为两个阶段:
render()
,和commit()
; render 阶段会根据v-dom找出所有节点的变更(增删更新),然后构建出一棵Fiber 树,这个阶段是可以中断的。commit阶段就是将构建的Fiber three渲染成真实的dom, 这个阶段是不可中断的。 - 这里我们通过window.requestIdleCallback 来实现浏览器空闲时执行低优先级任务。
所以说 React Fiber
其实就是通过遍历将 VDom 转换成了 Fiber three,其中每个 Fiber都具有 child、singling、return属性;
遍历遵循深度优先遍历,自上而下,自左向右;从根节点出发,找到他的第一个子元素,找到则返回,没有则找他的兄弟元素,如果无兄弟元素,则直接返回其父元素, 父 ——> 第一个子 ——> 兄弟 ——> 父亲;
在遍历生成Fiber three 的时候根据节点的变更收集 effect list
, 通过tag(UPDATE、DELETE、PLACEMENT)
,直到没有下一个任务,commit 到DOM树上。
在此之前,我们的 vdom
是一颗树,它在 diff 的过程中是没法中断的,于是将其改造成一个链表结构,之前是只有 children 进行递归遍历,现在是包含了父——>子, 子——> 父, 子——> 兄弟这几层关系的链表。
Fiber reconcile
至此,我们可以跟着思路来实现 fiber, 说到 fiber, 它其实就是一个具有各种标识的对象,如:
{
dom: null, // 真实dom,这里function 组件的dom是null
type,
props,
child,
return,
sibling,
alternate: null, // 旧值,用于比对更新
effectTag: 'PLACEMENT'
}
正题来了,首先我们还是来实现createElement(type, config, ...children)
最终返回 虚拟dom 树,这里不是重点,所以不过多介绍,详细可以查看createElement原理,直接贴代码:
/**
* jsx语法糖,接受三个参数,返回v-dom(js对象)
* @param {*} type 元素类型:native HTML | Function | Class
* @param {*} config 属性
* @param {...any} children 子元素
* @returns v-dom 对象
*/
function createElement(type, config, ...children) {
delete config.__self
delete config.__source
const { key, ref, ...rest } = config
const vdom = {
$$typeof: Symbol('react.element'),
type,
props: {
...rest,
children: children.map(c => typeof c === 'object' ? c : createTextNode(c))
}
}
return vdom
}
/**
* 创建文本节点对象
* @param {*} nodeValue 文本值
* @returns v-dom 对象
*/
function createTextNode(nodeValue) {
return {
type: 'TEXT',
props: {
nodeValue,
children: []
}
}
}