作者:秦策

《IE 浏览器 DOM 树结构概览(上)》

四、DOM 流的修改

现代浏览器中,为了更好的用户体验,页面经常需要根据不同情况动态进行变化,DOM 流也需要相应的进行修改。为了提高对于流的访问效率,IE 浏览器采用 Splay tree 来对这个流进行操作。SplayTree 虽然名义上称作 Tree,其实并不是一个真正意义上树结构,其本质是为了高效操作流结构而产生的一套改进算法。

IE 中SpalyTree 的基本数据结构为 CTreePos,用于表示流中各数据在 SplayTree 中的逻辑关系,再看一遍 CTreePos 的数据结构,其_pFirstChild_pNext 指针便是用于描述当前节点在 SplayTree 中的逻辑关系。

class CTreePos
{
public:
    DWORD       _cElemLeftAndFlags;  // 左子树中的 Begin Element 数量以及一些与 tag 有关的属性
    DWORD       _cchLeft;           // 左子树中的字符数量
    CTreePos*   _pFirstChild;      //  指向第一个子节点
    CTreePos*   _pNext;             //  若当前节点为父节点的最后一个节点,则该指针指向父节点,否则指向兄弟节点
    CTreePos*   _pLeft;
    CTreePos*   _pRight;
}

SplayTree 的主要功能既是在保证流结构顺序的情况下,使得最后访问的节点处在树的最顶层,从而提升访问效率。以如下页面举例想要在在页面[p1]、[p2] 位置插入标签<p>

<html>[p1]<textarea></textarea>[p2]<html>
首先访问 [p1] 位置,通过 Splay 操作将 [p1] 所指节点旋转置树顶,此时 SplayTree 如下左树所示;接着访问 [p2] 位置,SplayTree 变为如下右图,此时针对DOM 的操作只需要发生在 [p1] 的右子树上即可

p1                                 p2
/    \                             /    \
hh    th                           p1    he
         \                         /  \
            te            =>       hh    th
                \                            \
                 p2                           te 
                     \
                        he

// hh 表示 html tag 的头结点,he表示html tag 的尾节点。依次类推

SplayTree 的核心函数 Splay()逆向如下,部分冗余代码没有给出

void CTreePos::Splay(CTreePos * t_node)
{
    DWORD t1;
    CTreePos *v1;
    CTreePos *v2;
    CTreePos *v3;
    CTreePos *v5;
    CTreePos *v6;
    CTreePos *v7;
    // v2 = t_node->parent->parent
    v1 = t_node->Parent();
    v2 = v1->Parent();

    while (v2)   // while grandparent
    {
        v3 = v2->Parent();
        // 如果 v3 没有(当前节点已经旋转到第三层,没有曾祖父节点了)则只进行一次单旋;否则进行一次之字旋或者一字旋;
        if (v3)
        {
            t1 = t_node->_cElemLeftAndFlags ^ v1->_cElemLeftAndFlags;
            if (t1 & TPF_LEFT_CHILD)  // 如果 t_node 与其父节点形成左右、或者右左的关系, 则进行之字形旋转
            {
                //           v3                         v3                               v3
                //          /  \                       /  \                            /    \
                //         v2   E                     v2   E                          t      E
                //        /  \                       /  \                           /    \ 
                //       D    v1          =>        D    t             =>         v2      v1
                //           /  \                       /  \                     /  \    /  \
                //          t    C                     A    v1                  D    A  B    C
                //         / \                             /  \
                //        A   B                           B    C
                v1->CTreePos::RotateUp(t_node, v2);   //RotateUp(t_node<eax>,v1<ecx>,v2);
                v5 = v2;
            }
            else           // 如果 t_node 与 其父节点均为左节点、或均为右节点,则进行一字型旋转
            {
                //               v3                            v3                              v3
                //              /  \                         /    \                           /  \
                //             v2   E                       v1     E                         t    E
                //            /  \                        /    \                            / \
                //           v1   D           =>         t      v2            =>           A   v1
                //          /  \                        / \    /  \                           /  \
                //         t    C                      A   B  C    D                         B    v2
                //        / \                                                                    /  \
                //       A   B                                                                  C    D
                v2->CTreePos::RotateUp(v1, v3);      //RotateUp(v1<eax>,v2<ecx>,v3);
                v5 = v1;
            }
            v6 = v3;
        }
        else
        {
            v5 = v1;
            v6 = v2;
        }
        v5->RotateUp(t_node, v6);     //RotateUp(t_node<eax>,v5<ecx>,v6);
        // v2 = t_node->parent->parent
        v1 = t_node->Parent();
        v2 = v1->Parent();
    }
    return;
}

void CTreePos::RotateUp(CTreePos* childNode, CTreePos* parentNode)
{
    CTreePos *v1;
    CTreePos *v2;
    CTreePos *v3;
    CTreePos *v4;
    CTreePos *v5;
    CTreePos *v6;
    DWORD v7;
    DWORD v8;

    if (childNode->IsLeftChild())
    {
        // 右旋
        //        this               child
        //       /    \              /    \
        //    child     c     =>    a     this
        //    /   \                       /   \
        //   a     b                     b     c 
        //
        // 得到 childNode 的左节点 ,通过 v2 指示出来
        v1 = childNode->_pFirstChild; 
        if (v1 && v1->IsLeftChild())
            v2 = v1;
        else
            v2 = NULL
        //得到 childNode 的右节点,通过 v3 表示
        v1 = childNode->_pFirstChild;
        v3 = 0;
        if (v1)
        {
            if (v1->IsLeftChild())
            {
                // 如果其左节点有兄弟节点,则该兄弟节点为右节点
                if (!v1->IsLastChild()) 
                    v3 = v1->_pNext;
            }
            else
            {
                v3 = v1;
            }
        }
        //得到 this 的右节点,通过 v5 表示
        v4 = this->_pFirstChild;
        v5 = 0;
        if (v4)
        {
            if (v4->IsLeftChild())
            {
                // 如果其左节点有兄弟节点,则该兄弟节点为右节点
                if (!v4->IsLastChild())
                    v5 = v4->_pNext;
            }
            else
            {
                v5 = v4;
            }
        }
        //替换 this 节点和 childNode 节点的上下关系
        this->ReplaceChild(childNode, parentNode);
        // 如果 childNode 有左节点,则该节点为 childNode 的第一个子节点,且该节点的兄弟节点应为 this
        // 如果 childNode 没有左节点,则 childNode 的第一个子节点为 this
        if (v2)
        {
            v2->MarkFirst();
            v2->_pNext = this;
        }
        else
        {
            childNode->_pFirstChild = this;
        }
        // 如果 childNode 有右节点,则 this 节点的第一个节点为该节点
        // 如果 childNode 没有右节点,则 this 节点的第一个节点为其右节点
        if (v3)
        {
            this->_pFirstChild = v3;
            v3->MarkLeft();
            // 如果 this 节点也有右节点,则此节点为原 childNode 右节点的兄弟节点
            // 如果 this 节点没有右节点,则此节点变为 this 最后一节点,需要为其设置父节点指针
            if (v5)
            {
                v3->MarkFirst();
                v3->_pNext = v5;
            }
            else
            {
                v3->MarkLast();
                v3->_pNext = this;
            }
        }
        else
        {
            this->_pFirstChild = v5;
        }
        //this 节点变为 childNode 节点的右节点,也即最后一个节点,将其父节点指针设置为 childNode
        this->MarkRight();
        this->MarkLast();
        this->_pNext = childNode;
        // 调整 this 节点和 childNode 节点的 subtree num
        v7 = ((childNode->_cElemLeftAndFlags >> TPF_FLAGS_SHIFT) << TPF_FLAGS_SHIFT);   //GetElemLeft : 清除flag 位的干扰
        this->_cElemLeftAndFlags - v7;
        this->SetFlag(_cElemLeftAndFlags);
        this->_cchLeft = this->_cchLeft - childNode->_cchLeft;
        // childNode 节点
        v8 = this->_cchLeft;
        if (childNode->IsNode())  // Begin,End
        {
            if (childNode->IsData2Pos())
            {
                this->_cchLeft = v8 - 1;
                if (childNode->IsBeginNode())  // NodeBeg
                    this->SetFlag(_cElemLeftAndFlags - 0x100);   // ElemLeft 减一
            }
        }
        else if (childNode->IsText())
        {
            v8 = v8 - (childNode->GetInterNode()->_tpEnd._cchLeft) & 0x3FFFFFFF;
            this->_cchLeft = v8;
        }
        return;
    }
    else
    {
        // 左旋
        //        child                 this
        //       /     \               /    \
        //      a     this    =>    child     c
        //           /   \          /   \
        //          b     c        a     b
        //代码总体和右旋差异不大,这里不再逆向
    }
}

在通过 SplayTree 高效的实现了 DOM 流的访问之后,IE 设计了一套专门用于操作 DOM 树的机制称为 CSpliceTreeEngine,对于 DOM 流的一系列修改操作均通过它来进行。SpliceTreeInternal() 函数部分功能逆向如下

CMarkup::SpliceTreeInternal( CTreePosGap *tpg_Begin,  CTreePosGap *tpg_End, CMarkup* target, CTreePosGap *tpg_tar, DWORD opt1,DWORD *opt2)
{

  CDoc *v1;
  CSpliceTreeEngine v2;
  HRESULT result;

  v1 = this->Doc();
  v2 = CSpliceTreeEngine::CSpliceTreeEngine(v1);
  EnsureTotalOrder(tpg1, tpg2);

  result = CSpliceTreeEngine::Init(this, tpg_Begin, tpg_End, target, tpg_tar, opt1,  opt2);
   // ...
   case copy:
    {
        result = v1->CSpliceTreeEngine::RecordSplice();
        result = v1->CSpliceTreeEngine::InsertSplice();
    }
   case move:
   {
    result = v1->CSpliceTreeEngine::RecordSplice();
    result = v1->CSpliceTreeEngine::RemoveSplice();
    result = v1->CSpliceTreeEngine::InsertSplice();
    }
   // ...
  CSpliceTreeEngine::~CSpliceTreeEngine(v1);
  return result;
}

函数首先调用 RecordSplice函数将源 DOM 流中的节点信息备份一遍,接着根据操作要求决定是否将源 DOM 流中的节点信息删除,最后将之前备份的节点信息插入目标 DOM 流中。

对 DOM 流结构进行操作还需要有一个重要的结构 CTreePosGap,该结构用于指示两个 CTreePos 之间的内容,在对流进行插入和删除操作时都需要用到CTreePosGap结构来指示需要操作的区间。CTreePosGap 数据结构如下所示

class CTreePosGap{
    CElement    *_pElemRestrict; // 指定 CTreePosGap 所在的Element范围
    CTreePos    *_ptpAttach;    // 指示与 CTreePosGap 相关联的 CTreePos
    unsigned    _fLeft : 1;       // 当前 Gap 是否在  CTreePos 的左边
    unsigned    _eAttach : 2;
    unsigned    _fMoveLeft : 1;
}

当然上述操作均要通过 CMarkupPointer来作为 DOM 流的指针才能完成。

通常情况下,一个页面内容被修改之后, 页面中的CMarkupPointer还会保留在之前未修改时的位置。举例来说

abc[p1]defg[p2]hij
abc[p1]deXYZfg[p2]hij

当第一个页面被修改为第二个页面之后,虽然页面的内容发生了改变,但是 CMarkupPointer的相对位置仍然保持不变。但如果页面的修改发生在 CMarkupPointer 指向的位置,如上例中,向c、d之间插入一个Z,p 的位置就会出现二义性。

abcZ[p1]de or abc[p1]Zde
这时就需要引用另一个重要的概念gravity,每一个 CMarkupPointer 都有一个 gravity 值标识着其左偏或右偏。仍以上述页面为例

abc[p1,right]defg[p2,left]hij
分别在p1,p2的位置插入一对 <B>标签。这时由于gravity的存在,页面会变成如下

abc<B>[p1,right]defg[p2,left]</B>hij
默认情况下CMarkupPointergravity 值是 left。下面的函数负责查看或者修改CMarkupPointergravity

enum POINTER_GRAVITY {
    POINTER_GRAVITY_Left,
    POINTER_GRAVITY_Right
};

HRESULT Gravity(
    POINTER_GRAVITY *pGravityOut
);

HRESULT SetGravity(
    POINTER_GRAVITY newGravity
);

再考虑如下例子

[p2]ab[p1]cdxy
当bc 段被移动到 xy之间时p1的位置也出现了二义性,是应该随着bc移动,还是应该继续保持在原位呢

[p2]a[p1]dxbcy or [p2]adxb[p1]cy
这就需要 cling 的存在,如果p1指定了cling属性,那么页面操作之后就会成为右边所示的情况,否则就会出现左边所示的情况

clinggravity可以协同作用,考虑下面的例子

a[p1]bcxy
b移动到x、y之间,如果p1指定了 cling属性,并且 gravity 值为 right,那么p1便会跟随b一起到xy之间。这种情况下如果b被删除,那么p1也会跟着从DOM 流中移除,但并不会销毁,因为p1还有可能重新被使用。cling相关的函数,函数原型如下

HRESULT Cling(
    BOOL *pClingOut
);

HRESULT SetCling(
    BOOL NewCling
);

下面通过实际的 js 操作来说明如何对 DOM 流进行修改的

appendChild

appendChild 意为在目标 Element 的最后添加一个子节点,其内部其实是通过 InsertBefore来实现的,

CElement::InsertBeforeHelper()
{
    cDoc = CElement::Doc(this);
     CMarkupPointer::CMarkupPointer(markupPtr, cDoc);   

      markupPointer->MoveAdjacentToElement( this, ELEMENT_ADJ_BeforeEnd); 
     CDoc::InsertElement();
}

函数首先通过 CMarkupPointer 指定到 parent 的 BeforeEnd位置,再调用 CDoc::InsertElement() -> CMarkup::InsertElementInternal进行实际的插入操作,一般而言标签都是成对出现的,因此这里需要使用两个 CMarkupPointer分别指定新插入标签的 Begin 和 End 位置

HRESULT CMarkup::InsertElementInternal(CMarkup *this, int a2, struct CElement *a3, struct CTreePosGap *a4, struct CTreePosGap *a5, unsigned __int32 a6)
{
    CTreePosGap::PartitionPointers(v10, a5, a3, v7);
    CTreePosGap::PartitionPointers(v12, v11, a3, v69);
    //......
    CTreePosGap::SetAttachPreference(((*((_BYTE *)a5 + 8) & 1) == 0) + 1, (int)&v78);
    CTreePosGap::MoveTo((CTreePosGap *)&v78, v62);
    CTreePosGap::SetAttachPreference(((*((_BYTE *)v11 + 8) & 1) == 0) + 1, (int)&v78);
    CTreePosGap::MoveTo((CTreePosGap *)&v75, v16);
    //.....
    CTreePosGap::MoveTo((CTreePosGap *)&v71, v63);
    v69 = (int)CTreePosGap::Branch(v19);
    v21 = CTreePosGap::Branch(v20);
    //......
      if ( CMarkup::SearchBranchForNodeInStory(v22, v21, v62) )
        v70 = 1;
      v23 = (CTreeNode *)HeapAlloc(g_hProcessHeap, 8u, 0x4Cu);
        //......
        v25 = CTreeNode::CTreeNode(v23, v66, 0, (int)v62);
        //......
        CElement::SetMarkupPtr(v24, v62);
        CElement::PrivateEnterTree(v26);
        //......
      v27 = CTreeNode::InitBeginPos(v24, v67 == 0);
      CMarkup::Insert(v28, a3, v27);
        //......
        v30 = CTreePos::GetCpAndMarkup(v29, 0, 0);
        //......
      v34 = CTreeNode::InitEndPos(v33, v70);
      CMarkup::Insert(v35, a3, v34);
      CTreePosGap::MoveImpl(v36, (int)&v71, 0, 0);
}

函数的主要逻辑为,首先通过一系列的 CTreePosGap 操作,指定 Begin 和 End 的位置;接着新建一个 CTreeNode并与 Element 关联。调用 CTreeNode::InitBeginPos初始化标签对象的 BeginPos ;接着调用 CMarkup::Insert 将 BeginPos 插入 DOM 流中,同时也插入 SpalyTree 中,并调用 CTreePos::GetCpAndMarkup 获取cp 信息,更新 SpalyTree 结构,同时触发 Notify ,进行响应事件的分发。完成了 BeginPos 的操作之后,对 EndPos 也执行相同的操作,最终完成功能。

replaceNode

replaceNode 用于将 DOM 流中一个节点替换为另一个节点,其主要功能函数我这里显示不出符号表,其逆向主要功能代码如下

HRESULT sub_74D359BA(CDOMTextNode *a1, int a2, int a3, struct CMarkupPointer *a4)
{
// .....
  CMarkupPointer::CMarkupPointer(v6, v7);
  CMarkupPointer::CMarkupPointer(v8, v7);
  result = CElement::GetMarkupPtrRange(v9, (struct CMarkupPointer *)&v15, (struct CMarkupPointer *)&v16, v13);
  if ( result == SUCCESS )
    v11 = CDoc::Move(v10, (struct CMarkupPointer *)&v15, (struct CMarkupPointer *)&v16, (struct CMarkupPointer *)1, v14);
  CMarkupPointer::~CMarkupPointer((CMarkupPointer *)&v16);
  CMarkupPointer::~CMarkupPointer((CMarkupPointer *)&v15);
  return v11;
}

函数的主要逻辑为,通过两个 CMarkupPointer 指针指定需要替换的目标节点在 DOM 流中的 Begin 和 End 位置,接着调用 CDoc::Move() 函数完成功能。CDoc::Move() 则直接通过调用 CDoc::CutCopyMove 来实现

HRESULT  CDoc::CutCopyMove(CDoc *this, int a2, struct CMarkupPointer *a3, struct CMarkupPointer *a4, struct CMarkupPointer *a5, int a6, DWORD a7)
{
    //......
      CTreePosGap::MoveTo(v12, TPG_LEFT);
      CTreePosGap::MoveTo(v13, TPG_RIGHT);
      CTreePosGap::MoveTo(v14, TPG_LEFT);
    // ......
      if ( v7 )
        result = CMarkup::SpliceTreeInternal((CMarkup *)&v19,v15,(struct CTreePosGap *)&v19,(struct CTreePosGap *)&v22,*(struct CMarkup **)(v7 + 28),(struct CTreePosGap *)&v16,(int)a5,a6);
      else
        result = CMarkup::SpliceTreeInternal((CMarkup *)&v19,v15,(struct CTreePosGap *)&v19,(struct CTreePosGap *)&v22,0,0,(int)a5,a6);

  return result;
}

CDoc::CutCopyMove 根据传入的 CMarkupPointer 位置信息构造三个 CTreePosGap 对象,并根据调用者的要求,选择是进行 Copy操作还是 进行Move操作,最终将请求传递给 CSpliceTreeEngine

五、总结

IE 的这种 DOM 流结构是由于历史原因形成的一种特殊情况,随着浏览器功能的越来越丰富,这种 DOM 组织方式出现越来越多的问题。在 Edge 中微软已经抛弃了 DOM 流的设计,转而构建了一个真正意义上的 DOM 树。

IE 中与 DOM 相关的内容还有很多,这里仅仅列出了一点微小的工作,还有很多复杂的结构需要进一步分析。

六、Reference

[1] [https://msdn.microsoft.com/en-us/library/bb508514(v=vs.85).aspx](https://msdn.microsoft.com/en-us/library/bb508514(v=vs.85).aspx)

文章来源于腾讯云开发者社区,点击查看原文