作者:秦策
四、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
默认情况下CMarkupPointer
的gravity
值是 left。下面的函数负责查看或者修改CMarkupPointer
的 gravity
值
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
属性,那么页面操作之后就会成为右边所示的情况,否则就会出现左边所示的情况
cling
和 gravity
可以协同作用,考虑下面的例子
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)