1. 程式人生 > >Node.js And C++__6.V8和Node介面抽象

Node.js And C++__6.V8和Node介面抽象

在這本書的前五章中,我們一直在假設我們正在建立的addon使用的Node.js版衛0.12到6.0。這是一個相當寬泛的版本,但是有較早版本的節點。我們的addons不會編譯/工作的js。在使用雲託管服務時,這可能是一個很重要的問題,因為它可能會使用早期版本的node.js。當你在npm上釋出你的外掛時,它也會帶來問題——因為你的addon不能被分配給所有使用者。

​ 為什麼我們的addons不能在早期版本中工作?我們為什麼要擔心Node.js的未來版本能破壞我們的外掛嗎?答案是,它不是Node.js而是V8,V8定義了我們與JavaScript互動的API!V8的API隨著時間的推移而發生了變化,並不能保證它不會再次發生。而V8開發者試圖保持API的穩定,經常會有一些(可能小)打破了每一個新的變化,新的發行包。當新的V8版本被植入到Node.js中,我們執行addons有被修改的風險。​

​ 考慮到Node.js的發行速度(許多應用程式仍然執行在版本為 v0.10、v0.12、v4.0等的Node.js上),確實需要某種形式的抽象,這樣我們就可以將目標鎖定在更穩定的(並且乾淨的)API上。對於Node(NAN)的原生抽象就是這樣。​

​ NAN由io.js工作組管理,目標是提供一組巨集和實用工具,實現兩個目標:

1.在所有Node.js 版本上維護一個穩定的API;

2.提供一組實用工具,以幫助簡化該Node.js和V8 API最困難的部分(例如非同步的addon開發)。

​ 如果您打算是Addon都能得到Node.js版本為v0.10-4.0的支援,那麼只需要使用NAN來開發。最好熟悉它,並在任何你希望在很長一段時間內擁有大量使用者的專案中使用它。​

​ 應該注意的是,NAN並不是一個高階API。雖然很少有實用程式可以簡化非同步的addon開發,但是大多數的NAN只是為標準的V8操作提供了替換的巨集呼叫,比如在JavaScript堆中建立新的原始/物件、定義匯出的函式和Node.js物件包裝。到目前為止,我們已經瞭解到的V8程式設計的基本規則仍然適用,NAN只是提供了一種稍微不同的API來做我們以前做過的事情。

​ 在這一章中,我將快速地展示相應的“NAN方法”來完成我們在第1-5章所完成的事情。在後面的章節中,我們將使用NAN,因為它對複雜的addon開發非常有幫助,並且在向npm公開發布addons時非常有意義。

非抽象匯出函式

​ 為了演示簡單的功能,讓我們來回顧一下第2章的addon示例,特別是在我們從JavaScript和C++中傳遞原語和物件/陣列的地方。下面是我們在addon中建立的函式:

// adds 42 to the number passed and returns result
NODE_SET_METHOD(exports, "pass_number", PassNumber);
// adds 42 to the integer passed and returns result
NODE_SET_METHOD(exports, "pass_integer", PassInteger);
// reverses the string passed and returns the result
NODE_SET_METHOD(exports, "pass_string", PassString);
// nots the boolean passed and returns the result
NODE_SET_METHOD(exports, "pass_boolean", PassBoolean);
// extracts the x/y numeric properties in object
// passed to it and return a new object with sum
// and product
NODE_SET_METHOD(exports, "pass_object", PassObject);
// increments each numeric value in the input array, then returns
// an array a with a[0] equal to input[0]+1, a[1] equal to the
// "none_index" property defined on the input array, and a[2] equal
// to input[2]+1.
NODE_SET_METHOD(exports, "increment_array", IncrementArray);

​ 這些示例函式是基本的,但是它們允許我們看到V8變數的所有典型用例。現在,讓我們建立一個新的addon,具有相同的功能,只使用NAN。我們將逐一檢查每一個,這樣我們就可以清楚地瞭解原始V8 API和NAN之間的區別。

依賴設定

​ 在開始之前,我們需要配置我們的addon以使用NAN。當使用NAN建立addon時,NAN將成為您的模組的依賴項。因此,在包中。json檔案必須將NAN宣告為一個依賴項:

$ npm install –save nan

​ 不過,與大多數使用npm安裝的模組不同,安裝並沒有安裝JavaScript包,它只是簡單地下載了NAN的C++(標頭檔案)的分佈。您不需要從JavaScript中引用NAN,但是您需要從您的C++程式碼中引用它,特別是通過包含nan.h。

​ 為了將依賴項新增到您的addon中,我們在繫結中添加了節點node-gyp檔案:

"include_dirs" : [
    "<!(node -e \"require('nan')\")"
]

​ 當我們編譯C++addon時,這個指令將導致在構建路徑上的nan模組的標頭檔案。相當於添加了一個包含標頭檔案的配置。

這裡寫圖片描述

匯出函式

​ ​讓我們從實現示例中pass_number最簡單的函式開始。我們將建立一個包含addon的資料夾,建立一個package.json,通過執行npm安裝NAN:npm install nan –save,並配置下面的 binding.gyp 檔案:

{
  "targets": [
    {
      "target_name": "basic_nan",
      "sources": [ "basic_nan.cpp" ], 
      "include_dirs" : [
        "<!(node -e \"require('nan')\")"
      ]
    }
  ]
}

​ 現在我們將建立basic_nan.cpp並設定我們的addon。

#include <nan.h>

using namespace Nan;
using namespace v8;

NAN_METHOD(PassNumber) {
   // do nothing ... for now.  
}

NAN_MODULE_INIT(Init) {
   Nan::Set(target, New<String>("pass_number").ToLocalChecked(),
        GetFunction(New<FunctionTemplate>(PassNumber)).ToLocalChecked());
}

NODE_MODULE(basic_nan, Init)

​ 對比標準V8 API:

#include <node.h>

using namespace v8;

void PassNumber(const FunctionCallbackInfo<Value>& args) {
    // do nothing ... for now.
}

void Init(Local<Object> exports) {
    NODE_SET_METHOD(exports, "pass_number", PassNumber);
}

NODE_MODULE(basic_nan, Init)​

​ 讓我們從最底部開始——注意NODE_MODULE 命令是完全相同的。NODE_MODULE 已經是一個巨集了—在不同版本的 Node.js 之間保持相容性。與NAN的模式一樣,作者沒有發明新的方法來做事情,除非有理由相信v8/node的API改變會產生問題。​

​ 用我們的方法,我們有Init方法。在純V8方法中,我們為node模組中使用的初始化函式有一個特定的呼叫簽名。它必須具有返回型別的void,並接受匯出(以及可選的模組)。在NAN中,提供了一個名為NAN_MODULE_INIT的巨集。巨集是引數化的,因此我們仍然可以命名初始化例程(Init),但是巨集使我們避免了與呼叫簽名的任何不一致。​

​ 在Init內部,我們會看到一些主要的變化,奇怪的是,NAN實際上會變得稍微複雜一些!我們不再使用 NODE_SET_METHOD 巨集,而是在NAN中使用Set函式。注意,在Init的NAN版本中有一個“神奇”變數,稱為target。這是由NAN_MODULE_INIT巨集提供的,實際上是我們已經習慣使用的匯出物件。為了在任何使用NAN的物件上設定一個值,我們必須使用實際的JavaScript字串和函式—因此額外的代將“PassNumber”轉換為V8字串和 PassNumber作為JavaScript函式。我們將在稍後介紹這些轉換的詳細資訊,因為它們使用了NAN非常依賴的標準的New 和ToLocalChecked方法。​

​ 再往上看,我們看一下PassNumber函式。在原始的V8程式碼中,我們必須有一個特定的呼叫簽名——在這裡返回void,我們接受一個包含呼叫資訊的FunctionCallbackInfo物件。這個簽名尤其在V8版本中發生了巨大的變化,因此,NAN為我們提供了一個巨集來解決這個問題也就不足為奇了。NAN_METHOD為Node.js不同的版本建立一個適當的函式簽名。稍後我們將看到,它還建立了一個info引數,該引數允許我們以V8版本的未知方式獲取函式引數和holder(this)。

​ 在這兩程式碼的頂部,您會注意到我們現在使用了一個額外的名稱空間Nan。v8的名稱空間仍然被使用,還有node.h已經包含在nan.h中。

數值

​ 讓我們從PassNumber。在最簡單的形式中,沒有錯誤檢查,原始的V8實現看起來如下:

void PassNumber(const FunctionCallbackInfo<Value>& args) {
    Isolate * isolate = args.GetIsolate();
    double value = args[0]->NumberValue();
    Local<Number> retval = Number::New(isolate, value + 42);
    args.GetReturnValue().Set(retval);
}

從典型用法轉換到使用NAN是不容易的。主要變化:

1.不需要獲得Isolate;

2.我們將使用內建的資訊引數,由NAN_METHOD巨集提供,而不是直接使用FunctionCallbackInfo;

3.我們將使用Nan::New來建立一個新數字,而不是V8::number,這就是我們不需要Isolate的原因。

NAN_METHOD(PassNumber) {
    double value = info[0]->NumberValue();
    Local<Number> retval = Nan::New(value + 42);
    info.GetReturnValue().Set(retval);
}

​ 執行錯誤檢查是很簡單的,因為info物件的功能與FunctionCallbackInfo args功能相同,在最初的V8示例中有很多相同的功能:

if ( info.Length() < 1 ) {
    info.GetReturnValue().Set(Nan::New(0));
    return;
}

if ( !info[0]->IsNumber()) {
    info.GetReturnValue().Set(Nan::New(-1));
    return;
}​

​ 這些函式的整型和布林型變化遵循相同的模式 Nan::New方法過載來建立適當的值,而info具有IntegerValue和BooleanValue。

NAN_METHOD(PassInteger) {
    if ( info.Length() < 1 ) {
        return;
    }

    if ( !info[0]->IsInt32()) {
        return;
    }

    int value = info[0]->IntegerValue();
    Local<Integer> retval = Nan::New(value + 42);
    info.GetReturnValue().Set(retval);
}

NAN_METHOD(PassBoolean) {
    if ( info.Length() < 1 ) {
      return;
    }

    if ( !info[0]->IsBoolean()) {
      return;
    }

    bool value = info[0]->BooleanValue();
    Local<Boolean> retval = Nan::New(!value);
    info.GetReturnValue().Set(retval);
}

弱型別 and ToLocalChecked

​ 如果您閱讀了NAN文件,您很快就會注意到,它非常強調Maybe type(弱型別)的概念,這是V8的一個相對較新的特性。同樣地,當我們的例子以簡潔的格式處理引數和新原語時,大部分的NAN例子都是在建立JavaScript原語和物件(包括函式)時使用ToLocalChecked。實際上,我們在Init方法中看到了這種風格。讓我們來看看這些概念是什麼意思,以及為什麼要使用它們。

​ 讓我們想象一下,我們的PassNumber方法是匯出的,然後用JavaScript程式碼呼叫它:

console.log( addon.pass_number("xyz") );

​ 在我們的原始程式碼中,info[0]是一個Local,並且在傳遞給add的引數時獲取 “correct” 的值是很簡單的,如果引數不是有效的數字,那麼info[0]->NumberValue() 將返回NaN。還有一種使用Nan::to和ToLocalChecked的附加方式來執行轉換到數字。

NAN_METHOD(PassNumber) {
    Nan::MaybeLocal<Number> value = Nan::To<Number>(info[0]);
    // will crash if value is empty
    Local<Number> checked = value.ToLocalChecked();
    Local<Number> retval = Nan::New(checked->Value() + 42);
    info.GetReturnValue().Set(retval);
}​

​ 在這裡,我們看到了Maybe types的(簡單的)使用,它可能或可能不包含值。Nan::To返回可Maybe types,並轉換為真正的Local型別引數,通過呼叫ToLocalChecked來實現。注意,如果值實際上是空的,ToLocalChecked()執行將會是程式崩潰。有趣的是,值總是有一個實際的值,因為在JavaScript中,對數字的轉換定義很好——任何不是數字的都是NaN。在這種情況下,不管我們傳遞給PassNumber的是什麼,ToLocalChecked 都會成功。

​ 我們也可以直接移動到double,而不是建立一個Local :

NAN_METHOD(PassNumber) {
    Nan::Maybe<double> value = Nan::To<double>(info[0]);
    Local<Number> retval = Nan::New(value.FromJust() + 42);
    info.GetReturnValue().Set(retval);
}​

​ 我們會得到和之前一樣的結果——這裡我們使用了Maybe type’s的FromJust 方法來得到實際的double 值。同樣,因為數字總是有來自任何其他JavaScript型別的定義值——這些例子可能有點多餘——它們永遠不會崩潰!然而,您將看到這種風格在許多地方都使用了。

Strings​

​ 從引數中提取字串與數字和布林值類似,但是將它們轉換為標準的C++字串需要使用Nan::Utf8String,就像我們需要使用v8時API v8::String::Utf8Value。

NAN_METHOD(PassString) {
    Nan::MaybeLocal<String> tmp = Nan::To<String>(info[0]);
    Local<String> local_string = tmp.ToLocalChecked();
    Nan::Utf8String val(local_string);
    std::string str (*val);
    std::reverse(str.begin(), str.end());
    info.GetReturnValue().Set(Nan::New<String>(str.c_str()).ToLocalChecked());
}​

​ 當建立新的字串物件時,我們現在不得不再次處理 Maybe types ,這與建立新數字時不同。這是因為當Nan::New對於大多數JavaScript型別來說返回 Local 型別,它會在建立字串、日期、正則表示式和指令碼物件時返回 MaybeLocal 。這種不一致是由底層V8 API驅動的。正如我們在上面所看到的,轉換到 Local 只需要我們使用ToLocalChecked 的返回值 Nan::New 。由於我們特別地傳遞了一個已知的字串,所以我們不必擔心ToLocalChecked實際上會失敗。

​ 還有一種更方便的方法,可以從info物件中獲取Utf8String,使用在 Local定義的ToString方法。

NAN_METHOD(PassString) {
    v8::String::Utf8Value val(info[0]->ToString());
    std::string str (*val);
    std::reverse(str.begin(), str.end());
    info.GetReturnValue().Set(
    Nan::New<String>(str.c_str()).ToLocalChecked());
}

Objects

​ So far you might have noticed that the real changes when moving to NAN are centered around the use (or lack) of Isolate and how new variables are allocated.NAN does not replace the direct usage of much of the V8 API - specifically Local . As we move to working with objects, this becomes even more apparent. Let’s look at the first, pure V8 implementation of our PassObject addon function from Chapter 2.

​ 到目前為止,您可能已經注意到,遷移到NAN的真正變化是圍繞著Isolate的使用(或缺乏)以及如何分配新的變數。NAN並沒有替代很多V8 API的直接使用——特別是Local 。當我們開始使用物件時,這就變得更加明顯了。讓我們來看看第一個,從第2章開始,我們的PassObject addon函式的純V8實現。

Local<Value> make_return(Isolate * isolate, const Local<Object> input ) {
    Local<String> x_prop = String::NewFromUtf8(isolate, "x");
    Local<String> y_prop = String::NewFromUtf8(isolate, "y");
    Local<String> sum_prop = String::NewFromUtf8(isolate, "sum");
    Local<String> product_prop =String::NewFromUtf8(isolate, "product");

    double x = input->Get(x_prop)->NumberValue();
    double y = input->Get(y_prop)->NumberValue();

    HandleScope scope(isolate); // bad..
    Local<Object> obj = Object::New(isolate);
    obj->Set(sum_prop, Number::New(isolate, x + y));
    obj->Set(product_prop, Number::New(isolate, x * y));
    return obj;
}

void PassObject(const FunctionCallbackInfo<Value>& args) {
    Isolate * isolate = args.GetIsolate();
    Local<Object> target = args[0]->ToObject();
    Local<Value> obj = make_return(isolate, target);
    args.GetReturnValue().Set(obj);
}

​ 該函式期望用數值屬性x和y傳遞一個物件。它構建一個包含sum和product的返回物件,並將其返回給JavaScript。使用NAN的更改非常簡單。

Local<Value> make_return(const Local<Object> input ) {
    Local<String> x_prop = Nan::New<String>("x").ToLocalChecked();
    Local<String> y_prop = Nan::New<String>("y").ToLocalChecked();
    Local<String> sum_prop = Nan::New<String>("sum").ToLocalChecked();
    Local<String> product_prop = Nan::New<String>("product").ToLocalChecked();

    Local<Object> obj = Nan::New<Object>();
    double x = Nan::Get(input, x_prop).ToLocalChecked()->NumberValue();
    double y = Nan::Get(input, y_prop).ToLocalChecked()->NumberValue();

    Nan::Set(obj, sum_prop, Nan::New<Number>(x+y));
    Nan::Set(obj, product_prop, Nan::New<Number>(x*y));
    return obj;
}

​ 現在似乎是時候指出可能已經顯而易見的事情了。使用NAN和V8 API並不是一個“或者”命題,混合是沒有問題的。也就是說,如果您正在使用NAN,那麼用“NAN方法”編寫所有您可以使用的內容是有意義的,這樣您就可以充分利用版本相容性。

Arrays

​ 在基本的第二章的轉換中,讓我們來看看處理陣列時的變化。回想一下,我們構建了一種愚蠢的陣列addon函式,它接受了一系列的數字陣列,但也有一個名為“not_index”的屬性來演示如何在陣列中訪問指定的屬性。addon返回一個包含3個元素的陣列——第一個和最後一個只是輸入陣列中的第一個和最後一個元素,增加了一個元素。返回陣列中的第二個索引設定為在 “not_index”.中找到的值。以下是我們使用V8的情況:

void PassArray(const FunctionCallbackInfo<Value>& args) {
    Isolate * isolate = args.GetIsolate();
    Local<Array> array = Local<Array>::Cast(args[0]);

    if ( args.Length() < 1 || ! args[0]->IsArray()) {
        return;
    }

    if (array->Length() < 3) {
        return;
    }

    Local<String> prop = String::NewFromUtf8(isolate, "not_index");
    if (array->Get(prop)->IsUndefined() ){
        return;
    }

    for (unsigned int i = 0; i < 3; i++ ) {
      if (array->Has(i)) {
        Local<Value> v = array->Get(i);
        if ( !v->IsNumber()) return;

        double value = v->NumberValue();
        array->Set(i, Number::New(isolate, value + 1));
      }
      else {
        return;
      }
    }

    Local<Array> a = Array::New(isolate);
    a->Set(0, array->Get(0));
    a->Set(1, array->Get(prop));
    a->Set(2, array->Get(2));

    args.GetReturnValue().Set(a);
}

​ 讓我們直接跳到完整的NAN實現。關鍵的變化當然是建立新陣列、新值,並使用NAN getter和setter來訪問陣列索引和屬性。注意使用Nan:::Set和Nan::Get,它允許您對陣列進行索引。

NAN_METHOD(IncrementArray) {
    Local<Array> array = Local<Array>::Cast(info[0]);

    for (unsigned int i = 0; i < array->Length(); i++ ) {
      if (Nan::Has(array, i).FromJust()) {
        double value = Nan::Get(array, i).ToLocalChecked()->NumberValue();
        Nan::Set(array, i, Nan::New<Number>(value + 1));
      }
    }

    Local<String> prop = Nan::New<String>("not_index").ToLocalChecked();
    Local<Array> a = New<v8::Array>(3);
    Nan::Set(a, 0, Nan::Get(array, 0).ToLocalChecked());
    Nan::Set(a, 1, Nan::Get(array, prop).ToLocalChecked());
    Nan::Set(a, 2, Nan::Get(array, 2).ToLocalChecked());

    info.GetReturnValue().Set(a);
}

​ 在上面的例子中,您可能會注意到一個明顯的不一致之處在於:我們使用了基本的V8Local::Cast 函式來將我們的單個函式引數轉換為一個數組。奇怪的是,與原語和物件不同,NAN目前還沒有提供對陣列進行轉換的““NAN way” 。幸運的是,上面的程式碼將適用於所有版本的 Node.js,這可能是為什麼“NAN way”從來沒有進入開發管道的原因。

回撥和非同步​

​ 在這本書中,我們已經看到了幾個例子,其中包括將JavaScript函式傳送給C++外掛,然後從C++中呼叫。特別是,我們看到了同步和非同步回撥在第四章,我們在c++計算降雨統計。在處理回撥函式和非同步執行緒時,除了簡單的巨集和API重新定義之外,還有一個領域。對於這些主題,NAN實際上提供了一些額外的實用程式類,使完成任務比原始的V8更容易(和不同)。​

​ 我們不是從第4章中恢復降水的例子,這是相當大的,我們來關注一個更簡潔的addon函式,它計算質數。我們將從同步版本開始,然後重新配置它,以建立非同步版本。在這兩種情況下,我們將匯出一個名為prime的函式,該函式接受兩個引數。第一個是一個整數N,我們將生成所有質數小於N。第二個引數是一個回撥函式,一旦計算了素數,就會呼叫這個函式。回撥應該接受一個包含結果的素數陣列。我們會像這樣使用它:

addon.primes(20, function (primes) {
    // prints 2, 3, 5, 7, 11, 13, 17, 19
    console.log("Primes less than 20 = " + primes);
});

​ 讓我們首先建立一個標準C++函式,它接受N和一個向量來填充素數——我們將在我們的addon的同步和非同步版本中使用這個函式。find質數實現了一種簡化的主篩演算法。

void find_primes(int limit, vector<int> & primes) {
    std::vector<int> is_prime(limit, true);
    for (int n = 2; n < limit; n++ ) {
        if (is_prime[n] ) primes.push_back(n);
        for (int i = n * n; i < limit; i+= n) {
            is_prime[i] = false;
        }
    }
}

現在讓我們來看看在同步版本中addon程式碼會是什麼樣子:

NAN_METHOD(Primes) {
    int limit = info[0]->IntegerValue();
    Callback *callback = new Callback(info[1].As<Function>());

    vector<int> primes;
    find_primes(limit, primes);
    Local<Array> results = New<Array>(primes.size());

    for ( unsigned int i = 0; i < primes.size(); i++ ) {
        Nan::Set(results, i, New<v8::Number>(primes[i]));
    }

    Local<Value> argv[] = { results };
    callback->Call(1, argv);
}

NAN_MODULE_INIT(Init) {
    Nan::Set(target, New<String>("primes").ToLocalChecked(),
    GetFunction(New<FunctionTemplate>(Primes)).ToLocalChecked());
}

NODE_MODULE(primes, Init)

​ 這個外掛最有趣的方面是我們如何使用NAN來幫助我們處理回撥,一旦我們有了結果,它就必須被呼叫。注意回撥物件的使用,它是一個封裝了標準v8::Function的NAN類。在上面的同步程式碼中,它提供了有限的新增值,而不是直接使用v8::Function,但是回撥保護它從垃圾收集中封裝的函式(使用持久化控制代碼)。在使用非同步外掛時,該功能極大地簡化了回撥使用,因此它們使用他們是一個很好的習慣。​

​ 大多數接受回撥的addons是非同步的(否則,為什麼不直接返回結果!)在第4章中,我們直接與libuv一起工作,建立了一個多執行緒的addon,我們在工作執行緒和節點事件迴圈執行緒之間管理資料的移動。這是一項很重要的工作,而且很容易犯錯誤。NAN提供了幾個實用程式類,它們抽象了在事件迴圈和工作執行緒之間移動資料的概念,這讓我們可以更快速地關注實際的程式碼。​

​ NAN提供的關鍵類是NAN::AsyncWorker。這個類允許您建立一個新的工作執行緒,並有助於持久地儲存資料,呼叫工作執行緒程式碼,並在完成時呼叫持久回撥。您的第一步是設定一個繼承自AsyncWorker的類,並實現抽象的Execute方法。通常,您的工作執行緒的輸入將被儲存在您的類中,通常通過它的建構函式傳遞。當您的執行函式完成時(我們將看到如何讓它執行),AsyncWorker將呼叫它的HandleOKCallback—通常您將重寫呼叫一個回撥來將結果傳送回JavaScript。AsyncWorker確保在後臺執行緒和HandleOKCallback(以及HandleErrorCallback,如果需要的話)被執行,作為事件迴圈的一部分。

​ 下面的程式碼設定了一個基本的AsyncWorker實現,它與我們的質數程式碼一起工作:

class PrimeWorker : public AsyncWorker {
  public:
  PrimeWorker(Callback * callback, int limit): AsyncWorker(callback), limit(limit) { }

  // Executes in worker thread
  void Execute() 
  {
     find_primes(limit, primes);
  }

  // Executes in event loop
  void HandleOKCallback ()
  {
    Local<Array> results = New<Array>(primes.size());
    for ( unsigned int i = 0; i < primes.size(); i++ ) 
    {
         Nan::Set(results, i, New<v8::Number>(primes[i]));
    }
    Local<Value> argv[] = { results };
    callback->Call(1, argv);
  }
  private:
     int limit;
     vector<int> primes;
};​

​ 請注意,在PrimeWorker的建構函式中,我們呼叫基類的建構函式來傳遞迴調函式,這是一個AsyncWorker將持久儲存的。執行工作是在執行中執行的,它使用成員變數的限制和素數來計算結果。一旦執行完成,就會呼叫HandleOKCallback(在事件迴圈執行緒上),並呼叫回撥。該程式碼實際上與同步版本完全相同,只是在兩個成員函式上展開。當然,這個類必須被例項化。我們將在addon函式中這樣做,但我們不會直接呼叫執行。相反,NAN提供了一個稱為AsyncQueueWorker的實用程式方法,它接受一個AsyncWorker,並執行佇列的執行,這樣執行就會在一個工作執行緒中執行。這使我們免於直接與libuv打交道。

NAN_METHOD(Primes) {
    int limit = To<int>(info[0]).FromJust();
    Callback *callback = new Callback(info[1].As<Function>());
    AsyncQueueWorker(new PrimeWorker(callback, limit));
}

​ 呼叫的同步和非同步版本’外掛從JavaScript以同樣的方式工作,我們會得到相同的結果,但是這個例子中非同步遠遠比同步好,沉重的CPU計算做大N值不會佔用事件迴圈的工作在其他的東西!

非同步更新進度

​ 在上面的addon中,我們在核心for迴圈中生成素數,這可能需要一段時間才能完成。我們在一個向量中收集所有的質數,然後在最後把它全部儲存在Vector中。如果我們能顯示當前的進展,那將是一件好事,因此使用者不會在瞭解這個過程會花費多長時間。雖然我們可以直接使用libuv來完成這個工作,但是在這種情況下,通過繼承AsyncProgressWorker類(繼承AsyncWorker),NAN確實可以幫助我們解決這個問題。AsyncProgressWorker構建了一個方便的模式,用於在非同步執行期間向JavaScript傳送進度更新。​

​ AsyncProgressWorker背後的核心原則是傳遞給我們執行函式的附加引數。型別為ExecutionProgress的引數是我們的切入點,它將對第二個回撥的呼叫進行排隊,我們現在必須實現HandleProgressCallback。HandleProgressCallback 將在事件迴圈中執行,而ExecutionProgresss提供了一種方法,它允許我們儲存資料(持久地),並新增一個呼叫將HandleProgressCallback送回給佇列執行。這種方法,progress.Send 很簡單,它接受一個指向資料的指標、大小。資料被儲存在ExecutionProgress中的一個Persistent handle,直到事件迴圈中呼叫回撥。

​ 下面程式碼繼承與PrimeWorker ,它是跟進度更新,表明它離完成有多近。在Execute函式中,我們實際上是在計算素數,而不是委託給原始的find的素數,因為我們需要傳送進度更新(當然,您也可以修改find素數來接受一個進度物件)。請注意,我們也在迴圈之後程序睡眠,只是為了效果(否則我們很快就會完成的!)睡眠程式碼需要C++11,並且包含了 和標頭檔案和形影的庫檔案。

class PrimeProgressWorker : public AsyncProgressWorker {
public:
    PrimeProgressWorker(Callback * callback,Callback * progress, int limit): AsyncProgressWorker(callback),progress(progress), limit(limit) {}

  // Executes in worker thread
    void Execute(const AsyncProgressWorker::ExecutionProgress & progress) {
        std::vector<int> is_prime(limit, true);
        for (int n = 2; n < limit; n++ ) {
            double p = (100.0 * n) / limit;
            progress.Send(reinterpret_cast<const char*>(&p), sizeof(double));

            if (is_prime[n]) 
              primes.push_back(n);

            for (int i = n * n; i < limit; i+= n) 
            {
                 is_prime[i] = false;
            }

            std::this_thread::sleep_for(chrono::milliseconds(100));
        }
    }

    // Executes in event loop
    void HandleOKCallback () {
        Local<Array> results = New<Array>(primes.size());
        for ( unsigned int i = 0; i < primes.size(); i++ ) 
        {
            Nan::Set(results, i, New<v8::Number>(primes[i]));
        }
        Local<Value> argv[] = { results };
        callback->Call(1, argv);
    }

  void HandleProgressCallback(const char *data, size_t size) {
      // Required, this is not created automatically
      Nan::HandleScope scope;
      Local<Value> argv[] = {New<v8::Number>(*reinterpret_cast<double*>(const_cast<char*>(data)))};
      progress->Call(1, argv);
  }
private:
    Callback *progress;
    int limit;
    vector<int> primes;
};

​ 在一個奇怪的情況下,與HandleOKCallback不同,HandleScope不會在HandleProgressCallback呼叫之前自動為我們自動建立。因此,在分配新的V8記憶體之前,我們需要建立一個HandleScope。注意,AsyncProgressWorker將“進度更新”處理為重寫的意思,這意味著每次一個程序訊息被記錄時,它會覆蓋之前記錄的日誌。​

​ 在上面的例子中,因為我們在for迴圈的每個回合中等待100毫秒,很可能你會看到0%,1%,2%…100%列印在螢幕上。如果我們要刪除100毫秒的等待,您可能會驚訝地看到100%完整的訊息——並且可能沒有其他的進展更新。這是因為每次呼叫SendProgress時,工作執行緒都會排隊到事件迴圈中(作用是呼叫HandleProgressCallback)。無法保證在您的工作執行緒傳送另一個進度訊息之前,該作業將執行,在我們的示例中,對於相對較小的N值,可能所有的進度更新都將在libuv之前被記錄下來,直到呼叫HandleProgressCallback。

​ 對於進度更新,所描述的行為是有意義的,因為在刪除過時的進度更新中不應該有損害。然而,要將實際資料傳送到JavaScript(比如前部分處理結果),這種功能是災難性的!最終,AsyncWorker和AsyncProgressWorker是真正的例子,他們不應該被看作是我們所能對scope做的事情的代表。在第7章中,我們將看到擴充套件進度條更新的例子,以提供從addons提供的流媒體介面。我們將使用額外的佇列來確保沒有“進展”訊息被覆蓋,從而使我們的實現適合傳送真實的資料。順便說一句,我們剛剛看到的質數示例也是附錄A的主題,這裡討論了整合現有C++程式碼的替代方法。

物件抓換

​ 在學習NAN之前,我們討論過的C++ addon的最後一個問題是ObjectWrap,它允許我們將完整的C++類傳遞給JavaScript,只要我們用大量的Node/V8模板和API進行轉義。這個Node/V8 的模板程式碼讓我們容易受到版本問題的影響——ObjectWrap API實際上已經通過幾個版本的node.js改變了。

​ 幸運的是,學習“NAN”方法來進行物件包裝是非常簡單的,因為第5章的討論是基於最新版本(v4+)Node.js,,您會注意到,NAN ObjectWrap實現與我們已經學過的幾乎完全相同。使用它的好處,而不是在第5章中使用的API,是因為NAN擴充套件支援早期版本的node.js。​

​ 讀者不應該重複大量的程式碼,而是應該使用在第五章中提到WrappedPoly的方式最終實現。您將注意到類定義擴充套件node::ObjectWrap。要使用NAN實現,只需擴充套件NAN::ObjectWrap(當然,您必須包含nan.h標頭檔案)。在從典型 ObjectWrap遷移到NAN時最大的更改是New , no Isolate , etc.,您將最終完成的大多數更改都是針對NAN::ObjectWrap。​

​ 首先,讓我們看一下第五章的 WrappedPoly 類宣告,並強調我們想要做的改變:

class WrappedPoly : public node::ObjectWrap {
public:
    static void Init(v8::Local<v8::Object> exports);
private:
    explicit WrappedPoly(double a = 0, double b = 0, double c = 0): a_(a), b_(b), c_(c) {}
    ~WrappedPoly() {}
    static void New(const v8::FunctionCallbackInfo<v8::Value>& args) ;
    static void At(const v8::FunctionCallbackInfo<v8::Value>& args);
    static void Roots(const v8::FunctionCallbackInfo<v8::Value>& args);
    static void GetCoeff(Local<String> property,const PropertyCallbackInfo<Value>& info);
    static void SetCoeff(Local<String> property,Local<Value> value, const PropertyCallbackInfo<void>& info);
    static v8::Persistent<v8::Function> constructor;

    double a_;
    double b_;
    double c_;
};​

​ 遷移到Nan::ObjectWrap在方法宣告中發生的主要變化。在這種情況下,在Init、訪問器和成員方法中使用NAN巨集是明智的。此外,由於名稱空間問題,建構函式物件必須WrappedPoly之外,v8::Persistent 的v8::Function改Nan::Persistent 的 v8::FunctionTemplate。這是完整的宣告:

static Persistent<v8::FunctionTemplate> constructor;
class WrappedPoly : public Nan::ObjectWrap {
public:
    static NAN_MODULE_INIT(Init) ;
private:
    explicit WrappedPoly(double a = 0, double b = 0, double c = 0): a_(a), b_(b), c_(c) {}
    ~WrappedPoly() {}

    static NAN_METHOD(New) ;
    static NAN_METHOD(At) ;
    static NAN_METHOD(Roots) ;
    static NAN_GETTER(GetCoeff);
    static NAN_SETTER(SetCoeff);

    double a_;
    double b_;
    double c_;
};

​ 在方法中,更改主要涉及Nan::New和之類的,但是Init中有一些有趣的變化。以下是第5章的原始實現:

static void Init(v8::Local<v8::Object> exports) {
    Isolate* isolate = exports->GetIsolate();

    Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
    tpl->SetClassName(String::NewFromUtf8(isolate, "Polynomial"));
    tpl->InstanceTemplate()->SetInternalFieldCount(1);

    NODE_SET_PROTOTYPE_METHOD(tpl, "at", At);
    NODE_SET_PROTOTYPE_METHOD(tpl, "roots", Roots);

    tpl->InstanceTemplate()->SetAccessor(String::NewFromUtf8(isolate, "a"), GetCoeff, SetCoeff);
    tpl->InstanceTemplate()->SetAccessor(String::NewFromUtf8(isolate, "b"),GetCoeff, SetCoeff);
    tpl->InstanceTemplate()->SetAccessor(String::NewFromUtf8(isolate, "c"),GetCoeff, SetCoeff);

    constructor.Reset(isolate, tpl->GetFunction());
    exports->Set(String::NewFromUtf8(isolate, "Polynomial"),
    tpl->GetFunction());
}

​ 遷移到NAN,我們需要使用不同的方法來設定原型方法和訪問器:

 static NAN_MODULE_INIT(Init) {
      v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(WrappedPoly::New);
      constructor.Reset(tpl);
      tpl->SetClassName(Nan::New<v8::String>("Polynomial").ToLocalChecked());
      tpl->InstanceTemplate()->SetInternalFieldCount(1);
      SetPrototypeMethod(tpl, "at", WrappedPoly::At);
      SetPrototypeMethod(tpl, "roots", WrappedPoly::Roots);
      v8::Local<v8::ObjectTemplate> itpl = tpl->InstanceTemplate();
      SetAccessor(itpl, Nan::New<v8::String>("a").ToLocalChecked(), WrappedPoly::GetCoeff, WrappedPoly::SetCoeff);
      SetAccessor(itpl, Nan::New<v8::String>("b").ToLocalChecked(), WrappedPoly::GetCoeff, WrappedPoly::SetCoeff);
      SetAccessor(itpl, Nan::New<v8::String>("c").ToLocalChecked(), WrappedPoly::GetCoeff, WrappedPoly::SetCoeff);
      Set(target, Nan::New<v8::String>("Polynomial").ToLocalChecked(), tpl->GetFunction());
  }

​ 從側面看,你可以看到這些變化主要是轉義,包括新的NAN 的方法法SetAccessor和SetPrototypeMethod。對於WrappedPoly 類的其他更改是自解釋的,它們只是將變數分配/轉換轉換為NAN語法。下面是使用Nan完成的一個完整示例的完整程式碼清單。

#include <cmath>
#include <nan.h>
#include <string>
using namespace Nan;

static Persistent<v8::FunctionTemplate> constructor;

class WrappedPoly : public Nan::ObjectWrap {
 public:
  static NAN_MODULE_INIT(Init) {
      v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(WrappedPoly::New);
      constructor.Reset(tpl);
      tpl->SetClassName(Nan::New<v8::String>("Polynomial").ToLocalChecked());
      tpl->InstanceTemplate()->SetInternalFieldCount(1);
      SetPrototypeMethod(tpl, "at", WrappedPoly::At);
      SetPrototypeMethod(tpl, "roots", WrappedPoly::Roots);
      v8::Local<v8::ObjectTemplate> itpl = tpl->InstanceTemplate();
      SetAccessor(itpl, Nan::New<v8::String>("a").ToLocalChecked(), WrappedPoly::GetCoeff, Wrapped