webkit Parser模块

前面把Loader数据加载模块介绍完了,那有了数据以后就可以开始解析了,但在介绍parser模块之前,需要知道数据从curl怎么过来的,因此,本篇先介绍一下ResourceHandleManager.cpp里下面这几个函数:

headerCallback

writeCallback
readCallback

顾名思义,这三个函数是http请求发出以后的回调函数(为了实现异步操作),分别为写回调、http头回调、读回调, 大家去翻代码会发现在ResourceHandleManagerinitializeHandle函数里会调用以下函数,把这几个回调注册到libcurl里:

curl_easy_setopt(d->m_handle, CURLOPT_HEADERFUNCTION, headerCallback);

curl_easy_setopt(d->m_handle, CURLOPT_WRITEFUNCTION, writeCallback);   

curl_easy_setopt(d->m_handle, CURLOPT_READFUNCTION, readCallback);

重点先讲一下readCallback函数,它有点特殊,它是在ResourceHandleManagersetupPOST里函数里设置的,为什么呢?

下面解说之前,读者先要了解http协议的GetPost才行,这是必须滴。因为一般请求URL地址,都只是Get请求,请求发出去后,只要接收http数据显示就行了。并不需要再发送啥数据了。

只有当Post(上传)一个文件或者提交Form表单的Post数据时,才需要读的回调, 读回调被调用的时机是: socketconnect建立成功后,先发送的是post请求的http头部数据,再发送Body的数据,也就是说readCallback是用来发送body数据的。能理解吗?

下面分别列举一下GetPost的数据(我比较喜欢举例子,有例子更形象也容易理解):

Get

GET /books/?name=maxxiang HTTP/1.1

Host: www.qq.com

User-Agent: QQBrowser/1.3 (MTK 6235; U; MTK 1.3; ch-china; rv:1.7.6)

Connection: Keep-Alive


POST

POST / HTTP/1.1

Host: www.qq.com

User-Agent: QQBrowser/1.3 (MTK 6235; U; MTK 1.3; ch-china; rv:1.7.6)

Content-Type: application/x-www-form-urlencoded

Content-Length: 29

Connection: Keep-Alive

     (此处空一行)

name=maxxiang&bookname=webkit

注意: POSThttp头和Body部分,中间要有2”\r\n”来区分。没记错的话应该是RFC 2616规定的。

 

下面来分别跟踪一下代码:

 

1)       headerCallback

从上面的函数调用栈能看出,源头就是Loader章节讲到的downloadTimerCallback函数,它是读取libcurl数据的总控制函数。

Curl库的Curl_client_write函数分别把http头,按”\r\n”切隔分多次调用headerCallback传递给Loader,每次只传一个http字段,这样的做法好处是,curl需要自已解析并保存http头部字段的数据值,同时,也方便了webkit的处理,因为webkit也是需要处理这些数据的,它需要把http头部字段都setm_response里,后面用得着。


2)       writeCallback

发现writeCallback的函数调用栈比headerCallback深了一些,是因为html body的数据是可以做压缩的,而curl发现http头部标识的是gzip压缩的数据,会先解压后,再传给webkitwriteCallback函数。

         writeCallback里,首先会判断Http 返回Code, 200表示成功等。

    if (CURLE_OK == err && httpCode >= 300 && httpCode < 400)

        return totalSize;

    if (d->client())

        d->client()->didReceiveData(job, static_cast<char*>(ptr), totalSize, 0);

         可以看到,httpCode判断了300—400之间,如果属于这个区间,就直接返回了,为什么呢,

         这是因为300-400之间的代码,都是需要再处理的或再跳转请求的。

举例如下(目前有用到好像只有是300-307,具体可以去查看RFC规范,这里仅举两个例子):

如果301,302都表示跳转,就不用去解析body部分了,直接跳转。

         如果303表示请求是 POST,那么就要转为调用 GET再请求才能取到正确的数据。

 

3)       readCallback

readCallback里,直接调用的m_formDataStream.read(ptr, size, nmemb)函数来上传。

FormDataStream类可以重点看一下,它的read函数实现了上传文件和上传Form表单(httpbody)的两个分支流程。


 1).我们都知道一个HTML文件,都是以<HTML>开头。(WML是以<wml>开头)webcore <html>标签对应的是HTMLHtmlElement节点。

但实际上<html>标签(HTMLHtmlElement)并不是DOM树的根节点,webkitDOM树的真正根节点是:HTMLDocument 当我们在浏览器里打开一个空白页面的时候,webkit首先会生成DOM Tree的根节点HTMLDocumentRender Tree的根节点RenderView。也就是说,当前是空白页而没有html网页显示时,这个DOM TreeRender Tree的根节点就已经存在了。只是这两棵Tree的子节点都是“空”的而已。理解了吗?

    2).当用户在同一个浏览器页面中,先后打开两个不同的HTML文本文件时,会发现HTMLDocumentRenderView两个根节点并没有发生改变,改变的是HTMLHtmlElement及下面的子树,以及对应的Rendering Tree的子树。

         为什么这样设计?原因是HTMLDocumentRenderView服从于浏览器页面的设置,譬如页面的大小和在整个屏幕中的位置等等。这些设置与页面中要显示什么的内容无关。

同时HTMLDocument包含一个HTMLTokenizer的成员,而HTMLTokenizer绑定HTMLParser(下面具体介绍这两个组件)

         这样设计的好处是: 因为HTMLElement是一开始就创建的,不会随着打开一个新的URL页面而释放(free)再创建(malloc),节省了CPU时间。

   

    1).语言的解析一般分为词法分析(lexical analysis)和语法分析(Syntax analysis)两个阶段,WebKit中的html解析也不例外。

    词法分析的任务是对输入字节流进行逐字扫描,根据构词规则识别单词和符号,分词。

    2).状态机:有穷状态机是一个五元组 (Q,Σ,δ,q0F),后面省略....

 

3.下面介绍Parser模块里两个“非常非常”重要的组件:

    HTMLTokenizerHTMLParser

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

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

4.Parser模块代码走读(如何生成DOM树和Render树的):

  1). 上文提到的writeCallback接收到html数据后,首先调用ResourceLoader的didReceiveData。一路往下传:ResourceLoader::didReceiveData->MainResourceLoader::didReceiveData->..->FrameLoader::receivedData->DocumentLoader::receivedData->FrameLoader::committedLoad

  2). FrameLoader::committedLoad函数这里注意,它会判断ArchiveMimeType,如果是同类的,则不需要往下走。(也就是上面第1节说的,不需往下创建HTMLTokenizer了,因为可以复用嘛)

    注: 有兴趣可继续往下看代码,HTMLTokenizer是在HTMLDocument里被创建的。

  3). 接下来会进入FrameLoader::addData-->write函数。 write函数首先处理字符编码的问题,把解码后的html数据,继续往下丢给HTMLTokenizer的write函数:

    if (tokenizer) {

        ASSERT(!tokenizer->wantsRawData());

        tokenizer->write(decoded, true);

  4). 真正的词法分析开始啦

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

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

  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的兼容性非常好,举例如有“<begin>”而没有“</end>”时,这里能兼容到,而不会因为网页设计人员的失误,导致网页绘制失败。(该函数还有另外一个作用,下面会介绍)

  *: parseTag函数,看名字就知道啦,它就是真正开始词法分析一个html tag标签的函数。

  5).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<Node> n = processToken();

  6). processToken就是真正生成DOM节点和Render节点的函数。

   processToken函数会调用parser->parseToken(&currToken);  

   该函数定义:PassRefPtr<Node> HTMLParser::parseToken(Token* t)。 返回的就是一个Node的节点, Node类是所有DOM节点的父类。

  7).  HTMLParser::parseToken函数重点代码介绍:

    RefPtr<Node> n = getNode(t);    //这里返回Node节点,  往里面跟,会发现它用了很多设计模式的东东

    if (!insertNode(n.get(), t->flat)) {        //会调用Node* newNode = current->addChild(n); 把当前的新节点加入到DOM Tree中。

  8).接下来会调用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;

        }

    }

  9:真正创建Render的地方:

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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值