近日,鸿蒙终于发布了,开发者们也着实“沸腾”了。笔者也第一时间下载了源码,研究了一个晚上,顺带写了一个 hello world 程序,还顺手给鸿蒙文档提了 2 个 PR。

当然我最感兴趣的就是鸿蒙的 JS 框架 ace_lite_jsfwk,从名字中可以看出来这是一个非常轻量级的框架,官方介绍说是“轻量级 JS 核心开发框架”。

当我看完源码后发现它确实轻。其核心代码只有 5 个 js 文件,大概也就 300-400 行代码吧。(没有单元测试)

  • runtime-core\src\core\index.js

  • runtime-core\src\observer\observer.js

  • runtime-core\src\observer\subject.js

  • runtime-core\src\observer\utils.js

  • runtime-core\src\profiler\index.js

从名字可以看出来,这些代码实现了一个观察者模式。也就是说,它实现了一个非常轻量级的 MVVM 模式。通过使用和 vue2 相似的属性劫持技术实现了响应式系统。这个应该是目前培训班的“三大自己实现”之一了吧。(自己实现 Promise,自己实现 vue,自己实现 react)

utils 里面定义了一个 Observer 栈,存放了观察者。subject 定义了被观察者。当我们观察某个对象时,也就是劫持这个对象属性的操作,还包括一些数组函数,比如 push、pop 等。这个文件应该是代码最多的,160 行。observer 的代码就更简单了,五六十行。

而当我们开发的时候,通过 Toolkit 将开发者编写的 HML、CSS 和 JS 文件编译打包成 JS Bundle,然后再将 JS Bundle 解析运行成C++ native UI 的 View 组件进行渲染。

“通过支持三方开发者使用声明式的 API 进行应用开发,以数据驱动视图变化,避免了大量的视图操作,大大降低了应用开发难度,提升开发者开发体验”。基本上就是一个小程序式的开发体验。

在 src\core\base\framework_min_js.h 文件中,这段编译好的 js 被编译到了 runtime 里面。编译完的 js 文件不到 3K,确实够轻量。

js runtime 没有使用 V8,也没有使用 jscore。而是选择了 JerryScript。JerryScript 是用于物联网的超轻量 JavaScript 引擎。它能够在内存少于 64 KB 的设备上执行 ECMAScript 5.1 源代码。这也是为什么在文档中说鸿蒙 JS 框架支持 ECMAScript 5.1 的原因。

从整体看这个 js 框架大概使用了 96% 的 C/C++ 代码,1.8% 的 JS 代码。在 htm 文件中写的组件会被编译为原生组件。而 app_style_manager.cpp 和同级的七八个文件则用来解析 css,最终生成原生布局。

虽然在 SDK 中有几个 weex 包,也发现了 react 的影子。但是在 C/C++ 代码中并没有看到 yoga 相关的内容(全局搜索没发现)。而 SDK 中的那些包仅仅是做 loader 用的,大概是为了在 webpack 打包时解析 htm 组件用的。将 htm 的 template 编译为 js 代码。

整体而言,比预想的要好一些。

下面我们就来逐行分析鸿蒙系统中的 JS 框架。

文中的所有代码都基于鸿蒙的当前最新版(版本为 677ed06,提交日期为 2020-09-10)。

鸿蒙系统使用 JavaScript 开发 GUI 是一种类似于微信小程序、轻应用的模式。而这个 MVVM 模式中,V 其实是由 C++ 来承担的。JavaScript 代码只是其中的 ViewModel 层。

鸿蒙 JS 框架是零依赖的,只在开发打包过程中使用到了一些 npm 包。打包完之的代码是没有依赖任何 npm 包的。我们先看一下使用鸿蒙 JS 框架写出来的 JS 代码到底长什么样。

export default {data() {return { count: 1 };},increase() {++this.count;},decrease() {--this.count;},
}

如果我不告诉你这是鸿蒙,你甚至会以为它是 vue 或小程序。如果单独把 JS 拿出来使用(脱离鸿蒙系统),代码是这样:

const vm = new ViewModel({data() {return { count: 1 };},increase() {++this.count;},decrease() {--this.count;},
});console.log(vm.count); // 1vm.increase();
console.log(vm.count); // 2vm.decrease();
console.log(vm.count); // 1

仓库中的所有 JS 代码实现了一个响应式系统,充当了 MVVM 中的 ViewModel。

下面我们逐行分析。

src 目录中一共有 4 个目录,总计 8 个文件。其中 1 个是单元测试。还有 1 个性能分析。再除去 2 个 index.js 文件,有用的文件一共是 4 个。也是本文分析的重点。

src
├── __test__
│   └── index.test.js
├── core
│   └── index.js
├── index.js
├── observer
│   ├── index.js
│   ├── observer.js
│   ├── subject.js
│   └── utils.js
└── profiler└── index.js

首先是入口文件,src/index.js,只有 2 行代码:

import { ViewModel } from './core';
export default ViewModel;

其实就是重新导出。

另一个类似的文件是 src/observer/index.js,也是 2 行代码:

export { Observer } from './observer';
export { Subject } from './subject';

observer 和 subject 实现了一个观察者模式。subject 是主题,也就是被观察者。observer 是观察者。当 subject 有任何变化时需要主动通知被观察者。这就是响应式。

这 2 个文件都使用到了 src/observer/utils.js,所以我们先分析一下 utils 文件。分 三部分。

第一部分

export const ObserverStack = {stack: [],push(observer) {this.stack.push(observer);},pop() {return this.stack.pop();},top() {return this.stack[this.stack.length - 1];}
};

首先是定义了一个用来存放观察者的栈,遵循后进先出的原则,内部使用 stack 数组来存储。

  • 入栈操作 push,和数组的 push 函数一样,在栈顶放入一个观察者 observer。

  • 出栈操作 pop,和数组的 pop 函数一样,在将栈顶的观察者删除,并返回这个被删除的观察者。

  • 取栈顶元素 top,和 pop 操作不同,top 是把栈顶元素取出来,但是并不删除。

第二部分

export const SYMBOL_OBSERVABLE = '__ob__';
export const canObserve = target => typeof target === 'object';

定义了一个字符串常量 SYMBOL_OBSERVABLE。为了后面用着方便。

定义了一个函数 canObserve,目标是否可以被观察。只有对象才能被观察,所以使用 typeof 来判断目标的类型。等等,好像有什么不对。如果 target 为 null 的话,函数也会返回 true。如果 null 不可观察,那么这就是一个 bug。(写这篇文章的时候我已经提了一个 PR,并询问了这种行为是否是期望的行为)。

第三部分

export const defineProp = (target, key, value) => {Object.defineProperty(target, key, { enumerable: false, value });
};

这个没有什么好解释的,就是 Object.defineProperty 代码太长了,定义一个函数来避免代码重复。

下面再来分析观察者 src/observer/observer.js,分四部分。

第一部分

export function Observer(context, getter, callback, meta) {this._ctx = context;this._getter = getter;this._fn = callback;this._meta = meta;this._lastValue = this._get();
}

构造函数。接受 4 个参数。

context 当前观察者所处的上下文,类型是 ViewModel。当第三个参数 callback 调用时,函数的 this 就是这个 context

getter 类型是一个函数,用来获取某个属性的值。

callback 类型是一个函数,当某个值变化后执行的回调函数。

meta 元数据。观察者(Observer)并不关注 meta 元数据。

在构造函数的最后一行,this._lastValue = this._get()。下面来分析 _get 函数。

第二部分

Observer.prototype._get = function() {try {ObserverStack.push(this);return this._getter.call(this._ctx);} finally {ObserverStack.pop();}
};

ObserverStack 就是上面分析过的用来存储所有观察者的栈。将当前观察者入栈,并通过 _getter 取得当前值。结合第一部分的构造函数,这个值存储在了 _lastValue 属性中。

执行完这个过程后,这个观察者就已经初始化完成了。

第三部分

Observer.prototype.update = function() {const lastValue = this._lastValue;const nextValue = this._get();const context = this._ctx;const meta = this._meta;if (nextValue !== lastValue || canObserve(nextValue)) {this._fn.call(context, nextValue, lastValue, meta);this._lastValue = nextValue;}
};

这部分实现了数据更新时的脏检查(Dirty checking)机制。比较更新后的值和当前值,如果不同,那么就执行回调函数。如果这个回调函数是渲染 UI,那么则可以实现按需渲染。如果值相同,那么再检查设置的新值是否可以被观察,再决定到底要不要执行回调函数。

第四部分

Observer.prototype.subscribe = function(subject, key) {const detach = subject.attach(key, this);if (typeof detach !== 'function') {return;}if (!this._detaches) {this._detaches = [];}this._detaches.push(detach);
};Observer.prototype.unsubscribe = function() {const detaches = this._detaches;if (!detaches) {return;}while (detaches.length) {detaches.pop()();}
};

订阅与取消订阅。

我们前面经常说观察者和被观察者。对于观察者模式其实还有另一种说法,叫订阅/发布模式。而这部分代码则实现了对主题(subject)的订阅。

先调用主题的 attach 方法进行订阅。如果订阅成功,subject.attach 方法会返回一个函数,当调用这个函数就会取消订阅。为了将来能够取消订阅,这个返回值必需保存起来。

subject 的实现很多人应该已经猜到了。观察者订阅了 subject,那么 subject 需要做的就是,当数据变化时即使通知观察者。subject 如何知道数据发生了变化呢,机制和 vue2 一样,使用 Object.defineProperty 做属性劫持。

下面再来分析观察者 src/observer/subject.js,分七部分。

第一部分

export function Subject(target) {const subject = this;subject._hijacking = true;defineProp(target, SYMBOL_OBSERVABLE, subject);if (Array.isArray(target)) {hijackArray(target);}Object.keys(target).forEach(key => hijack(target, key, target[key]));
}

构造函数。基本没什么难点。设置 _hijacking 属性为 true,用来标示这个对象已经被劫持了。Object.keys 通过遍历来劫持每个属性。如果是数组,则调用 hijackArray

第二部分

两个静态方法。

Subject.of = function(target) {if (!target || !canObserve(target)) {return target;}if (target[SYMBOL_OBSERVABLE]) {return target[SYMBOL_OBSERVABLE];}return new Subject(target);
};Subject.is = function(target) {return target && target._hijacking;
};

Subject 的构造函数并不直接被外部调用,而是封装到了 Subject.of 静态方法中。

如果目标不能被观察,那么直接返回目标。

如果 target[SYMBOL_OBSERVABLE] 不是 undefined,说明目标已经被初始化过了。

否则,调用构造函数初始化 Subject。

Subject.is 则用来判断目标是否被劫持过了。

第三部分

Subject.prototype.attach = function(key, observer) {if (typeof key === 'undefined' || !observer) {return;}if (!this._obsMap) {this._obsMap = {};}if (!this._obsMap[key]) {this._obsMap[key] = [];}const observers = this._obsMap[key];if (observers.indexOf(observer) < 0) {observers.push(observer);return function() {observers.splice(observers.indexOf(observer), 1);};}
};

这个方法很眼熟,对,就是上文的 Observer.prototype.subscribe 中调用的。作用是某个观察者用来订阅主题。而这个方法则是“主题是怎么订阅的”。

观察者维护这一个主题的哈希表 _obsMap。哈希表的 key 是需要订阅的 key。比如某个观察者订阅了 name 属性的变化,而另一个观察者订阅了 age 属性的变化。而且属性的变化还可以被多个观察者同时订阅,因此哈希表存储的值是一个数组,数据的每个元素都是一个观察者。

第四部分

Subject.prototype.notify = function(key) {if (typeof key === 'undefined' ||!this._obsMap ||!this._obsMap[key]) {return;}this._obsMap[key].forEach(observer => observer.update());
};

当属性发生变化是,通知订阅了此属性的观察者们。遍历每个观察者,并调用观察者的 update 方法。我们上文中也提到了,脏检查就是在这个方法内完成的。

第五部分

Subject.prototype.setParent = function(parent, key) {this._parent = parent;this._key = key;
};Subject.prototype.notifyParent = function() {this._parent && this._parent.notify(this._key);
};

这部分是用来处理属性嵌套(nested object)的问题的。就是类似这种对象:{ user: { name: 'JJC' } }

第六部分

function hijack(target, key, cache) {const subject = target[SYMBOL_OBSERVABLE];Object.defineProperty(target, key, {enumerable: true,get() {const observer = ObserverStack.top();if (observer) {observer.subscribe(subject, key);}const subSubject = Subject.of(cache);if (Subject.is(subSubject)) {subSubject.setParent(subject, key);}return cache;},set(value) {cache = value;subject.notify(key);}});
}

这一部分展示了如何使用 Object.defineProperty 进行属性劫持。当设置属性时,会调用 set(value),设置新的值,然后调用 subject 的 notify 方法。这里并不进行任何检查,只要设置了属性就会调用,即使属性的新值和旧值一样。notify 会通知所有的观察者。

第七部分

劫持数组方法。

const ObservedMethods = {PUSH: 'push',POP: 'pop',UNSHIFT: 'unshift',SHIFT: 'shift',SPLICE: 'splice',REVERSE: 'reverse'
};const OBSERVED_METHODS = Object.keys(ObservedMethods).map(key => ObservedMethods[key]
);

ObservedMethods 定义了需要劫持的数组函数。前面大写的用来做 key,后面小写的是需要劫持的方法。

function hijackArray(target) {OBSERVED_METHODS.forEach(key => {const originalMethod = target[key];defineProp(target, key, function() {const args = Array.prototype.slice.call(arguments);originalMethod.apply(this, args);let inserted;if (ObservedMethods.PUSH === key || ObservedMethods.UNSHIFT === key) {inserted = args;} else if (ObservedMethods.SPLICE) {inserted = args.slice(2);}if (inserted && inserted.length) {inserted.forEach(Subject.of);}const subject = target[SYMBOL_OBSERVABLE];if (subject) {subject.notifyParent();}});});
}

数组的劫持和对象不同,不能使用 Object.defineProperty

我们需要劫持 6 个数组方法。分别是头部添加、头部删除、尾部添加、尾部删除、替换/删除某几项、数组反转。

通过重写数组方法实现了数组的劫持。但是这里有一个需要注意的地方,数据的每一个元素都是被观察过的,但是当在数组中添加了新元素时,这些元素还没有被观察。因此代码中还需要判断当前的方法如果是 pushunshiftsplice,那么需要将新的元素放入观察者队列中。

另外两个文件分别是单元测试和性能分析,这里就不再分析了。

RECOMMEND

推荐阅读

01

《JavaScript权威指南(第6版)》

作者:[美] David Flanagan

译者:淘宝前端团队

ISBN:978-7-111-37661-3

长按识别二维码了解详情并购买

卖点:

  • 经典的JavaScript工具书,从1996年以来,本书已经成为JavaScript程序员心中公认的的权威指南;

  • 凭着完整的内容、细致的讲解以及海量针对性的示例而受到读者的一致好评;

  • 适合希望学习Web编程语言的初、中级程序员和希望精通JavaScript的程序员阅读。

推荐语:

这本厚达1010页的巨著主要讲述的内容涵盖JavaScript语言本身,以及Web浏览器所实现的JavaScriptAPI。初学者读完本书,将会对JS有全面的认识,快速掌握JS最核心的技术。而有经验的开发者读完本书,会让你对JS的理解有从量变到质变的深层次飞跃。

02

《JavaScript编程精解(原书第3版)》

作者:[美]Marijn Haverbeke

译者:卢涛、李颖

ISBN:978-7-111-64836-9

长按识别二维码了解详情并购买

卖点:

  • JavaScript之父Brendan Eich强力推荐;

  • 根据ES6新功能全面更新;

  • 系统介绍如何编写高效的代码。在游戏式开发中学会JS程序设计。

推荐语:

比肩犀牛书的《JavaScript编程精解》中文版更新第3版。本书从JavaScript的基本语言特性入手,提纲挈领地介绍JavaScript的主要功能和特色,包含实战章节及总结、习题,配套码源提供下载。帮助你快速入门,循序渐进地掌握基本的编程概念、技术和思想。是系统学习JS的优选之作。

更多精彩回顾

书讯 |10月书讯(下)| 双节同庆,读书正当时

书讯 |10月书讯(上)| 双节同庆,读书正当时

资讯 |TIOBE 9 月编程语言:C++ 突起、Java 流行度下降

上新 | 一本书带你吃透Nginx应用与运维
书单 | 开学季——计算机专业学生必读的10本畅销经典

干货 | 用户画像从0到100的构建思路

收藏 | 更新!更薄!更精华:《JavaScript编程精解》来了

视频 | 大佬出镜推荐不可不读系列———B站知名Up主寒食君

赠书 |【第23期】令人舒心又伤脑的12张数学原理动图!你能看懂几个

逐行分析鸿蒙系统的 JavaScript 开发框架相关推荐

  1. 概括鸿蒙系统的优势,从开发语言分析鸿蒙系统有何优势

    华为 鸿蒙系统 采用开源的方式,源代码已经在官方公布的网站挂出,感兴趣的可以自行下载.根据其源代码来看,内核基于 C++ 语言开发,部分功能模块通过C语言以及C++混合编写.总体上来看,鸿蒙系统是以C ...

  2. 鸿蒙系统未来跟苹果能,华为推出鸿蒙2.0系统,想要超越谷歌与苹果?未来可期...

    序言 9月10日,华为发布了鸿蒙系统2.0和建设"软硬件双轮驱动"的 全场景智慧生态的目标. 面对美国的制裁,华为已经做好了"最坏"打算,放弃"幻想& ...

  3. 小米高管鸿蒙测试,小米在测试华为鸿蒙系统?高管侧面给出回应,网友的结论很真实...

    这段时间,关于华为鸿蒙系统的消息有不少.并且据说,国内有不少手机厂商,像vivo.OPPO以及小米,都是有参与测试华为鸿蒙系统.而对于这件事,无论是华为还是另外三家官方,都并没有正面给出回应.不过,就 ...

  4. 关于鸿蒙系统 JS UI 框架源码的分析

    鸿蒙是华为研发的新一代终端操作系统,能适用于 IoT.手表.手机.Pad.电视等各种类型的设备上,扛起"国产操作系统"的大旗,也遭受了很多非议.2021 年 6 月初发布了 Ope ...

  5. 鸿蒙系统评论简单分析(nlp)

    NLP学习 实战1 鸿蒙系统评论简单分析(nlp) 前言 随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文将介绍nlp中常见的情感分析.其中数据来源于B站某些关于 ...

  6. Javascript 匀速运动停止条件——逐行分析代码,让你轻松了运动的原理

    原文:Javascript 匀速运动停止条件--逐行分析代码,让你轻松了运动的原理 我们先来看下之前的匀速运动的代码,修改了速度speed后会出现怎么样的一个bug.这里加了两个标杆用于测试 < ...

  7. 华为鸿蒙系统源码_鸿蒙系统 IO 栈分析 | 解读鸿蒙源码

    华为的鸿蒙系统开源之后第一个想看的模块就是 FS 模块,想了解一下它的 IO 路径与 linux 的区别.现在鸿蒙开源的仓库中有两个内核系统,一个是 liteos_a 系统,一个是 liteos_m ...

  8. 鸿蒙系统的结构图,一图看懂鸿蒙系统中的JS开发框架!

    原标题:一图看懂鸿蒙系统中的JS开发框架! " 前端这两年玩起来了三国杀,Vue,React,Angular 三足鼎立,其中 Vue 派系在国内打的 Angular 找不到北了. 因此,小程 ...

  9. 三星手机可以装鸿蒙系统吗,国产手机厂商会用鸿蒙系统吗?从这几点分析他们用鸿蒙系统的可能性有多大...

    其它国产手机厂商会不会用鸿蒙系统,我们先看看会或不会用鸿蒙的前提条件或现实因素. 一,不会用鸿蒙: 1.华为之外的国产手机厂商可能被威胁若用鸿蒙就断供芯片,没有芯片那就是无法做手机了. 2. 鸿蒙生态 ...

最新文章

  1. 【计算机视觉】EmguCV学习笔记(1)Hello World
  2. python简单代码表白-python浪漫表白源码
  3. 基于JWT的Token认证机制实现
  4. 导师没项目怎么办?不如看看这些
  5. sql查看表的数据大小_查看Oracle 数据库的每天归档量及数据库大小
  6. Android的简介
  7. teamviewer 可用设备上限_河北环保碎石机价格-设备_久诺机械设备
  8. 运气真不错:3月取到的TeaVM恰好能够运行,之前之后都有问题
  9. linux环境sphinx搭建,linux系统环境下搭建coreseek(sphinx+mmseg3)
  10. jq 改数组的k值_在JSON jq中修改键值数组
  11. 图扑案例合集丨用赛博朋克语言诠释数字孪生
  12. sqlyog恢复查询记录
  13. nginx正向代理解决跨域问题
  14. 对于M1卡密钥控制字设置的总结
  15. Revit (3) - 二开 -创建柱子
  16. 213. 字符串压缩--LintCode领扣编程题
  17. HTML实战案例4:制作淘宝店铺列表页面
  18. 使用matlab实现ISD悬架离散仿真分析
  19. 自问自答——使用视图能提高查询效率么?
  20. “龙王宝”小程序,送水站老板轻松赚钱的神秘武器

热门文章

  1. iar one or more breakpoints be set
  2. Win11触控板如何关闭 Win11关闭触控板的方法
  3. 7种情绪,人类心智的通用模块
  4. 豆瓣即将上映电影爬虫作业
  5. kafka 创建topic,查看topic
  6. java硬币兑换_java程序题:把一元钞票换成一分、二分、五分硬币(每种至少一枚),有哪些种换法...
  7. 计算机夏令营英语自我介绍,保研夏令营英语自我介绍
  8. (二)Druid数据库连接池如何获取Connection原理和源码分析?
  9. A2DP和AVRCP 播放音视频
  10. 本地图片保存映射到Markdown文件中