這博客是越來越難寫了,參考資料少,難度又高,看到什么寫什么吧!
眾多周知,在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對象。
------------------------------------------------------------------------------------------------分割線-------------------------------------------------------------------------------------------
雖然解釋的差不多了,但是還是有必要做一個補充,就是關於一個類是否為null值的判斷。
正推過去很簡單,指定地址的值就是null,如果反推的話,那么想想也很簡單,判斷當前類的地址是否與指定地址相等。但是在源碼里,這個過程可以說是相當的惡心了……
這里簡單的過一遍,首先是測試代碼:
auto t = Null(env->isolate());
t->IsNull();
每一個生成的null雖然是個廢物,但是爸爸很厲害,父類Value有一個方法IsNull專門檢測當前類是否是null值。
這個方法非常簡單:
bool Value::IsNull() const { #ifdef V8_ENABLE_CHECKS return FullIsNull(); #else return QuickIsNull(); #endif }
根據情況有兩種檢測,一種快速的,一種完全體的。默認都是走完全檢測分支,里面會同時調用快速檢測。
方法源碼如下:
bool Value::FullIsNull() const { // 通過這個可以獲取到當前的isolate實例 i::Handle<i::Object> object = Utils::OpenHandle(this); bool result = false; // 判斷object是否為空值 if (!object->IsSmi()) { // 內部方法 result = object->IsNull(i::HeapObject::cast(*object)->GetIsolate()); } // 調用快速檢測與返回結果進行比對 DCHECK_EQ(result, QuickIsNull()); return result; }
那個內部方法,就是完全體的核心,看似簡單,實則跟廁所里的石頭一樣,又臭又硬。因為從這里開始,就要進入宏的地獄了。
因為調試模式對於宏的跳轉十分不友好,所以只能一個一個的把宏復制到本地,然后進行拼接,看看最后出來的是什么。這里僅僅給出一系列的截圖,看看什么是宏的地獄:
這兩步,還原了object->IsNull(Isolate* isolate)究竟是個什么東西,整理后如下:
bool Object::IsNull(Isolate* isolate) const { return this == isolate->heap()->null_value(); }
看起來很簡單,這里的null_value又是一個坑,如下:
這三個宏定義了isolate->heap()->null_value()是個什么東西,整理后如下:
Oddball* Heap::null_value() { return Oddball::cast(roots_[kNullValueRootIndex]); }
你以為這就完了???nonono,這個Oddball::cast(Object* object)又要搞事,如下:
轉換成人話如下:
Oddball* Oddball::cast(Object* object) { SLOW_DCHECK(object->IsOddball()); return reinterpret_cast<type*>(object); }
發現沒,SLOW_DCHECK,大寫+下划線分割,又是一個宏,真的是無窮無盡,不過這個宏只是檢測表達式是否為真。
這個Oddball是繼承於HeapObject,而HeapObject繼承於Object,這里只是簡單判斷當前類是否來源於Object,在上面生成null值的最后轉換有這么一行代碼:
return reinterpret_cast<internal::Object**>(addr + index * kApiPointerSize);
因此如果是null值,其內置類型必然為Object。
拋去一切上面無關的因素,最終判斷條件其實就是那一行代碼:Heap::null_value() { Oddball::cast(roots_[kNullValueRootIndex]); }
而這個roots_定義也是很魔性,來源於heap.h,簡單的一行:
Object* roots_[kRootListLength];
這個length長達511,定義非常非常多的特殊值,初始化方式也是宏,這里僅僅調出null的定義:
簡單講,roots_在V8引擎初始化時已經預存了所有特殊值的地址,這里直接取this的地址與root_中保存的null值地址進行比較,最后得出結果。
因為宏調試很不直觀,也很不方便,這里就不貼圖了。