zoukankan      html  css  js  c++  java
  • 从chrome源码看JavaScript执行 dom

    缘由

    来源于ph师傅小密圈里的一个问题

    例子1:

    <!DOCTYPE html>
    <html>
    <head>
        <title></title>
    </head>
    <body>
    <div id="id"></div>
    <script>
    var a="<script>alert(1)</s"+"cript>";
    //var a="<img src=1 onerror=alert(1)>";
    document.getElementById("id").innerHTML=a;
    </script>
    </body>
    </html>

    这段代码,虽然成功插入了script标签,但是没有执行,如果改为插入img标签则可以执行

    首先的猜想是:

    onerror 事件在 DOM 树构建完成之后触发,而 script 标签内的内容在 DOM 解析中触发,但是可以看一下这个例子

    例子2:

    <script type="text/javascript" id="id2"></script>
    <script>
      var a = "alert(/script/)";
      document.getElementById("id2").innerText = a;
    </script>

    这里正确执行了JS,说明浏览器在解析并执行第二个 script 标签时,回头执行了第一个 script 标签内的内容,而第一个 script 标签此时已经成功加入了 DOM 树里,这可能是因为浏览器在遇到 script 标签,或者对 script 标签进行 DOM 操作的时候,会刷新一次页面渲染,因为浏览器在碰到 script 标签的时候,需要渲染页面保证 script 能获取到最新的 DOM 元素信息,可以看下这个:链接:原来 CSS 与 JS 是这样阻塞 DOM 解析和渲染的 - 掘金

    所以,最开始的例子是对 div 元素进行的 DOM 操作,浏览器不会再触发页面渲染并执行动态创建的 script,创建的 script 无法在解析中执行,关于浏览器的执行流程可以看下这个:链接:浏览器的工作原理:现代网络浏览器幕后揭秘 - HTML5 Rocks

    再看一个例子:

    <div id="id"></div>
    <script>
    var a = document.createElement('script');
    a.innerText = 'alert(/xx/)';
    document.getElementById("id").appendChild(a);
    </script>

    这个例子里JS成功触发,于是可能最开始的猜想就是正确的,对script标签的DOM操作,无论他是被操作还是操作,都会进行一次渲染,那么这个怎么用正确的术语描述呢?

    掘金那篇文章的一个评论

    可以翻墙的看看下面这篇文章: https://developers.google.com/web/fundamentals/performance/critical-rendering-path/?hl=zh-cn CSSOM树和DOM树是分开构建,之所以把link标签放抬头而script放body尾部,是因为浏览器遇到script标签时,会去下载并执行js脚本,从而导致浏览器暂停构建DOM。然而JS脚本需要查询CSS信息,所以JS脚本还必须等待CSSOM树构建完才可以执行。 这将相当于CSS阻塞了JS脚本,JS脚本阻塞了DOM树构建。是这样子的关联才对。 只要设置CSS脚本提前加载避免阻塞JS脚本执行时CSSOM树还没构建好,同时给script标签设置async就可以解决这个问题

    https://developers.google.com/web/fundamentals/?hl=zh-cn 这个貌似比较详细的解释了浏览器

    简言之,JavaScript 在 DOM、CSSOM 和 JavaScript 执行之间引入了大量新的依赖关系,从而可能导致浏览器在处理以及在屏幕上渲染网页时出现大幅延迟:
    
    - 脚本在文档中的位置很重要。
    - 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。
    - JavaScript 可以查询和修改 DOM 与 CSSOM。
    - JavaScript 执行将暂停,直至 CSSOM 就绪。
    
    “优化关键渲染路径”在很大程度上是指了解和优化 HTML、CSS 和 JavaScript 之间的依赖关系谱。

    看了一些文章,了解了reflow和repaint的概念,可能需要重新审视最开始的代码为什么没有进入JS环境

    毫无疑问,innerHTML会执行回流操作,并且 innerHTML 中的东西会被加入DOM树,

    innerHTML会解析其中的 html 代码并将其加入 DOM 树:https://developer.mozilla.org/en-US/docs/Web/API/DOMParser

    那么就有点奇怪,如果 innerHTML 使 script 加入了 DOM 树,并且触发了回流操作,那么为什么不会执行呢?

    同样的,innerText给 script 标签加入代码,也会回流(reflow) DOM 树,并且执行了 JS 代码

    需要调试一下chrome,确定他什么时候会进入JS环境

    Chrome是如何执行JavaScript的

    对于例子1和例子2,猜测他们存在一个分流点,例子1进入了不能调用JS引擎的分支,例子2则进入了能够调用JS引擎的分支,要找到这个地方,就需要找到调用JS引擎的入口函数,从而回溯调用链,为了找到这个入口函数,我们先调试一个简单的例子

    <script>alert(1)</script>

    第一个断点断在 HTMLTreeBuilder::ProcessToken(AtomicHTMLToken* token) 这个函数上,看一下从浏览器打开页面到开始处理token之间经过了什么,这里不是我们这篇文章的重点,所以准备一笔带过,关于token是什么以及浏览器如何构建DOM树,可以看这篇文章:https://zhuanlan.zhihu.com/p/24911872

    打开页面之后首先是创建进程,创建消息循环,创建渲染线程,创建渲染视图,接着创建Main frame,加载document对象,解析document对象,然后进入token的循环处理阶段(token的循环处理函数定义在html_document_parser.cc里的HTMLDocumentParser::ProcessTokenizedChunkFromBackgroundParser 函数中)

    接着,处理第一个token AtomicHTMLToken kEndOfFile(只在第一次打开这个标签页的时候需要处理,刷新当前标签页的话没有这个token),紧接着的是第二个token:script的起始标签 AtomicHTMLToken kStartTag name "script",然后是第三个token:script标签中的字符 AtomicHTMLToken kCharacter data "alert(1);",最后,就是script的闭合标签 AtomicHTMLToken kEndTag name "script",紧接着就会进入js执行环境,从这里开始单步调试

    1.一开始是 ProcessEndTag 函数,将token当做参数传递进去

    2.进入函数后调用了GetInsertionMode() 检查token的类型,粗略看了一下有 kAfterHeadMode, kInBodyMode 之类的,我们这里的script闭合标签是 kTextMode

    case kTextMode:
          if (token->GetName() == scriptTag &&
              tree_.CurrentStackItem()->HasTagName(scriptTag)) {
            // Pause ourselves so that parsing stops until the script can be
            // processed by the caller.
            if (ScriptingContentIsAllowed(tree_.GetParserContentPolicy()))
              script_to_process_ = tree_.CurrentElement();
            tree_.OpenElements()->Pop();
            SetInsertionMode(original_insertion_mode_);
    
            if (parser_->Tokenizer()) {
              // We must set the tokenizer's state to DataState explicitly if the
              // tokenizer didn't have a chance to.
              parser_->Tokenizer()->SetState(HTMLTokenizer::kDataState);
            }
            return;
          }
          tree_.OpenElements()->Pop();
          SetInsertionMode(original_insertion_mode_);
          break;

    3.首先判断了是否是script标签,然后判断这个script是否可以执行,都判断通过了,则标记为script_toprocess

    判断script是否能执行的函数:

    static inline bool ScriptingContentIsAllowed(
        ParserContentPolicy parser_content_policy) {
      return parser_content_policy == kAllowScriptingContent ||
             parser_content_policy ==
                 kAllowScriptingContentAndDoNotMarkAlreadyStarted;
    }

    4.接着执行了一个函数SetInsertionMode(original_insertion_mode_);,把之前的kTextMode改成了kInHeadMode,难道说对script标签都默认在head里??

    5.return之后回到了HTMLTreeBuilder::ConstructTree(AtomicHTMLToken* token) 函数中,执行最后一步tree_.ExecuteQueuedTasks()

    void HTMLConstructionSite::ExecuteQueuedTasks() {
      // This has no affect on pendingText, and we may have pendingText remaining
      // after executing all other queued tasks.
      const size_t size = task_queue_.size();
      if (!size)
        return;
    
      // Fast path for when |size| is 1, which is the common case
      if (size == 1) {
        HTMLConstructionSiteTask task = task_queue_.front();
        task_queue_.pop_back();
        ExecuteTask(task);
        return;
      }
    
      // Copy the task queue into a local variable in case executeTask re-enters the
      // parser.
      TaskQueue queue;
      queue.swap(task_queue_);
    
      for (auto& task : queue)
        ExecuteTask(task);
    
      // We might be detached now.
    }

    在判断task_size那里直接return了,因为队列里没有其他任务了,回到了最开始的HTMLDocumentParser::ProcessTokenizedChunkFromBackgroundParser 函数,触发了pause并且是script类型的pause,进入判断script的函数HTMLDocumentParser::IsWaitingForScripts()

    bool HTMLDocumentParser::IsWaitingForScripts() const {
      // When the TreeBuilder encounters a </script> tag, it returns to the
      // HTMLDocumentParser where the script is transfered from the treebuilder to
      // the script runner. The script runner will hold the script until its loaded
      // and run. During any of this time, we want to count ourselves as "waiting
      // for a script" and thus run the preload scanner, as well as delay completion
      // of parsing.
      bool tree_builder_has_blocking_script =
          tree_builder_->HasParserBlockingScript();
      bool script_runner_has_blocking_script =
          script_runner_ && script_runner_->HasParserBlockingScript();
      // Since the parser is paused while a script runner has a blocking script, it
      // should never be possible to end up with both objects holding a blocking
      // script.
      DCHECK(
          !(tree_builder_has_blocking_script && script_runner_has_blocking_script));
      // If either object has a blocking script, the parser should be paused.
      return tree_builder_has_blocking_script ||
             script_runner_has_blocking_script ||
             reentry_permit_->ParserPauseFlag();
    }

    判断了是在tree_builder里触发的script pause还是在script_runner里,这里是在tree_builder里触发的block,判断依据是之前第二部标记的script_toprocess ,tree_builder_has_blocking_script返回true,暂停DOM树的构建,转去执行JS

    if (IsPaused()) {
          // The script or stylesheet should be the last token of this bunch.
          DCHECK_EQ(it + 1, tokens->end());
          if (IsWaitingForScripts())
            RunScriptsForPausedTreeBuilder();
          ValidateSpeculations(std::move(chunk));
          break;
        }

    6.首先判断script是否是最后一个token,然后进入RunScriptsForPausedTreeBuilder()函数

    void HTMLDocumentParser::RunScriptsForPausedTreeBuilder() {
      DCHECK(ScriptingContentIsAllowed(GetParserContentPolicy()));
    
      TextPosition script_start_position = TextPosition::BelowRangePosition();
      Element* script_element =
          tree_builder_->TakeScriptToProcess(script_start_position);
      // We will not have a scriptRunner when parsing a DocumentFragment.
      if (script_runner_)
        script_runner_->ProcessScriptElement(script_element, script_start_position);
      CheckIfBodyStylesheetAdded();
    }

    到scriptrunner->ProcessScriptElement(script_element, script_start_position);(html_parser_script_runner.cc,跟进去

    void HTMLParserScriptRunner::ProcessScriptElement(
        Element* script_element,
        const TextPosition& script_start_position) {
      DCHECK(script_element);
    
      // FIXME: If scripting is disabled, always just return.
    
      bool had_preload_scanner = host_->HasPreloadScanner();
    
      // Spec: An end tag whose tag name is "script" ... [spec text]
      //
      // Try to execute the script given to us.
      ProcessScriptElementInternal(script_element, script_start_position);
    
      // Spec: ... At this stage, if there is a pending parsing-blocking script,
      // then: [spec text]
      if (HasParserBlockingScript()) {
        // Step A. If the script nesting level is not zero: ... [spec text]
        if (IsExecutingScript()) {
          // Step A. ... Set the parser pause flag to true, and abort the processing
          // of any nested invocations of the tokenizer, yielding control back to
          // the caller. (Tokenization will resume when the caller returns to the
          // "outer" tree construction stage.) [spec text]
          //
          // TODO(hiroshige): set the parser pause flag to true here.
    
          // Unwind to the outermost HTMLParserScriptRunner::processScriptElement
          // before continuing parsing.
          return;
        }
    
        // - "Otherwise":
    
        TraceParserBlockingScript(ParserBlockingScript(),
                                  !document_->IsScriptExecutionReady());
        parser_blocking_script_->MarkParserBlockingLoadStartTime();
    
        // If preload scanner got created, it is missing the source after the
        // current insertion point. Append it and scan.
        if (!had_preload_scanner && host_->HasPreloadScanner())
          host_->AppendCurrentInputStreamToPreloadScannerAndScan();
    
        ExecuteParsingBlockingScripts();
      }
    }

    ProcessScriptElementInternal(script_element, script_start_position); 执行完后 JS执行成功

    因为ProcessScriptElementInternal函数有点长所以不全放进来了,最重要的是这一段:

    if (!IsExecutingScript())
          Microtask::PerformCheckpoint(V8PerIsolateData::MainThreadIsolate());
    
        // Spec: ... Let the old insertion point have the same value as the current
        // insertion point. Let the insertion point be just before the next input
        // character. ... [spec text]
        InsertionPointRecord insertion_point_record(host_->InputStream());
    
        // Spec: ... Increment the parser's script nesting level by one. ... [spec
        // text]
        HTMLParserReentryPermit::ScriptNestingLevelIncrementer
            nesting_level_incrementer =
                reentry_permit_->IncrementScriptNestingLevel();
    
        // Spec: ... Prepare the script. This might cause some script to execute,
        // which might cause new characters to be inserted into the tokenizer, and
        // might cause the tokenizer to output more tokens, resulting in a reentrant
        // invocation of the parser. ... [spec text]
        script_loader->PrepareScript(script_start_position);

    最后一行调用了script_loader->PrepareScript,这是一个非常重要的函数,里面进行了多次判断当前的script是否可以执行,并对当前的执行上下文进行了检测和分类,同时看到这里的注释:这一步可能导致JS的执行并增加更多的token,可以说从这个函数开始就是每个script执行时必须要经过的地方,最后调用

    ScriptLoader::ExecuteScriptBlock
    script->RunScript(frame, element_->GetDocument().GetSecurityOrigin())

    执行了JS,建议看下PrepareScript这个函数里的几十个判断,能理解chromium对script的执行有什么限制

    7.之后,因为当前的token都已经被处理,所以ProcessTokenizedChunkFromBackgroundParser执行完成,回到了HTMLDocumentParser::PumpPendingSpeculations继续下一步,进行一个检查后也结束了PumpPendingSpeculations的过程

    // Always check isParsing first as m_document may be null. Surprisingly,
        // isScheduledForUnpause() may be set here as a result of
        // processTokenizedChunkFromBackgroundParser running arbitrary javascript
        // which invokes nested event loops. (e.g. inspector breakpoints)
        CheckIfBodyStylesheetAdded();
        if (!IsParsing() || IsPaused() || IsScheduledForUnpause())
          break;
    
        if (speculations_.IsEmpty() ||
            parser_scheduler_->YieldIfNeeded(
                session, speculations_.front()->starting_script))
          break;

    但是token的循环仍然没有结束,还有最后一个kEndOfFile的token需要处理,当这个token处理完成后,DOM树构建,如果这时继续单步调试可以发现call stack里的函数越来越少

    下篇将解析DOM操作以及相关思考。

    DOM操作

    回到正题,通过调试找到文章最开头两个例子的区别,先回想一下刚才调试的过程中我们拿到了什么能够帮助我们的信息

    1. 浏览器在获取到script end tag的token的时候会进入JS环境
    2. 浏览器在每次ProcessToken的时候都会Flush一遍当前的DOM树
    3. 如果浏览器在解析过程中遇到了可以执行的script,会在HTMLDocumentParser::ProcessTokenizedChunkFromBackgroundParser这个函数中pause并且进入JS环境
    4. PrepareScript可能是所有JS执行前需要执行的函数

    那么就来调试一下简化的能触发alert的版本

    <script type="text/javascript" id="id"></script>
    <script>
    var a="alert(1)";
    document.getElementById("id").innerHTML=a;
    </script>

    首先看一下一共有多少token需要处理:

    AtomicHTMLToken kStartTag name "script"
    AtomicHTMLToken kEndTag name "script"
    AtomicHTMLToken kCharacter data "
    "
    AtomicHTMLToken kStartTag name "script"
    AtomicHTMLToken kCharacter data "
    var a="alert(1)";
    document.getElementById("id").innerHTML=a;
    "
    AtomicHTMLToken kEndTag name "script"
    AtomicHTMLToken kCharacter data "alert(1)"
    AtomicHTMLToken kEndOfFile
    AtomicHTMLToken kCharacter data "
    "
    AtomicHTMLToken kEndOfFile

    可以发现,alert(1)单独作为一个token出现了,并且他前后都没有自己的script token,他被插入的目标script token在最开始就已经被加入

    这次我们断在script的入口点 ScriptLoader::PrepareScript ,很明显的,进入了三次 ScriptLoader::PrepareScript 函数,每进入一次scriptloader,chromium都会判断这个script能否被执行,第一次进入scriptloader的时候没有通过其中这个判断

    if (!element_->HasSourceAttribute() && !element_->HasChildren())
        return false;

    因为他是一个空的script标签,所以没有执行,而第二,三次进入scriptloader都通过了所有判断最后执行

    先来看第三次进入scriptloader的时候,即执行alert的时候通过了什么函数

    经过层层判断,并且按照是inline的script还是有src属性的script进行分类之后,进入ScriptLoader::ExecuteScriptBlock(TakePendingScript(), script_url) 这个函数里

    然后又是经过一系列的判断和check...进入script->RunScript(frame, element_->GetDocument().GetSecurityOrigin());这个函数就是我们上个章节没有跟进去的地方

    给RunScript下个断点,继续深入

    进入了classic_script.cc里

    void ClassicScript::RunScript(LocalFrame* frame,
                                  const SecurityOrigin* security_origin) const {
      frame->GetScriptController().ExecuteScriptInMainWorld(
          GetScriptSourceCode(), BaseURL(), FetchOptions(), access_control_status_);
    }

    获取了sourcecode,baseurl和options之后,终于要进入v8的世界了

    script_controller.cc

    void ScriptController::ExecuteScriptInMainWorld(
        const ScriptSourceCode& source_code,
        const KURL& base_url,
        const ScriptFetchOptions& fetch_options,
        AccessControlStatus access_control_status) {
      v8::HandleScope handle_scope(GetIsolate());
      EvaluateScriptInMainWorld(source_code, base_url, fetch_options,
                                access_control_status,
                                kDoNotExecuteScriptWhenScriptsDisabled);
    }

    一条很长的调用链:

    ScriptController::ExecuteScriptInMainWorld
    ScriptController::ExecuteScriptAndReturnValue
    V8ScriptRunner::RunCompiledScript
    Script::Run(Local<Context> context)
    MaybeHandle<Object> Execution::Call
    MaybeHandle<Object> CallInternal
    V8_WARN_UNUSED_RESULT MaybeHandle<Object> Invoke
    IntrinsicsGenerator::Call

    暂时就看到这里,我们先看看没有执行alert的例子1在目前这个阶段和当前的例子2有什么不同:

    <div id="id"></div>
    <script>
    var a="<script>alert(1)</s"+"cript>";
    document.getElementById("id").innerHTML=a;
    </script>

    还是先断在ProcessToken,看下当前这个html有几个token需要处理

    AtomicHTMLToken kEndOfFile
    AtomicHTMLToken kStartTag name "div"
    AtomicHTMLToken kEndTag name "div"
    AtomicHTMLToken kCharacter data "
    "
    AtomicHTMLToken kStartTag name "script"
    AtomicHTMLToken kCharacter data "
    var a="<script>alert(1)</s"+"cript>";
    document.getElementById("id").innerHTML=a;
    "
    AtomicHTMLToken kEndTag name "script"
    AtomicHTMLToken kStartTag name "script"
    AtomicHTMLToken kCharacter data "alert(1)"
    AtomicHTMLToken kEndTag name "script"
    AtomicHTMLToken kEndOfFile
    AtomicHTMLToken kEndOfFile

    这下看出了一些问题,被DOM操作添加的script标签,竟然也在token里面,但是他并没有执行,这就比较好办,直接单步看最后一个script EndTag是如何处理的,和上一个html的alert token对比就行,要是没有这个token的话,就需要看执行DOM操作之后的回调操作了,这里也就验证了最开始的一个观点:

    毫无疑问,innerHTML会执行回流操作,并且查到innerHTML中的东西会被加入DOM树

    那么来看一下这里为什么没有进入JS的执行环境吧

    把断点打在ProcessToken和PrepareScript上,执行到最后一个script EndTag

    好的,出现问题了,最后一个script闭合标签根本没有走到PrepareScript里,需要再往前打断点

    试试打在这里:HTMLParserScriptRunner::ProcessScriptElement

    也没进入….需要直接在ProcessToken后单步调

    在HTMLDocumentParser::PumpTokenizer() 这个函数里的IsPaused() 是false,所以没有进入JS环境,同时注意到这里的函数是PumpTokenizer而不是之前执行JS的时候进入的HTMLDocumentParser::ProcessTokenizedChunkFromBackgroundParser,而在PumpTokenizer里isPaused()函数就算判断成功了好像也不会进入JS环境?

    PumpTokenizer的判断

    if (IsPaused()) {
        DCHECK_EQ(tokenizer_->GetState(), HTMLTokenizer::kDataState);
    
        DCHECK(preloader_);
        // TODO(kouhei): m_preloader should be always available for synchronous
        // parsing case, adding paranoia if for speculative crash fix for
        // crbug.com/465478
        if (preloader_) {
          if (!preload_scanner_) {
            preload_scanner_ = CreatePreloadScanner(
                TokenPreloadScanner::ScannerType::kMainDocument);
            preload_scanner_->AppendToEnd(input_.Current());
          }
          ScanAndPreload(preload_scanner_.get());
        }
      }

    ProcessTokenizedChunkFromBackgroundParser的判断

    if (IsPaused()) {
          // The script or stylesheet should be the last token of this bunch.
          DCHECK_EQ(it + 1, tokens->end());
          if (IsWaitingForScripts())
            RunScriptsForPausedTreeBuilder();
          ValidateSpeculations(std::move(chunk));
          break;
        }

    是在处理token之前就进行的判断

    看下PumpTokenizer之前的函数调用栈,发现果然出现了 V8Element::innerHTMLAttributeSetterCallback ,

    在其中的 V8Element::innerHTMLAttributeSetterCustom 下个断点,对比两个例子的执行差异

    先来看不能执行JS的例子1

    整个调用链是:

    V8Element::innerHTMLAttributeSetterCallback
    V8Element::innerHTMLAttributeSetterCustom
    Element::setInnerHTML
    Element::SetInnerHTMLFromString
    ReplaceChildrenWithFragment

    然后发现了封装的地方,是ContainerNode::AppendChild :

    container_node->RemoveChildren();
      container_node->AppendChild(fragment, exception_state);

    首先remove需要操作的node的所有children,然后再把当前node添加进去,其中container_node的数据类型是blink::HTMLDivElement *一个指向div元素的指针

    Node* ContainerNode::AppendChild(Node* new_child,
                                     ExceptionState& exception_state) {
      DCHECK(new_child);
      // Make sure adding the new child is ok
      if (!EnsurePreInsertionValidity(*new_child, nullptr, nullptr,
                                      exception_state))
        return new_child;
    
      NodeVector targets;
      DOMTreeMutationDetector detector(*new_child, *this);
      if (!CollectChildrenAndRemoveFromOldParent(*new_child, targets,
                                                 exception_state))
        return new_child;
      if (!detector.HadAtMostOneDOMMutation()) {
        if (!RecheckNodeInsertionStructuralPrereq(targets, nullptr,
                                                  exception_state))
          return new_child;
      }
    
      NodeVector post_insertion_notification_targets;
      {
        ChildListMutationScope mutation(*this);
        InsertNodeVector(targets, nullptr, AdoptAndAppendChild(),
                         &post_insertion_notification_targets);
      }
      DidInsertNodeVector(targets, nullptr, post_insertion_notification_targets);
      return new_child;
    }

    其中ContainerNode包含了大多数的DOM操作,appendChild, getElementById, getElementByName...

    之后经过了许多判断后返回,这里因为我们首先调试的是不能执行JS的,不好判断是哪里触发,所以准备先通过调试能执行JS的判断触发点

    接着来看下能执行JS的回调链

    整个调用链是:

    V8Element::innerHTMLAttributeSetterCallback
    V8Element::innerHTMLAttributeSetterCustom
    Element::setInnerHTML
    Element::SetInnerHTMLFromString
    ReplaceChildrenWithFragment
    ContainerNode::AppendChild

    和前面的调用链一样,但是在ContainerNode::AppendChild中的DidInsertNodeVector(targets, nullptr, post_insertion_notification_targets); 执行后触发了JS,可以从这个函数开始调

    DidInsertNodeVector调用了container_node里的ChildrenChange结构体的ForInsertion函数进行对node的添加

    static ChildrenChange ForInsertion(Node& node,
                                           Node* unchanged_previous,
                                           Node* unchanged_next,
                                           ChildrenChangeSource by_parser) {
          ChildrenChange change = {
              node.IsElementNode() ? kElementInserted : kNonElementInserted, &node,
              unchanged_previous, unchanged_next, by_parser};
          return change;
        }

    这里node.IsElementNode() 是false,因为这里的node只有alert(1),而前面不能执行JS的例子这里就是true,因为他是一个完整的script node

    继续往下走,直到这个函数 html_script_element.cc

    void HTMLScriptElement::ChildrenChanged(const ChildrenChange& change) {
      HTMLElement::ChildrenChanged(change);
      if (change.IsChildInsertion())
        loader_->ChildrenChanged();
    }

    直觉这可能就是我们要找的地方,对script标签的ChildrenChange,这里,于是调用了loader里的ChildrenChanged() script_loader.cc

    void ScriptLoader::ChildrenChanged() {
      if (!parser_inserted_ && element_->IsConnected())
        PrepareScript();  // FIXME: Provide a real starting line number here.
    }

    看到了熟悉的PrepareScript,执行完这个函数后JS执行

    于是我们可以猜测,有一个类似的 HTMLDivElement::ChildrenChanged 或者是其余常规标签单独一个ChildrenChanged,并且不会进入JS环境,调试看看

    是后一种猜测:

    void HTMLElement::ChildrenChanged(const ChildrenChange& change) {
      Element::ChildrenChanged(change);
      AdjustDirectionalityIfNeededAfterChildrenChanged(change);
    }

    对div标签进行的innerHTML操作调用了HTMLElement::ChildrenChanged而非能进入JS环境的ScriptLoader::ChildrenChanged,至此,关于开头提出的问题已经解释清楚了,最开始的猜想基本是正确的

    思考延伸

    1.除了script,还有什么标签独立设置了ChildrenChanged,为什么?

    没有调用 HTMLElement::ChildrenChanged,有自己的处理的:

    styleElement:

    StyleElement::ProcessingResult StyleElement::ChildrenChanged(Element& element)

    Object:

    void HTMLObjectElement::ChildrenChanged(const ChildrenChange& change)

    Menu:

    void MenuItemView::ChildrenChanged()

    调用了 HTMLElement::ChildrenChanged 但是有其他操作的:

    Input_element:

    void HTMLInputElement::ChildrenChanged(const ChildrenChange& change)

    textarea:

    void HTMLTextAreaElement::ChildrenChanged(const ChildrenChange& change)

    title:

    void HTMLTitleElement::ChildrenChanged(const ChildrenChange& change)

    Svg:

    void SVGElement::ChildrenChanged(const ChildrenChange& change)

    2.innerHTML+=和innerHTML=会有什么不同

    innerHTML+=和innerHTML在DOM树上的操作基本一样,都需要对目标节点里的东西重新添加一遍,而如果目标节点是script标签的话:

    在PrepareScript函数中,第一步就是判断这个script是否已经start

    // Step 1. If the script element is marked as having "already started", then
      // return. The script is not executed. [spec text]
      if (already_started_)
        return false;

    在第九步的时候,设置already_started_为true

    // Step 9. Set the element's "already started" flag. [spec text]
      already_started_ = true;

    所以,如果对一个已经执行过的scirpt标签用innerHTML+=添加一个javascript语句,这个语句也是不会执行的,如果对一个空的script标签用innerHTML+=的话则会执行,因为在第五步的时候会判断script标签是否有内容,没有的话直接return,则没有进入第九步设置already_started_处

    // Step 5. If the element has no src attribute, and source text is the empty
      // string, then return. The script is not executed.
      //
      // TODO(hiroshige): Update the behavior according to the spec.
      if (!element_->HasSourceAttribute() && !element_->HasChildren())
        return false;

    3.什么时候会进入JS环境

    如果需要进入JS环境,则必须要有PrepareScript的过程,通过查找PrepareScript函数的调用则可以明确知道什么时候的JS有机会执行,什么时候没机会执行

    4.在文章的最开头缘由部分我还举了这样一个例子:

    <div id="id"></div>
    <script>
    var a = document.createElement('script');
    a.innerText = 'alert(/xx/)';
    document.getElementById("id").appendChild(a);
    </script>

    这样是可以成功执行的,那么这里是为什么可以执行,JS的触发点在哪里?

    根据之前的调试经验,先猜测这里一共会有这几个token:

    div start
    div end
    data 
    
    script start
    data "var a = document.createElement('script');..."
    script end
    script start
    data alert
    script end

    并且很可能执行alert是在appencChild的回调函数里

    来调试试试,首先断在ProcessToken处

    token里并没有appendchild加入的script节点,因为断点断在ProcessToken,而这个函数是用于处理标签文本的,用createElement创建的节点不需要再经过一次这样的处理

    AtomicHTMLToken kStartTag name "div"
    AtomicHTMLToken kEndTag name "div"
    AtomicHTMLToken kCharacter data "
    "
    AtomicHTMLToken kStartTag name "script"
    AtomicHTMLToken kCharacter data "
    var a = document.createElement('script');
    a.innerText = 'alert(/xx/)';
    document.getElementById("id").appendChild(a);
    "
    AtomicHTMLToken kEndTag name "script"
    AtomicHTMLToken kEndOfFile

    要注意的一个细节是,前面的例子里innerHTML的回调是在处理完所有的token之后才进行的, 那么这里的JS触发点到底在哪,有了前面的经验,可以直接在PrepareScript处下一个断点,查看调用栈,PrepareScript一共调用了3次,第三次时alert触发,这不意外,因为每一次刷新DOM树都会导致script节点进入PrepareScript函数,但是已经执行过的script节点则不会走到最后的执行阶段,于是断点下的再深一点,下在ScriptLoader::ExecuteScriptBlock函数里的script->run处,这次只调用了两次,第二次调用时alert执行

    查看执行alert的时候的调用栈,直接调用PrepareScript函数的地方是这个

    void ScriptLoader::DidNotifySubtreeInsertionsToDocument() {
      if (!parser_inserted_)
        PrepareScript();  // FIXME: Provide a real starting line number here.
    }

    往上看,在执行完 ContainerNode::AppendChild 之后,也就是插入子节点之后,调用了这个函数:

    void ContainerNode::DidInsertNodeVector(
        const NodeVector& targets,
        Node* next,
        const NodeVector& post_insertion_notification_targets) {
      Node* unchanged_previous =
          targets.size() > 0 ? targets[0]->previousSibling() : nullptr;
      for (const auto& target_node : targets) {
        ChildrenChanged(ChildrenChange::ForInsertion(
            *target_node, unchanged_previous, next, kChildrenChangeSourceAPI));
      }
      for (const auto& descendant : post_insertion_notification_targets) {
        if (descendant->isConnected())
          descendant->DidNotifySubtreeInsertionsToDocument();
      }
      for (const auto& target_node : targets) {
        if (target_node->parentNode() == this)
          DispatchChildInsertionEvents(*target_node);
      }
      DispatchSubtreeModifiedEvent();
    }

    其中 descendant->DidNotifySubtreeInsertionsToDocument(); 即是调用的ScriptLoader::DidNotifySubtreeInsertionsToDocument(),所以又验证了一个猜想,如果appendChild的child是一个script节点,那么就会进入JS环境

  • 相关阅读:
    21、Java并发性和多线程-Java中的锁
    20、Java并发性和多线程-Slipped Conditions
    19、Java并发性和多线程-嵌套管程锁死
    Java 架构师眼中的 HTTP 协议
    MonolithFirst
    【SpringMVC】从Fastjson迁移到Jackson,以及对技术选型的反思
    SpringMVC接口测试异常:Can not deserialize instance of int out of START_OBJECT token
    请不要盲目的进行横向扩展,优先考虑对单台服务器的性能优化,只有单台服务器的性能达到最优化之后,集群才会被最大的发挥作用
    Android Fragment中调用getActivity为null的问题
    Android 那些年,处理getActivity()为null的日子
  • 原文地址:https://www.cnblogs.com/bigben0123/p/13253770.html
Copyright © 2011-2022 走看看