这博客是越来越难写了, 参考资料少, 难度又高, 看到什么写什么吧!
众多周知, 在 JavaScript 中有几个基本类型, 包括字符串, 数字, 布尔, null,undefined,Symbol, 其中大部分都可以在我之前那篇博客 (https://www.cnblogs.com/QH-Jimmy/p/9212923.html) 中找到, 均继承于 Primitive 类. 但是仔细看会发现少了两个, null 和 undefined 呢? 这一节, 就来探索一下, V8 引擎是如何处理 null,undefined 两种类型的.
在没有看源码之前, 我以为是这样的:
- class Null : public Primitive {
- public:
- // Type testing.
- bool IsNull() const { return true; }
- // ...
- }
然而实际上没有这么简单粗暴, V8 对 null,undefined(实际上还包括了 true,false, 空字符串)都做了特殊的处理.
回到故事的起点, 是我在研究 LoadEnvironment 函数的时候发现的. 上一篇博客其实就是在讲这个方法, 包装完函数名, 函数体, 最后一步就是配合函数参数来执行函数了, 代码如下:
- // Bootstrap internal loaders
- Local<Value> bootstrapped_loaders;
- if (!ExecuteBootstrapper(env, loaders_bootstrapper,
- arraysize(loaders_bootstrapper_args),
- loaders_bootstrapper_args,
- &bootstrapped_loaders)) {
- return;
- }
这里的参数分别为:
1,env => 当前 V8 引擎的环境变量, 包含 Isolate,context 等.
2,loaders_bootstrapper => 函数体
3,arraysize(loaders_bootstrapper_args) => 参数长度, 就是 4
4,loaders_bootstrapper_args => 参数数组, 包括 process 对象及 3 个 C++ 内部方法
5,&bootstrapped_loaders => 一个局部变量指针
参数是啥并不重要, 进入方法, 源码如下:
- static bool ExecuteBootstrapper(Environment* env, Local<Function> bootstrapper,
- int argc, Local<Value> argv[],
- Local<Value>* out) {
- bool ret = bootstrapper->Call(
- env->context(), Null(env->isolate()), argc, argv).ToLocal(out);
- if (!ret) {
- env->async_hooks()->clear_async_id_stack();
- }
- return ret;
- }
看起来就像 JS 里面的 call 方法, 其中函数参数包括 context,null, 形参数量, 形参, 当时看到 Null 觉得比较好奇, 就仔细的看了一下实现.
这个方法其实很简单, 但是实现的方式非常有意思, 源码如下:
- Local<Primitive> Null(Isolate* isolate) {
- typedef internal::Object* S;
- typedef internal::Internals I;
- // 检测当前 V8 引擎实例是否存活
- I::CheckInitialized(isolate);
- // 核心方法
- S* slot = I::GetRoot(isolate, I::kNullValueRootIndex);
- // 类型强转 直接是 Primitive 类而不是继承
- return Local<Primitive>(reinterpret_cast<Primitive*>(slot));
- }
只有 GetRoot 是真正生成 null 值的地方, 注意第二个参数 I::kNullValueRootIndex , 这是一个静态整形值, 除去 null 还有其他几个, 所有的类似值定义如下:
- static const int kUndefinedValueRootIndex = 4;
- static const int kTheHoleValueRootIndex = 5;
- static const int kNullValueRootIndex = 6;
- static const int kTrueValueRootIndex = 7;
- static const int kFalseValueRootIndex = 8;
- static const int kEmptyStringRootIndex = 9;
上面的数字就是区分这几个类型的关键所在, 继续进入 GetRoot 方法:
- V8_INLINE static internal::Object** GetRoot(v8::Isolate* isolate,int index) {
- // 获取当前 isolate 地址并进行必要的空间指针偏移
- // static const int kIsolateRootsOffset = kExternalMemoryLimitOffset + kApiInt64Size + kApiInt64Size + kApiPointerSize + kApiPointerSize;
- uint8_t* addr = reinterpret_cast<uint8_t*>(isolate) + kIsolateRootsOffset;
- // 根据上面的数字以及当前操作系统指针大小进行偏移
- // const int kApiPointerSize = sizeof(void*); // NOLINT
- return reinterpret_cast<internal::Object**>(addr + index * kApiPointerSize);
- }
这个方法就对应了标题, 指针偏移.
实际上根本不存在一个正规的 null 类来生成一个对应的对象, 而只是把一个特定的地址当成一个 null 值.
敢于用这个方法, 是因为对于每一个 V8 引擎来说 isolate 对象是独一无二的, 所以在当前引擎下, 获取到的 isolate 地址也是唯一的.
如果还不明白, 我这个灵魂画手会让你明白, 超级简单:
最后返回一个地址, 这个地址就是 null, 强转成 Local<Primitive > 也只是为了垃圾回收与类型区分, 实际上并不关心这个指针指向什么, 因为 null 本身不存在任何方法可以调用, 大多数情况下也只是用来做变量重置.
就这样, 只用了很小的空间便生成了一个 null 值, 并且每一次获取都会返回同一个值.
验证的话就很简单了, 随意的在 node 启动代码里加一段:
auto test = Null(env->isolate());
然后看局部变量的调试框, 当前 isolate 的地址如下:
第一次指针偏移后, addr 的地址为:
通过简单计算, 这个差值是 72(16 进制的 48), 跟第一次偏移量大小一致, 这里根本不关心指针指向什么东西, 所以字符无效也没事.
第二次偏移后, 得到的 null 地址为:
通过计算得到差值为 48(16 进制的 30), 算一算, 刚好是 6*8.
最后对这个地址进行强转, 返回一个 Local<Primitive > 类型的 null 对象.
来源: https://www.cnblogs.com/QH-Jimmy/p/9317297.html