深入理解 WKWebView (渲染篇) —— DOM 树的构建

全文12003字,预计阅读时间24分钟

当客户端 App 主进程创建 WKWebView 对象时,会创建另外两个子进程:渲染进程与网络进程。主进程 WKWebView 发起请求时,先将请求转发给渲染进程,渲染进程再转发给网络进程,网络进程请求服务器。如果请求的是一个网页,网络进程会将服务器的响应数据 HTML 文件字符流吐给渲染进程。渲染进程拿到 HTML 文件字符流,首先要进行解析,将 HTML 文件字符流转换成 DOM 树,然后在 DOM 树的基础上,进行渲染操作,也就是布局、绘制。最后渲染进程通知主进程 WKWebView 创建对应的 View 展现视图。整个流程如下图所示:

深入理解 WKWebView (渲染篇) —— DOM 树的构建_第1张图片

一、什么是DOM树

渲染进程获取到 HTML 文件字符流,会将HTML文件字符流转换成 DOM 树。下图中左侧是一个 HTML 文件,右边就是转换而成的 DOM 树。

深入理解 WKWebView (渲染篇) —— DOM 树的构建_第2张图片

可以看到 DOM 树的根节点是 HTMLDocument,代表整个文档。根节点下面的子节点与 HTML 文件中的标签是一一对应的,比如 HTML 中的 标签就对应 DOM 树中的 head 节点。同时 HTML 文件中的文本,也成为 DOM 树中的一个节点,比如文本 ‘Hello, World!’,在 DOM 树中就成为div节点的子节点。

在 DOM 树中每一个节点都是具有一定方法与属性的对象,这些对象由对应的类创建出来。比如 HTMLDocument 节点,它对应的类是 class HTMLDocument,下面是 HTMLDocument 的部分源码:

class HTMLDocument : public Document { // 继承自 Document
   ...
WEBCORE_EXPORT int width();
WEBCORE_EXPORT int height();
    ...
 }

从源码中可以看到,HTMLDocument 继承自类 Document,Document 类的部分源码如下:

class Document
    : public ContainerNode  // Document继承自 ContainerNode,ContainerNode继承自Node
    , public TreeScope
    , public ScriptExecutionContext
    , public FontSelectorClient
    , public FrameDestructionObserver
    , public Supplementable
    , public Logger::Observer
    , public CanvasObserver {
      WEBCORE_EXPORT ExceptionOr> createElementForBindings(const AtomString& tagName);  // 创建Element的方法
      WEBCORE_EXPORT Ref createTextNode(const String& data); // 创建文本节点的方法
      WEBCORE_EXPORT Ref createComment(const String& data); // 创建注释的方法
      WEBCORE_EXPORT Ref createElement(const QualifiedName&, bool createdByParser); // 创建Element方法
      ....
     }

上面源码可以看到 Document 继承自 Node,而且还可以看到前端十分熟悉的 createElement、createTextNode 等方法,JavaScript 对这些方法的调用,最后都转换为对应 C++ 方法的调用。

类 Document 有这些方法,并不是没有原因的,而是 W3C 组织给出的标准规定的,这个标准就是 DOM(Document Object Model,文档对象模型)。DOM 定义了 DOM 树中每个节点需要实现的接口和属性,下面是 HTMLDocument、Document、HTMLDivElement 的部分 IDL(Interactive Data Language,接口描述语言,与具体平台和语言无关)描述,完整的 IDL 可以参看 W3C 。

在 DOM 树中,每一个节点都继承自类 Node,同时 Node 还有一个子类 Element,有的节点直接继承自类 Node,比如文本节点,而有的节点继承自类 Element,比如 div 节点。因此针对上面图中的 DOM 树,执行下面的 JavaScript 语句返回的结果是不一样的:

document.childNodes; // 返回子Node集合,返回DocumentType与HTML节点,都继承自Node
document.children; // 返回子Element集合,只返回HTML节点,DocumentType不继承自Element

下图给出部分节点的继承关系图:

深入理解 WKWebView (渲染篇) —— DOM 树的构建_第3张图片

二、DOM树构建

DOM 树的构建流程可以分为4个步骤: 解码、分词、创建节点、添加节点

2.1 解码

渲染进程从网络进程接收过来的是 HTML 字节流,而下一步分词是以字符为单位进行的。由于各种编码规范的存在,比如 ISO-8859-1、UTF-8 等,一个字符常常可能对应一个或者多个编码后的字节,解码的目的就是将 HTML 字节流转换成 HTML 字符流,或者换句话说,就是将原始的 HTML 字节流转换成字符串。

深入理解 WKWebView (渲染篇) —— DOM 树的构建_第4张图片

2.1.1 解码类图

深入理解 WKWebView (渲染篇) —— DOM 树的构建_第5张图片

从类图上看,类 HTMLDocumentParser 处于解码的核心位置,由这个类调用解码器将 HTML 字节流解码成字符流,存储到类 HTMLInputStream 中。

2.1.2 解码流程

深入理解 WKWebView (渲染篇) —— DOM 树的构建_第6张图片

整个解码流程当中,最关健的是如何找到正确的编码方式。只有找到了正确的编码方式,才能使用对应的解码器进行解码。解码发生的地方如下面源代码所示,这个方法在上图第3个栈帧被调用:

// HTMLDocumentParser是DecodedDataDocumentParser的子类
void DecodedDataDocumentParser::appendBytes(DocumentWriter& writer, const uint8_t* data, size_t length)
{
if (!length)
return;

    String decoded = writer.decoder().decode(data, length); // 真正解码发生在这里
if (decoded.isEmpty())
return;

    writer.reportDataReceived();
    append(decoded.releaseImpl());
}

上面代码第7行 writer.decoder() 返回一个 TextResourceDecoder 对象,解码操作由 TextResourceDecoder::decode 方法完成。下面逐步查看 TextResourceDecoder::decode 方法的源码:

// 只保留了最重要的部分
String TextResourceDecoder::decode(const char* data, size_t length)
{
    ...

    // 如果是HTML文件,就从head标签中寻找字符集
     if ((m_contentType == HTML || m_contentType == XML) && !m_checkedForHeadCharset) // HTML and XML
         if (!checkForHeadCharset(data, length, movedDataToBuffer))
             return emptyString();
             
      ...

     // m_encoding存储者从HTML文件中找到的编码名称
     if (!m_codec)
         m_codec = newTextCodec(m_encoding);  // 创建具体的编码器

     ...

    // 解码并返回
    String result = m_codec->decode(m_buffer.data() + lengthOfBOM, m_buffer.size() - lengthOfBOM, false, m_contentType == XML && !m_useLenientXMLDecoding, m_sawError);
     m_buffer.clear(); // 清空存储的原始未解码的HTML字节流
     return result;
}

从源码中可以看到,TextResourceDecoder 首先从 HTML 的 标签中去找编码方式,因为 标签可以包含 标签, 标签可以设置 HTML 文件的字符集:


 
DOM Tree


如果能找到对应的字符集,TextResourceDeocder 将其存储在成员变量 m_encoding 当中,并且根据对应的编码创建真正的解码器存储在成员变量 m_codec 中,最终使用 m_codec 对字节流进行解码,并且返回解码后的字符串。如果带有字符集的 标签没有找到,TextResourceDeocder 的 m_encoding 有默认值 windows-1252(等同于ISO-8859-1)。

下面看一下 TextResourceDecoder 寻找 标签中字符集的流程,也就是上面源码中第8行对 checkForHeadCharset 函数的调用:

// 只保留了关健代码
bool TextResourceDecoder::checkForHeadCharset(const char* data, size_t len, bool& movedDataToBuffer)
{
    ...

// This is not completely efficient, since the function might go
// through the HTML head several times.

size_t oldSize = m_buffer.size();
    m_buffer.grow(oldSize + len);
memcpy(m_buffer.data() + oldSize, data, len); // 将字节流数据拷贝到自己的缓存m_buffer里面

    movedDataToBuffer = true;

// Continue with checking for an HTML meta tag if we were already doing so.
if (m_charsetParser)
return checkForMetaCharset(data, len);  // 如果已经存在了meta标签解析器,直接开始解析

     ....

    m_charsetParser = makeUnique(); // 创建meta标签解析器
return checkForMetaCharset(data, len);
}

上面源代码中第11行,类 TextResourceDecoder 内部存储了需要解码的 HTML 字节流,这一步骤很重要,后面会讲到。先看第17行、21行、22行,这3行主要是使用标签解析器解析字符集,使用了懒加载的方式。下面看下 checkForMetaCharset 这个函数的实现:

bool TextResourceDecoder::checkForMetaCharset(const char* data, size_t length)
{
if (!m_charsetParser->checkForMetaCharset(data, length))  // 解析meta标签字符集
return false;

    setEncoding(m_charsetParser->encoding(), EncodingFromMetaTag); // 找到后设置字符编码名称
    m_charsetParser = nullptr;
    m_checkedForHeadCharset = true;
return true;
}

上面源码第3行可以看到,整个解析 标签的任务在类 HTMLMetaCharsetParser::checkForMetaCharset 中完成。

// 只保留了关健代码
bool HTMLMetaCharsetParser::checkForMetaCharset(const char* data, size_t length)
{
if (m_doneChecking) // 标志位,避免重复解析
return true;


// We still don't have an encoding, and are in the head.
    // The following tags are allowed in :
// SCRIPT|STYLE|META|LINK|OBJECT|TITLE|BASE
//
// We stop scanning when a tag that is not permitted in 
// is seen, rather when  is seen, because that more closely
// matches behavior in other browsers; more details in
// .
//
// Additionally, we ignore things that looks like tags in , <script>
// and <noscript>; see <http://bugs.webkit.org/show_bug.cgi?id=4560>,
// <http://bugs.webkit.org/show_bug.cgi?id=12165> and
// <http://bugs.webkit.org/show_bug.cgi?id=12389>.
//
// Since many sites have charset declarations after <body> or other tags
// that are disallowed in <head>, we don't bail out until we've checked at
// least bytesToCheckUnconditionally bytes of input.

constexpr int bytesToCheckUnconditionally = 1024;  // 如果解析了1024个字符还未找到带有字符集的<meta>标签,整个解析也算完成,此时没有解析到正确的字符集,就使用默认编码windows-1252(等同于ISO-8859-1)

bool ignoredSawErrorFlag;
    m_input.append(m_codec->decode(data, length, false, false, ignoredSawErrorFlag)); // 对字节流进行解码

while (auto token = m_tokenizer.nextToken(m_input)) { // m_tokenizer进行分词操作,找meta标签也需要进行分词,分词操作后面讲
bool isEnd = token->type() == HTMLToken::EndTag;
if (isEnd || token->type() == HTMLToken::StartTag) {
AtomString tagName(token->name());
if (!isEnd) {
                m_tokenizer.updateStateFor(tagName);
if (tagName == metaTag && processMeta(*token)) { // 找到meta标签进行处理
                    m_doneChecking = true;
return true; // 如果找到了带有编码的meta标签,直接返回
                }
            }

        if (tagName != scriptTag && tagName != noscriptTag
                && tagName != styleTag && tagName != linkTag
                && tagName != metaTag && tagName != objectTag
                && tagName != titleTag && tagName != baseTag
                && (isEnd || tagName != htmlTag)
                && (isEnd || tagName != headTag)) {
                m_inHeadSection = false;
            }
        }

if (!m_inHeadSection && m_input.numberOfCharactersConsumed() >= bytesToCheckUnconditionally) { // 如果分词已经进入了<body>标签范围,同时分词数量已经超过了1024,也算成功
            m_doneChecking = true;
return true;
        }
    }

return false;
}
</code></pre> 
  <p>上面源码第29行,类 HTMLMetaCharsetParser 也有一个解码器 m_codec,解码器是在 HTMLMetaCharsetParser 对象创建时生成,这个解码器的真实类型是 TextCodecLatin1(Latin1编码也就是ISO-8859-1,等同于windows-1252编码)。之所以可以直接使用 TextCodecLatin1 解码器,是因为 标签如果设置正确,都是英文字符,完全可以使用 TextCodecLatin1 进行解析出来。这样就避免了为了找到 标签,需要对字节流进行解码,而要解码就必须要找到 标签这种鸡生蛋、蛋生鸡的问题。</p> 
  <p>代码第37行对找到的 标签进行处理,这个函数比较简单,主要是解析 标签当中的属性,然后查看这些属性名中有没有 charset。</p> 
  <pre><code>bool HTMLMetaCharsetParser::processMeta(HTMLToken& token)
{
    AttributeList attributes;
for (auto& attribute : token.attributes()) { // 获取meta标签属性
        String attributeName = StringImpl::create8BitIfPossible(attribute.name);
        String attributeValue = StringImpl::create8BitIfPossible(attribute.value);
        attributes.append(std::make_pair(attributeName, attributeValue));
    }

    m_encoding = encodingFromMetaAttributes(attributes); // 从属性中找字符集设置属性charset
return m_encoding.isValid();
}
</code></pre> 
  <p>上面分析 TextResourceDecoder::checkForHeadCharset 函数时,讲过第11行 TextResourceDecoder 类存储 HTML 字节流的操作很重要。原因是可能整个 HTML 字节流里面可能确实没有设置 charset 的 标签,此时 TextResourceDecoder::checkForHeadCharset 函数就要返回 false,导致 TextResourceDecoder::decode 函数返回空字符串,也就是不进行任何解码。是不是这样呢?真实的情况是,在接收HTML字节流整个过程中由于确实没有找到带有 charset 属性的 标签,那么整个接收期间都不会解码。但是完整的 HTML 字节流会被存储在 TextResourceDecoder 的成员变量 m_buffer 里面,当整个 HTML 字节流接收结束的时,会有如下调用栈:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/d1eb96a567a9491d82fb77585d6277d4.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/d1eb96a567a9491d82fb77585d6277d4.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第7张图片" width="650" height="295" style="border:1px solid black;"></a></p> 
  <p>从调用栈可以看到,当 HTML 字节流接收完成,最终会调用 TextResourceDecoder::flush 方法,这个方法会将 TextResourceDecoder 中有 m_buffer 存储的 HTML 字节流进行解码,由于在接收 HTML 字节流期间未成功找到编码方式,因此 m_buffer 里面存储的就是所有待解码的 HTML 字节流,然后在这里使用默认的编码 windows-1252 对全部字节流进行解码。因此,如果 HTML 字节流中包含汉字,那么如果不指定字符集,最终页面就会出现乱码。解码完成后,会将解码之后的字符流存储到 HTMLDocumentParser 中。</p> 
  <pre><code>void DecodedDataDocumentParser::flush(DocumentWriter& writer)
{
String remainingData = writer.decoder().flush();
if (remainingData.isEmpty())
return;

    writer.reportDataReceived();
    append(remainingData.releaseImpl()); // 解码后的字符流存储到HTMLDocumentParser
}
</code></pre> 
  <h3><strong>2.1.3 解码总结</strong></h3> 
  <p>整个解码过程可以分为两种情形: 第一种情形是 HTML 字节流可以解析出带有 charset 属性的 标签,这样就可以获取相应的编码方式,那么每接收到一个 HML 字节流,都可以使用相应的编码方式进行解码,将解码后的字符流添加到 HTMLInputStream 当中;第二种是 HTML 字节流不能解析带有 charset 属性的 标签,这样每接收到一个 HTML 字节流,都缓存到 TextResourceDecoder 的 m_buffer 缓存,等完整的 HTML 字节流接收完毕,就会使用默认的编码 windows-1252 进行解码。</p> 
  <p><a href="http://img.e-com-net.com/image/info8/76b9a327ce0d41cca4cb9f404e79e1d7.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/76b9a327ce0d41cca4cb9f404e79e1d7.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第8张图片" width="650" height="258" style="border:1px solid black;"></a></p> 
  <p><a href="http://img.e-com-net.com/image/info8/137ad145f13e4b75bba321b3500d3bd9.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/137ad145f13e4b75bba321b3500d3bd9.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第9张图片" width="650" height="211" style="border:1px solid black;"></a></p> 
  <h2><strong>2.2 分词</strong></h2> 
  <p>接收到的 HTML 字节流经过解码,成为存储在 HTMLInputStream 中的字符流。分词的过程就是从 HTMLInputStream 中依次取出每一个字符,然后判断字符是否是特殊的 HTML 字符’ <’、’/’、’>’、’=’ 等。根据这些特殊字符的分割,就能解析出 HTML 标签名以及属性列表,类 HTMLToken 就是存储分词出来的结果。</p> 
  <h3><strong>2.2.1 分词类图</strong></h3> 
  <p><a href="http://img.e-com-net.com/image/info8/6fc26a770a66409ab103830a24c7379b.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/6fc26a770a66409ab103830a24c7379b.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第10张图片" width="650" height="267" style="border:1px solid black;"></a></p> 
  <p>从类图中可以看到,分词最重要的是类 HTMLTokenizer 和类 HTMLToken。下面是类 HTMLToken 的主要信息:</p> 
  <pre><code>// 只保留了主要信息
 class HTMLToken {
 public:
     enum Type { // Token的类型
         Uninitialized, // Token初始化时的类型
         DOCTYPE, // 代表Token是DOCType标签
         StartTag, // 代表Token是一个开始标签
         EndTag, // 代表Token是一个结束标签
         Comment, // 代表Token是一个注释
         Character, // 代表Token是文本
         EndOfFile, // 代表Token是文件结尾
     };

     struct Attribute { // 存储属性的数据结构
         Vector<UChar, 32> name; // 属性名
         Vector<UChar, 64> value; // 属性值
         // Used by HTMLSourceTracker.
         unsigned startOffset;
         unsigned endOffset;
     };

     typedef Vector<Attribute, 10> AttributeList; // 属性列表
     typedef Vector<UChar, 256> DataVector; // 存储Token名

  ...

 private:
     Type m_type;
     DataVector m_data;
     // For StartTag and EndTag
     bool m_selfClosing; // Token是注入<img>一样自结束标签
     AttributeList m_attributes;
     Attribute* m_currentAttribute; // 当前正在解析的属性
 };
</code></pre> 
  <h3><strong>2.2.2 分词流程</strong></h3> 
  <p><a href="http://img.e-com-net.com/image/info8/14511e832ee34cd8b4233173d27115bc.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/14511e832ee34cd8b4233173d27115bc.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第11张图片" width="650" height="339" style="border:1px solid black;"></a></p> 
  <p>上面分词流程中 HTMLDocumentParser::pumpTokenizerLoop 方法是最重要的,从方法名字可以看出这个方法里面包含循环逻辑:</p> 
  <pre><code>// 只保留关健代码
bool HTMLDocumentParser::pumpTokenizerLoop(SynchronousMode mode, bool parsingFragment, PumpSession& session)
{
do { // 分词循环体开始
        ...

if (UNLIKELY(mode == AllowYield && m_parserScheduler->shouldYieldBeforeToken(session))) // 避免长时间处于分词循环中,这里根据条件暂时退出循环
return true;

if (!parsingFragment)
            m_sourceTracker.startToken(m_input.current(), m_tokenizer);

auto token = m_tokenizer.nextToken(m_input.current()); // 进行分词操作,取出一个token
if (!token)
return false; // 分词没有产生token,就跳出循环

if (!parsingFragment)
            m_sourceTracker.endToken(m_input.current(), m_tokenizer);

        constructTreeFromHTMLToken(token); // 根据token构建DOM树
    } while (!isStopped());

return false;
}
</code></pre> 
  <p>上面代码中第7行会有一个 yield 退出操作,这是为了避免长时间处于分词循环,占用主线程。当退出条件为真时,会从分词循环中返回,返回值为 true。下面是退出判断代码:</p> 
  <pre><code>// 只保留关健代码
bool HTMLParserScheduler::shouldYieldBeforeToken(PumpSession& session)
    {
        ...

// numberOfTokensBeforeCheckingForYield是静态变量,定义为4096
// session.processedTokensOnLastCheck表示从上一次退出为止,以及处理过的token个数
// session.didSeeScript表示在分词过程中是否出现过script标签
if (UNLIKELY(session.processedTokens > session.processedTokensOnLastCheck + numberOfTokensBeforeCheckingForYield || session.didSeeScript))
return checkForYield(session);

        ++session.processedTokens;
return false;
    }


bool HTMLParserScheduler::checkForYield(PumpSession& session)
    {
        session.processedTokensOnLastCheck = session.processedTokens;
        session.didSeeScript = false;

        Seconds elapsedTime = MonotonicTime::now() - session.startTime;
return elapsedTime > m_parserTimeLimit; // m_parserTimeLimit的值默认是500ms,从分词开始超过500ms就要先yield
    }
</code></pre> 
  <p>如果命中了上面的 yield 退出条件,那么什么时候再次进入分词呢?下面的代码展示了再次进入分词的过程:</p> 
  <pre><code>// 保留关键代码
void HTMLDocumentParser::pumpTokenizer(SynchronousMode mode)
{
    ...

if (shouldResume) // 从pumpTokenizerLoop中yield退出时返回值为true
        m_parserScheduler->scheduleForResume();

}



void HTMLParserScheduler::scheduleForResume()
{
    ASSERT(!m_suspended);
    m_continueNextChunkTimer.startOneShot(0_s); // 触发timer(0s后触发),触发后的响应函数为HTMLParserScheduler::continueNextChunkTimerFired
}


// 保留关健代码
void HTMLParserScheduler::continueNextChunkTimerFired()
{
    ...

    m_parser.resumeParsingAfterYield(); // 重新Resume分词过程
}


void HTMLDocumentParser::resumeParsingAfterYield()
{
// pumpTokenizer can cause this parser to be detached from the Document,
// but we need to ensure it isn't deleted yet.
    Ref<HTMLDocumentParser> protectedThis(*this);

// We should never be here unless we can pump immediately.
// Call pumpTokenizer() directly so that ASSERTS will fire if we're wrong.
    pumpTokenizer(AllowYield); // 重新进入分词过程,该函数会调用pumpTokenizerLoop
    endIfDelayed();
}
</code></pre> 
  <p>从上面代码可以看出,再次进入分词过程是通过触发一个 Timer 来实现的,虽然这个 Timer 在0s后触发,但是并不意味着 Timer 的响应函数会立刻执行。如果在此之前主线程已经有其他任务到达了执行时机,会有被执行的机会。</p> 
  <p>继续看 HTMLDocumentParser::pumpTokenizerLoop 函数的第13行,这一行进行分词操作,从解码后的字符流中分出一个 token。实现分词的代码位于 HTMLTokenizer::processToken:</p> 
  <pre><code>// 只保留关键代码
bool HTMLTokenizer::processToken(SegmentedString& source)
{

    ...

if (!m_preprocessor.peek(source, isNullCharacterSkippingState(m_state))) // 取出source内部指向的字符,赋给m_nextInputCharacter
return haveBufferedCharacterToken();
    UChar character = m_preprocessor.nextInputCharacter(); // 获取character

// https://html.spec.whatwg.org/#tokenization
switch (m_state) { // 进行状态转换,m_state初始值为DataState
    ...
    }

return false;
}
</code></pre> 
  <p>这个方法由于内部要做很多状态转换,总共有1200多行,后面会有4个例子来解释状态转换的逻辑。</p> 
  <p>首先来看 InputStreamPreprocessor::peek 方法:</p> 
  <pre><code>// Returns whether we succeeded in peeking at the next character.
// The only way we can fail to peek is if there are no more
// characters in |source| (after collapsing \r\n, etc).
 ALWAYS_INLINE bool InputStreamPreprocessor::peek(SegmentedString& source, bool skipNullCharacters = false)
 {
if (UNLIKELY(source.isEmpty()))
return false;

     m_nextInputCharacter = source.currentCharacter(); // 获取字符流source内部指向的当前字符

// Every branch in this function is expensive, so we have a
// fast-reject branch for characters that don't require special
// handling. Please run the parser benchmark whenever you touch
// this function. It's very hot.
constexpr UChar specialCharacterMask = '\n' | '\r' | '\0';
if (LIKELY(m_nextInputCharacter & ~specialCharacterMask)) {
         m_skipNextNewLine = false;
return true;
     }

return processNextInputCharacter(source, skipNullCharacters); // 跳过空字符,将\r\n换行符合并成\n
 } 
 
 
bool InputStreamPreprocessor::processNextInputCharacter(SegmentedString& source, bool skipNullCharacters)
    {
    ProcessAgain:
        ASSERT(m_nextInputCharacter == source.currentCharacter());

// 针对\r\n换行符,下面if语句处理\r字符并且设置m_skipNextNewLine=true,后面处理\n就直接忽略
if (m_nextInputCharacter == '\n' && m_skipNextNewLine) {
            m_skipNextNewLine = false;
            source.advancePastNewline(); // 向前移动字符
if (source.isEmpty())
return false;
            m_nextInputCharacter = source.currentCharacter();
        }

// 如果是\r\n连续的换行符,那么第一次遇到\r字符,将\r字符替换成\n字符,同时设置标志m_skipNextNewLine=true
if (m_nextInputCharacter == '\r') {
            m_nextInputCharacter = '\n';
            m_skipNextNewLine = true;
return true;
        }
        m_skipNextNewLine = false;
if (m_nextInputCharacter || isAtEndOfFile(source))
return true;

// 跳过空字符
if (skipNullCharacters && !m_tokenizer.neverSkipNullCharacters()) {
            source.advancePastNonNewline();
if (source.isEmpty())
return false;
            m_nextInputCharacter = source.currentCharacter();
goto ProcessAgain; // 跳转到开头
        }
        m_nextInputCharacter = replacementCharacter;
return true;
    }
</code></pre> 
  <p>由于 peek 方法会跳过空字符,同时合并 \r\n 字符为 \n 字符,所以一个字符流 source 如果包含了空格或者 \r\n 换行符,实际上处理起来如下图所示:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/f1dd90b760d3477bac4826e680043dc8.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/f1dd90b760d3477bac4826e680043dc8.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第12张图片" width="650" height="173" style="border:1px solid black;"></a></p> 
  <p>HTMLTokenizer::processToken 内部定义了一个状态机,下面以四种情形来进行解释。</p> 
  <p><strong>Case1:标签</strong></p> 
  <pre><code>BEGIN_STATE(DataState) // 刚开始解析是DataState状态if (character == '&')            ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInDataState);if (character == '<') {// 整个字符流一开始是'<',那么表示是一个标签的开始if (haveBufferedCharacterToken())                RETURN_IN_CURRENT_STATE(true);            ADVANCE_PAST_NON_NEWLINE_TO(TagOpenState); // 跳转到TagOpenState状态,并取去下一个字符是'!"        }if (character == kEndOfFileMarker)return emitEndOfFile(source);        bufferCharacter(character);        ADVANCE_TO(DataState);END_STATE()// ADVANCE_PAST_NON_NEWLINE_TO定义#define ADVANCE_PAST_NON_NEWLINE_TO(newState)                   \do {                                                        \if (!m_preprocessor.advancePastNonNewline(source, isNullCharacterSkippingState(newState))) { \ // 如果往下移动取不到下一个字符            m_state = newState;                                 \ // 保存状态return haveBufferedCharacterToken();                \ // 返回        }                                                       \        character = m_preprocessor.nextInputCharacter();        \ // 先取出下一个字符        goto newState;                                          \ // 跳转到指定状态    } while (false)BEGIN_STATE(TagOpenState)if (character == '!') // 满足此条件            ADVANCE_PAST_NON_NEWLINE_TO(MarkupDeclarationOpenState); // 同理,跳转到MarkupDeclarationOpenState状态,并且取出下一个字符'D'if (character == '/')            ADVANCE_PAST_NON_NEWLINE_TO(EndTagOpenState);if (isASCIIAlpha(character)) {            m_token.beginStartTag(convertASCIIAlphaToLower(character));            ADVANCE_PAST_NON_NEWLINE_TO(TagNameState);        }if (character == '?') {            parseError();// The spec consumes the current character before switching// to the bogus comment state, but it's easier to implement// if we reconsume the current character.            RECONSUME_IN(BogusCommentState);        }        parseError();        bufferASCIICharacter('<');        RECONSUME_IN(DataState);END_STATE()BEGIN_STATE(MarkupDeclarationOpenState)if (character == '-') {            auto result = source.advancePast("--");if (result == SegmentedString::DidMatch) {                m_token.beginComment();                SWITCH_TO(CommentStartState);            }if (result == SegmentedString::NotEnoughCharacters)                RETURN_IN_CURRENT_STATE(haveBufferedCharacterToken());        } else if (isASCIIAlphaCaselessEqual(character, 'd')) { // 由于character == 'D',满足此条件            auto result = source.advancePastLettersIgnoringASCIICase("doctype"); // 看解码后的字符流中是否有完整的"doctype"if (result == SegmentedString::DidMatch)                SWITCH_TO(DOCTYPEState); // 如果匹配,则跳转到DOCTYPEState,同时取出当前指向的字符,由于上面source字符流已经移动了"doctype",因此此时取出的字符为'>'if (result == SegmentedString::NotEnoughCharacters) // 如果不匹配                RETURN_IN_CURRENT_STATE(haveBufferedCharacterToken()); // 保存状态,直接返回        } else if (character == '[' && shouldAllowCDATA()) {            auto result = source.advancePast("[CDATA[");if (result == SegmentedString::DidMatch)                SWITCH_TO(CDATASectionState);if (result == SegmentedString::NotEnoughCharacters)                RETURN_IN_CURRENT_STATE(haveBufferedCharacterToken());        }        parseError();        RECONSUME_IN(BogusCommentState);END_STATE()#define SWITCH_TO(newState)                                     \do {                                                        \if (!m_preprocessor.peek(source, isNullCharacterSkippingState(newState))) { \            m_state = newState;                                 \return haveBufferedCharacterToken();                \        }                                                       \        character = m_preprocessor.nextInputCharacter();        \ // 取出下一个字符        goto newState;                                          \ // 跳转到指定的state    } while (false)#define RETURN_IN_CURRENT_STATE(expression)                     \do {                                                        \        m_state = currentState;                                 \ // 保存当前状态return expression;                                      \    } while (false)BEGIN_STATE(DOCTYPEState)if (isTokenizerWhitespace(character))        ADVANCE_TO(BeforeDOCTYPENameState);if (character == kEndOfFileMarker) {        parseError();        m_token.beginDOCTYPE();        m_token.setForceQuirks();return emitAndReconsumeInDataState();    }    parseError();    RECONSUME_IN(BeforeDOCTYPENameState);END_STATE()#define RECONSUME_IN(newState)                                  \do {                                                        \ // 直接跳转到指定state        goto newState;                                          \    } while (false) BEGIN_STATE(BeforeDOCTYPENameState)if (isTokenizerWhitespace(character))            ADVANCE_TO(BeforeDOCTYPENameState);if (character == '>') { // character == '>',匹配此处,到此DOCTYPE标签匹配完毕            parseError();            m_token.beginDOCTYPE();            m_token.setForceQuirks();return emitAndResumeInDataState(source);        }if (character == kEndOfFileMarker) {            parseError();            m_token.beginDOCTYPE();            m_token.setForceQuirks();return emitAndReconsumeInDataState();        }        m_token.beginDOCTYPE(toASCIILower(character));        ADVANCE_PAST_NON_NEWLINE_TO(DOCTYPENameState);END_STATE()inline bool HTMLTokenizer::emitAndResumeInDataState(SegmentedString& source){    saveEndTagNameIfNeeded();    m_state = DataState; // 重置状态为初始状态DataState    source.advancePastNonNewline(); // 移动到下一个字符return true;}
</code></pre> 
  <p>DOCTYPE Token 经历了6个状态最终被解析出来,整个过程如下图所示:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/dd63b25ed32648baa533444a8ed8765c.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/dd63b25ed32648baa533444a8ed8765c.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第13张图片" width="650" height="228" style="border:1px solid black;"></a></p> 
  <p>当 Token 解析完毕之后,分词状态又被重置为 DataState,同时需要注意的时,此时字符流 source 内部指向的是下一个字符 ‘<’。</p> 
  <p>上面代码第61行在用字符流 source 匹配字符串 “doctype” 时,可能出现匹配不上的情形。为什么会这样呢?这是因为整个 DOM 树的构建流程,并不是先要解码完成,解码完成之后获取到完整的字符流才进行分词。从前面解码可以知道,解码可能是一边接收字节流,一边进行解码的,因此分词也是这样,只要能解码出一段字符流,就会立即进行分词。整个流程会出现如下图所示:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/3746d0740ec94cc9b8aa6b4da3919579.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/3746d0740ec94cc9b8aa6b4da3919579.jpg" alt="图片" width="650" height="72"></a></p> 
  <p>由于这个原因,用来分词的字符流可能是不完整的。对于出现不完整情形的 DOCTYPE 分词过程如下图所示:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/01d338cabe1b4bb180ddecab0e8df315.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/01d338cabe1b4bb180ddecab0e8df315.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第14张图片" width="650" height="513" style="border:1px solid black;"></a></p> 
  <p>上面介绍了解码、分词、解码、分词处理 DOCTYPE 标签的情形,可以看到从逻辑上这种情形与完整解码再分词是一样的。后续介绍时都会只针对完整解码再分词的情形,对于一边解码一边分词的情形,只需要正确的认识 source 字符流内部指针的移动,并不难分析。</p> 
  <p><strong>Case2:标签</strong></p> 标签的分词过程和 类似,其相关代码如下: 
  <pre><code>BEGIN_STATE(TagOpenState)
if (character == '!')
        ADVANCE_PAST_NON_NEWLINE_TO(MarkupDeclarationOpenState);
if (character == '/')
        ADVANCE_PAST_NON_NEWLINE_TO(EndTagOpenState);
if (isASCIIAlpha(character)) { // 在开标签状态下,当前字符为'h'
        m_token.beginStartTag(convertASCIIAlphaToLower(character)); // 将'h'添加到Token名中
        ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); // 跳转到TagNameState,并移动到下一个字符't'
    }
if (character == '?') {
        parseError();
// The spec consumes the current character before switching
// to the bogus comment state, but it's easier to implement
// if we reconsume the current character.
        RECONSUME_IN(BogusCommentState);
    }
    parseError();
    bufferASCIICharacter('<');
    RECONSUME_IN(DataState);
END_STATE()


BEGIN_STATE(TagNameState)
if (isTokenizerWhitespace(character))
        ADVANCE_TO(BeforeAttributeNameState);
if (character == '/')
        ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState);
if (character == '>') // 在这个状态下遇到起始标签终止字符
return emitAndResumeInDataState(source); // 当前分词结束,重置分词状态为DataState
if (m_options.usePreHTML5ParserQuirks && character == '<')
return emitAndReconsumeInDataState();
if (character == kEndOfFileMarker) {
        parseError();
        RECONSUME_IN(DataState);
    }
    m_token.appendToName(toASCIILower(character)); // 将当前字符添加到Token名
    ADVANCE_PAST_NON_NEWLINE_TO(TagNameState); // 继续跳转到当前状态,并移动到下一个字符
END_STATE()
</code></pre> 
  <p><a href="http://img.e-com-net.com/image/info8/9eb01b4a24e14b7592db9de6fbb6b8d5.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/9eb01b4a24e14b7592db9de6fbb6b8d5.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第15张图片" width="650" height="272" style="border:1px solid black;"></a></p> 
  <p><strong>Case3:带有属性的标签 </strong></p> 
  <div></div> 
  <p></p> 
  <p>HTML 标签可以带有属性,属性由属性名和属性值组成,属性之间以及属性与标签名之间用空格分隔:</p> 
  <pre><code><!-- div标签有两个属性,属性名为class和align,它们的值都带有引号 -->
<div class="news" align="center">Hello,World!</div>
<!-- 属性值也可以不带引号 -->
<div class=news align=center>Hello,World!</div>
</code></pre> 
  <p>整个 </p> 
  <div>
    标签的解析中,标签名 div 的解析流程和上面的 标签解析一样,当在解析标签名的过程中,碰到了空白字符,说明要开始解析属性了,下面是相关代码: 
  </div> 
  <p></p> 
  <pre><code>BEGIN_STATE(TagNameState)if (isTokenizerWhitespace(character)) // 在解析TagName时遇到空白字符,标志属性开始        ADVANCE_TO(BeforeAttributeNameState);if (character == '/')        ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState);if (character == '>')return emitAndResumeInDataState(source);if (m_options.usePreHTML5ParserQuirks && character == '<')return emitAndReconsumeInDataState();if (character == kEndOfFileMarker) {        parseError();        RECONSUME_IN(DataState);    }    m_token.appendToName(toASCIILower(character));    ADVANCE_PAST_NON_NEWLINE_TO(TagNameState);END_STATE()#define ADVANCE_TO(newState)                                    \do {                                                        \if (!m_preprocessor.advance(source, isNullCharacterSkippingState(newState))) { \ // 移动到下一个字符            m_state = newState;                                 \return haveBufferedCharacterToken();                \        }                                                       \        character = m_preprocessor.nextInputCharacter();        \        goto newState;                                          \ // 跳转到指定状态    } while (false)BEGIN_STATE(BeforeAttributeNameState)if (isTokenizerWhitespace(character)) // 如果标签名后有连续空格,那么就不停的跳过,在当前状态不停循环        ADVANCE_TO(BeforeAttributeNameState);if (character == '/')        ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState);if (character == '>')return emitAndResumeInDataState(source);if (m_options.usePreHTML5ParserQuirks && character == '<')return emitAndReconsumeInDataState();if (character == kEndOfFileMarker) {        parseError();        RECONSUME_IN(DataState);    }if (character == '"' || character == '\'' || character == '<' || character == '=')        parseError();    m_token.beginAttribute(source.numberOfCharactersConsumed()); // Token的属性列表增加一个,用来存放新的属性名与属性值    m_token.appendToAttributeName(toASCIILower(character)); // 添加属性名    ADVANCE_PAST_NON_NEWLINE_TO(AttributeNameState); // 跳转到AttributeNameState,并且移动到下一个字符END_STATE()BEGIN_STATE(AttributeNameState)if (isTokenizerWhitespace(character))        ADVANCE_TO(AfterAttributeNameState);if (character == '/')        ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState);if (character == '=')        ADVANCE_PAST_NON_NEWLINE_TO(BeforeAttributeValueState); // 在解析属性名的过程中如果碰到=,说明属性名结束,属性值就要开始if (character == '>')return emitAndResumeInDataState(source);if (m_options.usePreHTML5ParserQuirks && character == '<')return emitAndReconsumeInDataState();if (character == kEndOfFileMarker) {        parseError();        RECONSUME_IN(DataState);    }if (character == '"' || character == '\'' || character == '<' || character == '=')        parseError();    m_token.appendToAttributeName(toASCIILower(character));    ADVANCE_PAST_NON_NEWLINE_TO(AttributeNameState);END_STATE()BEGIN_STATE(BeforeAttributeValueState)if (isTokenizerWhitespace(character))        ADVANCE_TO(BeforeAttributeValueState);if (character == '"')        ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueDoubleQuotedState); // 有的属性值有引号包围,这里跳转到AttributeValueDoubleQuotedState,并移动到下一个字符if (character == '&')        RECONSUME_IN(AttributeValueUnquotedState);if (character == '\'')        ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueSingleQuotedState);if (character == '>') {        parseError();return emitAndResumeInDataState(source);    }if (character == kEndOfFileMarker) {        parseError();        RECONSUME_IN(DataState);    }if (character == '<' || character == '=' || character == '`')        parseError();    m_token.appendToAttributeValue(character); // 有的属性值没有引号包围,添加属性值字符到Token    ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueUnquotedState); // 跳转到AttributeValueUnquotedState,并移动到下一个字符END_STATE()BEGIN_STATE(AttributeValueDoubleQuotedState)if (character == '"') { // 在当前状态下如果遇到引号,说明属性值结束        m_token.endAttribute(source.numberOfCharactersConsumed()); // 结束属性解析        ADVANCE_PAST_NON_NEWLINE_TO(AfterAttributeValueQuotedState); // 跳转到AfterAttributeValueQuotedState,并移动到下一个字符    }if (character == '&') {        m_additionalAllowedCharacter = '"';        ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInAttributeValueState);    }if (character == kEndOfFileMarker) {        parseError();        m_token.endAttribute(source.numberOfCharactersConsumed());        RECONSUME_IN(DataState);    }    m_token.appendToAttributeValue(character); // 将属性值字符添加到Token    ADVANCE_TO(AttributeValueDoubleQuotedState); // 跳转到当前状态END_STATE()BEGIN_STATE(AfterAttributeValueQuotedState)if (isTokenizerWhitespace(character))        ADVANCE_TO(BeforeAttributeNameState); // 属性值解析完毕,如果后面继续跟着空白字符,说明后续还有属性要解析,调回到BeforeAttributeNameStateif (character == '/')        ADVANCE_PAST_NON_NEWLINE_TO(SelfClosingStartTagState);if (character == '>')return emitAndResumeInDataState(source); // 属性值解析完毕,如果遇到'>'字符,说明整个标签也要解析完毕了,此时结束当前标签解析,并且重置分词状态为DataState,并移动到下一个字符if (m_options.usePreHTML5ParserQuirks && character == '<')return emitAndReconsumeInDataState();if (character == kEndOfFileMarker) {        parseError();        RECONSUME_IN(DataState);    }    parseError();    RECONSUME_IN(BeforeAttributeNameState);END_STATE()BEGIN_STATE(AttributeValueUnquotedState)if (isTokenizerWhitespace(character)) { // 当解析不带引号的属性值时遇到空白字符(这与带引号的属性值不一样,带引号的属性值可以包含空白字符),说明当前属性解析完毕,后面还有其他属性,跳转到BeforeAttributeNameState,并且移动到下一个字符        m_token.endAttribute(source.numberOfCharactersConsumed());        ADVANCE_TO(BeforeAttributeNameState);    }if (character == '&') {        m_additionalAllowedCharacter = '>';        ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInAttributeValueState);    }if (character == '>') { // 解析过程中如果遇到'>'字符,说明整个标签也要解析完毕了,此时结束当前标签解析,并且重置分词状态为DataState,并移动到下一个字符        m_token.endAttribute(source.numberOfCharactersConsumed());return emitAndResumeInDataState(source);    }if (character == kEndOfFileMarker) {        parseError();        m_token.endAttribute(source.numberOfCharactersConsumed());        RECONSUME_IN(DataState);    }if (character == '"' || character == '\'' || character == '<' || character == '=' || character == '`')        parseError();    m_token.appendToAttributeValue(character); // 将遇到的属性值字符添加到Token    ADVANCE_PAST_NON_NEWLINE_TO(AttributeValueUnquotedState); // 跳转到当前状态,并且移动到下一个字符END_STATE()
</code></pre> 
  <p>从代码中可以看到,当属性值带引号和不带引号时,解析的逻辑是不一样的。当属性值带有引号时,属性值里面是可以包含空白字符的。如果属性值不带引号,那么一旦碰到空白字符,说明这个属性就解析结束了,会进入下一个属性的解析当中。</p> 
  <p><a href="http://img.e-com-net.com/image/info8/b492d0814e7649a482700c56724a975e.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/b492d0814e7649a482700c56724a975e.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第16张图片" width="650" height="925" style="border:1px solid black;"></a></p> 
  <p><strong>Case4:纯文本解析</strong></p> 
  <p>这里的纯文本指起始标签与结束标签之间的任何纯文字,包括脚本文、CSS 文本等等,如下所示:</p> 
  <pre><code><!-- div标签中的纯文本 Hello,Word! -->
<div class=news align=center>Hello,World!</div>

<!-- script标签中的纯文本 window.name = 'Lucy'; -->
<script>window.name = 'Lucy';</script>
</code></pre> 
  <p>纯文本的解析过程比较简单,就是不停的在 DataState 状态上跳转,缓存遇到的字符,直到遇见一个结束标签的 ‘<’ 字符,相关代码如下:</p> 
  <pre><code>BEGIN_STATE(DataState)
if (character == '&')
        ADVANCE_PAST_NON_NEWLINE_TO(CharacterReferenceInDataState);
if (character == '<') { // 如果在解析文本的过程中遇到开标签,分两种情况
if (haveBufferedCharacterToken()) // 第一种,如果缓存了文本字符就直接按当前DataState返回,并不移动字符,所以下次再进入分词操作时取到的字符仍为'<'
            RETURN_IN_CURRENT_STATE(true);
        ADVANCE_PAST_NON_NEWLINE_TO(TagOpenState); // 第二种,如果没有缓存任何文本字符,直接进入TagOpenState状态,进入到起始标签解析过程,并且移动下一个字符
    }
if (character == kEndOfFileMarker)
return emitEndOfFile(source);
    bufferCharacter(character); // 缓存遇到的字符
    ADVANCE_TO(DataState); // 循环跳转到当前DataState状态,并且移动到下一个字符
END_STATE()
</code></pre> 
  <p>由于流程比较简单,下面只给出解析div标签中纯文本的结果:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/a2db77ca588f424c93025819ce8f990f.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/a2db77ca588f424c93025819ce8f990f.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第17张图片" width="650" height="173" style="border:1px solid black;"></a></p> 
  <h3><strong>2.3 创建节点与添加节点</strong></h3> 
  <h3><strong>2.3.1 相关类图</strong></h3> 
  <p><strong><a href="http://img.e-com-net.com/image/info8/333ea5b648e640d089e2bf9224e39347.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/333ea5b648e640d089e2bf9224e39347.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第18张图片" width="650" height="354" style="border:1px solid black;"></a></strong></p> 
  <h3><strong>2.3.2 创建、添加流程</strong></h3> 
  <p>上面的分词循环中,每分出一个 Token,就会根据 Token 创建对应的 Node,然后将 Node 添加到 DOM 树上(HTMLDocumentParser::pumpTokenizerLoop 方法在上面分词中有介绍)。</p> 
  <p><a href="http://img.e-com-net.com/image/info8/e97a4286c6964227a741c15c7ff947d9.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/e97a4286c6964227a741c15c7ff947d9.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第19张图片" width="650" height="347" style="border:1px solid black;"></a></p> 
  <p>上面方法中首先看 HTMLTreeBuilder::constructTree,代码如下:</p> 
  <pre><code>// 只保留关健代码
void HTMLTreeBuilder::constructTree(AtomHTMLToken&& token)
{
    ...

if (shouldProcessTokenInForeignContent(token))
        processTokenInForeignContent(WTFMove(token));
else
        processToken(WTFMove(token)); // HTMLToken在这里被处理

    ...

    m_tree.executeQueuedTasks(); // HTMLContructionSiteTask在这里被执行,有时候也直接在创建的过程中直接执行,然后这个方法发现队列为空就会直接返回
// The tree builder might have been destroyed as an indirect result of executing the queued tasks.
}


void HTMLConstructionSite::executeQueuedTasks()
{
if (m_taskQueue.isEmpty()) // 队列为空,就直接返回
return;

// Copy the task queue into a local variable in case executeTask
// re-enters the parser.
    TaskQueue queue = WTFMove(m_taskQueue);

for (auto& task : queue) // 这里的task就是HTMLContructionSiteTask
        executeTask(task); // 执行task

// We might be detached now.
}
</code></pre> 
  <p>上面代码中 HTMLTreeBuilder::processToken 就是处理 Token 生成对应 Node 的地方,代码如下所示:</p> 
  <pre><code>void HTMLTreeBuilder::processToken(AtomHTMLToken&& token)
{
switch (token.type()) {
case HTMLToken::Uninitialized:
        ASSERT_NOT_REACHED();
break;
case HTMLToken::DOCTYPE: // HTML中的DOCType标签
        m_shouldSkipLeadingNewline = false;
        processDoctypeToken(WTFMove(token));
break;
case HTMLToken::StartTag: // 起始HTML标签
        m_shouldSkipLeadingNewline = false;
        processStartTag(WTFMove(token));
break;
case HTMLToken::EndTag: // 结束HTML标签
        m_shouldSkipLeadingNewline = false;
        processEndTag(WTFMove(token));
break;
case HTMLToken::Comment: // HTML中的注释
        m_shouldSkipLeadingNewline = false;
        processComment(WTFMove(token));
return;
case HTMLToken::Character: // HTML中的纯文本
        processCharacter(WTFMove(token));
break;
case HTMLToken::EndOfFile: // HTML结束标志
        m_shouldSkipLeadingNewline = false;
        processEndOfFile(WTFMove(token));
break;
    }
}
</code></pre> 
  <p>可以看到上面代码对7类 Token 做了处理,由于处理的流程都是类似的,这里分析5 个节点case的创建添加过程,分别是 <strong><!DOCTYPE> 标签, 起始标签, 起始标签,</strong> 文本,<strong><title> 结束标签,剩下的过程都使用图表示。</p> 
  <p>Case1:!DOCTYPE 标签</p> 
  <pre><code>// 只保留关健代码
void HTMLTreeBuilder::processDoctypeToken(AtomHTMLToken&& token)
{
    ASSERT(token.type() == HTMLToken::DOCTYPE);
if (m_insertionMode == InsertionMode::Initial) { // m_insertionMode的初始值就是InsertionMode::Initial
        m_tree.insertDoctype(WTFMove(token)); // 插入DOCTYPE标签
        m_insertionMode = InsertionMode::BeforeHTML; // 插入DOCTYPE标签之后,m_insertionMode设置为InsertionMode::BeforeHTML,表示下面要开是HTML标签插入
return;
    }

   ...
}

// 只保留关健代码
void HTMLConstructionSite::insertDoctype(AtomHTMLToken&& token)
{
    ...

// m_attachmentRoot就是Document对象,文档根节点
// DocumentType::create方法创建出DOCTYPE节点
// attachLater方法内部创建出HTMLContructionSiteTask
    attachLater(m_attachmentRoot, DocumentType::create(m_document, token.name(), publicId, systemId));

    ...
}

// 只保留关健代码
void HTMLConstructionSite::attachLater(ContainerNode& parent, Ref<Node>&& child, bool selfClosing)
{
   ...

    HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert); // 创建HTMLConstructionSiteTask
    task.parent = &parent; // task持有当前节点的父节点
    task.child = WTFMove(child); // task持有需要操作的节点
    task.selfClosing = selfClosing; // 是否自关闭节点

// Add as a sibling of the parent if we have reached the maximum depth allowed.
// m_openElements就是HTMLElementStack,在这里还看不到它的作用,后面会讲。这里可以看到这个stack里面加入的对象个数是有限制的,最大不超过512个。
// 所以如果一个HTML标签嵌套过多的子标签,就会触发这里的操作
if (m_openElements.stackDepth() > m_maximumDOMTreeDepth && task.parent->parentNode())
        task.parent = task.parent->parentNode(); // 满足条件,就会将当前节点添加到爷爷节点,而不是父节点

    ASSERT(task.parent);
    m_taskQueue.append(WTFMove(task)); // 将task添加到Queue当中
}
</code></pre> 
  <p>从代码可以看到,这里只是创建了 DOCTYPE 节点,还没有真正添加。真正执行添加的操作,需要执行 HTMLContructionSite::executeQueuedTasks,这个方法在一开始有列出来。下面就来看下每个 Task 如何被执行。</p> 
  <pre><code>// 方法位于HTMLContructionSite.cpp
static inline void executeTask(HTMLConstructionSiteTask& task)
{
switch (task.operation) { // HTMLConstructionSiteTask存储了自己要做的操作,构建DOM树一般都是Insert操作
case HTMLConstructionSiteTask::Insert:
        executeInsertTask(task); // 这里执行insert操作
return;
// All the cases below this point are only used by the adoption agency.
case HTMLConstructionSiteTask::InsertAlreadyParsedChild:
        executeInsertAlreadyParsedChildTask(task);
return;
case HTMLConstructionSiteTask::Reparent:
        executeReparentTask(task);
return;
case HTMLConstructionSiteTask::TakeAllChildrenAndReparent:
        executeTakeAllChildrenAndReparentTask(task);
return;
    }
    ASSERT_NOT_REACHED();
}

// 只保留关健代码,方法位于HTMLContructionSite.cpp
static inline void executeInsertTask(HTMLConstructionSiteTask& task)
{
    ASSERT(task.operation == HTMLConstructionSiteTask::Insert);

    insert(task); // 继续调用插入方法

    ...
}

// 只保留关健代码,方法位于HTMLContructionSite.cpp
static inline void insert(HTMLConstructionSiteTask& task)
{
   ...

    ASSERT(!task.child->parentNode());
if (task.nextChild)
        task.parent->parserInsertBefore(*task.child, *task.nextChild);
else
        task.parent->parserAppendChild(*task.child); // 调用父节点方法继续插入
}

// 只保留关健代码
void ContainerNode::parserAppendChild(Node& newChild)
{
   ...

    executeNodeInsertionWithScriptAssertion(*this, newChild, ChildChange::Source::Parser, ReplacedAllChildren::No, [&] {
if (&document() != &newChild.document())
            document().adoptNode(newChild);

        appendChildCommon(newChild); // 在Block回调中调用此方法继续插入

        ...
    });
}

// 最终调用的是这个方法进行插入
void ContainerNode::appendChildCommon(Node& child)
{
    ScriptDisallowedScope::InMainThread scriptDisallowedScope;

    child.setParentNode(this);

if (m_lastChild) { // 父节点已经插入子节点,运行在这里
        child.setPreviousSibling(m_lastChild);
        m_lastChild->setNextSibling(&child);
    } else
        m_firstChild = &child; // 如果父节点是首次插入子节点,运行在这里

    m_lastChild = &child; // 更新m_lastChild
}
</code></pre> 
  <p>经过执行上面方法之后,原来只有一个根节点的 DOM 树变成了下面的样子:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/0250e61b61d14ca7abd538f0b7c10408.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/0250e61b61d14ca7abd538f0b7c10408.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第20张图片" width="650" height="270" style="border:1px solid black;"></a></p> 
  <p><strong>Case2:html 起始标签</strong></p> 
  <pre><code>// processStartTag内部有很多状态处理,这里只保留关健代码
void HTMLTreeBuilder::processStartTag(AtomHTMLToken&& token)
{
    ASSERT(token.type() == HTMLToken::StartTag);
switch (m_insertionMode) {
case InsertionMode::Initial:
        defaultForInitial();
        ASSERT(m_insertionMode == InsertionMode::BeforeHTML);
        FALLTHROUGH;
case InsertionMode::BeforeHTML:
if (token.name() == htmlTag) { // html标签在这里处理
            m_tree.insertHTMLHtmlStartTagBeforeHTML(WTFMove(token));
            m_insertionMode = InsertionMode::BeforeHead; // 插入完html标签,m_insertionMode = InsertionMode::BeforeHead,表明即将处理head标签
return;
        }

    ...
    }
}

// 只保留关健代码
void HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML(AtomHTMLToken&& token)
{
    auto element = HTMLHtmlElement::create(m_document); // 创建html节点
    setAttributes(element, token, m_parserContentPolicy);
    attachLater(m_attachmentRoot, element.copyRef()); // 同样调用了attachLater方法,与DOCTYPE类似
    m_openElements.pushHTMLHtmlElement(HTMLStackItem::create(element.copyRef(), WTFMove(token))); // 注意这里,这里向HTMLElementStack中压入了正在插入的html起始标签

    executeQueuedTasks(); // 这里在插入操作直接执行了task,外面HTMLTreeBuilder::constructTree方法调用的executeQueuedTasks方法就会直接返回

    ...
}
</code></pre> 
  <p>执行上面代码之后,DOM 树变成了如下图所示:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/b252c4d43d6448f7ba1920c572c9554a.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/b252c4d43d6448f7ba1920c572c9554a.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第21张图片" width="650" height="175" style="border:1px solid black;"></a></p> 
  <p><strong>Case3:title 起始标签</strong></p> 
  <p>当插入 起始标签之后,DOM 树以及 HTMLElementStack m_openElements 如下图所示:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/9096ab52665d42009a47d9aa2f3f668e.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/9096ab52665d42009a47d9aa2f3f668e.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第22张图片" width="650" height="274" style="border:1px solid black;"></a></p> 
  <p><strong>Case4:title 标签文本</strong></p> 标签的文本作为文本节点插入,生成文本节点的代码如下: 
  <p><code>// 只保留关健代码 void HTMLConstructionSite::insertTextNode(const String& characters, WhitespaceMode whitespaceMode) { HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert); task.parent = ¤tNode(); // 直接取HTMLElementStack m_openElements的栈顶节点,此时节点是title</code></p> 
  <p>unsigned currentPosition = 0;<br> unsigned lengthLimit = shouldUseLengthLimit(*task.parent) ? Text::defaultLengthLimit : std::numeric_limits::max(); // 限制文本节点最大包含的字符个数为65536</p> 
  <p>// 可以看到如果文本过长,会将分割成多个文本节点<br> while (currentPosition < characters.length()) {<br> AtomString charactersAtom = m_whitespaceCache.lookup(characters, whitespaceMode);<br> auto textNode = Text::createWithLengthLimit(task.parent->document(), charactersAtom.isNull() ? characters : charactersAtom.string(), currentPosition, lengthLimit);<br> // If we have a whole string of unbreakable characters the above could lead to an infinite loop. Exceeding the length limit is the lesser evil.<br> if (!textNode->length()) {<br> String substring = characters.substring(currentPosition);<br> AtomString substringAtom = m_whitespaceCache.lookup(substring, whitespaceMode);<br> textNode = Text::create(task.parent->document(), substringAtom.isNull() ? substring : substringAtom.string()); // 生成文本节点<br> }</p> 
  <pre><code>    currentPosition += textNode->length(); // 下一个文本节点包含的字符起点
    ASSERT(currentPosition <= characters.length());
    task.child = WTFMove(textNode);

    executeTask(task); // 直接执行Task插入
}
</code></pre> 
  <p>}</p> 
  <pre><code>
  

从代码可以看到,如果一个节点后面跟的文本字符过多,会被分割成多个文本节点插入。下面的例子将 <title> 节点后面的文本字符个数设置成85248,使用 Safari 查看确实生成了2个文本节点:

![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/584362db5ed04e2fb9d761f8d6f2d83c~tplv-k3u1fbpfcp-zoom-1.image)

  

**Case5:结束标签**

当遇到 <title> 结束标签,代码处理如下:

</code></pre> 
  <p>// 代码内部有很多状态处理,这里只保留关健代码<br> void HTMLTreeBuilder::processEndTag(AtomHTMLToken&& token)<br> {<br> ASSERT(token.type() == HTMLToken::EndTag);<br> switch (m_insertionMode) {<br> …</p> 
  <p>case InsertionMode::Text: // 由于遇到title结束标签之前插入了文本,因此此时的插入模式就是InsertionMode::Text</p> 
  <pre><code>    m_tree.openElements().pop(); // 因为遇到了title结束标签,整个标签已经处理完毕,从HTMLElementStack栈中弹出栈顶元素title
    m_insertionMode = m_originalInsertionMode; // 恢复之前的插入模式
</code></pre> 
  <p>break;</p> 
  <p>}</p> 
  <p>每当遇到一个标签的结束标签,都会像上面一样将 HTMLElementStack m_openElementsStack 的栈顶元素弹出。执行上面代码之后,DOM 树与 HTMLElementStack 如下图所示:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/541b80d72a7b41e4a0891763fd6d5e2b.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/541b80d72a7b41e4a0891763fd6d5e2b.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第23张图片" width="650" height="313" style="border:1px solid black;"></a></p> 
  <h1><strong>三、内存中的DOM树</strong></h1> 
  <p>当整个 DOM 树构建完成之后,DOM 树和 HTMLElementStack m_openElements 如下图所示:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/c44fefaa6c6e4ea288a90b4e001e0028.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/c44fefaa6c6e4ea288a90b4e001e0028.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第24张图片" width="650" height="233" style="border:1px solid black;"></a></p> 
  <p>从上图可以看到,当构建完 DOM,HTMLElementStack m_openElements 并没有将栈完全清空,而是保留了2个节点: html 节点与 body 节点。这可以从 Xcode 的控制台输出看到:</p> 
  <p><a href="http://img.e-com-net.com/image/info8/98ba33c5773d4f53a58ee36210b6df16.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info8/98ba33c5773d4f53a58ee36210b6df16.jpg" alt="深入理解 WKWebView (渲染篇) —— DOM 树的构建_第25张图片" width="650" height="347" style="border:1px solid black;"></a></p> 
  <p>同时可以看到,内存中的 DOM 树结构和文章开头画的逻辑上的 DOM 树结构是不一样的。逻辑上的 DOM 树父节点有多少子节点,就有多少指向子节点的指针,而内存中的 DOM 树,不管父节点有多少子节点,始终只有2个指针指向子节点: m_firstChild 与 m_lastChild。同时,内存中的 DOM 树兄弟节点之间也相互有指针引用,而逻辑上的 DOM 树结构是没有的。</p> 
  <p>举个例子,如果一棵 DOM 树只有1个父节点,100个子节点,那么使用逻辑上的 DOM 树结构,父节点就需要100个指向子节点的指针。如果一个指针占8字节,那么总共占用800字节。使用上面内存中 DOM 树的表示方式,父节点需要2个指向子节点的指针,同时兄弟节点之间需要198个指针,一共200个指针,总共占用1600字节。相比逻辑上的 DOM 树结构,内存上并不占优势,但是内存中的 DOM 树结构,无论父节点有多少子节点,只需要2个指针就可以了,不需要添加子节点时,频繁动态申请内存,创建新的指向子节点的指针。</p> 
  <p>---------- END ----------</p> 
  <p>百度 Geek 说</p> 
  <p>百度官方技术公众号上线啦!</p> 
  <p>技术干货 · 行业资讯 · 线上沙龙 · 行业大会</p> 
  <p>招聘信息 · 内推信息 · 技术书籍 · 百度周边</p> 
 </div> 
</div>
                            </div>
                        </div>
                    </div>
                    <!--PC和WAP自适应版-->
                    <div id="SOHUCS" sid="1590740338421637120"></div>
                    <script type="text/javascript" src="/views/front/js/chanyan.js"></script>
                    <!-- 文章页-底部 动态广告位 -->
                    <div class="youdao-fixed-ad" id="detail_ad_bottom"></div>
                </div>
                <div class="col-md-3">
                    <div class="row" id="ad">
                        <!-- 文章页-右侧1 动态广告位 -->
                        <div id="right-1" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad">
                            <div class="youdao-fixed-ad" id="detail_ad_1"> </div>
                        </div>
                        <!-- 文章页-右侧2 动态广告位 -->
                        <div id="right-2" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad">
                            <div class="youdao-fixed-ad" id="detail_ad_2"></div>
                        </div>
                        <!-- 文章页-右侧3 动态广告位 -->
                        <div id="right-3" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad">
                            <div class="youdao-fixed-ad" id="detail_ad_3"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="container">
        <h4 class="pt20 mb15 mt0 border-top">你可能感兴趣的:(百度,数据库,web)</h4>
        <div id="paradigm-article-related">
            <div class="recommend-post mb30">
                <ul class="widget-links">
                    <li><a href="/article/1835511912843014144.htm"
                           title="理解Gunicorn:Python WSGI服务器的基石" target="_blank">理解Gunicorn:Python WSGI服务器的基石</a>
                        <span class="text-muted">范范0825</span>
<a class="tag" taget="_blank" href="/search/ipython/1.htm">ipython</a><a class="tag" taget="_blank" href="/search/linux/1.htm">linux</a><a class="tag" taget="_blank" href="/search/%E8%BF%90%E7%BB%B4/1.htm">运维</a>
                        <div>理解Gunicorn:PythonWSGI服务器的基石介绍Gunicorn,全称GreenUnicorn,是一个为PythonWSGI(WebServerGatewayInterface)应用设计的高效、轻量级HTTP服务器。作为PythonWeb应用部署的常用工具,Gunicorn以其高性能和易用性著称。本文将介绍Gunicorn的基本概念、安装和配置,帮助初学者快速上手。1.什么是Gunico</div>
                    </li>
                    <li><a href="/article/1835506084224266240.htm"
                           title="网易严选官方旗舰店,优质商品,卓越服务" target="_blank">网易严选官方旗舰店,优质商品,卓越服务</a>
                        <span class="text-muted">高省_飞智666600</span>

                        <div>网易严选官方旗舰店是网易旗下的一家电商平台,以提供优质商品和卓越服务而闻名。作为一名SEO优化师,我将为您详细介绍网易严选官方旗舰店,并重点强调其特点和优势。大家好!我是高省APP最大团队&联合创始人飞智导师。相较于其他返利app,高省APP的佣金更高,模式更好,最重要的是,终端用户不会流失!高省APP佣金更高,模式更好,终端用户不流失。【高省】是一个自用省钱佣金高,分享推广赚钱多的平台,百度有几</div>
                    </li>
                    <li><a href="/article/1835504218178416640.htm"
                           title="Google earth studio 简介" target="_blank">Google earth studio 简介</a>
                        <span class="text-muted">陟彼高冈yu</span>
<a class="tag" taget="_blank" href="/search/%E6%97%85%E6%B8%B8/1.htm">旅游</a>
                        <div>GoogleEarthStudio是一个基于Web的动画工具,专为创作使用GoogleEarth数据的动画和视频而设计。它利用了GoogleEarth强大的三维地图和卫星影像数据库,使用户能够轻松地创建逼真的地球动画、航拍视频和动态地图可视化。网址为https://www.google.com/earth/studio/。GoogleEarthStudio是一个基于Web的动画工具,专为创作使用G</div>
                    </li>
                    <li><a href="/article/1835502704827396096.htm"
                           title="将cmd中命令输出保存为txt文本文件" target="_blank">将cmd中命令输出保存为txt文本文件</a>
                        <span class="text-muted">落难Coder</span>
<a class="tag" taget="_blank" href="/search/Windows/1.htm">Windows</a><a class="tag" taget="_blank" href="/search/cmd/1.htm">cmd</a><a class="tag" taget="_blank" href="/search/window/1.htm">window</a>
                        <div>最近深度学习本地的训练中我们常常要在命令行中运行自己的代码,无可厚非,我们有必要保存我们的炼丹结果,但是复制命令行输出到txt是非常麻烦的,其实Windows下的命令行为我们提供了相应的操作。其基本的调用格式就是:运行指令>输出到的文件名称或者具体保存路径测试下,我打开cmd并且ping一下百度:pingwww.baidu.com>./data.txt看下相同目录下data.txt的输出:如果你再</div>
                    </li>
                    <li><a href="/article/1835502578050363392.htm"
                           title="PHP环境搭建详细教程" target="_blank">PHP环境搭建详细教程</a>
                        <span class="text-muted">好看资源平台</span>
<a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/php/1.htm">php</a>
                        <div>PHP是一个流行的服务器端脚本语言,广泛用于Web开发。为了使PHP能够在本地或服务器上运行,我们需要搭建一个合适的PHP环境。本教程将结合最新资料,介绍在不同操作系统上搭建PHP开发环境的多种方法,包括Windows、macOS和Linux系统的安装步骤,以及本地和Docker环境的配置。1.PHP环境搭建概述PHP环境的搭建主要分为以下几类:集成开发环境:例如XAMPP、WAMP、MAMP,这</div>
                    </li>
                    <li><a href="/article/1835501821569888256.htm"
                           title="关于提高复杂业务逻辑代码可读性的思考" target="_blank">关于提高复杂业务逻辑代码可读性的思考</a>
                        <span class="text-muted">编程经验分享</span>
<a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E7%BB%8F%E9%AA%8C/1.htm">开发经验</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a>
                        <div>目录前言需求场景常规写法拆分方法领域对象总结前言实际工作中大部分时间都是在写业务逻辑,一般都是三层架构,表示层(Controller)接收客户端请求,并对入参做检验,业务逻辑层(Service)负责处理业务逻辑,一般开发都是在这一层中写具体的业务逻辑。数据访问层(Dao)是直接和数据库交互的,用于查数据给业务逻辑层,或者是将业务逻辑层处理后的数据写入数据库。简单的增删改查接口不用多说,基本上写好一</div>
                    </li>
                    <li><a href="/article/1835500751997202432.htm"
                           title="直返最高等级与直返APP:无需邀请码的返利新体验" target="_blank">直返最高等级与直返APP:无需邀请码的返利新体验</a>
                        <span class="text-muted">古楼</span>

                        <div>随着互联网的普及和电商的兴起,直返模式逐渐成为一种流行的商业模式。在这种模式下,消费者通过购买产品或服务,获得一定的返利,并可以分享给更多的人。其中,直返最高等级和直返APP是直返模式中的重要概念和工具。本文将详细介绍直返最高等级的概念、直返APP的使用以及与邀请码的关系。【高省】APP(高佣金领导者)是一个自用省钱佣金高,分享推广赚钱多的平台,百度有几百万篇报道,运行三年,稳定可靠。高省APP,</div>
                    </li>
                    <li><a href="/article/1835499681732456448.htm"
                           title="SQL Server_查询某一数据库中的所有表的内容" target="_blank">SQL Server_查询某一数据库中的所有表的内容</a>
                        <span class="text-muted">qq_42772833</span>
<a class="tag" taget="_blank" href="/search/SQL/1.htm">SQL</a><a class="tag" taget="_blank" href="/search/Server/1.htm">Server</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a><a class="tag" taget="_blank" href="/search/sqlserver/1.htm">sqlserver</a>
                        <div>1.查看所有表的表名要列出CrabFarmDB数据库中的所有表(名),可以使用以下SQL语句:USECrabFarmDB;--切换到目标数据库GOSELECTTABLE_NAMEFROMINFORMATION_SCHEMA.TABLESWHERETABLE_TYPE='BASETABLE';对这段SQL脚本的解释:SELECTTABLE_NAME:这个语句的作用是从查询结果中选择TABLE_NAM</div>
                    </li>
                    <li><a href="/article/1835498925755297792.htm"
                           title="DIV+CSS+JavaScript技术制作网页(旅游主题网页设计与制作)云南大理" target="_blank">DIV+CSS+JavaScript技术制作网页(旅游主题网页设计与制作)云南大理</a>
                        <span class="text-muted">STU学生网页设计</span>
<a class="tag" taget="_blank" href="/search/%E7%BD%91%E9%A1%B5%E8%AE%BE%E8%AE%A1/1.htm">网页设计</a><a class="tag" taget="_blank" href="/search/%E6%9C%9F%E6%9C%AB%E7%BD%91%E9%A1%B5%E4%BD%9C%E4%B8%9A/1.htm">期末网页作业</a><a class="tag" taget="_blank" href="/search/html%E9%9D%99%E6%80%81%E7%BD%91%E9%A1%B5/1.htm">html静态网页</a><a class="tag" taget="_blank" href="/search/html5%E6%9C%9F%E6%9C%AB%E5%A4%A7%E4%BD%9C%E4%B8%9A/1.htm">html5期末大作业</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E9%A1%B5%E8%AE%BE%E8%AE%A1/1.htm">网页设计</a><a class="tag" taget="_blank" href="/search/web%E5%A4%A7%E4%BD%9C%E4%B8%9A/1.htm">web大作业</a>
                        <div>️精彩专栏推荐作者主页:【进入主页—获取更多源码】web前端期末大作业:【HTML5网页期末作业(1000套)】程序员有趣的告白方式:【HTML七夕情人节表白网页制作(110套)】文章目录二、网站介绍三、网站效果▶️1.视频演示2.图片演示四、网站代码HTML结构代码CSS样式代码五、更多源码二、网站介绍网站布局方面:计划采用目前主流的、能兼容各大主流浏览器、显示效果稳定的浮动网页布局结构。网站程</div>
                    </li>
                    <li><a href="/article/1835497411179540480.htm"
                           title="深入理解 MultiQueryRetriever:提升向量数据库检索效果的强大工具" target="_blank">深入理解 MultiQueryRetriever:提升向量数据库检索效果的强大工具</a>
                        <span class="text-muted">nseejrukjhad</span>
<a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a>
                        <div>深入理解MultiQueryRetriever:提升向量数据库检索效果的强大工具引言在人工智能和自然语言处理领域,高效准确的信息检索一直是一个关键挑战。传统的基于距离的向量数据库检索方法虽然广泛应用,但仍存在一些局限性。本文将介绍一种创新的解决方案:MultiQueryRetriever,它通过自动生成多个查询视角来增强检索效果,提高结果的相关性和多样性。MultiQueryRetriever的工</div>
                    </li>
                    <li><a href="/article/1835496149843275776.htm"
                           title="关于城市旅游的HTML网页设计——(旅游风景云南 5页)HTML+CSS+JavaScript" target="_blank">关于城市旅游的HTML网页设计——(旅游风景云南 5页)HTML+CSS+JavaScript</a>
                        <span class="text-muted">二挡起步</span>
<a class="tag" taget="_blank" href="/search/web%E5%89%8D%E7%AB%AF%E6%9C%9F%E6%9C%AB%E5%A4%A7%E4%BD%9C%E4%B8%9A/1.htm">web前端期末大作业</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/html/1.htm">html</a><a class="tag" taget="_blank" href="/search/css/1.htm">css</a><a class="tag" taget="_blank" href="/search/%E6%97%85%E6%B8%B8/1.htm">旅游</a><a class="tag" taget="_blank" href="/search/%E9%A3%8E%E6%99%AF/1.htm">风景</a>
                        <div>⛵源码获取文末联系✈Web前端开发技术描述网页设计题材,DIV+CSS布局制作,HTML+CSS网页设计期末课程大作业|游景点介绍|旅游风景区|家乡介绍|等网站的设计与制作|HTML期末大学生网页设计作业,Web大学生网页HTML:结构CSS:样式在操作方面上运用了html5和css3,采用了div+css结构、表单、超链接、浮动、绝对定位、相对定位、字体样式、引用视频等基础知识JavaScrip</div>
                    </li>
                    <li><a href="/article/1835496148601761792.htm"
                           title="HTML网页设计制作大作业(div+css) 云南我的家乡旅游景点 带文字滚动" target="_blank">HTML网页设计制作大作业(div+css) 云南我的家乡旅游景点 带文字滚动</a>
                        <span class="text-muted">二挡起步</span>
<a class="tag" taget="_blank" href="/search/web%E5%89%8D%E7%AB%AF%E6%9C%9F%E6%9C%AB%E5%A4%A7%E4%BD%9C%E4%B8%9A/1.htm">web前端期末大作业</a><a class="tag" taget="_blank" href="/search/web%E8%AE%BE%E8%AE%A1%E7%BD%91%E9%A1%B5%E8%A7%84%E5%88%92%E4%B8%8E%E8%AE%BE%E8%AE%A1/1.htm">web设计网页规划与设计</a><a class="tag" taget="_blank" href="/search/html/1.htm">html</a><a class="tag" taget="_blank" href="/search/css/1.htm">css</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/dreamweaver/1.htm">dreamweaver</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a>
                        <div>Web前端开发技术描述网页设计题材,DIV+CSS布局制作,HTML+CSS网页设计期末课程大作业游景点介绍|旅游风景区|家乡介绍|等网站的设计与制作HTML期末大学生网页设计作业HTML:结构CSS:样式在操作方面上运用了html5和css3,采用了div+css结构、表单、超链接、浮动、绝对定位、相对定位、字体样式、引用视频等基础知识JavaScript:做与用户的交互行为文章目录前端学习路线</div>
                    </li>
                    <li><a href="/article/1835495170972413952.htm"
                           title="git - Webhook让部署自动化" target="_blank">git - Webhook让部署自动化</a>
                        <span class="text-muted">大猪大猪</span>

                        <div>我们现在有一个需求,将项目打包上传到gitlab或者github后,程序能自动部署,不用手动地去服务器中进行项目更新并运行,如何做到?这里我们可以使用gitlab与github的挂钩,挂钩的原理就是,每当我们有请求到gitlab与github服务器时,这时他俩会根据我们配置的挂钩地扯进行访问,webhook挂钩程序会一直监听着某个端口请求,一但收到他们发过来的请求,这时就知道用户有请求提交了,这时</div>
                    </li>
                    <li><a href="/article/1835493397553573888.htm"
                           title="2022现在哪个打车软件比较好用又便宜 实惠的打车软件合集" target="_blank">2022现在哪个打车软件比较好用又便宜 实惠的打车软件合集</a>
                        <span class="text-muted">高省APP珊珊</span>

                        <div>这是一个信息高速传播的社会。信息可以通过手机,微信,自媒体,抖音等方式进行传播。但同时这也是一个交通四通发达的社会。高省APP,是2022年推出的平台,0投资,0风险、高省APP佣金更高,模式更好,终端用户不流失。【高省】是一个自用省钱佣金高,分享推广赚钱多的平台,百度有几百万篇报道,也期待你的加入。珊珊导师,高省邀请码777777,注册送2皇冠会员,送万元推广大礼包,教你如何1年做到百万团队。高</div>
                    </li>
                    <li><a href="/article/1835493374514262016.htm"
                           title="MongoDB Oplog 窗口" target="_blank">MongoDB Oplog 窗口</a>
                        <span class="text-muted">喝醉酒的小白</span>
<a class="tag" taget="_blank" href="/search/MongoDB/1.htm">MongoDB</a><a class="tag" taget="_blank" href="/search/%E8%BF%90%E7%BB%B4/1.htm">运维</a>
                        <div>在MongoDB中,oplog(操作日志)是一个特殊的日志系统,用于记录对数据库的所有写操作。oplog允许副本集成员(通常是从节点)应用主节点上已经执行的操作,从而保持数据的一致性。它是MongoDB副本集实现数据复制的基础。MongoDBOplog窗口oplog窗口是指在MongoDB副本集中,从节点可以用来同步数据的时间范围。这个窗口通常由以下因素决定:Oplog大小:oplog的大小是有限</div>
                    </li>
                    <li><a href="/article/1835493267907637248.htm"
                           title="webpack图片等资源的处理" target="_blank">webpack图片等资源的处理</a>
                        <span class="text-muted">dmengmeng</span>

                        <div>需要的loaderfile-loader(让我们可以引入这些资源文件)url-loader(其实是file-loader的二次封装)img-loader(处理图片所需要的)在没有使用任何处理图片的loader之前,比如说css中用到了背景图片,那么最后打包会报错的,因为他没办法处理图片。其实你只想能够使用图片的话。只加一个file-loader就可以,打开网页能准确看到图片。{test:/\.(p</div>
                    </li>
                    <li><a href="/article/1835492869062881280.htm"
                           title="pyecharts——绘制柱形图折线图" target="_blank">pyecharts——绘制柱形图折线图</a>
                        <span class="text-muted">2224070247</span>
<a class="tag" taget="_blank" href="/search/%E4%BF%A1%E6%81%AF%E5%8F%AF%E8%A7%86%E5%8C%96/1.htm">信息可视化</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%8F%AF%E8%A7%86%E5%8C%96/1.htm">数据可视化</a>
                        <div>一、pyecharts概述自2013年6月百度EFE(ExcellentFrontEnd)数据可视化团队研发的ECharts1.0发布到GitHub网站以来,ECharts一直备受业界权威的关注并获得广泛好评,成为目前成熟且流行的数据可视化图表工具,被应用到诸多数据可视化的开发领域。Python作为数据分析领域最受欢迎的语言,也加入ECharts的使用行列,并研发出方便Python开发者使用的数据</div>
                    </li>
                    <li><a href="/article/1835492243260141568.htm"
                           title="如何成为段子手" target="_blank">如何成为段子手</a>
                        <span class="text-muted">欣雅阅读</span>

                        <div>我是一个尬聊大师,与朋友聊天经常把话题聊死,留我一个人在群里,望着自己打下的最后一句话无语凝噎。看到风趣幽默的朋友与人聊天,很是艳羡,觉得自己何时才能成为这样的段子手呢?一、段子是什么?“段子”一词在百度百科上的解释:本是相声中的一个艺术术语,指的是相声作品中一节或一段艺术内容。我的理解:段子就是一些搞笑的故事或者笑话。二、为什么要会说段子?不知道大家有没有这样的朋友,本来很无趣的聚会,只要有他参</div>
                    </li>
                    <li><a href="/article/1835490974911000576.htm"
                           title="python os 环境变量" target="_blank">python os 环境变量</a>
                        <span class="text-muted">CV矿工</span>
<a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a><a class="tag" taget="_blank" href="/search/numpy/1.htm">numpy</a>
                        <div>环境变量:环境变量是程序和操作系统之间的通信方式。有些字符不宜明文写进代码里,比如数据库密码,个人账户密码,如果写进自己本机的环境变量里,程序用的时候通过os.environ.get()取出来就行了。os.environ是一个环境变量的字典。环境变量的相关操作importos"""设置/修改环境变量:os.environ[‘环境变量名称’]=‘环境变量值’#其中key和value均为string类</div>
                    </li>
                    <li><a href="/article/1835489460372992000.htm"
                           title="【PG】常见数据库、表属性设置" target="_blank">【PG】常见数据库、表属性设置</a>
                        <span class="text-muted">江无羡</span>
<a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a>
                        <div>PG的常见属性配置方法数据库复制、备份相关表的复制标识单表操作批量表操作链接数据库复制、备份相关表的复制标识单表操作通过ALTER语句单独更改一张表的复制标识。ALTERTABLE[tablename]REPLICAIDENTITYFULL;批量表操作通过代码块的方式,对某个schema中的所有表一起更新其复制标识。SELECTtablename,CASErelreplidentWHEN'd'TH</div>
                    </li>
                    <li><a href="/article/1835483159630802944.htm"
                           title="nosql数据库技术与应用知识点" target="_blank">nosql数据库技术与应用知识点</a>
                        <span class="text-muted">皆过客,揽星河</span>
<a class="tag" taget="_blank" href="/search/NoSQL/1.htm">NoSQL</a><a class="tag" taget="_blank" href="/search/nosql/1.htm">nosql</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a><a class="tag" taget="_blank" href="/search/%E5%A4%A7%E6%95%B0%E6%8D%AE/1.htm">大数据</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/1.htm">数据分析</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/1.htm">数据结构</a><a class="tag" taget="_blank" href="/search/%E9%9D%9E%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">非关系型数据库</a>
                        <div>Nosql知识回顾大数据处理流程数据采集(flume、爬虫、传感器)数据存储(本门课程NoSQL所处的阶段)Hdfs、MongoDB、HBase等数据清洗(入仓)Hive等数据处理、分析(Spark、Flink等)数据可视化数据挖掘、机器学习应用(Python、SparkMLlib等)大数据时代存储的挑战(三高)高并发(同一时间很多人访问)高扩展(要求随时根据需求扩展存储)高效率(要求读写速度快)</div>
                    </li>
                    <li><a href="/article/1835477614848995328.htm"
                           title="insert into select 主键自增_mybatis拦截器实现主键自动生成" target="_blank">insert into select 主键自增_mybatis拦截器实现主键自动生成</a>
                        <span class="text-muted">weixin_39521651</span>
<a class="tag" taget="_blank" href="/search/insert/1.htm">insert</a><a class="tag" taget="_blank" href="/search/into/1.htm">into</a><a class="tag" taget="_blank" href="/search/select/1.htm">select</a><a class="tag" taget="_blank" href="/search/%E4%B8%BB%E9%94%AE%E8%87%AA%E5%A2%9E/1.htm">主键自增</a><a class="tag" taget="_blank" href="/search/mybatis/1.htm">mybatis</a><a class="tag" taget="_blank" href="/search/delete%E8%BF%94%E5%9B%9E%E5%80%BC/1.htm">delete返回值</a><a class="tag" taget="_blank" href="/search/mybatis/1.htm">mybatis</a><a class="tag" taget="_blank" href="/search/insert%E8%BF%94%E5%9B%9E%E4%B8%BB%E9%94%AE/1.htm">insert返回主键</a><a class="tag" taget="_blank" href="/search/mybatis/1.htm">mybatis</a><a class="tag" taget="_blank" href="/search/insert%E8%BF%94%E5%9B%9E%E5%AF%B9%E8%B1%A1/1.htm">insert返回对象</a><a class="tag" taget="_blank" href="/search/mybatis/1.htm">mybatis</a><a class="tag" taget="_blank" href="/search/plus/1.htm">plus</a><a class="tag" taget="_blank" href="/search/insert%E8%BF%94%E5%9B%9E%E4%B8%BB%E9%94%AE/1.htm">insert返回主键</a><a class="tag" taget="_blank" href="/search/mybatis/1.htm">mybatis</a><a class="tag" taget="_blank" href="/search/plus/1.htm">plus</a><a class="tag" taget="_blank" href="/search/%E6%8F%92%E5%85%A5%E7%94%9F%E6%88%90id/1.htm">插入生成id</a>
                        <div>前言前阵子和朋友聊天,他说他们项目有个需求,要实现主键自动生成,不想每次新增的时候,都手动设置主键。于是我就问他,那你们数据库表设置主键自动递增不就得了。他的回答是他们项目目前的id都是采用雪花算法来生成,因此为了项目稳定性,不会切换id的生成方式。朋友问我有没有什么实现思路,他们公司的orm框架是mybatis,我就建议他说,不然让你老大把mybatis切换成mybatis-plus。mybat</div>
                    </li>
                    <li><a href="/article/1835471689929027584.htm"
                           title="关于Mysql 中 Row size too large (> 8126) 错误的解决和理解" target="_blank">关于Mysql 中 Row size too large (> 8126) 错误的解决和理解</a>
                        <span class="text-muted">秋刀prince</span>
<a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a><a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a>
                        <div>提示:啰嗦一嘴,数据库的任何操作和验证前,一定要记得先备份!!!不会有错;文章目录问题发现一、问题导致的可能原因1、页大小2、行格式2.1compact格式2.2Redundant格式2.3Dynamic格式2.4Compressed格式3、BLOB和TEXT列二、解决办法1、修改页大小(不推荐)2、修改行格式3、修改数据类型为BLOB和TEXT列4、其他优化方式(可以参考使用)4.1合理设置数据</div>
                    </li>
                    <li><a href="/article/1835470931783413760.htm"
                           title="「豆包Marscode体验官」 | 云端 IDE 启动 & Rust 体验" target="_blank">「豆包Marscode体验官」 | 云端 IDE 启动 & Rust 体验</a>
                        <span class="text-muted">张风捷特烈</span>
<a class="tag" taget="_blank" href="/search/ide/1.htm">ide</a><a class="tag" taget="_blank" href="/search/rust/1.htm">rust</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a><a class="tag" taget="_blank" href="/search/%E5%90%8E%E7%AB%AF/1.htm">后端</a>
                        <div>theme:cyanosis我正在参加「豆包MarsCode初体验」征文活动MarsCode可以看作一个运行在服务端的远程VSCode开发环境。对于我这种想要学习体验某些语言,但不想在电脑里装环境的人来说非常友好。本文就来介绍一下在MarsCode里,我的体验rust开发体验。一、MarsCode是什么它的本质是:提供代码助手和云端IDE服务的web网站,可通过下面的链接访问https://www</div>
                    </li>
                    <li><a href="/article/1835455048277127168.htm"
                           title="Python神器!WEB自动化测试集成工具 DrissionPage" target="_blank">Python神器!WEB自动化测试集成工具 DrissionPage</a>
                        <span class="text-muted">亚丁号</span>
<a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a>
                        <div>一、前言用requests做数据采集面对要登录的网站时,要分析数据包、JS源码,构造复杂的请求,往往还要应付验证码、JS混淆、签名参数等反爬手段,门槛较高。若数据是由JS计算生成的,还须重现计算过程,体验不好,开发效率不高。使用浏览器,可以很大程度上绕过这些坑,但浏览器运行效率不高。因此,这个库设计初衷,是将它们合而为一,能够在不同须要时切换相应模式,并提供一种人性化的使用方法,提高开发和运行效率</div>
                    </li>
                    <li><a href="/article/1835454921990828032.htm"
                           title="Java爬虫框架(一)--架构设计" target="_blank">Java爬虫框架(一)--架构设计</a>
                        <span class="text-muted">狼图腾-狼之传说</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E6%A1%86%E6%9E%B6/1.htm">框架</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E4%BB%BB%E5%8A%A1/1.htm">任务</a><a class="tag" taget="_blank" href="/search/html%E8%A7%A3%E6%9E%90%E5%99%A8/1.htm">html解析器</a><a class="tag" taget="_blank" href="/search/%E5%AD%98%E5%82%A8/1.htm">存储</a><a class="tag" taget="_blank" href="/search/%E7%94%B5%E5%AD%90%E5%95%86%E5%8A%A1/1.htm">电子商务</a>
                        <div>一、架构图那里搜网络爬虫框架主要针对电子商务网站进行数据爬取,分析,存储,索引。爬虫:爬虫负责爬取,解析,处理电子商务网站的网页的内容数据库:存储商品信息索引:商品的全文搜索索引Task队列:需要爬取的网页列表Visited表:已经爬取过的网页列表爬虫监控平台:web平台可以启动,停止爬虫,管理爬虫,task队列,visited表。二、爬虫1.流程1)Scheduler启动爬虫器,TaskMast</div>
                    </li>
                    <li><a href="/article/1835454543471669248.htm"
                           title="Java:爬虫框架" target="_blank">Java:爬虫框架</a>
                        <span class="text-muted">dingcho</span>
<a class="tag" taget="_blank" href="/search/Java/1.htm">Java</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E7%88%AC%E8%99%AB/1.htm">爬虫</a>
                        <div>一、ApacheNutch2【参考地址】Nutch是一个开源Java实现的搜索引擎。它提供了我们运行自己的搜索引擎所需的全部工具。包括全文搜索和Web爬虫。Nutch致力于让每个人能很容易,同时花费很少就可以配置世界一流的Web搜索引擎.为了完成这一宏伟的目标,Nutch必须能够做到:每个月取几十亿网页为这些网页维护一个索引对索引文件进行每秒上千次的搜索提供高质量的搜索结果简单来说Nutch支持分</div>
                    </li>
                    <li><a href="/article/1835451016456269824.htm"
                           title="MongoDB知识概括" target="_blank">MongoDB知识概括</a>
                        <span class="text-muted">GeorgeLin98</span>
<a class="tag" taget="_blank" href="/search/%E6%8C%81%E4%B9%85%E5%B1%82/1.htm">持久层</a><a class="tag" taget="_blank" href="/search/mongodb/1.htm">mongodb</a>
                        <div>MongoDB知识概括MongoDB相关概念单机部署基本常用命令索引-IndexSpirngDataMongoDB集成副本集分片集群安全认证MongoDB相关概念业务应用场景:传统的关系型数据库(如MySQL),在数据操作的“三高”需求以及应对Web2.0的网站需求面前,显得力不从心。解释:“三高”需求:①Highperformance-对数据库高并发读写的需求。②HugeStorage-对海量数</div>
                    </li>
                    <li><a href="/article/1835450512426758144.htm"
                           title="手机上有什么兼职可以做?网上兼职 一单一结 手机就可以做?" target="_blank">手机上有什么兼职可以做?网上兼职 一单一结 手机就可以做?</a>
                        <span class="text-muted">优惠券高省</span>

                        <div>建议上班族和全职宝妈把空闲时间拿出来一点做做副业,什么也不耽搁还能多一笔收入!推荐大家一定要试一试!!!只要有手机就可以做,下面小编就为大家推荐用手机就可以做的三类网上兼职工作。一,高省APP高省APP佣金更高,模式更好,终端用户不流失。【高省】是一个自用省钱佣金高,分享推广赚钱多的平台,百度有几百万篇报道,也期待你的加入。万方导师高省邀请码005500,注册送双皇冠会员,送万元推广大礼包,教你如</div>
                    </li>
                    <li><a href="/article/1835447985601867776.htm"
                           title="Mongodb Error: queryTxt ETIMEOUT xxxx.wwwdz.mongodb.net" target="_blank">Mongodb Error: queryTxt ETIMEOUT xxxx.wwwdz.mongodb.net</a>
                        <span class="text-muted">佛一脚</span>
<a class="tag" taget="_blank" href="/search/error/1.htm">error</a><a class="tag" taget="_blank" href="/search/react/1.htm">react</a><a class="tag" taget="_blank" href="/search/mongodb/1.htm">mongodb</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a>
                        <div>背景每天都能遇到奇怪的问题,做个记录,以便有缘人能得到帮助!换了一台电脑开发nextjs程序。需要连接mongodb数据,对数据进行增删改查。上一台电脑好好的程序,新电脑死活连不上mongodb数据库。同一套代码,没任何修改,搞得我怀疑人生了,打开浏览器进入mongodb官网毫无问题,也能进入线上系统查看数据,网络应该是没问题。于是我尝试了一下手机热点,这次代码能正常跑起来,连接数据库了!!!是不</div>
                    </li>
                                <li><a href="/article/26.htm"
                                       title="设计模式介绍" target="_blank">设计模式介绍</a>
                                    <span class="text-muted">tntxia</span>
<a class="tag" taget="_blank" href="/search/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/1.htm">设计模式</a>
                                    <div>设计模式来源于土木工程师 克里斯托弗 亚历山大(http://en.wikipedia.org/wiki/Christopher_Alexander)的早期作品。他经常发表一些作品,内容是总结他在解决设计问题方面的经验,以及这些知识与城市和建筑模式之间有何关联。有一天,亚历山大突然发现,重复使用这些模式可以让某些设计构造取得我们期望的最佳效果。 
 
亚历山大与萨拉-石川佳纯和穆雷 西乐弗斯坦合作</div>
                                </li>
                                <li><a href="/article/153.htm"
                                       title="android高级组件使用(一)" target="_blank">android高级组件使用(一)</a>
                                    <span class="text-muted">百合不是茶</span>
<a class="tag" taget="_blank" href="/search/android/1.htm">android</a><a class="tag" taget="_blank" href="/search/RatingBar/1.htm">RatingBar</a><a class="tag" taget="_blank" href="/search/Spinner/1.htm">Spinner</a>
                                    <div>1、自动完成文本框(AutoCompleteTextView) 
 
 AutoCompleteTextView从EditText派生出来,实际上也是一个文本编辑框,但它比普通编辑框多一个功能:当用户输入一个字符后,自动完成文本框会显示一个下拉菜单,供用户从中选择,当用户选择某个菜单项之后,AutoCompleteTextView按用户选择自动填写该文本框。 
 使用AutoCompleteTex</div>
                                </li>
                                <li><a href="/article/280.htm"
                                       title="[网络与通讯]路由器市场大有潜力可挖掘" target="_blank">[网络与通讯]路由器市场大有潜力可挖掘</a>
                                    <span class="text-muted">comsci</span>
<a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C/1.htm">网络</a>
                                    <div>   
       如果国内的电子厂商和计算机设备厂商觉得手机市场已经有点饱和了,那么可以考虑一下交换机和路由器市场的进入问题..... 
 
       这方面的技术和知识,目前处在一个开放型的状态,有利于各类小型电子企业进入 
 
 &nbs</div>
                                </li>
                                <li><a href="/article/407.htm"
                                       title="自写简单Redis内存统计shell" target="_blank">自写简单Redis内存统计shell</a>
                                    <span class="text-muted">商人shang</span>
<a class="tag" taget="_blank" href="/search/Linux+shell/1.htm">Linux shell</a><a class="tag" taget="_blank" href="/search/%E7%BB%9F%E8%AE%A1Redis%E5%86%85%E5%AD%98/1.htm">统计Redis内存</a>
                                    <div>#!/bin/bash
address="192.168.150.128:6666,192.168.150.128:6666"  
hosts=(${address//,/ })  

sfile="staticts.log"

for hostitem in ${hosts[@]}  
do  
    ipport=(${hostitem</div>
                                </li>
                                <li><a href="/article/534.htm"
                                       title="单例模式(饿汉 vs懒汉)" target="_blank">单例模式(饿汉 vs懒汉)</a>
                                    <span class="text-muted">oloz</span>
<a class="tag" taget="_blank" href="/search/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/1.htm">单例模式</a>
                                    <div>package 单例模式;
/*
 * 应用场景:保证在整个应用之中某个对象的实例只有一个
 * 单例模式种的《 懒汉模式》  
 * */
public class Singleton {
	
	//01 将构造方法私有化,外界就无法用new Singleton()的方式获得实例
	private Singleton(){};
	
	//02 申明类得唯一实例
	priva</div>
                                </li>
                                <li><a href="/article/661.htm"
                                       title="springMvc json支持" target="_blank">springMvc json支持</a>
                                    <span class="text-muted">杨白白</span>
<a class="tag" taget="_blank" href="/search/json+springmvc/1.htm">json springmvc</a>
                                    <div>1.Spring mvc处理json需要使用jackson的类库,因此需要先引入jackson包 
 
 
2在spring mvc中解析输入为json格式的数据:使用@RequestBody来设置输入 
 

@RequestMapping("helloJson")
public @ResponseBody
    JsonTest helloJson() {
   </div>
                                </li>
                                <li><a href="/article/788.htm"
                                       title="android播放,掃描添加本地音頻文件" target="_blank">android播放,掃描添加本地音頻文件</a>
                                    <span class="text-muted">小桔子</span>

                                    <div>        最近幾乎沒有什麽事情,繼續鼓搗我的小東西。想在項目中加入一個簡易的音樂播放器功能,就像華為p6桌面上那麼大小的音樂播放器。用過天天動聽或者QQ音樂播放器的人都知道,可已通過本地掃描添加歌曲。不知道他們是怎麼實現的,我覺得應該掃描設備上的所有文件,過濾出音頻文件,每個文件實例化為一個實體,記錄文件名、路徑、歌手、類型、大小等信息。具體算法思想,</div>
                                </li>
                                <li><a href="/article/915.htm"
                                       title="oracle常用命令" target="_blank">oracle常用命令</a>
                                    <span class="text-muted">aichenglong</span>
<a class="tag" taget="_blank" href="/search/oracle/1.htm">oracle</a><a class="tag" taget="_blank" href="/search/dba/1.htm">dba</a><a class="tag" taget="_blank" href="/search/%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/1.htm">常用命令</a>
                                    <div>1 创建临时表空间 
create temporary tablespace user_temp  
tempfile 'D:\oracle\oradata\Oracle9i\user_temp.dbf' 
size 50m  
autoextend on  
next 50m maxsize 20480m  
extent management local</div>
                                </li>
                                <li><a href="/article/1042.htm"
                                       title="25个Eclipse插件" target="_blank">25个Eclipse插件</a>
                                    <span class="text-muted">AILIKES</span>
<a class="tag" taget="_blank" href="/search/eclipse%E6%8F%92%E4%BB%B6/1.htm">eclipse插件</a>
                                    <div>提高代码质量的插件1. FindBugsFindBugs可以帮你找到Java代码中的bug,它使用Lesser GNU Public License的自由软件许可。2. CheckstyleCheckstyle插件可以集成到Eclipse IDE中去,能确保Java代码遵循标准代码样式。3. ECLemmaECLemma是一款拥有Eclipse Public License许可的免费工具,它提供了</div>
                                </li>
                                <li><a href="/article/1169.htm"
                                       title="Spring MVC拦截器+注解方式实现防止表单重复提交" target="_blank">Spring MVC拦截器+注解方式实现防止表单重复提交</a>
                                    <span class="text-muted">baalwolf</span>
<a class="tag" taget="_blank" href="/search/spring+mvc/1.htm">spring mvc</a>
                                    <div>原理:在新建页面中Session保存token随机码,当保存时验证,通过后删除,当再次点击保存时由于服务器端的Session中已经不存在了,所有无法验证通过。      
1.新建注解: 
     
?       1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   17   18   </div>
                                </li>
                                <li><a href="/article/1296.htm"
                                       title="《Javascript高级程序设计(第3版)》闭包理解" target="_blank">《Javascript高级程序设计(第3版)》闭包理解</a>
                                    <span class="text-muted">bijian1013</span>
<a class="tag" taget="_blank" href="/search/JavaScript/1.htm">JavaScript</a>
                                    <div>“闭包是指有权访问另一个函数作用域中的变量的函数。”--《Javascript高级程序设计(第3版)》 
  
      看以下代码: 
<script type="text/javascript">
    function outer() {
        var i = 10;
        return f</div>
                                </li>
                                <li><a href="/article/1423.htm"
                                       title="AngularJS Module类的方法" target="_blank">AngularJS Module类的方法</a>
                                    <span class="text-muted">bijian1013</span>
<a class="tag" taget="_blank" href="/search/JavaScript/1.htm">JavaScript</a><a class="tag" taget="_blank" href="/search/AngularJS/1.htm">AngularJS</a><a class="tag" taget="_blank" href="/search/Module/1.htm">Module</a>
                                    <div>        AngularJS中的Module类负责定义应用如何启动,它还可以通过声明的方式定义应用中的各个片段。我们来看看它是如何实现这些功能的。 
一.Main方法在哪里 
        如果你是从Java或者Python编程语言转过来的,那么你可能很想知道AngularJS里面的main方法在哪里?这个把所</div>
                                </li>
                                <li><a href="/article/1550.htm"
                                       title="[Maven学习笔记七]Maven插件和目标" target="_blank">[Maven学习笔记七]Maven插件和目标</a>
                                    <span class="text-muted">bit1129</span>
<a class="tag" taget="_blank" href="/search/maven%E6%8F%92%E4%BB%B6/1.htm">maven插件</a>
                                    <div>插件(plugin)和目标(goal) 
Maven,就其本质而言,是一个插件执行框架,Maven的每个目标的执行逻辑都是由插件来完成的,一个插件可以有1个或者几个目标,比如maven-compiler-plugin插件包含compile和testCompile,即maven-compiler-plugin提供了源代码编译和测试源代码编译的两个目标 
  
使用插件和目标使得我们可以干预</div>
                                </li>
                                <li><a href="/article/1677.htm"
                                       title="【Hadoop八】Yarn的资源调度策略" target="_blank">【Hadoop八】Yarn的资源调度策略</a>
                                    <span class="text-muted">bit1129</span>
<a class="tag" taget="_blank" href="/search/hadoop/1.htm">hadoop</a>
                                    <div>1. Hadoop的三种调度策略 
Hadoop提供了3中作业调用的策略, 
 
 FIFO Scheduler 
 Fair Scheduler 
 Capacity Scheduler 
 
以上三种调度算法,在Hadoop MR1中就引入了,在Yarn中对它们进行了改进和完善.Fair和Capacity Scheduler用于多用户共享的资源调度 
  2. 多用户资源共享的调度 </div>
                                </li>
                                <li><a href="/article/1804.htm"
                                       title="Nginx使用Linux内存加速静态文件访问" target="_blank">Nginx使用Linux内存加速静态文件访问</a>
                                    <span class="text-muted">ronin47</span>

                                    <div>Nginx是一个非常出色的静态资源web服务器。如果你嫌它还不够快,可以把放在磁盘中的文件,映射到内存中,减少高并发下的磁盘IO。 
先做几个假设。nginx.conf中所配置站点的路径是/home/wwwroot/res,站点所对应文件原始存储路径:/opt/web/res 
shell脚本非常简单,思路就是拷贝资源文件到内存中,然后在把网站的静态文件链接指向到内存中即可。具体如下:      </div>
                                </li>
                                <li><a href="/article/1931.htm"
                                       title="关于Unity3D中的Shader的知识" target="_blank">关于Unity3D中的Shader的知识</a>
                                    <span class="text-muted">brotherlamp</span>
<a class="tag" taget="_blank" href="/search/unity/1.htm">unity</a><a class="tag" taget="_blank" href="/search/unity%E8%B5%84%E6%96%99/1.htm">unity资料</a><a class="tag" taget="_blank" href="/search/unity%E6%95%99%E7%A8%8B/1.htm">unity教程</a><a class="tag" taget="_blank" href="/search/unity%E8%A7%86%E9%A2%91/1.htm">unity视频</a><a class="tag" taget="_blank" href="/search/unity%E8%87%AA%E5%AD%A6/1.htm">unity自学</a>
                                    <div>首先先解释下Unity3D的Shader,Unity里面的Shaders是使用一种叫ShaderLab的语言编写的,它同微软的FX文件或者NVIDIA的CgFX有些类似。传统意义上的vertex shader和pixel shader还是使用标准的Cg/HLSL 编程语言编写的。因此Unity文档里面的Shader,都是指用ShaderLab编写的代码,然后我们来看下Unity3D自带的60多个S</div>
                                </li>
                                <li><a href="/article/2058.htm"
                                       title="CopyOnWriteArrayList vs ArrayList" target="_blank">CopyOnWriteArrayList vs ArrayList</a>
                                    <span class="text-muted">bylijinnan</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a>
                                    <div>package com.ljn.base;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 总述:
 * 1.ArrayListi不是线程安全的,CopyO</div>
                                </li>
                                <li><a href="/article/2185.htm"
                                       title="内存中栈和堆的区别" target="_blank">内存中栈和堆的区别</a>
                                    <span class="text-muted">chicony</span>
<a class="tag" taget="_blank" href="/search/%E5%86%85%E5%AD%98/1.htm">内存</a>
                                    <div>  
1、内存分配方面: 
 
    堆:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式是类似于链表。可能用到的关键字如下:new、malloc、delete、free等等。 
 
    栈:由编译器(Compiler)自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中</div>
                                </li>
                                <li><a href="/article/2312.htm"
                                       title="回答一位网友对Scala的提问" target="_blank">回答一位网友对Scala的提问</a>
                                    <span class="text-muted">chenchao051</span>
<a class="tag" taget="_blank" href="/search/scala/1.htm">scala</a><a class="tag" taget="_blank" href="/search/map/1.htm">map</a>
                                    <div>本来准备在私信里直接回复了,但是发现不太方便,就简要回答在这里。  问题 写道   对于scala的简洁十分佩服,但又觉得比较晦涩,例如一例,Map("a" -> List(11,111)).flatMap(_._2),可否说下最后那个函数做了什么,真正在开发的时候也会如此简洁?谢谢  
   先回答一点,在实际使用中,Scala毫无疑问就是这么简单。</div>
                                </li>
                                <li><a href="/article/2439.htm"
                                       title="mysql 取每组前几条记录" target="_blank">mysql 取每组前几条记录</a>
                                    <span class="text-muted">daizj</span>
<a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a><a class="tag" taget="_blank" href="/search/%E5%88%86%E7%BB%84/1.htm">分组</a><a class="tag" taget="_blank" href="/search/%E6%9C%80%E5%A4%A7%E5%80%BC/1.htm">最大值</a><a class="tag" taget="_blank" href="/search/%E6%9C%80%E5%B0%8F%E5%80%BC/1.htm">最小值</a><a class="tag" taget="_blank" href="/search/%E6%AF%8F%E7%BB%84%E4%B8%89%E6%9D%A1%E8%AE%B0%E5%BD%95/1.htm">每组三条记录</a>
                                    <div>一、对分组的记录取前N条记录:例如:取每组的前3条最大的记录    1.用子查询:   SELECT * FROM tableName a  WHERE 3>   (SELECT COUNT(*) FROM  tableName b WHERE b.id=a.id AND b.cnt>a. cnt)   ORDER BY a.id,a.account DE</div>
                                </li>
                                <li><a href="/article/2566.htm"
                                       title="HTTP深入浅出 http请求" target="_blank">HTTP深入浅出 http请求</a>
                                    <span class="text-muted">dcj3sjt126com</span>
<a class="tag" taget="_blank" href="/search/http/1.htm">http</a>
                                    <div>    HTTP(HyperText Transfer Protocol)是一套计算机通过网络进行通信的规则。计算机专家设计出HTTP,使HTTP客户(如Web浏览器)能够从HTTP服务器(Web服务器)请求信息和服务,HTTP目前协议的版本是1.1.HTTP是一种无状态的协议,无状态是指Web浏览器和Web服务器之间不需要建立持久的连接,这意味着当一个客户端向服务器端发出请求,然后We</div>
                                </li>
                                <li><a href="/article/2693.htm"
                                       title="判断MySQL记录是否存在方法比较" target="_blank">判断MySQL记录是否存在方法比较</a>
                                    <span class="text-muted">dcj3sjt126com</span>
<a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a>
                                    <div>把数据写入到数据库的时,常常会碰到先要检测要插入的记录是否存在,然后决定是否要写入。 
  我这里总结了判断记录是否存在的常用方法: 
  sql语句:  select   count ( * )  from  tablename;  
  然后读取count(*)的值判断记录是否存在。对于这种方法性能上有些浪费,我们只是想判断记录记录是否存在,没有必要全部都查出来。</div>
                                </li>
                                <li><a href="/article/2820.htm"
                                       title="对HTML XML的一点认识" target="_blank">对HTML XML的一点认识</a>
                                    <span class="text-muted">e200702084</span>
<a class="tag" taget="_blank" href="/search/html/1.htm">html</a><a class="tag" taget="_blank" href="/search/xml/1.htm">xml</a>
                                    <div>感谢http://www.w3school.com.cn提供的资料 
HTML 文档中的每个成分都是一个节点。 
节点 
根据 DOM,HTML 文档中的每个成分都是一个节点。 
 
DOM 是这样规定的: 
 
整个文档是一个文档节点 
每个 HTML 标签是一个元素节点 
包含在 HTML 元素中的文本是文本节点 
每一个 HTML 属性是一个属性节点 
注释属于注释节点 
Node 层次 
</div>
                                </li>
                                <li><a href="/article/2947.htm"
                                       title="jquery分页插件" target="_blank">jquery分页插件</a>
                                    <span class="text-muted">genaiwei</span>
<a class="tag" taget="_blank" href="/search/jquery/1.htm">jquery</a><a class="tag" taget="_blank" href="/search/Web/1.htm">Web</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/%E5%88%86%E9%A1%B5/1.htm">分页</a><a class="tag" taget="_blank" href="/search/%E6%8F%92%E4%BB%B6/1.htm">插件</a>
                                    <div>//jquery页码控件// 创建一个闭包    (function($) {      // 插件的定义      $.fn.pageTool = function(options) {          var totalPa</div>
                                </li>
                                <li><a href="/article/3201.htm"
                                       title="Mybatis与Ibatis对照入门于学习" target="_blank">Mybatis与Ibatis对照入门于学习</a>
                                    <span class="text-muted">Josh_Persistence</span>
<a class="tag" taget="_blank" href="/search/mybatis/1.htm">mybatis</a><a class="tag" taget="_blank" href="/search/ibatis/1.htm">ibatis</a><a class="tag" taget="_blank" href="/search/%E5%8C%BA%E5%88%AB/1.htm">区别</a><a class="tag" taget="_blank" href="/search/%E8%81%94%E7%B3%BB/1.htm">联系</a>
                                    <div>一、为什么使用IBatis/Mybatis 
        对于从事 Java EE 的开发人员来说,iBatis 是一个再熟悉不过的持久层框架了,在 Hibernate、JPA 这样的一站式对象 / 关系映射(O/R Mapping)解决方案盛行之前,iBaits 基本是持久层框架的不二选择。即使在持久层框架层出不穷的今天,iBatis 凭借着易学易用、</div>
                                </li>
                                <li><a href="/article/3328.htm"
                                       title="C中怎样合理决定使用那种整数类型?" target="_blank">C中怎样合理决定使用那种整数类型?</a>
                                    <span class="text-muted">秋风扫落叶</span>
<a class="tag" taget="_blank" href="/search/c/1.htm">c</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/1.htm">数据类型</a>
                                    <div>如果需要大数值(大于32767或小于32767), 使用long 型。 否则, 如果空间很重要 (如有大数组或很多结构), 使用 short 型。 除此之外, 就使用 int 型。 如果严格定义的溢出特征很重要而负值无关紧要, 或者你希望在操作二进制位和字节时避免符号扩展的问题, 请使用对应的无符号类型。 但是, 要注意在表达式中混用有符号和无符号值的情况。 
  
 &nbs</div>
                                </li>
                                <li><a href="/article/3455.htm"
                                       title="maven问题" target="_blank">maven问题</a>
                                    <span class="text-muted">zhb8015</span>
<a class="tag" taget="_blank" href="/search/maven%E9%97%AE%E9%A2%98/1.htm">maven问题</a>
                                    <div>  
问题1: 
Eclipse 中 新建maven项目 无法添加src/main/java 问题 
   eclipse创建maevn web项目,在选择maven_archetype_web原型后,默认只有src/main/resources这个Source Floder。 
    按照maven目录结构,添加src/main/ja</div>
                                </li>
                                <li><a href="/article/3582.htm"
                                       title="(二)androidpn-server tomcat版源码解析之--push消息处理" target="_blank">(二)androidpn-server tomcat版源码解析之--push消息处理</a>
                                    <span class="text-muted">spjich</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/androdipn/1.htm">androdipn</a><a class="tag" taget="_blank" href="/search/%E6%8E%A8%E9%80%81/1.htm">推送</a>
                                    <div>在 (一)androidpn-server tomcat版源码解析之--项目启动这篇中,已经描述了整个推送服务器的启动过程,并且把握到了消息的入口即XmppIoHandler这个类,今天我将继续往下分析下面的核心代码,主要分为3大块,链接创建,消息的发送,链接关闭。 
先贴一段XmppIoHandler的部分代码 
/**
     * Invoked from an I/O proc</div>
                                </li>
                                <li><a href="/article/3709.htm"
                                       title="用js中的formData类型解决ajax提交表单时文件不能被serialize方法序列化的问题" target="_blank">用js中的formData类型解决ajax提交表单时文件不能被serialize方法序列化的问题</a>
                                    <span class="text-muted">中华好儿孙</span>
<a class="tag" taget="_blank" href="/search/JavaScript/1.htm">JavaScript</a><a class="tag" taget="_blank" href="/search/Ajax/1.htm">Ajax</a><a class="tag" taget="_blank" href="/search/Web/1.htm">Web</a><a class="tag" taget="_blank" href="/search/%E4%B8%8A%E4%BC%A0%E6%96%87%E4%BB%B6/1.htm">上传文件</a><a class="tag" taget="_blank" href="/search/FormData/1.htm">FormData</a>
                                    <div>
var formData = new FormData($("#inputFileForm")[0]);
$.ajax({
		type:'post',
		url:webRoot+"/electronicContractUrl/webapp/uploadfile",
		data:formData,
		async: false,
		ca</div>
                                </li>
                                <li><a href="/article/3836.htm"
                                       title="mybatis常用jdbcType数据类型" target="_blank">mybatis常用jdbcType数据类型</a>
                                    <span class="text-muted">ysj5125094</span>
<a class="tag" taget="_blank" href="/search/mybatis/1.htm">mybatis</a><a class="tag" taget="_blank" href="/search/mapper/1.htm">mapper</a><a class="tag" taget="_blank" href="/search/jdbcType/1.htm">jdbcType</a>
                                    <div>  
MyBatis 通过包含的jdbcType
类型 
BIT         FLOAT      CHAR          </div>
                                </li>
                </ul>
            </div>
        </div>
    </div>

<div>
    <div class="container">
        <div class="indexes">
            <strong>按字母分类:</strong>
            <a href="/tags/A/1.htm" target="_blank">A</a><a href="/tags/B/1.htm" target="_blank">B</a><a href="/tags/C/1.htm" target="_blank">C</a><a
                href="/tags/D/1.htm" target="_blank">D</a><a href="/tags/E/1.htm" target="_blank">E</a><a href="/tags/F/1.htm" target="_blank">F</a><a
                href="/tags/G/1.htm" target="_blank">G</a><a href="/tags/H/1.htm" target="_blank">H</a><a href="/tags/I/1.htm" target="_blank">I</a><a
                href="/tags/J/1.htm" target="_blank">J</a><a href="/tags/K/1.htm" target="_blank">K</a><a href="/tags/L/1.htm" target="_blank">L</a><a
                href="/tags/M/1.htm" target="_blank">M</a><a href="/tags/N/1.htm" target="_blank">N</a><a href="/tags/O/1.htm" target="_blank">O</a><a
                href="/tags/P/1.htm" target="_blank">P</a><a href="/tags/Q/1.htm" target="_blank">Q</a><a href="/tags/R/1.htm" target="_blank">R</a><a
                href="/tags/S/1.htm" target="_blank">S</a><a href="/tags/T/1.htm" target="_blank">T</a><a href="/tags/U/1.htm" target="_blank">U</a><a
                href="/tags/V/1.htm" target="_blank">V</a><a href="/tags/W/1.htm" target="_blank">W</a><a href="/tags/X/1.htm" target="_blank">X</a><a
                href="/tags/Y/1.htm" target="_blank">Y</a><a href="/tags/Z/1.htm" target="_blank">Z</a><a href="/tags/0/1.htm" target="_blank">其他</a>
        </div>
    </div>
</div>
<footer id="footer" class="mb30 mt30">
    <div class="container">
        <div class="footBglm">
            <a target="_blank" href="/">首页</a> -
            <a target="_blank" href="/custom/about.htm">关于我们</a> -
            <a target="_blank" href="/search/Java/1.htm">站内搜索</a> -
            <a target="_blank" href="/sitemap.txt">Sitemap</a> -
            <a target="_blank" href="/custom/delete.htm">侵权投诉</a>
        </div>
        <div class="copyright">版权所有 IT知识库 CopyRight © 2000-2050 E-COM-NET.COM , All Rights Reserved.
<!--            <a href="https://beian.miit.gov.cn/" rel="nofollow" target="_blank">京ICP备09083238号</a><br>-->
        </div>
    </div>
</footer>
<!-- 代码高亮 -->
<script type="text/javascript" src="/static/syntaxhighlighter/scripts/shCore.js"></script>
<script type="text/javascript" src="/static/syntaxhighlighter/scripts/shLegacy.js"></script>
<script type="text/javascript" src="/static/syntaxhighlighter/scripts/shAutoloader.js"></script>
<link type="text/css" rel="stylesheet" href="/static/syntaxhighlighter/styles/shCoreDefault.css"/>
<script type="text/javascript" src="/static/syntaxhighlighter/src/my_start_1.js"></script>





</body>

</html>