【SDU Chart Team - Core】基于哈希的DOM Diff算法

基于哈希的DOM Diff算法

DOM基本操作

DOM基本操作类型

对DOM的基本操作可概括如下:

操作原语描述
child i将游标设为当前的第 i i i个子节点
parent将游标设为当前的父节点
remove i移除第 i i i个子节点
append html增加一个子节点,内容为html
modify attr x / reset attr修改或重置attr属性
sort indices按照indices对子节点顺序重排

其中childparent对应了树上移动,removeappendsort能对树形结构进行更改,modifyreset对节点进行修改。显然用这些基本操作来描述如何将一个DOM树变换为另外一个DOM树是完备的。

事实上,去除sort,其他操作也是完备的,因为sort等价于removeappend的结合。但是由于代价因素,有必要引入此操作。

DOM基本操作代价

之所以需要Diff算法,就是要寻找一个低的代价将一个DOM树转化成另一个DOM树。如果抛开代价,那么Diff算法就失去了实际意义。(不计代价则可直接使用整体替换的方式)

所以说引入基本操作的概念,也是为了将整个过程细粒度化,从而优化掉部分非必要的操作。当然,基本操作在执行代价上并非等价的,通过经验估计可以得到它们的开销级别:

操作代价解释
append解析HTML,生成完整的节点或子树
remove指针变化,触发GC时较大
sort多指针变化,需要遍历一遍孩子
modify赋值;少数情况下需要解析
child极小指针变化
parent极小指针变化

通过对代价的分析,我们可以得出结论:Diff中的结果应当尽可能少的使用append,其他操作可以近似同等考虑,并且指针操作可以忽略代价。

Diff算法

基于同层次的遍历

常见的Diff算法都是基于同层次的,例如其他框架中的DOM Diff、Git中的文件树Diff。这么做的好处就是不需要考虑纵向的复用,只需要考虑同层次的复用,从而使得算法更简单。

其步骤就是使用双指针,指针进行相同的移动,分别指向两棵树的同层级部分。于是我们可以边遍历,边处理差异。

指针的移动原则

指针的移动并非对孩子依次进行,而是按照一定原则进行。具体而言,就是按照算法认为差异的但修改的代价最小的进行。算法按照这样的原则选中的两个节点形成一个配对,从而递归进行一个子树到另一个子树的变换过程。

子树的Diff

差异分为三个级别子树儿子属性。最有利的情况就是子树相同。对于判断子树相同,我们可以使用之前的哈希树,以常数时间判断。显然,对于子树相同,我们可以直接跳过。否则我们需递归进行。

属性的Diff

对于算法中双指针移动到的两个节点,我们认为它们是差异的。首先一个必要的步骤是比较节点上的属性差异。基于哈希,我们首先检查属性的哈希,如果属性相同,显然可以跳过;否则遍历属性找不同。

子元素的Diff

子元素的Diff是一个复杂的复合问题,我们将它分解成三个子问题:
(1). 寻找两个子元素,递归进行Diff
(2). 找到哪些子元素被添加了
(3). 找到哪些子元素被删除了

  • 子问题(1)

    子问题(1)是算法的核心问题,属于是决策问题,它需要体现指针移动原则中的最小代价。基于贪心的思路,可以设计如下的配对过程:

    1. 找到子树完全相同的两个节点
    2. 对于子树不完全相同的两个节点:
      1. 找到标签相等,且内部元素相等的两个节点
      2. 找到标签相等,且属性相等的两个节点
      3. 找到标签相等的两个节点

    对于1,也就是子树Diff过程,这两个节点可以直接被跳过,代价最低;对于2.1,仅该层节点出现差异,其子孙无差异,代价很低;对于2.2,该层节点无差异,但其子孙有差异,代价未知;对于2.3,均有差异,但类型相同,代价未知但一定高于2.2。

    按照如上顺序执行配对,对于1和2.1显然是最优的,但2.2与2.3只能近似优化,原因是深于子节点层次的情况我们是不知道的,只能贪心地进行下去。

  • 子问题(2)和(3)

    在子问题(1)基础上,对于标签不等的,前一个子元素集合中剩下的未配对的就是被删除的元素;后一个子元素集合中剩下的未配对的就是被增加的元素。

算法伪代码

A - B:
- 比较tag
    1. tag不等 => 移除B,增加A
    2. tag相等 => dynamic_cast
- 比较属性
    1. A中属性与B中不等
        1. A中属性为默认值 => 重置属性
        2. A中属性不为默认值 => 设置属性 
- 比较inner_elements
    1. 对于outer_hash相等的配对(完全相同) => 排序
    2. 对于outer_hash不等的配对
        1. 寻找tag相等且inner_hash相等的配对(属性变更) => 递归
        2. 寻找tag相等且attribute_hash相等的配对(内部元素变更) => 递归
        3. 寻找tag相等的配对(内部元素变更且属性变更)=> 递归
        4. tag不等
            1. 在A中剩下的 => 增加
            2. 在B中剩下的 => 移除
    3. 按照配对排序

复杂度分析

对于子元素Diff算法,使用散列映射优化,能够将复杂度优化为 O ( m ) O(m) O(m) m m m表示子元素个数。

对总体,最坏情况下是 O ( n ) O(n) O(n)。在每次只进行一个操作的假设下,对满叉树而言,平均复杂度为 O ( l o g n ) O(logn) O(logn)

代价分析

最坏代价肯定是替换整棵树,这是基于两棵树完全不同的最坏假设而言的。在每次只进行一个操作的假设下,对满叉树而言,代价比较平均,除非出现子树的整体移动。

二看Diff算法

优点和缺点

  • 优点

    算法简单,执行效率高。

  • 缺点

    无法优化整体移动的代价:

    基于同层次的Diff算法无法识别这种移动情形,最终的结果仍然是先删后增。

对于本项目

本项目中SVG的DOM结构层级比较少,且整体移动的情况很罕见,故坏情况的出现概率非常低。相反,组件在本项目中被设计的的组织结构恰好为同层次结构,因此上述算法对本项目的针对性相当强。

核心代码

  • 子元素Diff

    void SVGElement::inner_differ(const SVGElement &element,
            std::vector<_el_idx> &removal,
            std::vector<_el_idx> &addition,
            std::vector<std::pair<_el_idx, _el_idx>> &unchanged,
            std::vector<std::pair<_el_idx, _el_idx>> &changed) const {
        std::function<const std::string(const std::shared_ptr<SVGElement> &)> get_tag;
        get_tag = [](const std::shared_ptr<SVGElement> &x){
            if (x->get_raw_HTML() != STR_NULL) return std::string("#") + std::to_string(x->get_outer_hash());
            return std::string(x->get_tag());
        };
        std::unordered_map<std::string, std::set<_el_idx>> tags_map;
        std::set<_el_idx> A, B;
        int c = 0;
        for (auto &a : _inner_elements) A.insert({a, c++}); c = 0;
        for (auto &b : element.get_inner_elements()) tags_map[get_tag(b)].insert({b, c}), B.insert({b, c++});
    
        c = 0;
        for (auto &_a : _inner_elements) { // with outer hash equal
            auto &tag  = get_tag(_a);
            _el_idx a = { _a, c++ };
            if (!A.count(a) || !tags_map.count(tag)) continue;
            _el_idx match = { nullptr, -1 };
            for (auto &b : tags_map[tag]) {
                if (b.ptr->get_outer_hash() == a.ptr->get_outer_hash()) {
                    match = b;
                    break;
                }
            }
            if (match.idx >= 0) {
                tags_map[tag].erase(match);
                A.erase(a), B.erase(match);
                unchanged.push_back({a, match});
            }
        }
        c = 0;
        for (auto &_a : _inner_elements) { // with inner hash equal
            auto &tag  = get_tag(_a);
            _el_idx a = { _a, c++ };
            if (!A.count(a) || !tags_map.count(tag)) continue;
            _el_idx match = { nullptr, -1 };
            for (auto &b : tags_map[tag]) {
                if (b.ptr->get_inner_hash() == a.ptr->get_inner_hash()) {
                    match = b;
                    break;
                }
            }
            if (match.idx >= 0) {
                tags_map[tag].erase(match);
                A.erase(a), B.erase(match);
                changed.push_back({a, match});
            }
        }
        c = 0;
        for (auto &_a : _inner_elements) { // with attribute hash equal
            auto &tag  = get_tag(_a);
            _el_idx a = { _a, c++ };
            if (!A.count(a) || !tags_map.count(tag)) continue;
            _el_idx match = { nullptr, -1 };
            for (auto &b : tags_map[tag]) {
                if (b.ptr->get_attribute_hash() == a.ptr->get_attribute_hash()) {
                    match = b;
                    break;
                }
            }
            if (match.idx >= 0) {
                tags_map[tag].erase(match);
                A.erase(a), B.erase(match);
                changed.push_back({a, match});
            }
        }
        c = 0;
        for (auto &_a : _inner_elements) { // with tag equal
            auto &tag  = get_tag(_a);
            _el_idx a = { _a, c++ };
            if (!A.count(a) || !tags_map.count(tag) || tags_map[tag].size() == 0) continue;
            _el_idx match = *tags_map[tag].begin();
            tags_map[tag].erase(match);
            A.erase(a), B.erase(match);
            changed.push_back({a, match});
        }
        for (auto &a : A) addition.push_back(a);
        for (auto &b : B) removal.push_back(b);
    }	
    
  • 属性Diff

    const std::string SVGElement::attribute_differ(const SVGElement &element) const {
        std::stringstream ss;
    
        if (_id != element.get_id()) {
            if (_id == STR_NULL) ss << "reset id" << std::endl;
            else ss << "modify id \"" << _id << "\"" << std::endl;
        }
        if (_lang != element.get_lang()) {
            if (_lang == STR_NULL) ss << "reset lang" << std::endl;
            else ss << "modify lang \"" << _lang << "\"" << std::endl;
        }
        // 省略其他数百个属性...
    }
    
  • Diff = 子树Diff + 属性Diff + 子元素Diff

    const std::string SVGElement::operator-(const SVGElement &element) const {
        std::stringstream ss;
    
        // tag differ
        if (get_tag() != element.get_tag()) {
            auto svg = outer_SVG();
            ss << "replace " << svg.size() << std::endl << svg << std::endl;
            return ss.str();
        }
    
        // cast
        auto _element = static_cast<const SVGElement &>(element);
    
        // attribute differ
        if (element.get_attribute_hash() != get_attribute_hash()) ss << attribute_differ(_element);
    
        // inner differ
        if (element.get_inner_hash() == get_inner_hash()) return ss.str();
    
        // extract change relation
        std::vector<_el_idx> removal;
        std::vector<_el_idx> addition;
        std::vector<std::pair<_el_idx, _el_idx>> unchanged;
        std::vector<std::pair<_el_idx, _el_idx>> changed;
        inner_differ(element, removal, addition, unchanged, changed);
        // remove
        int m = _inner_elements.size(), n = element.get_inner_elements().size();
        int *indices = new int[m], *removed = new int[n];
        std::fill(indices, indices + m, 0); std::fill(removed, removed + n, 0);
        for (auto &r : removal) removed[r.idx] = 1;
        for (int i = 1; i < n; i++) removed[i] += removed[i - 1];
        for (auto &r : removal) ss << "remove " << r.idx - (r.idx > 0 ? removed[r.idx - 1] : 0) << std::endl;
        // append
        for (auto &a : addition) {
            auto svg = a.ptr->outer_SVG();
            ss << "append " << svg.size() << std::endl << svg << std::endl;
        }
        // change recursively
        for (auto &c : changed) {
            auto &a = c.first; auto &b = c.second;
            ss << "child " << b.idx - removed[b.idx] << std::endl;
            ss << (*a.ptr - *b.ptr);
            ss << "parent" << std::endl;
        }
        // sort
        for (auto &c : unchanged) {
            auto &a = c.first; auto &b = c.second;
            indices[b.idx - removed[b.idx]] = a.idx;
        }
        for (auto &c : changed) {
            auto &a = c.first; auto &b = c.second;
            indices[b.idx - removed[b.idx]] =  a.idx;
        }
        for (int i = 0; i < addition.size(); i++) {
            auto &a = addition[i];
            indices[unchanged.size() + changed.size() + i] = a.idx;
        }
        bool ordered = true;
        for (int i = 0; i < m && ordered; i++) if (indices[i] != i) ordered = false;
        if (!ordered) {
            ss << "sort \"";
            for (int i = 0; i < m; i++) {
                ss << indices[i];
                if (i < m - 1) ss << ",";
            }
            ss << "\"" << std::endl;
        }
        delete[] removed; delete[] indices;
    
        return ss.str();
    }
    

    调用inner_differ后,我们只是获取了算法中认为被修改的、被添加的、被删除的元素。我们还需要对它们进行后处理:对索引进行编号,然后配对,以正确得到基本操作removesort中需要的索引参数。

    此外,还要在最开头判断标签是否相等。如果标签不相等,则进行整树替换;否则可以进行类型转换,并执行后续各Diff过程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值