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++的代码中,创建控件的过程如下:
-
- 调用 JS 的 _c 函数时,进入 C++的函数 RenderModule::CreateElement
-
- 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。
可以避免低端嵌入式平台的性能问题。