如何自己做一个 React Native(三):对接 React

192 阅读5分钟
原文链接: zhuanlan.zhihu.com

再说React

前面铺垫了两节背景知识,现在终于到了 React 了。

为了方便指明,我把 React 渲染到最终目标的部分称作渲染器。

早期做一个渲染器不是很容易,需要和特定的 React 版本强绑定,但到了最近几个版本,似乎已经基本有了定式。

做一个渲染器最重要的部分,就是要实现一个 React 的 reconciler。现在 npm 上已经有了非常容易用的 reconciler 的工厂,叫做 react-reconciler

这个包的用法非常简单,就是给它提供足够多的函数,它就可以帮你把 reconciler 生成出来。具体需要写哪些方法,可以看一下这篇 Making a custom React renderer,不过我自己在实践的时候,发现还缺了几个方法,各位可以看一眼仓库里的react/reconciler.js,我已经给补上了。

渲染器的模式

React 的渲染器一般有几个经典部分组成,除了刚才说到的 reconciler,还有几个部分,分别是主 render 函数、Root 组件、组件工厂。

用户使用 React 的时候,最直接接触到的是主 render 函数,比如ReacDOM.render就是 Web 版渲染器的主 render 函数。

Root 组件就好比是 HTML 中的 body,所有其他的组件都会在它的内部。由于是整个渲染树的最根部,初始化的时候会和其他组件略有不同,所以这个 Root 组件一般不会让用户去创建,而是交给 render 函数内部去自动做了。

剩下的组件工厂,也很简单。大家知道 React 写 JSX 的时候,写的都是标签名或者说是叫组件名,那么 React 拿着这个组件名称要去创建组件的时候,必然需要寻找到它对应的组件构造函数或者是工厂函数。组件工厂的作用就在这里了,它起到了一个映射表的作用。



所以,React 渲染器的渲染过程就可以概括成下面这样:

用户调起 render 函数,而 render 函数内部则通过组件工厂创建出 Root 组件,把它交给生成好的 reconciler,最后进入 React 的正常组件生命周期。

两个事件循环

渲染器的流程理清楚之后,就可以开始思考怎么把 Qt 融入到其中了。

Qt 的 GUI 实现和主流桌面 GUI 库没有太大区别,大体来说,就是主线程会开启一个事件循环,处理操作系统和用户产生的各种事件,根据事件来在屏幕上画出相应的内容。

在纯Qt程序里,这个主线程会被 Qt 的事件循环给占住,但是我们的原生模块运行在Node.js当中,如果把 node 的主线程给占住了,那整个 JS 执行环境就会停住。

如果贸然将其中一个循环挪到另一个线程中,就将会面临线程安全的挑战。作为一个 C++ 小白,我觉得这不是我想要的。

好在 Qt 还提供了类似于“步进”的处理模式,每调用一次只处理一批事件,处理完了之后就不会占住主线程了。有了这个方法就好办了,我只需要用setInterval周期性地去调用,就可以在不破坏 Node.js 事件循环的前提下,同时进行 Qt 的事件循环了。



另一方面,渲染器的 Root 组件是整个组件树的起点,Qt 事件循环的开始,完全可以放在 Root 组件的初始化当中。

衔接 Qt 组件

各位还记得最开始给出的一大长段原生模块代码么,现在我们要用它来提供Qt组件。

在Qt当中,组件可能有上百个,要从哪开始呢?

QWidget是所有 Qt GUI 组件的基类,如果单独实例化一个QWidget,那么它就是一个空窗口。为了验证我们是否能正常衔接 Qt,我决定就用它来做第一个组件。

#include <nan.h>
#include <QWidget>

// 这个class中未来会封装一些通用功能
class BasicWidget;

class Widget : public BasicWidget {
  public:
  static NAN_MODULE_INIT(Init) {
    v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New);
    tpl->InstanceTemplate()->SetInternalFieldCount(6);

    Nan::SetPrototypeMethod(tpl, "show", Show);

    constructor().Reset(Nan::GetFunction(tpl).ToLocalChecked());
  }

  static NAN_METHOD(NewInstance) {
    v8::Local<v8::Function> cons = Nan::New(constructor());
    info.GetReturnValue().Set(Nan::NewInstance(cons).ToLocalChecked());
  }

 private:
  explicit Widget() : BasicWidget(new QWidget) {}
  ~Widget() {}

  static NAN_METHOD(New) {
    if (info.IsConstructCall()) {
      Widget * obj = new Widget();
      obj->Wrap(info.This());
      info.GetReturnValue().Set(info.This());
    } else {
      v8::Local<v8::Function> cons = Nan::New(constructor());
      info.GetReturnValue().Set(Nan::NewInstance(cons).ToLocalChecked());
    }
  }

  static NAN_METHOD(Show) {
    Widget* obj = ObjectWrap::Unwrap<Widget>(info.Holder());
    // getWidget是BasicWidget的方法,我们用它来获取实例中存放的QWidget实例
    ((QWidget *)obj->getWidget())->show();
  }

  static inline Nan::Persistent<v8::Function> & constructor() {
    static Nan::Persistent<v8::Function> my_constructor;
    return my_constructor;
  }

};

这段代码和最开始的原生模块代码没有太大区别。这里我给Widget挂了一个方法,叫Show,因为我们需要这个方法来告诉Qt,我们需要QWidget显示出来。

在JS一侧:

class Widget extends Component {
  children = [];
  widget = null;

  constructor(root, props) {
    super(root, props);
    this.root = root;
    this.props = {
      ...props,
    };
    // 调用原生模块中的方法来创建组件
    this.widget = createWidget();
  }

  render(parent) {
    this.renderChildren(this);
    // 在组件渲染时通知Qt显示对应的`QWidget`
    this.widget.show();
  }
}

这个Widget类实例化出的对象,和QWidget的实例是一一对应的,我们需要做的就是把两边的方法桥接起来。

至此,我们已经可以成功地展示出一个 Qt 组件了:

class Example extends Component {

  render() {
    return (
      <Widget></Widget>
    );
  }
}

render(<Example/>);

实际效果: