Max—— 前端攻城狮 's Blog

A Simple pure blog generated by jekyll

WEBKIT内核学习笔记

<< Back

WEBKIT 内核页面机制

DOM树和RENDER树

WEBKIT内核中每个HTML页面对应于一颗DOM树和RENDER树(DOM树和RENDER树是同时生成的),DOM树用于描述HTML页面的的结构化信息(结构层),而RENDER树则用于布局(表现层),具体负责DOM树如何显示在屏幕上,从MVC的角度来说,可以将RENDER树看成是V,DOM树看成是M,C则是具体的调度者,比HTMLDocumentParser等。WEBKIT将这两部分开,可以看出其设计意图,同一个DOM,可以对应不同的RENDER,或者不同的XML文档,对应于相同的RENDER,显示出其极大的灵活性。

WEBKIT的整个解析过程,大致是这样,从网络返回数据后,将数据交给HTMLDocumentParser,然后HTMLDocumentParser将文本字符的解析交给HTMLDocumentTokenizer来负责,HTMLDocumentTokenizer解析出一个一个的标签(HTML文档时是以标签为单位),HTMLDocumentParser将标签交给,HTMLTreeBuilder来构建DOM树,这里的HTMLDocumentParser从MVC的角度来看,其作用和地位应该是C,这样,MVC各兼其责,最终生成一颗RENDER树,将其显示在屏幕上。

    从上图可以看出
  • a. WEBKIT所表现出的极大的灵活性,对于HTML文档,是生成HTMLDocumentParser,对于其他的XML文档,则可以自行设计对应的Parser,对于上层是需要拿到一个DocumentParser即可。
  • b. HTMLTokenizer就是词法分析器,执行第一步词法分析。
  • c. HTMLTreeBuilder中的职责就是类“树”结构的生成(包括DOM树和RENDER树)。
  • d. HTMLScriptRunner主要是用来运行脚本。
  • e. HTMLParserScheduler中的职责是实现异步解析。

WEBKIT 内核 解析机制

HTML词法解析器——HTML Tokenizer

HTML Tokenizer意为HTML词法解析器,它的任务,就是将输入的字节流解析成一个个的标记(HTMLToken),然后由语法解析器进行下一步的分析。

HTMLTokenizer是在HTMLDocument里被创建的。

HTMLParser类理解为语法分析器,它通过HTMLTokenizer识别出的一个一个的标识(tag)来创建Element(Node),把Element组织成一个DOM Tree, 同时,同步生成Render Tree。

构造DOM节点时序图描述:在HTMLTreeBuilder的processTag中,是根据不同的标签类型进行处理,上面的时序图仅仅体现出类型,下面将源码贴上:

if (tokenizer) {
	ASSERT(!tokenizer->wantsRawData());
	tokenizer->write(decoded, true);
    
void HTMLTreeBuilder::processToken(AtomicHTMLToken & token)  
{  
    switch (token.type()) {  
    case HTMLToken::Uninitialized:  
        ASSERT_NOT_REACHED();  
        break;  
    case HTMLToken::DOCTYPE:  
        processDoctypeToken(token);  
        break;  
    case HTMLToken::StartTag:  
        processStartTag(token);  
        break;  
    case HTMLToken::EndTag:  
        processEndTag(token);  
        break;  
    case HTMLToken::Comment:  
        processComment(token);  
        return;  
    case HTMLToken::Character:  
        processCharacter(token);  
        break;  
    case HTMLToken::EndOfFile:  
        processEndOfFile(token);  
        break;  
    }  
}  


    

HTML语法解析器——HTML Parser

HTML Parser意为语法解析器,它通过HTMLTokenizer识别出的一个一个的标识(tag)来创建Element(Node),把Element组织(解析)成一个DOM Tree, 同时,同步生成Render Tree。

因为webkit支持边解析边绘制,也支持多线程,所以HTMLTokenizer的write函数首先会判断上次丢过来的数据是否已解析完,否则追加到上次的数据后面。

write函数里有一个大的while循环,用于逐个字符的解析,这里代码太多,只贴一下重点,我补上注释说明:

 if (tokenizer) {
        ASSERT(!tokenizer->wantsRawData());
        tokenizer->write(decoded, true);
	
 while (!src.isEmpty() && (!frame || !frame->loader()->isScheduledLocationChangePending())) {
        UChar cc = *src;                            //从html数据Buffer中取出一个字符
        bool wasSkipLF = state.skipLF();            //是否要跳过回车
        if (wasSkipLF)
            state.setSkipLF(false);
        if (wasSkipLF && (cc == '\n'))
            src.advance();
        else if (state.needsSpecialWriteHandling()) {
            if (state.hasEntityState())
                state = parseEntity(src, dest, state, m_cBufferPos, false, state.hasTagState());
            else if (state.inComment())             //注释文本,如:<!--这里是注释 -->
                state = parseComment(src, state);
            else if (state.inDoctype())             //HTML的DocType
                state = parseDoctype(src, state);
            else if (state.inServer())              //asp或jsp的服务端代码,如:<%***%>
                state = parseServer(src, state);
            else if (state.startTag()) {            //重点注意:这里是检测到 '<'字符,

    

在检测到'<'字符后,表示后面跟的就是标签(html Tag)啦,这条分支里主要有两个函数:

processToken和parseToken。 这里是重点。。。。

*: processToken的作用是,在开始一个新的Tag之前,先看一下上一个tag是否已经处理完毕了?因为webkit的兼容性非常好,举例如有“”而没有“”时,这里能兼容到,而不会因为网页设计人员的失误,导致网页绘制失败。(该函数还有另外一个作用,下面会介绍) *: parseTag函数,看名字就知道啦,它就是真正开始词法分析一个html tag标签的函数。

parseTag函数里也是一个大的while,状态机,state变量维护这个状态机,有如下几种状态:

	enum TagState {
        NoTag = 0,
        TagName = 1,
        SearchAttribute = 2,
        AttributeName = 3,
        SearchEqual = 4,
        SearchValue = 5,
        QuotedValue = 6,
        Value = 7,
        SearchEnd = 8
    };

    

TagName: 一个HTML Tag的开始,它会把Tag的名字存在一个叫Token的成员变量里,它永远保存当前正在Parser的Tag的数据。 AttributeName: 在处理这个状态时,会把所有的大写转为小写。因为html标准中的attribute是不区分大小写的,这样做的目的是加快后面字符比较的速度。 SearchEqual: 循环到到'='时,会把attributeName添加到currToken这个Token里。 SearchEnd: 表示当前的Tag全部解析完了,噢,终于完整地解析完一个Tag了,这里该干嘛了? 当然是生成DOM节点啦, 这个时候,token成员类变里已经存下了:Tag的名字,所有的attribute和value,有了这些后,会调用: RefPtr n = processToken();

processToken就是真正生成DOM节点和Render节点的函数。 processToken函数会调用parser->parseToken(&currToken); 该函数定义:PassRefPtr HTMLParser::parseToken(Token* t)。 返回的就是一个Node的节点, Node类是所有DOM节点的父类。

HTMLParser::parseToken函数重点代码介绍: RefPtr n = getNode(t); //这里返回Node节点, 往里面跟,会发现它用了很多设计模式的东西 if (!insertNode(n.get(), t->flat)) { //会调用Node* newNode = current->addChild(n); 把当前的新节点加入到DOM Tree中。

接下来会调用Element::attach,创建相对应的Render节点,代码如下:

void Element::attach()
{
    createRendererIfNeeded();
    ContainerNode::attach();
    if (ElementRareData* rd = rareData()) {
        if (rd->m_needsFocusAppearanceUpdateSoonAfterAttach) {
            if (isFocusable() && document()->focusedNode() == this)
                document()->updateFocusAppearanceSoon();
            rd->m_needsFocusAppearanceUpdateSoonAfterAttach = false;
        }
    }

    

真正创建Render的地方: RenderObject::createObject(), 该函数会根据不同的type,而创建不同的Render节点。

参考webkit Parser模块

参考webkit中 html的解析及dom树和render树的生成

发表于: 24 Aug 2015