鸿蒙“小程序”工作原理研究笔记

1. 介绍

自从微信小程序出现以来,各种“小程序”如雨后春笋一般出现。事实证明小程序这种开发方式非常好,鸿蒙 JS UI 框架采用类似的方式也是在意料之中的。

一个小程序(在鸿蒙 OS 中,也就是 Ability)由多个页面组成,每个页面由三部分组成:

  • .hml 用来描述界面的元素

  • .css 用来描述界面的风格

  • .js 用来编写处理事件逻辑

我们来看个例子:

index.hml

<div class="container">
    <text>{{count}}</text>
    <input if="{{count < 10}}"type="button" value="Inc" οnclick="inc"/>
    <input if="{{count > 0}}" type="button" value="Dec" onclick="dec"/>
</div>

index.css

.container {
    flex-direction: column;
    justify-content: center;
    align-items: center;
    left: 0px;
    top: 0px;
    width: 454px;
    height: 454px;
}

index.js

export default {
    data: {
        count: 5
    },

    inc() {
        this.count++;
    },

    dec() {
        this.count--;
    }
}

2. 工作原理

要理解它的工作原理,先研究一下编译之后的代码是非常重要的。上面的三个文件,编译之后会生成一个文件,其位置在:./entry/build/intermediates/res/debug/lite/assets/js/default/pages/index/index.js

index.hml 变成了创建函数:

module.exports = function (vm) {
  var _vm = vm || this; return _c('div',
    {
      'staticClass': [
        "container"
      ]
    },
    [_c('text',
      {
        'attrs': {
          'value': function () {
            return _vm.count
          }
        }
      }),
    _i((function () {
      return _vm.count < 10
    }),
      function () {
        return _c('input',
          {
            'attrs': {
              'type': "button", 'value': "Inc"
            }, 'on': {
              'click': _vm.inc
            }
          })
      }), _i((function () {
        return _vm.count > 0
      }), function () {
        return _c('input',
          {
            'attrs': {
              'type': "button", 'value': "Dec"
            }, 'on': {
              'click': _vm.dec
            }
          })
      })
    ])
}

index.css 变成了 JSON 文件。

{
  "classSelectors": {
    "container": {
      "flexDirection": "column",
      "justifyContent": "center",
      "alignItems": "center",
      "left": 0,
      "top": 0,
      "width": 454,
      "height": 454
    }
  }
}

这种处理方式很妙,把 JS 不擅长处理的 XML/CSS 转换成了 JS 代码和 JSON 对象,这个转换由工具完成,避免了运行时的开销。

在没有研究编译之后的代码时,我尝试在 ace/graphic 两个包中寻找解析 HML 的代码,让我惊讶的是没有找到相关代码。看了这些生成的代码之后才恍然大悟。

2.1 控件的创建

在 C++的代码中,创建控件的过程如下:

    1. 调用 JS 的 _c 函数时,进入 C++的函数 RenderModule::CreateElement
    1. RenderModule::CreateElement 的实现如下:

把 控件的 tag 转换成控件的 ID

uint16_t componentNameId = KeyParser::ParseKeyId(componentName, tagNameLength);

调用 ComponentFactory::CreateComponent 创建控件

Component *component = ComponentFactory::CreateComponent(componentNameId, options, children);

调用控件的 Render 创建真正的控件和子控件。

bool renderResult = component->Render();

2.2 页面的 Render 过程

那么第一个_c 函数是在哪里调用的呢?答案很明显,进入一个页面的时候,会调用 RenderPage 渲染页面:

void StateMachine::RenderPage()
{
    START_TRACING(RENDER);
    nativeElement_ = appContext_->Render(viewModel_);
    // append scroll layer to the outermost view
    scrollLayer_ = new ScrollLayer();
    if (scrollLayer_ != nullptr) {
        scrollLayer_->AppendScrollLayer(nativeElement_);
    }
    Component::HandlerAnimations();
    // trigger an async full GC after completing the heavy work, which will
    // be executed after the whole page showing process
    JsAsyncWork::DispatchAsyncWork(ForceGC, nullptr);
    STOP_TRACING();
}

这里会调用 JS 的 render 函数:

jerry_value_t JsAppContext::Render(jerry_value_t viewModel) const
{
    if (jerry_value_is_error(viewModel)) {
        HILOG_ERROR(HILOG_MODULE_ACE, "Failed to render app cause by render error.");
        return UNDEFINED;
    }

    if (jerry_value_is_undefined(viewModel)) {
        HILOG_ERROR(HILOG_MODULE_ACE, "Nothing to render as it is undefined.");
        return UNDEFINED;
    }

    jerry_value_t renderFunction = jerryx_get_property_str(viewModel, ATTR_RENDER);
    if (jerry_value_is_undefined(renderFunction)) {
        ACE_ERROR_CODE_PRINT(EXCE_ACE_ROUTER_REPLACE_FAILED, EXCE_ACE_PAGE_NO_RENDER_FUNC);
        return UNDEFINED;
    }
    jerry_value_t nativeElement = CallJSFunction(renderFunction, viewModel, nullptr, 0);
    if (jerry_value_is_undefined(nativeElement)) {
        ACE_ERROR_CODE_PRINT(EXCE_ACE_ROUTER_REPLACE_FAILED, EXCE_ACE_PAGE_RENDER_FAILED);
    }
    jerry_release_value(renderFunction);
    return nativeElement;
}

这里的 renderFunction 又是哪个函数呢?这需要看看 ViewModel 的构造函数:

export function ViewModel(options) {
  startTracing(PHASE_FWK_EVAL);
  if (!(this instanceof ViewModel)) {
    return new ViewModel(options);
  }

  const vm = (this._vm = this);

  if (Object.prototype.toString.call(options) === '[object Object]') {
    Object.keys(options).forEach(key => {
      const value = options[key];
      if (key === 'render') {
        vm.$render = value;
      } else if (key === 'data') {
        initState(vm, value);
      } else if (key === 'styleSheet') {
        initStyleSheet(value);
      } else if (typeof value === 'function') {
        vm[key] = value.bind(vm);
      } else {
        // do nothing
      }
    });
  }

  stopTracing();
}

这里的$render 是由 options 提供的,而 options 又是从哪里来的呢?再来看看编译后的代码:

/***/ (function(module, exports, __webpack_require__) {

var $app_template$ = __webpack_require__(4)
var $app_style$ = __webpack_require__(5)
var $app_script$ = __webpack_require__(6)
var options=$app_script$
 if ($app_script$.__esModule) {

      options = $app_script$.default;
 }
options.styleSheet=$app_style$
options.render=$app_template$;
module.exports=new ViewModel(options);

把 options.render 打印出来,可以看到

function (vm) { var _vm = vm || this; return _c('div', {'staticClass' : ["container"]} , [_c('text', {'attrs' : {'value' : function () {return _vm.count}}} ),_i((fun
ction () {return _vm.count<10}),function(){return _c('input', {'attrs' : {'type' : "button",'value' : "Inc"},'on' : {'click' : _vm.inc}} )}),_i((function () {return
_vm.count>0}),function(){return _c('input', {'attrs' : {'type' : "button",'value' : "Dec"},'on' : {'click' : _vm.dec}} )})] ) }

正是前面那个创建控件的函数。

2.3 数据变化更新

从前面的代码,很容易理解初始渲染时数据的绑定过程。比如 text 的 value 属性从一个匿名函数获取,它包装了 xml 中些的表达式:

    <text>{{count}}</text>
_c('text',
      {
        'attrs': {
          'value': function () {
            return _vm.count
          }
        }
      }),

而运行时,数据变化时,需要监控匿名函数的值变化,变化时会调用下面这个函数,它负责更新控件的属性。

jerry_value_t WatcherCallbackFunc(const jerry_value_t func,
                                  const jerry_value_t context,
                                  const jerry_value_t *args,
                                  const jerry_length_t argsLength)
{
    if (argsLength != ARG_LENGTH_WATCHER_CALLBACK) {
        return UNDEFINED;
    }

    START_TRACING(WATCHER_CALLBACK_FUNC);
    jerry_value_t value = args[0];
    const uint8_t optionIndex = 2;
    jerry_value_t options = args[optionIndex];
    jerry_value_t nativeElement = jerryx_get_property_str(options, ARG_WATCH_EL);
    jerry_value_t attrKey = jerryx_get_property_str(options, ARG_WATCH_ATTR);

    Component *component = ComponentUtils::GetComponentFromBindingObject(nativeElement);
    if (component != nullptr) {
        uint16_t attrKeyStrLen = 0;
        char *attrKeyStr = MallocStringOf(attrKey, &attrKeyStrLen);
        if (attrKeyStr != nullptr) {
            uint16_t attrKeyId = KeyParser::ParseKeyId(attrKeyStr, attrKeyStrLen);
            bool updateResult = component->UpdateView(attrKeyId, value);
            if (updateResult) {
                component->Invalidate();
            }
            ace_free(attrKeyStr);
            attrKeyStr = nullptr;
        }
    }
    STOP_TRACING();
    ReleaseJerryValue(attrKey, nativeElement, VA_ARG_END_FLAG);
    return UNDEFINED;
}
jerry_value_t Component::ParseExpression(jerry_value_t attrKey, jerry_value_t attrValue)
{
    jerry_value_t options = jerry_create_object();
    JerrySetNamedProperty(options, ARG_WATCH_EL, nativeElement_);
    JerrySetNamedProperty(options, ARG_WATCH_ATTR, attrKey);
    jerry_value_t watcher = CallJSWatcher(attrValue, WatcherCallbackFunc, options);
    jerry_value_t propValue = UNDEFINED;
    if (IS_UNDEFINED(watcher) || jerry_value_is_error(watcher)) {
        HILOG_ERROR(HILOG_MODULE_ACE, "Failed to create Watcher instance.");
    } else {
        InsertWatcherCommon(watchersHead_, watcher);
        propValue = jerryx_get_property_str(watcher, "_lastValue");
    }
    jerry_release_value(options);
    return propValue;
}

在 initState 函数中,关注了 data 的变化:

function initState(vm, data) {
  startTracing(PHASE_INIT_STATE);

  startTracing(PHASE_INIT_DATA_GET_DATA);
  if (typeof data === 'function') {
    data = data.call(vm, vm);
  }
  stopTracing(); // PHASE_INIT_DATA_GET_DATA

  startTracing(PHASE_INIT_DATA_PROXY);
  Object.keys(data).forEach(key => proxy(vm, data, key));
  stopTracing(); // PHASE_INIT_DATA_PROXY

  startTracing(PHASE_INIT_DATA_OBSERVE);
  Subject.of(data);
  stopTracing(); // PHASE_INIT_DATA_OBSERVE

  stopTracing(); // PHASE_INIT_STATE
}

2.4 条件渲染

条件渲染由_i 函数处理,它有两个参数:

  • 第一个参数是一个函数,决定是否进行渲染。

  • 第二个参数是一个函数,决定如何进行渲染。

如:

<input if="{{count < 10}}"type="button" value="Inc" οnclick="inc"/>

编译成:

_i((function () {
      return _vm.count < 10
    }),
      function () {
        return _c('input',
          {
            'attrs': {
              'type': "button", 'value': "Inc"
            }, 'on': {
              'click': _vm.inc
            }
          })
      }),

C++中对应这个函数:

jerry_value_t RenderModule::ConditionalRender(const jerry_value_t func,
                                              const jerry_value_t context,
                                              const jerry_value_t *args,
                                              const jerry_length_t argsNum)
{
    if (argsNum != ARG_LENGTH_RENDER) {
        HILOG_ERROR(HILOG_MODULE_ACE, "Failed to ConditionalRender cause by invlaid paramters. Expect 2 but %d",
                    argsNum);
        return UNDEFINED;
    }

    return DescriptorUtils::CreateIfDescriptor(args[0], args[1]);
}

数据变化时,重新进行根据条件进行渲染,执行下面的函数:

JSValue DirectiveWatcherCallback::Handler(const JSValue func,
                                          const JSValue context,
                                          const JSValue args[],
                                          const JSSize argsSize)
{
    if (argsSize != CALLBACK_ARGS_LENGTH) {
        return JSUndefined::Create();
    }

    JSValue options = args[ARG_IDX_OPTIONS];

    JSValue parentElement = JSObject::Get(options, DescriptorUtils::WATCHER_OPTION_ELEMENT);
    JSValue descriptor = JSObject::Get(options, DescriptorUtils::WATCHER_OPTION_DESCRIPTOR);

    Component *component = ComponentUtils::GetComponentFromBindingObject(parentElement);
    if (component) {
        component->HandleChildrenChange(descriptor);
    }
    JSRelease(descriptor);
    JSRelease(parentElement);
    return JSUndefined::Create();
}

关注数据变化的回调函数。

void Component::AppendChildren(UIViewGroup *viewGroup)
{
    if (JSUndefined::Is(descriptors_)) {
        return;
    }

    children_ = JSArray::Create(0);
    uint16_t size = JSArray::Length(descriptors_);
    for (uint16_t index = 0; index < size; ++index) {
        JSValue descriptorOrElement = JSArray::Get(descriptors_, index);
        if (!JSUndefined::Is(descriptorOrElement)) {
            bool isDescriptor = AppendDescriptorOrElement(viewGroup, descriptorOrElement);
            if (isDescriptor) {
                CreateDirectiveWatcher(descriptorOrElement);
            }
        }
        JSRelease(descriptorOrElement);
    }
}

void Component::CreateDirectiveWatcher(jerry_value_t descriptor)
{
    JSValue watcher = DescriptorUtils::CreateDescriptorWatcher(nativeElement_, descriptor);
    if (!JSUndefined::Is(watcher)) {
        InsertWatcherCommon(watchersHead_, watcher);
    }
}

这个做法我觉得很有创意:

  • 不需要搞虚拟 DOM 进行比较。

  • 不需要重新建立整个 DOM。

可以避免低端嵌入式平台的性能问题。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值