五、关于性能的讨论

如第一章的分析,Electron并非毫无缺点。其主要的缺点就是:

  1. 体积大。

    因为需要背着一个浏览器和运行时,即使最简单的HelloWorld,也会有几十MB。这个是无解的。

  2. 内存消耗比较大。

    这个也无解,毕竟需要运行一个完整的浏览器。

  3. 性能比较弱。

    这是因为程序的主要逻辑都是使用JavaScript写的,即使使用的性能最强的V8引擎,脚本语言的运行效率与编译型语言相比依旧比较差。

由于现在的电脑硬盘和内存普遍都比较大,所以,前两个缺点,影响并没有那么的严重。但是,第三个缺点就不得不想办法克服了。这是因为JavaScript是一个单线程的语言。无法发挥现代CPU多核心的优势。遇到计算密集性操作就会出现肉眼可见的卡顿。典型的如数据压缩、加解密等等。很不幸,现代CPU全都是采用多核心来提高效率的。好在,这并非无解。我们可以采取很多种方法优化性能。

首先,我们在demo4的基础上,做以下几点修改:

App.vue修改如下:

<template>
  <div id="animation"></div> <br>
  <button @click="clac(10 * 1000)">运算</button> <br><br>
  <div id="log"></div>
</template>

<script>

export default {
  name: 'App',
  methods: {
    clac(time) {
      var timeStamp = new Date().getTime();
      var endTime = timeStamp + time;
      document.querySelector("#log").innerHTML += timeStamp + ": begin clac " + time + "<br>";
      while (new Date().getTime() < endTime) {
        timeStamp = new Date().getTime();
      }
      timeStamp = new Date().getTime();
      document.querySelector("#log").innerHTML += timeStamp + ": finish clac " + time + "<br>";
    }
  }

}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  margin: 10px;
  text-align: left;
  color: #2c3e50;
  margin-top: 60px;
}

#animation {
  width: 100px;
  height: 100px;
  background-color: red;
  animation-name: example;
  animation-duration: 0.5s;
  animation-direction: alternate-reverse;
  animation-iteration-count: infinite;
}

@keyframes example {
  from {
    background-color: green;
  }

  to {
    background-color: red;
  }
}
</style>
  1. 在模板中新增两个div,其中,animation是一个循环动画,log用来显示日志信息。

  2. 新增了一个按钮,当按钮被点击时,会执行clac函数,我们在这个函数中进行10秒的模拟运算。

于是,当点击运算按钮的时候,你就会发现,动画暂停了。不仅动画停了,其实这个时候整个渲染进程都被运算函数所占用了,整个界面处于卡死状态。

5.1 Web Worker

在H5中,新增了一个Web Worker的特性。它提供了一种简单的方法,让我们可以把大量运算的操作放到后台来做。这样线程可以执行任务而不干扰用户界面。

于是,我们可以App.vue中的clac函数改为:

clac(time) {
      const myWorker = new Worker("worker.js");
      myWorker.onmessage = (e) => {
        document.querySelector("#log").innerHTML += e.data.begin + ": begin clac " + time + "<br>";
        document.querySelector("#log").innerHTML += e.data.end + ": finish clac " + time + "<br>";
        console.log("Message received from worker");
      };
      myWorker.postMessage(time);
}

首先,创建一个Worker,运行脚本worker.js

然后,定义一个消息处理函数,以便接收worker线程发送过来的结果。

最后,我们给工作线程发送一个消息,让工作线程开始干活。

接下来,在public目录新增一个名为worker.js的脚本,内容如下:

do_clac = (time) => {
    var timeStamp = new Date().getTime();
    var endTime = timeStamp + time;
    while (new Date().getTime() < endTime) {
        timeStamp = new Date().getTime();
    }
    var end = new Date().getTime();
    return { begin: timeStamp, end: end }
}

onmessage = (e) => {
    console.log("Message received from main script: " + e.data);
    let result = do_clac(e.data)
    console.log("Posting message back to main script");
    postMessage(result);
};

这个脚本也不复杂,主要就是两个函数,

  1. do_clac,这个函数跟之前写在App.vue里面的clac几乎相同。唯一不同的是,在这儿我们无法直接操作dom,所以,我们把结果返回出来。
  2. onmessage,这个函数用来接收主线程发送过来的消息。当执行完成后,在使用postMessage将结果发送给主线程。

使用了Worker以后,再次点击运算按钮。你就会发现,这次动画可以正常运行了。我们做了大量运算,但是,并不会导致界面卡顿。

Worker是一个非常好的特性,合理的使用它可以让我们的程序有效使用CPU的多核算力。这儿只是简单的演示,更详细的用法可以参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers#web_workers_api

5.2 C++扩展

虽然,我们有了Web Worker可以用来做一些计算密集型的操作而不影响界面的流畅运行。但是,依旧存在一些任务无法在JavaScript里面完成。例如,视频编解码、数据压缩和解压缩等等。又或者授权检查,如果用JavaScript来写,就会很容易被破解。这个时候,我们就可以使用C/C++来扩展Electron了。

可能有人会有疑问,使用了C/C++是不是就会导致跨平台能力降低?其实,不会。无法跨平台的不是C/C++语言,而是GUI和操作系统接口部分。如果我们不用C/C++来调用操作系统的接口,就不会存在跨平台问题了。

5.2.1 编译环境

C/C++是编译型语言,所以,使用C/C++之前,首先需要准备好环境。不过,并不复杂。只需要安装一个node-gyp这个包就可以了。

npm install -g node-gyp

除了node-gyp,你还需要有一整套C/C++编译工具。

如果是Windows系统,需要提前安装一下Visual StudioPython。这两个都可以手动安装,安装方法可以在网上找一下,直接去官网下载安装包安装一下即可。如果想偷懒也可以使用Chocolatey,这是一个Windows下的软件包管理器,如同Linux下的Apt和yum,可以做到一键安装:

choco install python visualstudio2022-workload-vctools -y

另外,需要特别留意的是,Windows下需要用到Powershell

在Linux下是最简单的只需要一个命令就可以完成安装包的依赖,如果是Debian系,就是使用

sudo apt install -y build-essential python3 make

如果是MacOS,则需要使用xcode-select --install命令安装Xcode Command Line Tools

5.2.2 binding.gyp

首先,需要创建一个binding.gyp的文件,内容如下:

{
  "targets": [
    {
      "target_name": "hello",
      "sources": [ "hello.cpp" ]
    }
  ]
}

内容比较简单,就是定义一下最基本的信息。其中可以包含以下信息:

target_name:目标的名称,此名称将用作生成的 Visual Studio 解决方案中的项目名称。

type:可选项:static_library 静态库、executable 可执行文件、shared_library 共享库【默认】。

defines:将在编译命令行中传入的 C 预处理器定义(使用 -D 或 /D 选项)。

include_dirs:C++ 头文件所在的目录。

sources:C++ 源文件。

conditions:适配不同环境配置条件块。

copies:拷贝 dll 动态库到生成目录。

library_dirs: 配置 lib 库目录到 vs 项目中。

libraries:项目依赖的库。

msvs_settings:Visual Studio 中属性设置。

相对比较完整的例子如下:

{
    "targets": [
        {
            "target_name": "addon_name",
            "type": "static_library",
            'defines': ['DEFINE_FOO', 'DEFINE_A_VALUE=value',],
            'include_dirs': [
                './src/include',
                '<!(node -e "require(\'nan\')")' # include NAN in your project
            ],
            'sources': [
                'file1.cc',
                'file2.cc',
            ],
            'conditions': [[
                'OS=="win"', {
                    'copies': [{
                        'destination': '<(PRODUCT_DIR)',
                        'files': ['./dll/*']
                    }],
                    'defines': ['WINDOWS_SPECIFIC_DEFINE',],
                    'library_dirs': ['./lib/'],
                    'link_settings': {
                        'libraries': ['-lyou_sdk.lib']
                    },
                    'msvs_settings': {
                        'VCCLCompilerTool': {
                            'AdditionalOptions': ['/utf-8']
                        }
                    },
                }
            ]],
        },
    ]
}

当然,在咱们这个例子中由于没有任务依赖,所以,不需要用到这么复杂的配置。

5.2.3 C++源文件

hello.cpp文件的内容为:

#include <node.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;

void Hello(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world", NewStringType::kNormal).ToLocalChecked());
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "hello", Hello);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}

其中,

  • NODE_MODULE:这个用来定义一个Node模块的宏。当Node加载该模块的时候,就会按照这个宏的定义调用Initialize函数。
  • Initialize函数的功能是导出符号。可以导出函数和常量两种符号。我们这儿仅仅导出了一个名为hello的函数,然后,其实现函数为Hello
  • Hello函数中,所有的入参和出参都是通过args完成的。在这个例子中,我们仅仅返回了一个字符串world

5.2.4 调用C++扩展

我们只能在主进程中调用C++扩展,在渲染进程中,如果想使用C++扩展,就必须通过ipc调用完成。主进程运行的脚本index.js内容如下:

let { app, BrowserWindow, ipcMain } = require('electron');

let hello_file = "./build/Release/hello";
const hello = require(hello_file);

ipcMain.on("hello", (event) => {";
    console.log("call hello in main process");
    event.reply("hello_reply", hello.hello());
});

app.whenReady().then(() => {
    const win = new BrowserWindow({
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false
        }
    })
    win.loadFile("./index.html")
})

在渲染进程中通过ipcRenderer来调用主进程,index.html的内容如下:

<!DOCTYPE html>
<html>

<head>
    <title>demo for c++ in electron</title>
</head>

<body>
    <button onclick="sendMsg()">发送消息</button>
    <br>
    <div id="txt"></div>

    <script lang="javascript">
        let { ipcRenderer } = require("electron");

        ipcRenderer.on("hello_reply", (event, msg) => {
            document.querySelector("#txt").innerHTML += msg + "<br>";
        })
        function sendMsg() {
            ipcRenderer.send("hello");
        }
    </script>
</body>

</html>

5.2.5 编译和使用

有了这两个文件就可以进行编译了。运行下面这个命令即可:

node-gyp configure # 生成配置项,仅需要运行一次。
node-gyp build     # 编译

如果此次你运行,就无法发现,编译出来的C++扩展无法使用。报错信息大致如下:

App threw an error during load
Error: The module '/home/allan/Desktop/Electronå
¥é¨ç¯/demo5.1/build/Release/hello.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 93. This version of Node.js requires
NODE_MODULE_VERSION 101. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
    at process.func [as dlopen] (node:electron/js2c/asar_bundle:5:1812)
    at Object.Module._extensions..node (node:internal/modules/cjs/loader:1199:18)
    at Object.func [as .node] (node:electron/js2c/asar_bundle:5:1812)
    at Module.load (node:internal/modules/cjs/loader:988:32)
    at Module._load (node:internal/modules/cjs/loader:829:12)
    at Function.c._load (node:electron/js2c/asar_bundle:5:13343)
    at Module.require (node:internal/modules/cjs/loader:1012:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/home/allan/Desktop/Electron入门篇/demo5.1/index.js:4:15)
    at Module._compile (node:internal/modules/cjs/loader:1116:14)
A JavaScript error occurred in the main process

这是因为,我们编译的node.jsElectron中集成的node.js版本不一致导致的。node-gyp编译时使用的是编译环境中的node.js,而Electron中集成的node.js与编译环境中总是会有一些不同。所以,无法在Electron中正常加载使用。解决方法也容易,那就是使用electron-rebuild这个包来编译。

electron-rebuild的安装方法为:

npm add electron-rebuild --save-dev

为了方便使用,我们可以改一下package.json,内容如下:

{
  "name": "demo5.1",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "electron .",
    "build": "electron-rebuild"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "electron": "17.3.1",
    "electron-rebuild": "^3.2.9"
  }
}

其中,build命令就是用来替代node-gyp命令进行编译的。electron-rebuild会解决版本不一致问题,然后调用node-gyp完成编译。它解决版本不一致的方法其实是使用Electron提供的头文件,而不是使用node.js提供的头文件。而且会有一些版本上的差异,功能上并无区别。

需要留意的是,默认情况下node-gpy会自动去https://nodejs.org/dist下载node.js的头文件,而electron-rebuild会去https://artifacts.electronjs.org/headers/下载头文件。不过,electronjs中不允许访问目录,只能通过文件完整路径去下载。

接下来,我们只需要执行下面两个命令就可以了:

npm run build # 编译
npm run start # 运行

不出意外的话,你将看到下面的画面:

image-20240610012008447

关于C++扩展的参考资料:

5.2.6 Vue中使用C++扩展

如果想在vue中使用C++扩展,事情会变得稍微复杂一些。这是因为vue插件做了很多工作,导致我们无法直接通过require来加载C++编译出来的node文件了,这其实是封装带来的坏处。不过,并非做不到,只是没有那么的直白了而已。

首先,我们按照第4章中的步骤创建一个vue的工程出来,然后,在工程目录中在创建一个名为:hello_addon的目录。在该目录中执行以下命令:

npm init --yes

然后,创建两个文件,分别是binding.gyphello.cppindex.js。其中building.gyphello.cpp的内容跟前面完全相同。代码就不再贴出来了,想看的可以翻看前面的章节或者直接打开demo5.2的源码看一下。index.js有了较大不同,内容变为:

module.exports = require('./build/Release/hello_addon');

只有一行,那就是把hello_addon.node这个扩展导出来,以便使用者调用。到这儿,扩展部分就写好了。接下来,我们回到vue工程的根目录,执行下面这条命令:

vue install ./hello_addon --save

意思就是,把子目录hello_addon这个包添加到进来vue工程中来。你打开package.json,就会发现hello_addon这个依赖包已经被添加进来了。内容大致如下:

... ...
  "dependencies": {
    "core-js": "^3.8.3",
    "hello_addon": "file:hello_addon",
    "vue": "^3.2.13"
  },
... ...

接下来,我们在background.js中添加调用hello_addon的代码,内容如下:

... ...
protocol.registerSchemesAsPrivileged([
  { scheme: 'app', privileges: { secure: true, standard: true } }
])

let hello = require("hello_addon")
console.log("call hello_addon in main process: ", hello.hello())

async function createWindow() {
    ... ...

如果,你这个时候运行npm run electron:serve这条指令的话,你就会看到下面的错误信息:

image-20240610225022205

即使在这儿我们把hello_addon的路径改为全路径依旧会报找不到。这是因为我们在运行的时候使用webpack,加载的时候不会再直接加载,而是经过了webpack的处理,导致无法加载成功。为了解决这个问题,我们需要在vue.config.js中做一点修改,内容如下:

const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
  transpileDependencies: true,
  pluginOptions: {
    electronBuilder: {
      externals: ["hello_addon"],
    },
  },
})

其中,第7行是最关键的一行,它的意思是告诉webpack,它是一个外部模块,不要打包它。在官方文档也有相关的说明:

image-20240610225917283

不过,官方的文档并没有说明。这个配置是针对包的,而非文件。所以,很多人会在这个地方出现理解上的偏差,导致卡壳。

改完这个配置文件以后,再次执行npm run electron:serve就不会再有错误了。

最后。我们通过命令npm run electron:build打包以后看一下,你会发现打包好的程序也是可以正常使用的。但是,如果你查看一下app.asar文件就会发现有个不大不小的安全问题:

image-20240610234523268

打包以后的asar文件中居然存在hello.cpp这个源文件。这是因为webpack并不知道cpp文件不能打包进来。我们可以在vue.config.js中将这个信息告诉给它。

const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
  transpileDependencies: true,
  pluginOptions: {
    electronBuilder: {
      externals: ["hello_addon"],
      builderOptions: {
        files: [
          "**/*",
          "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
          "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
          "!**/node_modules/*.d.ts",
          "!**/node_modules/.bin",
          "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj,cpp,hpp,c,h}",
          "!.editorconfig",
          "!**/._*",
          "!**/node_gyp_bins",
          "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
          "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
          "!**/{appveyor.yml,.travis.yml,circle.yml}",
          "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
        ]
      },
    },
  },
})

其中,第15行就是告诉webpack,这些扩展名的文件不需要打包到asar文件中去的。

5.2.7 C++扩展中的异步

正常而言,我们调用JavaScript的话,是同一线程的,所以,对于性能有提升,但是有限。因为这样做依旧无法有效利用CPU的多核心。于是,我们需要在C++中使用多线程,使用异步处理的方式达到多核心同时运行的效果。既然使用了异步,执行完成后如何把执行完成的结果返回给JavaScript就是一个问题了。

在JavaScript中,有两种形式可以实现异步执行时返回结果。

  • 使用callback函数。

    在调用函数时传入一个函数指针,当处理完成后,由异步处理函数来调用这个callback函数返回执行结果。

  • 使用Promise对象。

    函数正常返回,但是,返回的不是结果,而是一个Promise对象,调用者再通过Promise对象拿到执行的结果。

本质上二者是相同的,但是,由于使用方式的不同,二者又具有不同的特点。其中,callback函数本身并不代表异步。只是说,如果callback函数可以用于异步。换句话说,同步也可以使用callback。另外,callback很容易出现回调地狱,也就是因为回调中嵌套回调导致代码变得复杂不容易理解。而Promise对象不存在这个问题。因为Promise对象本身是同步的,只有获取Promise对象中的结果的时候才是异步的。也就避免了嵌套。所以,推荐使用Promise对象来实现异步。

5.2.7.1 使用callback函数实现异步

基于demo5.2,我们把hello扩展中的cpp改写为:

#include <node.h>
#include "callback.h"

namespace demo
{

  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::NewStringType;
  using v8::Object;
  using v8::String;
  using v8::Value;

  v8::Global<v8::Function> func_callback;
  void worker_thread_func(uv_work_t *req)
  {
    callback::request_data *request = (callback::request_data *)req->data;
    uv_sleep(5000);
    request->result = "world";
  }
  void HelloWithCallback(const FunctionCallbackInfo<Value> &args)
  {
    v8::Isolate *isolate = args.GetIsolate();
    v8::Local<v8::Context> context = v8::Context::New(isolate);
    if (!callback::check_request_param(isolate, context, args))
    {
      args.GetReturnValue().Set(v8::Boolean::New(isolate, false));
      return;
    }
    callback::begin_worker_thread(isolate, context, args, worker_thread_func);
    args.GetReturnValue().Set(v8::Boolean::New(isolate, true));
  }

  void Initialize(Local<Object> exports)
  {
    NODE_SET_METHOD(exports, "HelloWithCallback", HelloWithCallback);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}

我们导出了一个名为HelloWithCallback的函数,这个函数里面并没有实际的逻辑,而是开启了一个worker线程。这个线程的逻辑是在callback.h里面实现的。

我们把实际的逻辑放在了函数worker_thread_func里面,这个函数是运行在worker线程中,所以,即使运行很长时间也不会有什么影响。

callback.h的内容如下:

#include <string>
#include <v8.h>
#include <uv.h>
namespace callback
{
    struct request_data
    {
        std::string argv;
        std::string result;
        v8::Global<v8::Function> func_callback;

        request_data(v8::Isolate *isolate, v8::Local<v8::String> request_json, v8::Local<v8::Function> func)
        {
            argv = *v8::String::Utf8Value(isolate, request_json);
            func_callback.Reset(isolate, func);
        }
    };

    void worker_callbakc_func(uv_work_t *req, int status)
    {
        if (status == UV_ECANCELED)
        {
            request_data *request = (request_data *)req->data;
            delete req;
            request->func_callback.Reset();
            delete request;
            return;
        }
        v8::Isolate *isolate = v8::Isolate::GetCurrent();
        request_data *request = (request_data *)req->data;
        delete req;

        // 构造JS回调函数的参数
        v8::Local<v8::Value> argv[1];
        argv[0] = v8::String::NewFromUtf8(isolate, request->result.c_str()) .ToLocalChecked();

        v8::TryCatch try_catch(isolate);
        v8::Local<v8::Function> cb = v8::Local<v8::Function>::New(isolate, request->func_callback);
        cb->Call(isolate->GetCurrentContext(), Null(isolate), 1, argv);
        if (try_catch.HasCaught())
        {
            fprintf(stderr, "callback failure");
        }
        // 回收资源
        request->func_callback.Reset();
        delete request;
    }

    //检查请求参数
    bool check_request_param(v8::Isolate *isolate, v8::Local<v8::Context> &context, const v8::FunctionCallbackInfo<v8::Value> &args)
    {
        // 判断入参是否满足条件
        v8::TryCatch try_catch(isolate);
        if (args.Length() < 2 || !args[0]->IsString() || !args[1]->IsFunction())
            return false;
        v8::MaybeLocal<v8::Value> jsonData = v8::JSON::Parse(context, args[0]->ToString(context).ToLocalChecked());
        if (jsonData.IsEmpty())
            return false;
        if (try_catch.HasCaught())
            return false;
        return true;
    }
    // 启动线程
    void begin_worker_thread(v8::Isolate *isolate, v8::Local<v8::Context> &context, const v8::FunctionCallbackInfo<v8::Value> &args, uv_work_cb work_cb)
    {
        //构造uv_work_t
        request_data *req = new request_data(isolate, args[0]->ToString(context).ToLocalChecked(), v8::Local<v8::Function>::Cast(args[1]));
        uv_work_t *uv_req = new uv_work_t();
        uv_req->data = req;
        //启动uv线程
        uv_queue_work(uv_default_loop(), uv_req, work_cb, worker_callbakc_func);
    }
}

在这个文件中,我们启动的一个uv线程,然后,将参数传入到uv线程中,在uv线程中执行我们函数的功能,然后再把结果通过回调函数返回给JavaScript。

使用方法跟普通的js函数无异:

const hello = require("hello");

ipcMain.on("hello", (event) => {
    hello.HelloWithCallback(JSON.stringify({}), (result) => {
        event.reply("hello_ack", result);
    })
}

5.2.7.2 使用Promise对象实现异步

同样基于demo5.2,我们把hello扩展中的cpp改写为:

#include <node.h>
#include "resolver.h"

namespace demo
{
  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::NewStringType;
  using v8::Object;
  using v8::String;
  using v8::Value;

  void worker_thread_Resolver(uv_work_t *req)
  {
    callback::request_data *request = (callback::request_data *)req->data;
    uv_sleep(5000);
    auto tid = syscall(SYS_gettid);
    request->result = "world, " + std::to_string(tid);
  }

  void HelloAsync(const FunctionCallbackInfo<Value> &args)
  {
    v8::Isolate *isolate = args.GetIsolate();
    v8::Local<v8::Context> context = v8::Context::New(isolate);
    Local<v8::Promise::Resolver> resolver = v8::Promise::Resolver::New(context).ToLocalChecked();

    resolver::begin_worker_thread(args, resolver, worker_thread_Resolver);
    args.GetReturnValue().Set(resolver->GetPromise());
  }

  void Initialize(Local<Object> exports)
  {
    NODE_SET_METHOD(exports, "HelloAsync", HelloAsync);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
}

resolver.h的内容如下:

#include <node.h>
#include <thread>

namespace resolver
{
    struct request_data
    {
        std::string argv;
        std::string result;
        v8::Global<v8::Promise::Resolver> resolver;

        request_data(v8::Isolate *isolate, v8::Local<v8::String> request_json, v8::Local<v8::Promise::Resolver> _resolver)
        {
            argv = *v8::String::Utf8Value(isolate, request_json);
            resolver.Reset(isolate, _resolver);
        }
    };

    using v8::FunctionCallbackInfo;

    void worker_callbakc_func(uv_work_t *req, int status)
    {
        if (status == UV_ECANCELED)
        {
            request_data *request = (request_data *)req->data;
            delete req;
            request->resolver.Reset();
            delete request;
            return;
        }
        v8::Isolate *isolate = v8::Isolate::GetCurrent();
        request_data *request = (request_data *)req->data;
        delete req;

        v8::TryCatch try_catch(isolate);
        v8::Local<v8::Promise::Resolver> resolver = v8::Local<v8::Promise::Resolver>::New(isolate, request->resolver);
        v8::Local<v8::Value> value = v8::String::NewFromUtf8(isolate, request->result.c_str()).ToLocalChecked();
        resolver->Resolve(isolate->GetCurrentContext(), value);
        if (try_catch.HasCaught())
        {
            fprintf(stderr, "resolver failure");
        }
        // 回收资源
        request->resolver.Reset();
        delete request;
    }

    void begin_worker_thread(const FunctionCallbackInfo<v8::Value> &args, v8::Local<v8::Promise::Resolver> &resolver, uv_work_cb work_cb)
    {
        v8::Isolate *isolate = args.GetIsolate();
        v8::Local<v8::Context> context = v8::Context::New(isolate);
        request_data *req = new request_data(isolate, args[0]->ToString(context).ToLocalChecked(), resolver);
        uv_work_t *uv_req = new uv_work_t();
        uv_req->data = req;
        //启动uv线程
        uv_queue_work(uv_default_loop(), uv_req, work_cb, worker_callbakc_func);
    }
}

可以看到,跟Callback最大的不同仅仅是把回调函数换成了Promise对象而已。另外,需要留意的是,无论是CallBack还是Promise对象回调回去以后都是在JavaScript的主线程里面。