前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >IE 浏览器 DOM 树结构概览(下)

IE 浏览器 DOM 树结构概览(下)

原创
作者头像
腾讯玄武实验室
修改2017-08-21 09:59:46
1.6K0
修改2017-08-21 09:59:46
举报

作者:秦策

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

四、DOM 流的修改

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

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

代码语言:javascript
复制
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] 的右子树上即可

代码语言:javascript
复制
p1                                 p2
/    \                             /    \
hh    th                           p1    he
         \                         /  \
            te            =>       hh    th
                \                            \
                 p2                           te 
                     \
                        he

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

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

代码语言:javascript
复制
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() 函数部分功能逆向如下

代码语言:javascript
复制
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 数据结构如下所示

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

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

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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相关的函数,函数原型如下

代码语言:javascript
复制
HRESULT Cling(
    BOOL *pClingOut
);

HRESULT SetCling(
    BOOL NewCling
);

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

appendChild

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

代码语言:javascript
复制
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 位置

代码语言:javascript
复制
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.aspx) 用于将 DOM 流中一个节点替换为另一个节点,其主要功能函数我这里显示不出符号表,其逆向主要功能代码如下

代码语言:javascript
复制
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 来实现

代码语言:javascript
复制
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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 四、DOM 流的修改
    • appendChild
    • 五、总结
    • 六、Reference
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档