看完本篇,你还敢说你会前端吗?

B站影视 韩国电影 2025-09-16 08:39 2

摘要:前端在固有印象中一般都是js,html,css这些,以及一些React、Vue、Angular前端框架,再加上js是弱类型的语言,虽然后面ts改进了这一行为,但总给人一种前端较弱的感觉,没啥技术含量。但稍深入,会发现前端的复杂度,可能隐隐超越了后端。本篇看下。

前端在固有印象中一般都是js,html,css这些,以及一些React、Vue、Angular前端框架,再加上js是弱类型的语言,虽然后面ts改进了这一行为,但总给人一种前端较弱的感觉,没啥技术含量。但稍深入,会发现前端的复杂度,可能隐隐超越了后端。本篇看下。

script>var n = 10;console.log(n);script>

这里面只有两行代码,很简单,分别赋值和控制台打印值。

看到这种代码会有这种疑问,计算机不是只认识二进制码?js在浏览器是被怎么执行的呢?

实际上执行这种js,跟rustc执行Rust代码,JVM执行Java,以及JIT执行.NET毫无区别。在chromium里面,因为V8(js编译和执行的组件)的极致优化,复杂度上可能超越了前面几个。

因为第一次运行,v8不可能生成优化的代码。一般都是解释性执行,它的字节码如下:

0x38f00800078 @ 0 : 13 00 LdaConstant [0]0x38f0080007a @ 2 : d1 Star10x38f0080007b @ 3 : 1b fe f7 Mov 0x38f0080007e @ 6 : 6e 70 01 f8 02 CallRuntime [DeclareGlobals], r1-r20x38f00800083 @ 11 : 0d 0a LdaSmi [10]0x38f00800085 @ 13 : 25 01 00 StaGlobal [1], [0]0x38f00800088 @ 16 : 23 02 02 LdaGlobal [2], [2]0x38f0080008b @ 19 : d0 Star20x38f0080008c @ 20 : 33 f7 03 04 GetNamedProperty r2, [3], [4]0x38f00800090 @ 24 : d1 Star10x38f00800091 @ 25 : 23 01 06 LdaGlobal [1], [6]0x38f00800094 @ 28 : cf Star30x38f00800095 @ 29 : 67 f8 f7 f6 08 CallProperty1 r1, r2, r3, [8]0x38f0080009a @ 34 : d2 Star00x38f0080009b @ 35 : b7 Return

这跟Java的字节码和.NET的MSIL有异曲同工之妙。下面就是执行这段js字节码了,与.NET不同的是V8第一次执行js字节码是解释性的执行,而JIT是在多次运行这段代码之后才会触发。

解释性的执行只有字节码,而JIT优化后的代码才是汇编代码

0x... ed3 6a14 push 0x14 //var n=100x... ed5 48b9... console map...0x... eea 49b8... JSFunction log...0x... efc 48bb... Builtin_ConsoleLog...0x... f15 call CEntry_Return1_ArgvOnStack_BuiltinExit //console.log(n)1e d1fa sarl rdx,1

我们看n==0x14(十进制20),而不是push 0xA之类的。

这是因为V8 中的 Smi(Small Integer)表示

0xA二进制:1010把它左移1位等于20也即是十六进制的0x14用的时候把它右移1位,还原成0xA,上面代码最后一行sarl rdx,1所以代码中看不到0xA

那么字节码是怎么跟计算机的二进制打交道的呢?

当V8执行字节码的时候,有一个桩入口( stub_entry.Call)会调用需要被执行的函数,比如:

var n=10;console.log(n);

会被视为一个函数,送入到桩入口

stub_entry.Call-》return fn_ptr_(args...)->

桩入口会启动一些事先准备好的环境函数,比如:

Builtins_JSEntryBuiltins_JSEntryTrampolineBuiltins_InterpreterEntryTrampoline

其中的 Builtins_InterpreterEntryTrampoline函数里面会进入字节码的入口,然后就是一行字节码接着一行字节码的解释执行了。

比如上面展示的字节码,它的第一个字节码是

0x38f00800078 @ 0 : 13 00 LdaConstant [0]

V8规则

Builtin_字节码名称Handler

根据字节码的名称(上面名称是LdaConstant)

构造一个函数

Builtins_LdaConstantHandler

所以进入字节码入口调用的第一个函数即是:

Builtins_LdaConstantHandler。

我们忽略掉Star1,Mov这种字节码,下一个是CallRuntime,它的原型是:

Builtins_CallRuntimeHandler

字节码的函数Builtins_LdaConstantHandler执行完成之后通过指令jmp rcx跳转到了 Builtins_CallRuntimeHandler。下一条字节码是LdaSmi,其原型:

Builtins_LdaSmiHandler

Builtins_CallRuntimeHandler通过jmp rcx跳转到

Builtins_LdaSmiHandler执行。

按照此种模式,解释完成整个字节码的执行。

V8极致优化

在看chromium source code时候,遇到了很多的汇编代码,最典型的就是解释执行引擎的几个函数:

这些函数很有意思,它们直接被以十六进制的形式存储在某个文件中

比如: Builtins_JSEntry 函数如下

"Builtins_JSEntry:\n" ".def Builtins_JSEntry; .scl 2; .type 32; .endef;\n" " .octa 0x56415541544108ec8348026ae5894855,0x7f0f48f3000000a0ec81485356575741\n" " .octa 0x2024447f0f4cf310247c7f0f48f32434,0x4cf34024547f0f4cf330244c7f0f4cf3\n" " .octa 0x7f0f4cf36024647f0f4cf350245c7f0f,0xf4cf30000008024b47f0f4cf370246c\n"//便于观看,后面省略

Builtins_JSEntry没有函数名称,函数参数,以及函数代码,有的只是这些十六进数据。它就是Builtins_JSEntry函数的代码。这种做法,自然是提高了性能和效率。

更关键的是字节码的执行他同样是这种十六进制的形式,比如例子里面第一个被执行的字节码函数是Builtins_LdaConstantHandler,它的原型如下:

"Builtins_LdaConstantHandler:\n"".def Builtins_LdaConstantHandler; .scl 2; .type 32; .endef;\n"" .octa 0x90ba0d74d93b48fffffff91d8d48,0x48266ae5894855cc0000602095ff4100\n"" .octa 0x894cf0e4834828ec8348e2894930ec83,0x4cd0458948e07d894ce865894c202454\n"" .octa 0x4500002470858b4d00000216bad84d89,0x60858b49e1894cc6034d0000e94b808b\n"//便于观看,后面省略

相对于代码:

var n=10;

如果你在汇编代码中找类似于:

mov rax 10mov rcx 10

这种是找不到的,因为它只有

push 0x14

它变成了Smi格式存储,这种也是一种优化的方式。举个例子,如果要检查一个数值是不是Smi,只需要如下:

; 检查是否为 Smi,只需要一个位操作testb rax, 0x1 ; 测试最低位jz is_smi ; 如果是0,跳转到Smi处理

而不是:

if(数值==Smi){ do something}else{ do something}

V8的解释执行和V8的JIT执行的差距急剧缩小。

V8和RustC,JVM,CLR没有本质上的区别

都是对前端的代码(js/rust/java/C)进行解释或者编译性的执行。前端的后面不简单。

来源:opendotnet

相关推荐