
众所周知,刚刚开源的「鸿蒙 2.0」以 JavaScript 作为 IoT 应用开发的框架语言。这标志着继 SpaceX 上天之后,JavaScript 再一次蹭到了新闻联播级的热点。这么好的机会,只拿来阴阳怪气实在太可惜了。作为科普,这篇文章不会拿着放大镜找出代码中的槽点来吹毛求疵,而是希望通俗地讲清楚它所支持的 GUI 到底是怎么一回事。只要对计算机基础有个大概的了解,应该就不会对本文有阅读上的障碍。
我们已经知道在「鸿蒙 2.0」上,开发者只需编写形如 Vue 组件式的 JavaScript 业务逻辑,即可将其渲染为智能手表等嵌入式硬件上的 UI 界面。这个过程中需要涉及哪些核心的模块呢?这些模块中又有哪些属于自研,哪些使用了现成的开源项目呢?这里将其分为自上而下的三个抽象层来介绍:
JS 框架层,可理解为一个大幅简化的 Vue 式 JavaScript 框架
JS 引擎与运行时层,可理解为一个大幅简化的 WebKit 式运行时
图形渲染层,可理解为一个大幅简化的 Skia 式图形绘制库
JS 框架层
从最顶层的视角出发,要想用「鸿蒙 2.0」渲染出一段动态的文本,你只需要编写如下的 HML(类 XML)格式代码:
<!-- hello.hml -->
<text onclick="boil">{{hello}}</text>
然后在同级目录编写这样的 JavaScript:
// hello.js
exportdefault {
data: {
hello: 'PPT'},
boil() {
this.hello = '核武器'; }}
这样只要点击文本,就会调用 boil
方法,让 PPT
变成 核武器
。
这背后发生了什么呢?熟悉 Vue 2.0 的同学应该会立刻联想到下面这几件事:
需要对 XML 的预处理机制,将其转换为 JS 中的嵌套函数结构。这样只需在运行时做一次简单 eval ,即可用 JS 生成符合 XML 结构的 UI。
需要事件机制,使得触发
onclick
事件时能执行相应回调。需要数据劫持机制,使得对
this.hello
赋值时能执行相应回调。需要能在回调中更新 UI 对象控件。
这几件事分别是怎么实现的呢?简单说来是这样的:
XML 预处理依赖现成的 NPM 开源包,从而把 XML 中的
onclick
属性转换为 JS 对象的属性字段。事件的注册和触发都直接由 C++ 实现。如上一步所获得的 JS 对象
onclick
属性会在 C++ 中被检查和注册,相当于全部组件均为原生。数据劫持机制用 JS 实现,是个基于
Object.defineProperty
的(几百行量级的)ViewModel。UI 控件的更新,会在 ViewModel 自动执行的 JS 回调中,调用 C++ 的原生方法实现。这部分完全隐式完成,并未开放
document.createElement
式的标准化 API。
由于大量常见 JS 框架中的能力都直接做进了 C++,所以整套 GUI 技术栈里用纯 JavaScript 所实现的东西(主要见 ace_lite_jsfwk
仓库下的 core/index.js
、observer.js
和 subject.js
),相当于有且只有这么一个功能:
一个可以 watch 的 ViewModel。
至于纯 JS 框架部分的实现复杂度和质量,客观地说如果是个人业余作品,可以当作校招面试中不错的加分项。
JS 引擎与运行时层
理解了 JS 框架层之后,我们既可以认为「鸿蒙 2.0」选择把高度简化后的 Vue 深度定制进了 C++ 里,也可以认为它紧密围绕着高度简化(且私有)的 DOM 实现了配套的前端框架。因此要想继续探索这套 GUI 的原理,我们就必须进入其 C++ 部分,了解其 JS 引擎与运行时层的实现。
JS 引擎和运行时之间,有什么区别与联系呢?JS 引擎一般只需符合 ECMA-262 规范,其中没有对任何带「副作用」的平台 API 的定义。从 setTimeout
到 document.getElementById
到 console.log
再到 fs.readFile
,这些能执行实际 IO 操作的功能,都需要由「将引擎 API 和平台 API 胶合到一起」的运行时提供。运行时本身的原理并不复杂,譬如在个人的文章《从 JS 引擎到 JS 运行时》中,你就可以看到如何借助现成的 QuickJS 引擎,自己搭建一个运行时。
JS 引擎选择了 JerryScript,这是一款由三星开发的嵌入式 JS 引擎。 每种形如 <text>
和<div>
的 XML 标签组件,都对应一个绑定到 JerryScript 上的 C++ Component 类,如TextComponent
和DivComponent
等。除 UI 原生对象外,还有一系列在 JS 中以 @system
为前缀的 built-in 模块,它们提供了 JS 中可用的 Router Audio File 等平台能力(参见ohos_module_config.h
)。
router_module.cpp、
js_router.cpp和
js_page_state_machine.cpp)。简单说来这个「路由」是这样实现的:
在 JS 中调用切换页面的 router.replace
原生方法,走进 C++。C++ 中根据新页面 URI 路径(如 pages/detail
)加载新页面 JS,新建页面状态机实例,将其切换至 Init 状态。在新状态机的 Init 过程中,调用 JS 引擎去 eval 新页面的 JS 代码,获得新页面的 ViewModel。 将路由参数附加到 ViewModel 上,销毁旧状态机及其上的 JS 对象。

JerryScript 在体积和内存占用上,相比 QuickJS 有更好的表现。 JerryScript 的稳定性弱于 QuickJS,有一些难以绕过的问题。 JerryScript 面对稍大(1M 以上)的 JS 代码库,就有些力不从心了。
图形绘制层
graphic_lite仓库了。可以认为,这里才是真正执行实际绘制的 GUI。像之前的
TextComponent等原生组件,都会对应到这里的某种图形库 View。它以一种相当经典的方式,在 C++ 层实现并提供了「Canvas 风格的立即模式 GUI」和「DOM 风格的保留模式 GUI」两套 API 体系(对于立即模式和保留模式 GUI 的区别与联系,可参见个人这篇 IMGUI 科普回答)。概括说来,这个图形子系统的要点大致如下:
图形库提供了 UIView
这个 C++ 控件基类,其中有一系列形如OnClick
OnLongPress
OnDrag
的虚函数。基本每种 JS 中可用的原生 Component 类,都对应于一种 UIView 的子类。除了各种定制化 View 之外,它还开放了一系列形如 DrawLine
DrawCurve
DrawText
等命令式的绘制方法。这个图形库具备名为 GFX 的 GPU 加速模块,但它目前似乎只有象征性的 FillArea
矩形单色填充能力。
支持了简易的 RecycleView 长列表。 支持了简易的 Flex 布局。 支持了内部的 Invalidate 脏标记更新机制。
libpng和
libjpeg做图像解码,然后即可使用内存中的 bitmap 图像做绘制。
void DrawCurve::DrawCubicBezier(const Point& start, const Point& control1, const Point& control2,const Point& end,
const Rect& mask, int16_t width, const ColorType& color, OpacityType opacity)
{
if (width == 0 || opacity == OPA_TRANSPARENT) {
return;
}
Point prePoint = start;
for (int16_t t = 1; t <= INTERPOLATION_RANGE; t++) {
Point point;
point.x = Interpolation::GetBezierInterpolation(t, start.x, control1.x, control2.x, end.x);
point.y = Interpolation::GetBezierInterpolation(t, start.y, control1.y, control2.y, end.y);
if (prePoint.x == point.x && prePoint.y == point.y) {
continue;
}
DrawLine::Draw(prePoint, point, mask, width, color, opacity);
prePoint = point;
}
}
INTERPOLATION_RANGE)作为插值输入,逐点计算出曲线表达式的 XY 坐标,然后直接修改像素位置所在的 framebuffer 内存即可。这种教科书式的实现是最经典的,不过如果要拿它对标 Skia 里的黑魔法,还是不要勉为其难了吧。
harfbuzz
- 用来告诉调用者,应该把「牢」的 glyph 字形放在哪里。freetype
- 从宋体、黑体等字体文件中解码出「牢」的 glyph 字形,将其光栅化为像素。icu
- 处理 Unicode 中许多奇葩的特殊情况,这块个人不了解,略过。
JS 中执行 this.hello = 'PPT'
之类的代码,触发依赖追踪。JS 依赖追踪回调触发原生函数,更新 C++ 的 Component 组件状态。 Component 更新其绑定的 UIView 子类状态,触发图形库更新。 图形库更新内存中的像素状态,完成绘制。
总结
特别声明:本部分主观评论仅针对「鸿蒙 2.0」当前的 GUI 框架部分,请勿随意曲解。
确实有务实(但和当年 PPT 介绍完全两码事)的代码。 不是 WebView 套壳,布局和绘制是自己做的。 无需超过大学本科水平的计算机知识,也能顺利阅读理解。
JS 框架层 没有基本的组件间通信(如 props emit 等)能力 没有基本的自定义组件能力 没有除基础依赖追踪以外的状态管理能力 JS 引擎与运行时层 标准支持过低,无法运行 Vue 3.0 这类需 Proxy 的下一代前端框架 性能水平弱,难以支持中大型 JS 应用 没有开放 DOM 式的对象模型 API,不利于上层抹平差异 图形渲染层 没有实质可用的 GPU 加速 没有 SVG 和富文本等高级渲染能力 Canvas 完成度低,缺状态栈和很多 API
当然,汽车厂商也不会说自己造的是飞机,对吧?
作者:doodlewind
链接:https://juejin.im/post/6872154561574862855
来源:掘金
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com

IT大咖说 | 关于版权
由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!
相关推荐
推荐文章




