使用链表描述复杂结构
halo 米娜过年好,俺是 132,首先大家过年好哇!大家知道我写过一个叫 fre 的框架,在 fre 中最主要的特点就是使用链表去描述 vdom 树,这是很少见的
今天给大家带来一篇文章,详细讲讲那些传统框架很少见到的,链表这个数据结构
链表替代数组
大家熟知的 3kb react like 框架 preact 最近在进行大重构,使用两阶段的 diff/patch 的新架构
它使用了一个叫做 commitQueue 的数组队列去装载 diff 的结果
另外,preact 还是一个经典的同步递归框架,当然像 vue 等框架也是一样
这就比较尴尬了,因为无论是递归还是数组,都会爆栈
解决爆栈的思路非常简单,就是使用链表和循环
let commitment = null // 构建单向环状链表 for (let i = 0; i < children.length; i++){ commitment.next = children[i] commitment = children[i] } commitment.next = children[0] // 循环遍历链表 let current = commitment.next while (current) { // do something current = current.next }
短短几行代码,就可以将一个数组(栈,队列)转化为一个单向链表
一不需要递归,二不需要数组这种连续内存占用,解决了爆栈的问题,适合大量数据的遍历替代使用
其中在 fre 中就使用了同样的方式,解决了大量节点的内存问题
链表替代树
我们平时看到的前端框架,vue,preact,angular 都是使用 vdom 树或者 idom 树
树是一个非常常用的结构,构建容易,遍历也方便(递归)
同样的道理,当数据量大了以后,无论是内存还是时间,递归的弊端就会很明显,此时我们就需要使用链表了
// 构建链表树 // https://github.com/yisar/fre/blob/master/src/reconciler.ts#L176 for (var i = 0, prev = null; i 0) { prev.sibling = child } else { if (WIP.tag & OP.SVG) child.tag |= OP.SVG WIP.child = child } prev = child }
这段代码是 fre 中,将树的数组转化为链表的代码,非常简单
它将第一个孩子变成 child,之后的用 sibling 串联
这个结构相信读过 react 文章的很熟悉,实际上这还是一棵树,只是使用链表去描述了而已
这样的好处是,可以用经典的循环遍历了,除了上面的内存方面的好处,循环还是异步友好的,可以很方便得打断,继续……
// 遍历链表树 // https://github.com/facebook/react/issues/7942 let root = fiber let node = fiber while (true) { // Do something with node if (node.child) { node = node.child continue } if (node === root) return while (!node.sibling) { if (!node.return || node.return === root) return node = node.return } node = node.sibling }
到这里,都是 fre 中对链表的使用,当然主要思路还是来自 react,但在这个满大街的同步框架的世界里,使用链表确实有它的好处,内存优势,异步友好等等
但这还没完呢?
使用双链表去描述 dom 树
大家有想过吗?如果 fiber 不是由框架 runtime 实现,而是浏览器内置,到底需要怎样的硬性条件呢?
很简单,那就是浏览器中复杂的树,比如 dom 树,同样使用链表去描述
写过 html parser 的人都知道,其实我们无论如何拿到的都是一串字符串,我们的遍历从左到右线性的
132[ # # [ ] ] Element > Node > Node > Element > End > End
所以它真是是一棵树吗?不从来都不是,它无论如何都是一串字符串而已
我们使用双向链表同样可以很方便地表述这串字符串的线性关系
const {ELEMENT, ATTRIBUTE, TEXT} = Node const div = new Node(ELEMENT) const id = new Node(ATTRIBUTE) tag.next = id id.prev = div const text = new Node(TEXT) id.next = text text.prev = id const br = new Node(ELEMENT) text.next = br br.prev = text br.end = new Node(br.End) div.end = new Node(div.End)
同样的,我们可以遍历这个链表,来做我们想做的事情
const childNodes = element => { const nodeList = [] let { next, end } = element while (next !== end) { if (next.type !== Node.ATTRIBUTE) { nodeList.push(next) if (next.type === Node.ELEMENT) next = next.end } next = next.next } return nodeList }
到这里,我们基本上是可以利用链表去描述 dom 树了,有了链表,我们在任何循环的地方都可以进行打断了……
其实无论如何,我们需要同步的地方只有两处,一是 js 行为,二是画图行为
不管是浏览器,还是我们自己实现渲染引擎,除了这两个行为之外的所有环节,比如排版,合成,统统都可以异步打断
过去 fre 实在框架 runtime 创造异步条件,那是因为我们没办法控制浏览器
我自己实现渲染引擎,我希望将这部分内容移交给引擎,相反 js runtime 越轻量越好
未来是物联网时代,没人乐意和浏览器玩♂耍了……
总结
其实说实话,我们使用链表而不是使用树,并不是看上了性能,至少我是对性能完全无感的,都什么年代了,还追求那点点时间
但是写树真的写腻了……尤其是渲染引擎这种复杂,大型的东西,数据结构一旦确定下来,就很难撼动了……
我希望从一开始就站在至高点,不要说什么先整个简单的,后续再重构……这是不现实的,越重要的东西越是从一开始就得确定好
最后大家过年好!年后一起搞事情鸭!