对码当歌,猿生几何?

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 emptyLocal<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, 19console.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 threadvoid 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 loopvoid 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之前被记录下来,直到调