Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[简介] fre 的结构和算法 #386

Open
yisar opened this issue Mar 20, 2025 · 0 comments
Open

[简介] fre 的结构和算法 #386

yisar opened this issue Mar 20, 2025 · 0 comments

Comments

@yisar
Copy link
Collaborator

yisar commented Mar 20, 2025

fre 是一个 react-like 框架,代码不多,总体上就一个 fiber 结构和一个 reconcile 算法

fiber 结构

fiber 结构本质上是使用一个三个指针的链表来描述 dom 结构,在这个链表的基础上进行深度遍历,目的是为了做异步渲染(时间切片,suspense)

let root = fiber
let node = fiber
while (true) {
  if (node.child) {
    node = node.child
    continue
  }
  if (node === root) return
  while (!node.sibling) {
    if (!node.parent || node.parent=== root) return
    node = node.parent
  }
  node = node.sibling
}

传统的 diff 算法,都是基于数组的同步 diff 算法,拿 vue3、inferno 为例,本质上是寻找一个“最长公共子序列”,然后保持不同,剩下的进行移动操作,而最长公共子序列可以转换为最长递增子序列问题,进而使用二分查找将复杂度优化到O(nlogn)

聪明的人已经发现了,传统的基于 LCS/LIS 的算法,是无法用到这个结构里的,甚至同样的思路都不适用于链表,因为链表是个线性结构,它的遍历限制了算法的发挥(只能从头到尾 onepass)

因为我不得不思考一个新的适用于链表的 onepass 的 reconcile 算法

在 fiber 中实现最长移动

我尝试过各种算法以后,想到一种方法,既然我们没办法在链表中寻找最长公共子序列,那么我们就寻找最长移动节点

[1, 2, 3, 4, 5] => [5, 1, 2, 3, 4] // 5 从 4 到了 0,明显的最大移动距离,因为我们就只移动 5

这个思路可能不能总是寻找到最短编辑距离,但大多数时候可以保持 onepass,可以和 fiber 结构结合起来,最好复杂度甚至可以做到 O(n)

function reconcile(bList, view, x) {
    let currentA = aList.head
    let currentB = bList.head
    let position = 0
    
    while (currentA || currentB) {
        if (!currentB) {
            // 删除多余节点
            removeNode(currentA)
            currentA = currentA.next
            continue;
        }

        if (!currentA) {
            // 插入新节点
            insertNode(currentB, null)
            currentB = currentB.next
            continue
        }

        const bKey = currentB.data.key
        const aKey = currentA.data.key

        if (aKey === bKey) {
            // 更新节点
            this.updateNode(currentA, currentB, view)
            currentA = currentA.next
            currentB = currentB.next
        } else {
            // 寻找最长距离
            let foundA = this.findKeyInRemaining(aList, currentA.next, bKey)
            let foundB = this.findKeyInRemaining(bList, currentB.next, aKey)

            if (foundA && foundB) {
                // 比较移动距离
                if (foundA.distance <= foundB.distance) {
                    // 移动A链表节点
                    this.moveNodeBefore(foundA.node, currentA)
                    currentA = foundA.node;
                } else {
                    // 移动B链表节点
                    this.insertNode(foundB.node, currentA)
                    currentB = foundB.node.next
                }
            } else if (foundA) {
                // 移动A链表节点
                this.moveNodeBefore(foundA.node, currentA);
                currentA = foundA.node
            } else {
                // 新增B链表节点
                this.insertNode(currentB, currentA)
                currentB = currentB.next
            }
        }
        position++;
    }
}

总结

fre 的意义在于使用 500 行代码复刻 react 的核心结构和异步渲染的思路,供大家参考

最近考研复试实在太紧张了,工作也好,面试也好,学术研究也好,因为俺的思路也能帮到大家,如果大家有更好的思路也可以发俺哈

@yisar yisar changed the title 链表中的 one pass diff 算法 [简介] fre 的结构和算法 Mar 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant