Devtools Console 面板输入了 1+1 ,浏览器内部发生了什么?

news/2024/5/2 12:20:14/文章来源:https://blog.csdn.net/web2022050901/article/details/127018715

背景

新来的实习生妹子问了一个问题:「你看 Chrome 的 Devtools 调试工具,代码写一半还没运行下面就会提示输出结果,这个咋做到的?」

咋做的?对于 Devtools 的内部执行逻辑,咱不了解,但咱也不能直说,先上一套方法论(5W1H):


对于这个问题,我没了解过,但我会从以下几个方面进行思考:

1.这个输出提示功能是什么(What)?1.我会关注是否所有代码都会提示?比如异常代码、产生副作用的代码、执行死循环的代码等等,内部该如何处理2.我会关注这个功能是否会带来较大的性能损坏,稳定性如何,以及是否可以手动关闭。
2.这个功能是在哪执行的(Where)?1.我们的操作是在 Devtools 上,但是显然 js 代码的解析和执行需要运行在 v8 上,因此必定由一个代码传到 v8 的过程。然后 v8 产生结果再返回给 Devtools
3.什么时机会触发此功能(When)?1.可以和「搜索框输入文案获取列表数据」这个例子进行类比,一般来说我们会做防抖,避免无效请求频繁触发浪费资源。在此处也一样,如果每次输入都会进行提示那么则会损坏性能。2.但防抖也会带来一个延迟结果的缺点,这块得看 devtools 怎么进行衡量。3.以我对 Chrome 的了解和日常该功能的使用,我认为是实时触发的。
4.这个功能是提供给谁的(Who)?显然是开发者
5.为什么有这个功能(Why)?显然,为了方便调试。我们可以快速的查看当前输入对象的属性,以及查看纯函数的输出结果等等
6.这个功能是怎么做的(How)?我主要会关注三点:1.数据是怎么传输的:据我所知,Devtools 的操作是通过 CDP 协议 传递给底层服务,底层服务运行完毕再返回 Devtools ,然后 Devtools 去做结果的渲染。2.怎么避免死循环:如果死循环代码跑在同一个事件渲染,必然导致页面挂掉。而单线程又无法控制某段代码的执行时长,那么我猜测只能另起一个线程,然后做了作用域和上下文的共享了。3.怎么检测副作用:这个不了解,不清楚 V8 的编译器能不能直接分析出来。


「你说的都是套话,所以内部具体是咋执行的??」,妹子轻语。

看来妹子不吃这套,不服气的我赶紧 Google 了起来,找到了这篇文章,并分享给了妹子。

先上结论:

1.每次输入或者回车运行代码,都会发送一个名为 Runtime.evaluate 的 CDP 请求,可能会带上 timeout、throwOnSideEffect、expression 等参数。expression 表示控制台当前输入的代码段;timeout 则在预览结果阶段发送,其值为 500ms;throwOnSideEffect 则表示需要考虑副作用影响,也是在预览结果阶段发送。
2.V8 收到此 CDP 请求后,根据不同参数进行相应处理。
3.若传递了 timeout ,则表明需要在指定时间内返回执行结果。这个功能是通过另外启动一个工作线程并共享运行时环境和上下文实现的。
4.若传递了 throwOnSideEffect ,则表明需要检查代码中是否存在副作用。检查时机在解释器生成字节码之后以及运行时。采用黑白名单的机制可以快速分析出哪些字节码存在副作用,但是部分操作仍不能检测出来,于是需要在运行时检查。此时需要创建一个内存追踪器,追踪本次执行创建的变量内容,字节码运行时若使用的变量无法被此追踪器找到,说明变量是之前创建的,可能存在副作用。

PS:故事纯属虚构,如有雷同,纯属巧合

接下来,我们开始分析~

第一层:功能溯源

记得这个功能是某个版本的 Chrome 才加入的,那么我们先去找当时的功能说明

google 上键入 chrome devtools console evaluate preview

搜索的第一篇博客就是介绍此功能(Eager Evaluation,预执行)的

developer.chrome.com/blog/new-in…

Live demo: youtu.be/mfuE53x4b3k…

在 console 面板上键入代码时,底下会进行结果的预览提示,也可以选择关闭此功能

文章最后还提到了一个副作用的概念,如果输入的代码会产生副作用,就不会预执行。

何为副作用

可以看这个解释

“纯函数” 是将其输入值映射到输出值的函数,例如 plus(x, y) { return x + y; }。 “副作用”是返回值以外的任何影响。

比如下面代码具有引发警报对话框(并需要用户交互)的副作用

function plusWithSideEffects(x, y) {alert('This is a side effect'); return x + y;
} 

因此如果当输入的代码会产生副作用,应该避免预执行。

其他不会执行的情况

使用人工测试,我们还发现了以下这些情况也不会生成预览结果

  • 语法解析错误
  • 运行时错误
  • 执行超时

第二层:原理初探

我们知道,devtools 上的绝大部分操作,都是通过 cdp 协议来控制的。

因此我们可以打开 Protocol monitor 来看下 devtools 都发了什么数据。

Protocol monitor

为避免有些同学不知道 Protocol monitor ,本节做下简单介绍,了解的可以跳过。

Protocol monitor 是一个 cdp 协议监控器,会记录当前用户在这个 devtool 面板上产生的所有 CDP 的请求和响应。

如果没找到这个选项,需要先到 Devtools setting 里开启

之后 reload devtools 就可以看到了

Runtime.evaluate 协议

PS: 找一个无痕环境的空白页,方便观察结果(避免受到 Chrome 拓展的影响)

在 console 面板键入 1+1 ,会发现发了三次 Runtime.evaluate CDP

// 第一次
// request
{"expression": "1","includeCommandLineAPI": true,"generatePreview": true,"userGesture": false,"awaitPromise": false,"throwOnSideEffect": true,"timeout": 500,"disableBreaks": true,"replMode": true,"uniqueContextId": "1952859779443296532.-6400643113353669212"
}
// response
{"result": {"type": "number","value": 1,"description": "1"}
}// 第二次
// request
{"expression": "1+","includeCommandLineAPI": true,"generatePreview": true,"userGesture": false,"awaitPromise": false,"throwOnSideEffect": true,"timeout": 500,"disableBreaks": true,"replMode": true,"uniqueContextId": "1952859779443296532.-6400643113353669212"
}
// response
{"result": {"type": "object","subtype": "error","className": "SyntaxError","description": "SyntaxError: Unexpected end of input","objectId": "952682363357450140.1.1972"},"exceptionDetails": {"exceptionId": 50,"text": "Uncaught","lineNumber": 0,"columnNumber": 2,"scriptId": "136","exception": {"type": "object","subtype": "error","className": "SyntaxError","description": "SyntaxError: Unexpected end of input","objectId": "952682363357450140.1.1973"}}
}// 第三次
// request
{"expression": "1+1","includeCommandLineAPI": true,"generatePreview": true,"userGesture": false,"awaitPromise": false,"throwOnSideEffect": true,"timeout": 500,"disableBreaks": true,"replMode": true,"uniqueContextId": "1952859779443296532.-6400643113353669212"
}
// response
{"type": "number","value": 2,"description": "2"
} 

按回车时也会发送一次

{"expression": "1+1","objectGroup": "console","includeCommandLineAPI": true,"silent": false,"returnByValue": false,"generatePreview": true,"userGesture": true,"awaitPromise": false,"replMode": true,"allowUnsafeEvalBlockedByCSP": false,"uniqueContextId": "1952859779443296532.-6400643113353669212"
}
{"type": "number","value": 2,"description": "2"
} 

对比下差异,主要是 timeout 和 throwOnSideEffect 参数不同。

预执行的情况下 timeout 为 500 ,throwOnSideEffect 为 true

找到对应的协议定义

  • throwOnSideEffect: Whether to throw an exception if side effect cannot be ruled out during evaluation. This implies disableBreaks below.
  • timeout: Terminate execution after timing out (number of milliseconds).

基于参数进行黑盒测试

使用不同的参数调试方法,并猜测实现原理

调试 CDP

了解的同学可以跳过本小节

如果是 node 端,可以使用 puppeteerchrome-remote-interface 等工具。

在浏览器上,可以使用 devtools 页面对外暴露的对象执行 CDP 操作。

如何使用 devtools 页面的对象?我们可以打开该 devtools 页面(A)的 devtools 页面(B)。

  • 打开 devtools 页面可以使用快捷键 Ctrl+Shift+I(Windows)或 Cmd+Opt+I(Mac)
  • devtools 使用独立窗口模式
  • 在调试窗口 A 上,使用快捷键打开调试窗口 B

至此,在调试窗口 B 上就可以调试 A 的代码,使用 devtools 导出的一些模块来进行 cdp 交互

接下来可以在 console 面板运行如下代码:

let Main = await import('./devtools-frontend/front_end/entrypoints/main/main.js');await Main.MainImpl.sendOverProtocol('Runtime.evaluate', {expression: 'alert(111)'}); 

可以发现在原始页面出现了一个 alert 弹窗

devtools 的目录结构随时可能调整,但是 MainImpl 这个对象不太会调整。

因此如果用上面的方式没有找到 Main 这个对象,大概率是目录调整过了。

可以先用 search 工具搜一下 MainImpl 导出的文件位置,然后在 source 面板看整个 devtools-frontend 项目的目录结构

timeout

执行死循环代码并配置不同的 timeout

await Main.MainImpl.sendOverProtocol('Runtime.evaluate', {"expression": " while(true){}","timeout": 500,
}); 

结果:在 500ms 后返回异常 Uncaught {code: -32000, message: 'Execution was terminated'}


await Main.MainImpl.sendOverProtocol('Runtime.evaluate', {"expression": " while(true){}",
}); 

结果:页面卡住

初步断定,如果配置了 timeout 参数,会使用不同的线程执行代码

throwOnSideEffect

判定副作用是在编译阶段发现还是在运行阶段发现。

可以通过一个死循环卡住执行来测试

await Main.MainImpl.sendOverProtocol('Runtime.evaluate', {"expression": "while(true){}; var a = 1;","timeout": 1000,"throwOnSideEffect": true,
}); 

提示 EvalError: Possible side-effect in debug-evaluate


await Main.MainImpl.sendOverProtocol('Runtime.evaluate', {"expression": "while(true){}; window.a = 1;","timeout": 1000,"throwOnSideEffect": true,
}); 

抛出异常:Uncaught {code: -32000, message: 'Execution was terminated'}


await Main.MainImpl.sendOverProtocol('Runtime.evaluate', {"expression": "while(true){}; var a = 1;","timeout": 1000,"throwOnSideEffect": false,
}); 

抛出异常:Uncaught {code: -32000, message: 'Execution was terminated'}


初步分析,定义全局变量会导致副作用,这个可以在编译时发现;而对 window 对象赋值,不一定会导出副作用,需要实际执行才知道。

因此这个副作用检测可能有多种手段

初探小结

  • 预执行阶段会传递 timeout 参数,不会导致死循环,猜测是采用另开线程实现
  • 预执行阶段会检查副作用,这个检测可能是编译时检测,也可能是运行时检测

第三层:源码解析

本节将深入分析 V8 源码,对初探小结的猜测进行验证

V8 基础概念

  • Isolate: 一个独立的 v8 运行时环境(runtime),包括堆栈、堆管理器(heap)、垃圾回收器(gc)等。同一时刻,只有一个线程能使用 isolate ,多个线程可以通过线程切换来共享同一个 isolate 。
  • Context: 上下文对象。单独的 Isolate 不足以运行脚本,需要 Context 来提供全局变量。Context 在其所处的 Isolate 管理的 Heap 中建立一个对象,并以此为全局变量构建出一个完整的执行环境供 js 脚本使用。
  • Handle: 对一个特定 JS 对象的索引。它指向此 JS对象 在 V8 所管理的 Heap 中的位置。需要注意的是,Handle 不存于 Heap 中,而是存在于 stack 中。只有一个 Handle 被释放后,此 Handle 才会从 stack 中推出。这就带来一个问题,在执行特定操作时,我们可能需要声明很多 Handle。如果要一个个手动释放,未免太麻烦。为此,我们使用 Handle Scope 来集中释放这些 Handle 。
  • Handle Scope: 一个包含很多 handle 的集合,用于对 handle 进行统一管理。当这个工作区 Handle Scope 被移出堆栈时,其所包含的所有 Handle 都会被移出堆栈,并且被垃圾管理器标注,从而在后续的垃圾回收过程快速的定位到这些可能需要被销毁的 Handle 。
  • Context Scope: 用来管理 Context 的句柄容器
  • Session: 一次 cdp 会话
  • SharedFunction: 编译 Javascript 源码得到的字节码流,不能直接执行
  • JSFunction: 将 SharedFunction 绑定上执行入口和 Context ,此时该字节码流才能被解释器(Ignition)执行
  • Builtin(Built-in function):编译好的内置代码块(chunk),存储在 snapshot_blob.bin 文件中,V8 启动时以反序列化方式加载,运行时可以直接调用。其功能包括 Ignition 实现、字节码实现、以及 ECMA 规范实现等,可以从 BUILTIN_LIST 定义中查阅。从实现角度上分为七种类型,比如 BUILD_CPP、BUILD_ASM 等子类型。

V8 编译一段 JS 代码(字符串)的流程如下:

1.将源码进行解析(词法分析、语法分析)得到抽象语法树(AST)。AST 是 JavaScript 代码的句法结构的树形表示形式
2.使用 Ignition 解释器将 AST 解释为字节码(共100+种),并负责执行,输入是一个字节码序列,输出是执行结果。
3.Ignition 是带有累加器的寄存器,每个字节码指定其输入和输出做成寄存器操作数。
4.对于热点代码还会进行优化,使用 TurboFan 编译器将字节码编译为机器码
5.在某些情况下(比如调试源码),可以选择退优化,将机器码再度变成字节码,方便分析和调试。

更具体的可以参考这篇文章。

源码分析

根据搜索关键字 throwOnSideEffect ,我们找到了 V8RuntimeAgentImpl::evaluate 方法,此方法为调用的主要入口。

void V8RuntimeAgentImpl::evaluate(// 参数定义,差不多同 CDP 协议定义,重点关注 throwOnSideEffect 和 timeoutconst String16& expression, Maybe<String16> objectGroup,Maybe<bool> includeCommandLineAPI, Maybe<bool> silent,Maybe<int> executionContextId, Maybe<bool> returnByValue,Maybe<bool> generatePreview, Maybe<bool> userGesture,Maybe<bool> maybeAwaitPromise, Maybe<bool> throwOnSideEffect,Maybe<double> timeout, Maybe<bool> disableBreaks, Maybe<bool> maybeReplMode,Maybe<bool> allowUnsafeEvalBlockedByCSP, Maybe<String16> uniqueContextId,Maybe<bool> generateWebDriverValue,std::unique_ptr<EvaluateCallback> callback) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"), "EvaluateScript");
int contextId = 0;// ... 上下文检查// 基于当前会话 session 和 context 创建一个作用域
InjectedScript::ContextScope scope(m_session, contextId);
response = scope.initialize();...// 注入 devtools 命令行 api 到该作用域,比如 $,$0 ,copy 等
if (includeCommandLineAPI) scope.installCommandLineAPI();...v8::MaybeLocal<v8::Value> maybeResultValue;
{// 在配置执行超时时间时,会额外再创建一个执行作用域V8InspectorImpl::EvaluateScope evaluateScope(scope);if (timeout.isJust()) {// 设置指定时间内返回结果。稍后分析response = evaluateScope.setTimeout(timeout.fromJust() / 1000.0);if (!response.IsSuccess()) {callback->sendFailure(response);return;}}// 姑且简单理解为,启动一个微任务来执行 js 脚本v8::MicrotasksScope microtasksScope(m_inspector->isolate(),v8::MicrotasksScope::kRunMicrotasks);// 执行模式,比如是否检测副作用,是否有断点等等v8::debug::EvaluateGlobalMode mode =v8::debug::EvaluateGlobalMode::kDefault;if (throwOnSideEffect.fromMaybe(false)) {mode = v8::debug::EvaluateGlobalMode::kDisableBreaksAndThrowOnSideEffect;} else if (disableBreaks.fromMaybe(false)) {mode = v8::debug::EvaluateGlobalMode::kDisableBreaks;}// 将普通字符串转换为 v8 string 对象const v8::Local<v8::String> source =toV8String(m_inspector->isolate(), expression);// 往下继续运行,会走解析->编译->运行流程,并返回结果。稍后分析maybeResultValue = v8::debug::EvaluateGlobal(m_inspector->isolate(), source, mode, replMode);
}// Run microtasks before returning result.// 重置作用域
response = scope.initialize();// ... 将返回结果或异常值进行包装并返回} 

本段代码描述了整个执行流程,如何控制超时以及如何解析代码需要进一步分析

分析超时中断

进入 setTimeout,可以观察到实际是另外开启了一个工作线程。

protocol::Response V8InspectorImpl::EvaluateScope::setTimeout(double timeout) {if (m_isolate->IsExecutionTerminating()) {return protocol::Response::ServerError("Execution was terminated");}m_cancelToken.reset(new CancelToken());// 在对应平台(比如 d8)上的工作线程中创建一个可中止的执行任务v8::debug::GetCurrentPlatform()->CallDelayedOnWorkerThread(// 创建一个执行任务std::make_unique<TerminateTask>(m_isolate, m_cancelToken), timeout);return protocol::Response::Success();
} 

创建工作线程是和具体的运行时相关的,比如 Chrome 上是 d8,同时还传递了 isolate 对象,保证运行时环境一致。

超过指定时间之后,若还没有返回结果,那么就会返回「执行中断」的错误。

EvaluateGlobal 具体流程

上面说到,EvaluateGlobal 方法做了「编译到运行」的整个过程

内部实际调用 DebugEvaluate::Global 得到的运行结果

1.进入 DebugEvaluate::Global 进一步分析

MaybeHandle<Object> DebugEvaluate::Global(Isolate* isolate,Handle<String> source,debug::EvaluateGlobalMode mode,REPLMode repl_mode) {
// 生成 SharedFunction:直接编译 Javascript 源码得到的字节码流 shared_info
Handle<SharedFunctionInfo> shared_info;
if (!GetFunctionInfo(isolate, source, repl_mode).ToHandle(&shared_info)) {
return MaybeHandle<Object>();
}Handle<NativeContext> context = isolate->native_context();// 生成 JSFunction:将 shared_info 绑定上 isolate 和 context 得到 JSFunction
Handle<JSFunction> fun =
Factory::JSFunctionBuilder{isolate, shared_info, context}.Build();
// 往下调用
return Global(isolate, fun, mode, repl_mode);
} 

该步骤主要做了字节码生成的工作并绑定了执行环境。后面再单独分析编译流程,先往下

2.进入 Global(isolate, fun, mode, repl_mode)

MaybeHandle<Object> DebugEvaluate::Global(Isolate* isolate,Handle<JSFunction> function,debug::EvaluateGlobalMode mode,REPLMode repl_mode) {...// 如果是副作用异常模式,执行 StartSideEffectCheckMode
if (mode == debug::EvaluateGlobalMode::kDisableBreaksAndThrowOnSideEffect) {
isolate->debug()->StartSideEffectCheckMode();
}
// TODO(cbruni, 1244145): Use host-defined options from script context.
Handle<FixedArray> host_defined_options(
Script::cast(function->shared().script()).host_defined_options(),
isolate);// 执行脚本
MaybeHandle<Object> result = Execution::CallScript(
isolate, function, Handle<JSObject>(context->global_proxy(), isolate),
host_defined_options);// 关闭
if (mode == debug::EvaluateGlobalMode::kDisableBreaksAndThrowOnSideEffect) {
isolate->debug()->StopSideEffectCheckMode();
}
return result;
} 

该步骤针对副作用异常模式做了处理,在脚本执行前后做了特殊处理(后面分析,先往下)

3.进入执行脚本流程 Execution::CallScript

MaybeHandle<Object> Execution::CallScript(Isolate* isolate,Handle<JSFunction> script_function,Handle<Object> receiver,Handle<Object> host_defined_options) {DCHECK(script_function->shared().is_script());DCHECK(receiver->IsJSGlobalProxy() || receiver->IsJSGlobalObject());return Invoke(isolate, InvokeParams::SetUpForCall(isolate, script_function, receiver, 1,&host_defined_options));
} 

使用 SetUpForCall 设置调用参数,然后执行 Invoke 方法

Invoke 内部做了字节码运行前的准备,然后开始执行字节码,更具体的可以看这篇文章 - 连载《Chrome V8 原理讲解》第十篇 V8 Execution源码分析

接下来我们来分析 Ignition 解释器如何执行字节码

Ignition 解释器

步骤步骤步骤 Ignition 解释器执行字节码的步骤如下:

1.预处理:包括构建堆栈,参数入压等等,具体工作由 InterpreterEntryTrampoline 负责
2.进入第一条字节码,执行完毕之后调用 Dispatch(),这个函数负责获取下一条字节码。
3.执行字节码时,会使用该字节码对应的处理器(bytecode handler),该处理器是一种 Builtin(编译好的内置代码块)
4.不断重复这个过程,直到字节码序列遍历完毕

JSFunction 调用 的内置代码块为例 Generate_CallFunction

内部执行过程如下:

// 不断向下执行,直到 RUNTIME_FUNCTION(Runtime_DebugOnFunctionCall)void Builtins::Generate_CallFunction(MacroAssembler* masm, ConvertReceiverMode mode) {...__ InvokeFunctionCode(rdi, no_reg, rbx, rax, InvokeType::kJump);
}//=====void MacroAssembler::InvokeFunctionCode(Register function, Register new_target,Register expected_parameter_count,Register actual_parameter_count,InvokeType type) {// ...// Deferred debug hook.bind(&debug_hook);CallDebugOnFunctionCall(function, new_target, expected_parameter_count,actual_parameter_count);b(&continue_after_hook);// Continue here if InvokePrologue does handle the invocation due to// mismatched parameter counts.bind(&done);
}// ======void MacroAssembler::CallDebugOnFunctionCall(Register fun, Register new_target, Register expected_parameter_count, Register actual_parameter_count) {// ...CallRuntime(Runtime::kDebugOnFunctionCall);// ...
}// ==== RUNTIME_FUNCTION 是一些全局的宏模板,由 FOR_EACH_INTRINSIC_DEBUG 定义(runtime-debug.h)// 在 JSFunction 调用时,根据情况,准备介入或执行副作用检查
RUNTIME_FUNCTION(Runtime_DebugOnFunctionCall) {HandleScope scope(isolate);DCHECK_EQ(2, args.length());Handle<JSFunction> fun = args.at<JSFunction>(0);Handle<Object> receiver = args.at(1);// 函数调用时需要需要执行检查if (isolate->debug()->needs_check_on_function_call()) {// Ensure that the callee will perform debug check on function call too.Handle<SharedFunctionInfo> shared(fun->shared(), isolate);isolate->debug()->DeoptimizeFunction(shared);// ...// 检查副作用if (isolate->debug_execution_mode() == DebugInfo::kSideEffects &&!isolate->debug()->PerformSideEffectCheck(fun, receiver)) {return ReadOnlyRoots(isolate).exception();}}return ReadOnlyRoots(isolate).undefined_value();
} 

如果为 SideEffects 模式,则进行副作用检查。

bool Debug::PerformSideEffectCheck(Handle<JSFunction> function, Handle<Object> receiver) {// ...// 获取副作用状态,并根据不同状态进行不同处理DebugInfo::SideEffectState side_effect_state =debug_info->GetSideEffectState(isolate_);switch (side_effect_state) {case DebugInfo::kHasSideEffects:if (FLAG_trace_side_effect_free_debug_evaluate) {PrintF("[debug-evaluate] Function %s failed side effect check.\n", function->shared().DebugNameCStr().get());}side_effect_check_failed_ = true;// Throw an uncatchable termination exception.isolate_->TerminateExecution();return false;case DebugInfo::kRequiresRuntimeChecks: {if (!shared->HasBytecodeArray()) {return PerformSideEffectCheckForObject(receiver);}// If function has bytecode array then prepare function for debug// execution to perform runtime side effect checks.DCHECK(shared->is_compiled());PrepareFunctionForDebugExecution(shared);ApplySideEffectChecks(debug_info);return true;}case DebugInfo::kHasNoSideEffect:return true;case DebugInfo::kNotComputed:default:UNREACHABLE();}
} 

副作用检测

副作用被定义在 v8/src/objects/debug-objects.h ,一共有 4 种类型:

 enum SideEffectState {kNotComputed = 0,kHasSideEffects = 1,kRequiresRuntimeChecks = 2,kHasNoSideEffect = 3,}; 

那么如何判定副作用状态?

DebugInfo::SideEffectState DebugEvaluate::FunctionGetSideEffectState(Isolate* isolate, Handle<SharedFunctionInfo> info) {// ...// 如果是字节码列表if (info->HasBytecodeArray()) {// Check bytecodes against allowlist.Handle<BytecodeArray> bytecode_array(info->GetBytecodeArray(isolate), isolate);// 遍历字节码列表,根据三种情况判定bool requires_runtime_checks = false;for (interpreter::BytecodeArrayIterator it(bytecode_array); !it.done(); it.Advance()) {interpreter::Bytecode bytecode = it.current_bytecode();if (BytecodeHasNoSideEffect(bytecode)) continue;if (BytecodeRequiresRuntimeCheck(bytecode)) {requires_runtime_checks = true;continue;}if (FLAG_trace_side_effect_free_debug_evaluate) {PrintF("[debug-evaluate] bytecode %s may cause side effect.\n", interpreter::Bytecodes::ToString(bytecode));}// Did not match allowlist.return DebugInfo::kHasSideEffects;}return requires_runtime_checks ? DebugInfo::kRequiresRuntimeChecks : DebugInfo::kHasNoSideEffect;} // 如果是内置 api ,可以直接判定是否有副作用else if (info->IsApiFunction()) {if (info->GetCode().is_builtin()) {return info->GetCode().builtin_id() == Builtin::kHandleApiCall ? DebugInfo::kHasNoSideEffect : DebugInfo::kHasSideEffects;}} else {// Check built-ins against allowlist.Builtin builtin =info->HasBuiltinId() ? info->builtin_id() : Builtin::kNoBuiltinId;if (!Builtins::IsBuiltinId(builtin)) return DebugInfo::kHasSideEffects;DebugInfo::SideEffectState state = BuiltinGetSideEffectState(builtin);return state;}return DebugInfo::kHasSideEffects;
} 

实现原理也比较简单,就是对 V8 涉及的基本操作(Bytecode / Builtin / Runtime function)和 Chrome Web APIs 进行标记,标记是否存在副作用,然后在 FunctionGetSideEffectState 中对操作进行遍历即可获知整体是否存在副作用。

无副作用的操作列表可以在 debug/debug-evaluate.cc 文件的 allowlist 中找到。

字节码定义见:bytecodes.h

副作用处理

根据上方 PerformSideEffectCheck 的执行,若检测出来了副作用则抛出异常;若检测出来为 kRequiresRuntimeChecks ,则代表该「操作」是否拥有副作用需要在代码运行时进行判断,V8 会将原本的字节码进行替换为 DebugBreak

ApplySideEffectChecks(debug_info);// ===// static
void DebugEvaluate::ApplySideEffectChecks(Handle<BytecodeArray> bytecode_array) {for (interpreter::BytecodeArrayIterator it(bytecode_array); !it.done(); it.Advance()) {interpreter::Bytecode bytecode = it.current_bytecode();if (BytecodeRequiresRuntimeCheck(bytecode)) it.ApplyDebugBreak();}
} 

之后解释器在执行到 DebugBreak 的字节码时,会通过 Debug::PerformSideEffectCheckForObject 检测参数是否存在于 temporary_objects_ 对象

// 标记临时对象,用于副作用检查。
class TemporaryObjectsTracker;
std::unique_ptr<TemporaryObjectsTracker> temporary_objects_; 

temporary_objects_ 是一个 TemporaryObjectsTracker 实例,在预加载时创建,用于记录之后的变量分配和 gc 移动。

因此若引用的变量不在 temporary_objects_ ,说明该变量早于预加载前创建,若执行「修改操作的字节码」会有副作用。

简单来说,就是创建了一个内存追踪器,若使用的变量不被此追踪器找到,则说明是之前创建的变量,对其赋值操作等会导致副作用

StartSideEffectCheckMode 做了什么?

前面提到,在执行脚本之前会执行 StartSideEffectCheckMode 操作。

根据上面的一些分析,我们也可以很容易猜到 StartSideEffectCheckMode 做了啥。

主要做的就是初始化 temporary_objects ,注册了一个内存追踪器。

并在 StopSideEffectCheckMode 时进行重置

整体流程

总结

本文对 devtools 执行 js 脚本的整个流程进行了分析。

从使用到黑盒测试,再到源码分析,层层深入,希望能对你有所帮助。

对于一开始的问题,结论也已在文章开头给出,可以做下回顾。

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_12911.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WhatsApp和WhatsApp Business之间的区别

关键词&#xff1a;WhatsApp、WhatsApp Business WhatsApp 无疑是一款超级方便、免费且毫不费力的即时通讯应用程序。这就是庞大的用户群的原因。对于企业来说&#xff0c;它也是一个超级方便、免费且不费吹灰之力的即时通讯应用程序。特别是自从 WhatsApp 推出了 WhatsApp Bus…

《MySQL DBA封神打怪之路》专栏学习大纲

《MySQL DBA封神打怪之路》专栏学习大纲 文章目录《MySQL DBA封神打怪之路》专栏学习大纲1、作者介绍2、专栏介绍3、专栏部分文章截图3.1.所有文章一栏3.2.文章内容截图4、专栏大纲学习指南4.1对数据库的初步认识4.2.四种类型的SQL语句基本使用4.3.超丰富的多表联查案例4.4.事物…

消除笔去水印怎么做?教你怎么用这些消除笔软件

在网上看到好看的照片想要用作壁纸&#xff0c;但是上面有水印看起来不好看怎么办&#xff1f;我们可以用有消除笔的软件将图片水印去除&#xff0c;那么消除笔去水印怎么做呢&#xff1f;今天的这个教程分享给你们。方法一&#xff1a;借助“Styler”进行去水印操作 这是一款拥…

Echarts绘制geo地图属性设置大全(一)

1、Echarts版本 "echarts": "^5.3.3", 2、基础地图绘制&#xff08;以中国地图为例&#xff09; 绘制地图需要有用于绘制地理坐标系的数据&#xff0c;如示例使用的china.js <template><div ref"mapBar" class"map-class"&…

面试之HashMap

1.初始大小&#xff1a;HashMap默认初始大小是16&#xff0c;这个默认值是可以设置的&#xff0c;如果事先知道大概的数据量有多大&#xff0c;可以通过修改默认初始大小&#xff0c;减少动态扩容的次数&#xff0c;这样会大大提高HashMap的性能 2.动态扩容&#xff1a;最大 装…

兼容性测试包含哪几类呢?

兼容性测试包含哪几类呢? (1)浏览器方面 关于浏览器的兼容性测试&#xff0c;主要是检查页面的交互、元素和样式展示是否正常。我们都知道&#xff0c;目前市面上主流的浏览器非常多&#xff0c;像&#xff1a;360、搜狗、火狐等等。 在进行测试的时候&#xff0c;由于兼容性问…

2022出海东南亚:越南电商市场现状及网红营销特点

近几年&#xff0c;东南亚整体发展态势非常好&#xff0c;加上国内市场饱和&#xff0c;不少国内企业在计划出海或已经出海东南亚。作为东南亚第二大电商市场&#xff0c;越南自然也成了香饽饽&#xff0c;越南蓬勃发展的数字经济和电商领域&#xff0c;让其成为了卖家挖掘新商…

Vue 力导图d3js 实现

PowerBI 自定义组件推荐用D3JS 实现&#xff0c;实现一个PowerBI 的力导图组件&#xff0c;为调试方便&#xff0c;先用Vue 实现一个&#xff0c;然后再移植到PowerBI 中&#xff0c;话不多说&#xff0c;上效果&#xff1a; 体验入口 上代码是最好的老师&#xff1a; <t…

【easyExcel】后端将模板文件写入流供前端下载报错,easyexcel下载模板文件出错

目录事件起因环境和工具操作过程解决办法参考内容&#xff1a;结束语事件起因 报错内容&#xff08;我主要搜索的两个错误内容点&#xff09;&#xff1a; com.alibaba.excel.exception.ExcelGenerateException: Create workbook failure 和 Caused by: org.apache.poi.openx…

kingbaseES(人大金仓)数据库语法和常用函数 以及 踩坑记录

前言 最近公司弄了个新项目&#xff0c;数据库指定使用kingbase数据库 刚开始一看这名字都不知道这是啥数据库&#xff0c;后来百度一搜&#xff0c;看到中文名字就知道了。 虽然没用过&#xff0c;但那几个国产数据库也听说过 这不&#xff0c;刚开始用&#xff0c;语法都不熟…

概率统计·概率论的基本概念【条件概率】

条件概率 定义 条件概率与积事件的区别 条件概率——在A事件发生的前提下&#xff0c;发生B 积事件——AB同时发生 性质 **注意&#xff08;3&#xff09;要在B1&#xff0c;B2……是互斥事件 &#xff08;6&#xff09;用得多 纯乐&#xff0c;自己都说了&#xff08;6&…

数智赋能,美创助力绍兴市上虞区建设医共体管理平台

什么是医共体&#xff1f; 开展县域医共体建设是深化医改的重要步骤和制度创新&#xff0c;旨在通过医共体模式&#xff0c;推动医疗卫生服务供给侧结构性改革&#xff0c;调整优化医疗资源结构布局&#xff0c;促进医疗卫生工作重心下移和资源下沉&#xff0c;提升基层服务能力…

瑞吉外卖(14) - 上传文件功能开发

文章目录文件上传介绍文件上传代码实现查看效果新建CommonController类断点测试转存文件取消拦截器过滤LoginCheckFilter.java运行测试完善功能编写application.xml编写CommonController.java运行测试完善二级目录编写application.yml编写CommonControler.java测试成功文件上传…

使用streamstring实现格式化输入

目的&#xff1a;在windows系统写c项目&#xff0c;常用sscanf_s进行格式化输入。但是在ubuntu系统上发现sscanf_s不可用&#xff08;只有sscanf可用&#xff0c;但是它是c 99 的函数了&#xff09;。后面查阅知道&#xff0c;这个sscanf_s函数是微软专有的函数&#xff0c;不具…

linux 竞态与并发编程

前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 例如&#xff1a;随着人工智能的不断发展&#xff0c;机器学习这门技术也越来越重要&#xff0c;很多人都开启了学习机器学习&#xff0c;本文就介绍了机器学习的基础内容。 提示&#xff1a;以下是本篇文…

Mapper代理开发

Mapper代理开发一. Mapper代理开发概述二. 使用Mapper代理要求(重点中的重点)三. 实操的代码main目录下的java目录中1. UserMapper接口2. User类3. MybatisDemo类main目录下的resources目录中1. UserMapper.xml2. logback.xml3. mybatis.xml一. Mapper代理开发概述 之前我们写的…

【SpringBoot2】02-SpringBoot中如何修改依赖的版本

Spring Boot 中修改依赖版本 如果默认版本号不合适&#xff0c;我们可以修改版本号。 首先&#xff0c;打开当前项目的 pom.xml 文件&#xff0c;查看 spring-boot-dependencies-2.7.4.pom 中规定当前依赖的版本。 例如&#xff0c;我想修改 MySQL 驱动版本&#xff0c;我先看…

OPSS-PEG-Acrylate,OPSS-PEG-AC,巯基吡啶-聚乙二醇-丙烯酸酯试剂供应

英文&#xff1a;OPSS-PEG-Acrylate&#xff0c;OPSS-PEG-AC 中文&#xff1a;巯基吡啶-聚乙二醇-丙烯酸酯 2、 CAS number&#xff1b; N/A 3、The category&#xff1b;Acrylate/Acrylamide PEG Orthopyridyl disulfide (OPSS) PEG 4、Molecular weight&#xff1a;巯基吡…

队列(C语言)

文章目录前言概念基本操作循环队列少用一个元素空间栈队列前言 本篇进行队列的学习。使用C语言实现 概念 排队是体现了“先来先服务”的原则。 在多道程序运行的计算机系统中&#xff0c;可以同时有多个作业运行&#xff0c;他们的运算结果都需要通过通道输出&#xff0c;若通…

[架构之路-3]:软件架构师也是魔法师,架构师应具备的四大方面的技能

目录 前言&#xff1a; 一、业务能力&#xff08;业务领域&#xff09;-- 面向业务 1.1 业务场景 1.2 业务技能 二、沟通能力&#xff08;管理领域&#xff09; -- 面向“人” 三、技术能力&#xff08;计算机领域&#xff09; -- 面向计算机 3.1 硬件技能 3.2 软件技能…